diff --git a/.eslintignore b/.eslintignore index f0cbbe76..32343497 100644 --- a/.eslintignore +++ b/.eslintignore @@ -24,7 +24,7 @@ pnpm-lock.yaml .docusaurus build docs/docs/reference/core -docs/docs/reference/04-sveltekit +docs/docs/reference/sveltekit static # --------------- Packages --------------- diff --git a/.github/ISSUE_TEMPLATE/1_bug_framework.yml b/.github/ISSUE_TEMPLATE/1_bug_framework.yml index 9e327045..9a20c07f 100644 --- a/.github/ISSUE_TEMPLATE/1_bug_framework.yml +++ b/.github/ISSUE_TEMPLATE/1_bug_framework.yml @@ -30,7 +30,7 @@ body: Run this command in your project's root folder and paste the result: ```sh - npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth" + npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth,@auth/*" ``` Alternatively, you can manually gather the version information from your package.json for these packages: "next", "react" and "next-auth". Please also mention your OS and Node.js version, as well as the browser you are using. validations: diff --git a/.github/ISSUE_TEMPLATE/2_bug_provider.yml b/.github/ISSUE_TEMPLATE/2_bug_provider.yml index 28f54450..e5d343b0 100644 --- a/.github/ISSUE_TEMPLATE/2_bug_provider.yml +++ b/.github/ISSUE_TEMPLATE/2_bug_provider.yml @@ -25,6 +25,7 @@ body: - "Custom provider" - "42 School" - "Apple" + - "Asgardeo" - "Atlassian" - "Auth0" - "Authentik" @@ -57,6 +58,7 @@ body: - "Medium" - "Naver" - "Netlify" + - "Notion" - "Okta" - "OneLogin" - "Osso" @@ -87,7 +89,7 @@ body: Run this command in your project's root folder and paste the result: ```sh - npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth" + npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth,@auth/*" ``` Alternatively, you can manually gather the version information from your package.json for these packages: "next", "react" and "next-auth". Please also mention your OS and Node.js version, as well as the browser you are using. validations: diff --git a/.github/ISSUE_TEMPLATE/3_bug_adapter.yml b/.github/ISSUE_TEMPLATE/3_bug_adapter.yml index aa4b6b0b..1eea75b3 100644 --- a/.github/ISSUE_TEMPLATE/3_bug_adapter.yml +++ b/.github/ISSUE_TEMPLATE/3_bug_adapter.yml @@ -44,7 +44,7 @@ body: Run this command in your project's root folder and paste the result: ```sh - npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth" && npx envinfo --npmPackages "@next-auth/*" + npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth,@auth/*" && npx envinfo --npmPackages "@next-auth/*" ``` Alternatively, if the above command did not work, we need the version of the following packages from your package.json: "next", "react", "next-auth" and your adapter. Please also mention your OS and Node.js version, as well as the browser you are using. validations: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34e91599..919e9390 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,21 +35,22 @@ jobs: UPSTASH_REDIS_KEY: ${{ secrets.UPSTASH_REDIS_KEY }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - - name: Run E2E tests - run: pnpm e2e - timeout-minutes: 15 - env: - AUTH0_USERNAME: ${{ secrets.AUTH0_USERNAME }} - AUTH0_PASSWORD: ${{ secrets.AUTH0_PASSWORD }} - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - - name: Upload E2E artifacts - uses: actions/upload-artifact@v3 - if: always() - with: - name: playwright-report - path: apps/dev/nextjs/playwright-report/ - retention-days: 30 + # - name: Run E2E tests + # if: github.repository == 'nextauthjs/next-auth' + # run: pnpm e2e + # timeout-minutes: 15 + # env: + # AUTH0_USERNAME: ${{ secrets.AUTH0_USERNAME }} + # AUTH0_PASSWORD: ${{ secrets.AUTH0_PASSWORD }} + # TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + # TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + # - name: Upload E2E artifacts + # if: github.repository == 'nextauthjs/next-auth' + # uses: actions/upload-artifact@v3 + # with: + # name: playwright-report + # path: apps/dev/nextjs/playwright-report/ + # retention-days: 30 # - name: Coverage # uses: codecov/codecov-action@v1 # with: diff --git a/.gitignore b/.gitignore index 8e73647c..1f4e2442 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,6 @@ packages/*/*.js packages/*/*.d.ts packages/*/*.d.ts.map - # Development app apps/dev/src/css apps/dev/prisma/migrations @@ -80,11 +79,12 @@ docs/.docusaurus docs/providers.json # Core +packages/core/src/providers/oauth-types.ts packages/core/lib packages/core/providers packages/core/src/lib/pages/styles.ts docs/docs/reference/core -docs/docs/reference/04-sveltekit +docs/docs/reference/sveltekit # SvelteKit diff --git a/.prettierignore b/.prettierignore index ff7fc055..fa687a93 100644 --- a/.prettierignore +++ b/.prettierignore @@ -21,7 +21,7 @@ pnpm-lock.yaml .docusaurus build docs/docs/reference/core -docs/docs/reference/04-sveltekit +docs/docs/reference/sveltekit static docs/providers.json diff --git a/apps/dev/nextjs/.env.local.example b/apps/dev/nextjs/.env.local.example index fa6a263d..51b9bc74 100644 --- a/apps/dev/nextjs/.env.local.example +++ b/apps/dev/nextjs/.env.local.example @@ -9,6 +9,10 @@ NEXTAUTH_URL=http://localhost:3000 # and/or verification tokens. NEXTAUTH_SECRET=secret +ASGARDEO_CLIENT_ID= +ASGARDEO_CLIENT_SECRET= +ASGARDEO_ISSUER= + AUTH0_ID= AUTH0_SECRET= AUTH0_ISSUER= @@ -17,6 +21,10 @@ KEYCLOAK_ID= KEYCLOAK_SECRET= KEYCLOAK_ISSUER= +NOTION_ID= +NOTION_SECRET= +NOTION_REDIRECT_URI= + IDS4_ID= IDS4_SECRET= IDS4_ISSUER= diff --git a/apps/dev/nextjs/pages/api/auth/[...nextauth].ts b/apps/dev/nextjs/pages/api/auth/[...nextauth].ts index c0885779..9daba53f 100644 --- a/apps/dev/nextjs/pages/api/auth/[...nextauth].ts +++ b/apps/dev/nextjs/pages/api/auth/[...nextauth].ts @@ -2,6 +2,7 @@ import { Auth, type AuthConfig } from "@auth/core" // Providers import Apple from "@auth/core/providers/apple" +import Asgardeo from "@auth/core/providers/asgardeo" import Auth0 from "@auth/core/providers/auth0" import AzureAD from "@auth/core/providers/azure-ad" import AzureB2C from "@auth/core/providers/azure-ad-b2c" @@ -23,6 +24,7 @@ import Instagram from "@auth/core/providers/instagram" import Line from "@auth/core/providers/line" import LinkedIn from "@auth/core/providers/linkedin" import Mailchimp from "@auth/core/providers/mailchimp" +import Notion from "@auth/core/providers/notion" // import Okta from "@auth/core/providers/okta" import Osu from "@auth/core/providers/osu" import Patreon from "@auth/core/providers/patreon" @@ -68,7 +70,7 @@ import WorkOS from "@auth/core/providers/workos" export const authConfig: AuthConfig = { // adapter, - // debug: process.env.NODE_ENV !== "production", + debug: process.env.NODE_ENV !== "production", theme: { logo: "https://next-auth.js.org/img/logo/logo-sm.png", brandColor: "#1786fb", @@ -82,6 +84,7 @@ export const authConfig: AuthConfig = { }, }), Apple({ clientId: process.env.APPLE_ID, clientSecret: process.env.APPLE_SECRET }), + Asgardeo({ clientId: process.env.ASGARDEO_CLIENT_ID, clientSecret: process.env.ASGARDEO_CLIENT_SECRET, issuer: process.env.ASGARDEO_ISSUER }), Auth0({ clientId: process.env.AUTH0_ID, clientSecret: process.env.AUTH0_SECRET, issuer: process.env.AUTH0_ISSUER }), AzureAD({ clientId: process.env.AZURE_AD_CLIENT_ID, @@ -105,6 +108,7 @@ export const authConfig: AuthConfig = { Line({ clientId: process.env.LINE_ID, clientSecret: process.env.LINE_SECRET }), LinkedIn({ clientId: process.env.LINKEDIN_ID, clientSecret: process.env.LINKEDIN_SECRET }), Mailchimp({ clientId: process.env.MAILCHIMP_ID, clientSecret: process.env.MAILCHIMP_SECRET }), + Notion({ clientId: process.env.NOTION_ID, clientSecret: process.env.NOTION_SECRET, redirectUri: process.env.NOTION_REDIRECT_URI }), // Okta({ clientId: process.env.OKTA_ID, clientSecret: process.env.OKTA_SECRET, issuer: process.env.OKTA_ISSUER }), Osu({ clientId: process.env.OSU_CLIENT_ID, clientSecret: process.env.OSU_CLIENT_SECRET }), Patreon({ clientId: process.env.PATREON_ID, clientSecret: process.env.PATREON_SECRET }), diff --git a/apps/dev/sveltekit/package.json b/apps/dev/sveltekit/package.json index 196eb8c1..921c5aee 100644 --- a/apps/dev/sveltekit/package.json +++ b/apps/dev/sveltekit/package.json @@ -19,8 +19,8 @@ "vite": "4.0.1" }, "dependencies": { - "@auth/core": "0.2.5", - "@auth/sveltekit": "0.1.12" + "@auth/core": "workspace:*", + "@auth/sveltekit": "workspace:*" }, "type": "module" } diff --git a/apps/playgrounds/nuxt/package.json b/apps/playgrounds/nuxt/package.json index 28f07b11..f082b590 100644 --- a/apps/playgrounds/nuxt/package.json +++ b/apps/playgrounds/nuxt/package.json @@ -2,11 +2,10 @@ "name": "playground-nuxt", "private": true, "scripts": { - "build": "nuxt build", - "dev": "export NODE_OPTIONS='--no-experimental-fetch' && nuxt dev", + "build": "nuxt prepare && nuxt build", + "dev": "nuxt prepare && export NODE_OPTIONS='--no-experimental-fetch' && nuxt dev", "generate": "nuxt generate", - "preview": "nuxt preview", - "postinstall": "nuxt prepare" + "preview": "nuxt preview" }, "devDependencies": { "@nuxt/eslint-config": "^0.1.1", diff --git a/docs/docs/getting-started/05-typescript.md b/docs/docs/getting-started/05-typescript.md index e205c870..b78adaa6 100644 --- a/docs/docs/getting-started/05-typescript.md +++ b/docs/docs/getting-started/05-typescript.md @@ -5,7 +5,7 @@ title: TypeScript Auth.js has its own type definitions to use in your TypeScript projects safely. Even if you don't use TypeScript, IDEs like VSCode will pick this up to provide you with a better developer experience. While you are typing, you will get suggestions about what certain objects/functions look like, and sometimes links to documentation, examples, and other valuable resources. Check out the example repository showcasing how to use `next-auth` on a Next.js application with TypeScript: -https://github.com/nextauthjs/next-auth-typescript-example +https://github.com/nextauthjs/next-auth-example --- diff --git a/docs/docs/guides/03-basics/role-based-authentication.md b/docs/docs/guides/03-basics/role-based-authentication.md new file mode 100644 index 00000000..2b5a00c1 --- /dev/null +++ b/docs/docs/guides/03-basics/role-based-authentication.md @@ -0,0 +1,153 @@ +--- +title: Role-based authentication +--- + +There are two ways to add role-based authentication (RBAC) to your application, based on the [session strategy](/concepts/session-strategies) you choose. Let's see an example for each of these. + +## Getting the role + +We are going to start by adding a `profile()` callback to the providers' config to determine the user role: + +```ts title="/pages/api/auth/[...nextauth].ts" +import NextAuth from "next-auth" +import Google from "next-auth/providers/google" + +export default NextAuth({ + providers: [ + Google({ + profile(profile) { + return { role: profile.role ?? "user", ... } + }, + ... + }) + ], +}) +``` + +:::tip +To determine the user's role, you can either add your logic or if your provider assigns roles already, use that instead. +::: + +## Persisting the role +### With JWT + +When you don't have a database configured, the role will be persisted in a cookie, by using the `jwt()` callback. On sign-in, the `role` property is exposed from the `profile` callback on the `user` object. Persist the `user.role` value by assigning it to `token.role`. That's it! + +If you also want to use the role on the client, you can expose it via the `session` callback. + +```ts title="/pages/api/auth/[...nextauth].ts" +import NextAuth from "next-auth" +import Google from "next-auth/providers/google" + +export default NextAuth({ + providers: [ + Google({ + profile(profile) { + return { role: profile.role ?? "user", ... } + }, + ... + }) + ], + // highlight-start + callbacks: { + jwt({ token, user }) { + if(user) token.role = user.role + return token + }, + session({ session, token }) { + session.user.role = token.role + return session + } + } + // highlight-end +}) +``` + +:::info +With this strategy, if you want to update the role, the user needs to be forced to sign in again. +::: + +### With Database + +When you have a database, you can save the user role on the [User model](/reference/adapters/models#user). The below example is showing you how to do this with Prisma, but the idea is the same for all adapters. + +First, add a `role` column to the User model. + +```ts title="/prisma/schema.prisma" +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + role String? // New column + accounts Account[] + sessions Session[] +} +``` + +The `profile()` callback's return value is used to create users in the database. That's it! Your newly created users will now have an assigned role. + +If you also want to use the role on the client, you can expose it via the `session` callback. + +```ts title="/pages/api/auth/[...nextauth].ts" +import NextAuth from "next-auth" +import Google from "next-auth/providers/google" + // highlight-next-line +import prisma from "lib/prisma" + +export default NextAuth({ + // highlight-next-line + adapter: PrismaAdapter(prisma), + providers: [ + Google({ + profile(profile) { + return { role: profile.role ?? "user", ... } + } + ... + }) + ], + // highlight-start + callbacks: { + session({ session, user }) { + session.user.role = user.role + return session + } + } + // highlight-end +}) +``` + +:::info +It is up to you how you want to manage to update the roles, either through direct database access or building your role update API. +::: + +## Using the role + +If you want to use the role in the client, for both cases above, when using the `useSession` hook, `session.user.role` will have the required role if you exposed it via the `session` callback. You can use this to render a different UI for different users. + +```ts title="/pages/admin.tsx" +import { useSession } from "next-auth/react" + +export default function Page() { + const session = await useSession() + + if (session?.user.role === "admin") { + return
You are an admin, welcome!
+ } + + returnYou are not authorized to view this page!
+} +``` + +:::tip +When using Next.js and JWT, you can alternatively also use [Middleware](https://next-auth.js.org/configuration/nextjs#wrap-middleware) to redirect the user based on their role, even before rendering the page. +::: + +## Resources + +- [Concepts: Session strategies](/concepts/session-strategies) +- [Next.js: Middleware](https://next-auth.js.org/configuration/nextjs#wrap-middleware) +- [Adapters: User model](/reference/adapters/models#user) +- [Adapters: Prisma adapter](/reference/adapters/prisma) +- [TypeScript](/getting-started/typescript) \ No newline at end of file diff --git a/docs/docs/guides/03-basics/role-based-login-strategy.md b/docs/docs/guides/03-basics/role-based-login-strategy.md deleted file mode 100644 index 299909fa..00000000 --- a/docs/docs/guides/03-basics/role-based-login-strategy.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Role based logins ---- - -To add role based authentication to your application, you must do three things. - -1. Update your database schema -2. Add the `role` to the session object -3. Check for `role` in your pages/components - -First modify the `user` table and add a `role` column with the type of `String?`. - -Below is an example Prisma schema file. - -```javascript title="/prisma/schema.prisma" -model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - role String? // New Column - accounts Account[] - sessions Session[] -} - -``` - -Next, implement a custom session callback in the `[...nextauth].js` file, as shown below. - -```javascript title="/pages/api/auth/[...nextauth].js" -callbacks: { - async session({ session, token, user }) { - session.user.role = user.role; // Add role value to user object so it is passed along with session - return session; -}, -``` - -Going forward, when using the `getSession` hook, check that `session.user.role` matches the required role. The example below assumes the role `'admin'` is required. - -```javascript title="/pages/admin.js" -import { getSession } from "next-auth/react" - -export default function Page() { - const session = await getSession({ req }) - - if (session && session.user.role === "admin") { - return ( -Welcome to the Admin Portal!
-(
)
// Confirm that profile photo was returned
- if (response.ok) {
- const pictureBuffer = await response.arrayBuffer()
- const pictureBase64 = Buffer.from(pictureBuffer).toString("base64")
- return {
- id: profile.sub,
- name: profile.name,
- email: profile.email,
- image: `data:image/jpeg;base64, ${pictureBase64}`,
- }
- } else {
- return {
- id: profile.sub,
- name: profile.name,
- email: profile.email,
- image: null,
- }
+ let image
+ // TODO: Do this without Buffer
+ if (response.ok && typeof Buffer !== "undefined") {
+ try {
+ const pictureBuffer = await response.arrayBuffer()
+ const pictureBase64 = Buffer.from(pictureBuffer).toString("base64")
+ image = `data:image/jpeg;base64, ${pictureBase64}`
+ } catch {}
+ }
+
+ return {
+ id: profile.sub,
+ name: profile.name,
+ email: profile.email,
+ image: image ?? null,
}
},
style: {
diff --git a/packages/core/src/providers/credentials.ts b/packages/core/src/providers/credentials.ts
index d8e71ad9..095678b9 100644
--- a/packages/core/src/providers/credentials.ts
+++ b/packages/core/src/providers/credentials.ts
@@ -49,10 +49,6 @@ export interface CredentialsConfig<
export type CredentialsProviderType = "Credentials"
-export type CredentialsConfigInternal<
- C extends Record (
diff --git a/packages/core/src/providers/index.ts b/packages/core/src/providers/index.ts
index ffa3819b..91da0513 100644
--- a/packages/core/src/providers/index.ts
+++ b/packages/core/src/providers/index.ts
@@ -65,12 +65,16 @@ export type Provider = (
| EmailConfig
| CredentialsConfig
) & {
+ /**
+ * Used to deep merge user-provided config with the default config
+ * @internal
+ */
options: Record (
+ options: OAuthUserConfig & AdditionalConfig
+): OAuthConfig {
+ return {
+ id: "notion",
+ name: "Notion",
+ type: "oauth",
+ token: {
+ url: `${NOTION_HOST}/v1/oauth/token`,
+ },
+ userinfo: {
+ url: `${NOTION_HOST}/v1/users`,
+
+ // The result of this method will be the input to the `profile` callback.
+ // We use a custom request handler, since we need to do things such as pass the "Notion-Version" header
+ // More info: https://next-auth.js.org/configuration/providers/oauth
+ async request(context) {
+ const profile = await fetch(`${NOTION_HOST}/v1/users/me`, {
+ headers: {
+ Authorization: `Bearer ${context.tokens.access_token}`,
+ "Notion-Version": NOTION_API_VERSION,
+ },
+ })
+
+ const {
+ bot: {
+ owner: { user },
+ },
+ } = await profile.json()
+
+ return user
+ },
+ },
+ authorization: {
+ params: {
+ client_id: options.clientId,
+ response_type: "code",
+ owner: "user",
+ redirect_uri: options.redirectUri,
+ },
+ url: `${NOTION_HOST}/v1/oauth/authorize`,
+ },
+
+ async profile(profile, tokens) {
+ return {
+ id: profile.id,
+ name: profile.name,
+ email: profile.person.email,
+ image: profile.avatar_url,
+ }
+ },
+ style: {
+ logo: "/notion.svg",
+ logoDark: "/notion.svg",
+ bg: "#fff",
+ text: "#000",
+ bgDark: "#fff",
+ textDark: "#000",
+ },
+ options,
+ }
+}
diff --git a/packages/core/src/providers/oauth-types.ts b/packages/core/src/providers/oauth-types.ts
deleted file mode 100644
index 66c75b2f..00000000
--- a/packages/core/src/providers/oauth-types.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-// THIS FILE IS AUTOGENERATED. DO NOT EDIT.
-export type OAuthProviderType =
- | "42-school"
- | "apple"
- | "atlassian"
- | "auth0"
- | "authentik"
- | "azure-ad-b2c"
- | "azure-ad"
- | "battlenet"
- | "box"
- | "boxyhq-saml"
- | "bungie"
- | "cognito"
- | "coinbase"
- | "credentials"
- | "discord"
- | "dropbox"
- | "duende-identity-server6"
- | "email"
- | "eveonline"
- | "facebook"
- | "faceit"
- | "foursquare"
- | "freshbooks"
- | "fusionauth"
- | "github"
- | "gitlab"
- | "google"
- | "hubspot"
- | "identity-server4"
- | "index"
- | "instagram"
- | "kakao"
- | "keycloak"
- | "line"
- | "linkedin"
- | "mailchimp"
- | "mailru"
- | "medium"
- | "naver"
- | "netlify"
- | "oauth-types.js"
- | "oauth"
- | "okta"
- | "onelogin"
- | "osso"
- | "osu"
- | "patreon"
- | "pinterest"
- | "pipedrive"
- | "reddit"
- | "salesforce"
- | "slack"
- | "spotify"
- | "strava"
- | "todoist"
- | "trakt"
- | "twitch"
- | "twitter"
- | "united-effects"
- | "vk"
- | "wikimedia"
- | "wordpress"
- | "workos"
- | "yandex"
- | "zitadel"
- | "zoho"
- | "zoom"
diff --git a/packages/core/src/providers/oauth.ts b/packages/core/src/providers/oauth.ts
index 82aa8840..87edda7f 100644
--- a/packages/core/src/providers/oauth.ts
+++ b/packages/core/src/providers/oauth.ts
@@ -66,7 +66,7 @@ export type TokenEndpointHandler = EndpointHandler<
params: CallbackParamsType
/**
* When using this custom flow, make sure to do all the necessary security checks.
- * Thist object contains parameters you have to match against the request to make sure it is valid.
+ * This object contains parameters you have to match against the request to make sure it is valid.
*/
checks: OAuthChecks
},
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index dd37d09e..9e4f0f12 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -203,7 +203,7 @@ export interface CallbacksOptions {
* Its content is forwarded to the `session` callback,
* where you can control what should be returned to the client.
* Anything else will be kept inaccessible from the client.
- *
+ *
* Returning `null` will invalidate the JWT session by clearing
* the user's cookies. You'll still have to monitor and invalidate
* unexpired tokens from future requests yourself to prevent
@@ -220,7 +220,7 @@ export interface CallbacksOptions {
account?: A | null
profile?: P
isNewUser?: boolean
- }) => Awaitable
+ *
+ *