Compare commits

..

6 Commits

Author SHA1 Message Date
Balázs Orbán
0099d40323 double quote 2023-01-10 11:56:54 +01:00
Balázs Orbán
cfbe565f39 add header 2023-01-09 16:53:03 +01:00
Balázs Orbán
b2b0c05da8 Merge branch 'main' into feat/bankid-no-provider 2023-01-09 13:48:52 +01:00
Balázs Orbán
ea923f152c chore: add BankID Norge to dev app 2023-01-09 13:31:06 +01:00
Balázs Orbán
0361cef622 feat(providers): Add BankID Norge provider 2023-01-09 13:30:54 +01:00
Balázs Orbán
740a2db07f feat: support userinfo.request with OIDC 2023-01-09 13:30:10 +01:00
110 changed files with 4550 additions and 4985 deletions

View File

@@ -23,8 +23,8 @@ pnpm-lock.yaml
.docusaurus
build
docs/docs/reference/core
docs/docs/reference/sveltekit
docs/docs/reference/03-core
docs/docs/reference/04-sveltekit
static
# --------------- Packages ---------------

View File

@@ -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,@auth/*"
npx envinfo --system --binaries --browsers --npmPackages "next,react,next-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:

View File

@@ -25,7 +25,6 @@ body:
- "Custom provider"
- "42 School"
- "Apple"
- "Asgardeo"
- "Atlassian"
- "Auth0"
- "Authentik"
@@ -58,7 +57,6 @@ body:
- "Medium"
- "Naver"
- "Netlify"
- "Notion"
- "Okta"
- "OneLogin"
- "Osso"
@@ -89,7 +87,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,@auth/*"
npx envinfo --system --binaries --browsers --npmPackages "next,react,next-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:

View File

@@ -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,@auth/*" && npx envinfo --npmPackages "@next-auth/*"
npx envinfo --system --binaries --browsers --npmPackages "next,react,next-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:

View File

@@ -35,22 +35,6 @@ jobs:
UPSTASH_REDIS_KEY: ${{ secrets.UPSTASH_REDIS_KEY }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
# - 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:

22
.gitignore vendored
View File

@@ -12,7 +12,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log
ui-debug.log
.pnpm-debug.log
@@ -35,10 +34,13 @@ packages/next-auth/utils
packages/next-auth/core
packages/next-auth/jwt
packages/next-auth/react
packages/next-auth/adapters.d.ts
packages/next-auth/adapters.js
packages/next-auth/index.d.ts
packages/next-auth/index.js
packages/next-auth/next
packages/*/*.js
packages/*/*.d.ts
packages/*/*.d.ts.map
packages/next-auth/middleware.d.ts
packages/next-auth/middleware.js
# Development app
apps/dev/src/css
@@ -79,12 +81,14 @@ docs/.docusaurus
docs/providers.json
# Core
packages/core/src/providers/oauth-types.ts
packages/core/*.js
packages/core/*.d.ts
packages/core/*.d.ts.map
packages/core/lib
packages/core/providers
packages/core/src/lib/pages/styles.ts
docs/docs/reference/core
docs/docs/reference/sveltekit
docs/docs/reference/03-core
docs/docs/reference/04-sveltekit
# SvelteKit
@@ -94,7 +98,3 @@ packages/frameworks-sveltekit/.svelte-kit
packages/frameworks-sveltekit/package
packages/frameworks-sveltekit/vite.config.js.timestamp-*
packages/frameworks-sveltekit/vite.config.ts.timestamp-*
# Adapters
docs/docs/reference/adapter

View File

@@ -20,8 +20,8 @@ pnpm-lock.yaml
.docusaurus
build
docs/docs/reference/core
docs/docs/reference/sveltekit
docs/docs/reference/03-core
docs/docs/reference/04-sveltekit
static
docs/providers.json

View File

@@ -9,10 +9,6 @@ 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=
@@ -21,10 +17,6 @@ KEYCLOAK_ID=
KEYCLOAK_SECRET=
KEYCLOAK_ISSUER=
NOTION_ID=
NOTION_SECRET=
NOTION_REDIRECT_URI=
IDS4_ID=
IDS4_SECRET=
IDS4_ISSUER=

View File

@@ -1,4 +0,0 @@
node_modules/
/test-results/
/playwright-report/
/playwright/.cache/

View File

@@ -9,12 +9,10 @@
"build": "next build",
"start": "next start",
"email": "fake-smtp-server",
"start:email": "pnpm email",
"e2e": "pnpm dlx playwright test"
"start:email": "pnpm email"
},
"license": "ISC",
"dependencies": {
"@auth/core": "workspace:*",
"@next-auth/fauna-adapter": "workspace:*",
"@next-auth/prisma-adapter": "workspace:*",
"@next-auth/supabase-adapter": "workspace:*",
@@ -24,16 +22,15 @@
"faunadb": "^4",
"next": "13.1.1",
"next-auth": "workspace:*",
"@auth/core": "workspace:*",
"nodemailer": "^6",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@playwright/test": "1.29.2",
"@types/jsonwebtoken": "^8.5.5",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"dotenv": "^16.0.3",
"fake-smtp-server": "^0.8.0",
"pg": "^8.7.3",
"prisma": "^3",

View File

@@ -2,10 +2,10 @@ 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"
import BankIDNorway from "@auth/core/providers/bankid-no"
import BoxyHQSAML from "@auth/core/providers/boxyhq-saml"
// import Cognito from "@auth/core/providers/cognito"
import Credentials from "@auth/core/providers/credentials"
@@ -24,7 +24,6 @@ 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"
@@ -70,7 +69,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",
@@ -84,7 +83,6 @@ 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,
@@ -92,6 +90,7 @@ export const authConfig: AuthConfig = {
tenantId: process.env.AZURE_AD_TENANT_ID,
}),
AzureB2C({ clientId: process.env.AZURE_B2C_ID, clientSecret: process.env.AZURE_B2C_SECRET, issuer: process.env.AZURE_B2C_ISSUER }),
BankIDNorway({ clientId: process.env.BANKID_NO_ID, clientSecret: process.env.BANKID_NO_SECRET }),
BoxyHQSAML({ issuer: "https://jackson-demo.boxyhq.com", clientId: "tenant=boxyhq.com&product=saml-demo.boxyhq.com", clientSecret: "dummy" }),
// Cognito({ clientId: process.env.COGNITO_ID, clientSecret: process.env.COGNITO_SECRET, issuer: process.env.COGNITO_ISSUER }),
Discord({ clientId: process.env.DISCORD_ID, clientSecret: process.env.DISCORD_SECRET }),
@@ -108,7 +107,6 @@ 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 }),

View File

@@ -1,107 +0,0 @@
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
export default config;

View File

@@ -1,39 +0,0 @@
import { test, expect } from "@playwright/test"
test("Sign in with Auth0", async ({ page }) => {
// Go to NextAuth example app
await page.goto("https://next-auth-example.vercel.app")
// Click 'Sign In'
await page.click("#__next > header > div > p > a")
// Auth0 Login Provider
await page.click('body > div > div form[action*="auth0"] > button')
// Enter Credentials (Username/Password Login) on Auth0 Widget
await page.type("#username", process.env.AUTH0_USERNAME!)
await page.type("#password", process.env.AUTH0_PASSWORD!)
// Snap a screenshot
// await page.screenshot({ path: "1-auth0-login.png", fullPage: true })
// Press submit on Auth0 form
await page.click('body > div > main > section > div button[type="submit"]')
// Wait for next-auth example page login status header to appear
await page.waitForTimeout(2000)
// Snap a screenshot
// await page.screenshot({
// path: "2-next-auth-redirect-result.png",
// fullPage: false,
// })
// Check session object after successful login
const response = await page.goto(
"https://next-auth-example.vercel.app/api/auth/session"
)
const session = await response?.json()
expect(session?.user?.email).toBe(process.env.AUTH0_USERNAME)
// TODO: Check whole object with .toEqual()
})

View File

@@ -19,8 +19,8 @@
"vite": "4.0.1"
},
"dependencies": {
"@auth/core": "workspace:*",
"@auth/sveltekit": "workspace:*"
"@auth/core": "latest",
"@auth/sveltekit": "latest"
},
"type": "module"
}

View File

@@ -17,7 +17,7 @@
"vite": "^3.1.0"
},
"dependencies": {
"@auth/core": "latest",
"@auth/core": "^0.1.4",
"@solid-auth/next": "^0.0.19",
"@solidjs/meta": "^0.28.0",
"@solidjs/router": "^0.6.0",

View File

@@ -20,10 +20,10 @@
$page.data.session.user?.name}</strong
>
</span>
<a href="/auth/signout" class="button" data-sveltekit-preload-data="off">Sign out</a>
<a href="/auth/signout" class="button">Sign out</a>
{:else}
<span class="notSignedInText">You are not signed in</span>
<a href="/auth/signin" class="buttonPrimary" data-sveltekit-preload-data="off">Sign in</a>
<a href="/auth/signin" class="buttonPrimary">Sign in</a>
{/if}
</p>
</div>

View File

@@ -12,7 +12,7 @@
"dependencies": {
"dotenv": "^16.0.0",
"gatsby": "next",
"next-auth": "workspace:*",
"next-auth": "latest",
"react": "^18",
"react-dom": "^18"
},

View File

@@ -2,10 +2,11 @@
"name": "playground-nuxt",
"private": true,
"scripts": {
"build": "nuxt prepare && nuxt build",
"dev": "nuxt prepare && export NODE_OPTIONS='--no-experimental-fetch' && nuxt dev",
"build": "nuxt build",
"dev": "export NODE_OPTIONS='--no-experimental-fetch' && nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview"
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.1.1",

View File

@@ -70,7 +70,7 @@ Auth.js is extremely customizable, [our guides section](/guides/overview) will t
To be able to use `useSession` first you'll need to expose the session context, [`<SessionProvider />`](/reference/react/#sessionprovider), at the top level of your application:
```ts title="pages/_app.tsx"
```ts title="pages/_app.ts"
import { SessionProvider } from "next-auth/react"
export default function App({
@@ -186,14 +186,14 @@ http://localhost:3000/api/auth/callback/github
Auth.js will already magically create this API endpoint for you when we start the application later. Note that because we're using Next.js, locally it starts our server on the port `3000`, hence the origin is `http://localhost:3000`.
:::
Next you'll be presented with the following screen which presents all the configuration for your new OAuth app. For now, we need two things from it: the **Client ID** and **Client Secret** for our new OAuth app:
Next you'll be presented with the following screen which presents all the configuration for your new OAuth app. For now, let's we need two things from it: the **Client ID** and **Client Secret** for our new OAuth app:
<img src={gettingClientIdSecretImg} />
The Client ID is always there, a public identifier of your OAuth application within Github. Click on the **Generate a new client Secret** button and should be presented with a new string (which is just a randomized string).
:::warning
🔥 Keep both your Client ID and Client Secret secure and never expose them to the public or shared with people outside your organization. With them, a malicious actor could hijack your application and cause you and your user serious problems!
🔥 Keep both your Client ID and Client Secret secure and never expose them to the public or shared with people outside your organization. With tem a malicious actor could hijack your application and cause you and your user serious problems!
:::
Now let's copy both the Client ID and Client Secret and paste them in an environment file in the root of your project like so:

View File

@@ -37,7 +37,7 @@ npm install -D nodemailer
Next we need a [SMTP service](https://sendgrid.com/blog/what-is-an-smtp-server/) which will be in charge of sending emails from our application. There's a number of services available for this, however [here are the ones](http://nodemailer.com/smtp/well-known/) known to work with `nodemailer`.
:::info
For this tutorial, we're going to be using [Sendgrid](https://sendgrid.com/), but any of the services linked above should work the same
For this tutorial, we're gonna be using [Sendgrid](https://sendgrid.com/), but any of the services linked above should work the same
:::
First create an account in and then login to the dashboard, then navigate to "Settings → API Keys" and create an API key:
@@ -60,7 +60,7 @@ SMTP_PORT=587
EMAIL_FROM={SENDER_EMAIL}
```
Note that we're also specifying from which domain email are going to be sent from. You're going to need to verify [a sender identity](https://docs.sendgrid.com/for-developers/sending-email/sender-identity) so that Sendgrid can send emails from your domain.
Note that we're also specifying from which domain email are going to be sent from. You're gonna need to verify [a sender identity](https://docs.sendgrid.com/for-developers/sending-email/sender-identity) so that Sendgrid can send emails from your domain.
Nice! We're getting there. Now we need to read supply this values as the configuration for our Email Provider. Open `pages/api/auth/[...nextauth].ts` and do the following:
@@ -170,7 +170,7 @@ Now that everything is properly configured, let's try to sign in via email on ou
Let's start by running a Next.js application with NextAuth, making sure the **EmailProvider** and a Database Adapter are properly configured as per the instructions above.
For this tutorial we're going to be using NextAuth example app. Launch the app and click on "Sign in", we're redirected to the Sign In page:
For this tutorial we're gonna be using NextAuth example app. Launch the app and click on "Sign in", we're redirected to the Sign In page:
<img src={startPageImg} alt="Screenshot of sign in page" />

View File

@@ -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-example
https://github.com/nextauthjs/next-auth-typescript-example
---

View File

@@ -22,7 +22,7 @@ Using a JWT to store the `refresh_token` is less secure than saving it in a data
#### JWT strategy
Using the [jwt](../../reference/core/types#jwt) and [session](../../reference/core/types#session) callbacks, we can persist OAuth tokens and refresh them when they expire.
Using the [jwt](../../reference/03-core/interfaces/types.CallbacksOptions.md#jwt) and [session](../../reference/03-core/interfaces/types.CallbacksOptions.md#session) callbacks, we can persist OAuth tokens and refresh them when they expire.
Below is a sample implementation using Google's Identity Provider. Please note that the OAuth 2.0 request in the `refreshAccessToken()` function will vary between different providers, but the core logic should remain similar.
@@ -136,7 +136,7 @@ export default Auth(new Request("https://example.com"), {
const [google] = await prisma.account.findMany({
where: { userId: user.id, provider: "google" },
})
if (google.expires_at < Date.now()) {
if (google.expires_at >= Date.now()) {
// If the access token has expired, try to refresh it
try {
// https://accounts.google.com/.well-known/openid-configuration

View File

@@ -1,153 +0,0 @@
---
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 <p>You are an admin, welcome!</p>
}
return <p>You are not authorized to view this page!</p>
}
```
:::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)

View File

@@ -0,0 +1,64 @@
---
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 (
<div>
<h1>Admin</h1>
<p>Welcome to the Admin Portal!</p>
</div>
)
} else {
return (
<div>
<h1>You are not authorized to view this page!</h1>
</div>
)
}
}
```
Then it is up to you how you manage your roles, either through direct database access or building your own role update API.

View File

@@ -16,4 +16,4 @@ sidebar_label: Email options
See our guides on magic links authentication for further tips on how to customize this provider:
- [Tutorial](/getting-started/email-tutorial)
- [Guide deep-dive](/guides/providers/email)
- [Guide deep-dive](guides/providers/email)

View File

@@ -15,7 +15,7 @@ https://develop.battle.net/access/clients
The **Battle.net Provider** comes with a set of default options:
- [Battle.net Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/battlenet.ts)
- [Battle.net Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/battlenet.js)
You can override any of the options to suit your own use case.

View File

@@ -19,7 +19,7 @@ https://github.com/settings/apps
The **GitHub Provider** comes with a set of default options:
- [GitHub Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/github.ts)
- [GitHub Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/github.js)
You can override any of the options to suit your own use case.

View File

@@ -15,7 +15,7 @@ https://gitlab.com/-/profile/applications
The **Gitlab Provider** comes with a set of default options:
- [Gitlab Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/gitlab.ts)
- [Gitlab Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/gitlab.js)
You can override any of the options to suit your own use case.

View File

@@ -15,7 +15,7 @@ https://developers.kakao.com/docs/latest/en/kakaologin/common
The **Kakao Provider** comes with a set of default options:
- [Kakao Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/kakao.ts)
- [Kakao Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/kakao.js)
You can override any of the options to suit your own use case.

View File

@@ -11,7 +11,7 @@ https://help.salesforce.com/articleView?id=remoteaccess_authenticate.htm&type=5
The **Salesforce Provider** comes with a set of default options:
- [Salesforce Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/salesforce.ts)
- [Salesforce Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/salesforce.js)
You can override any of the options to suit your own use case.

View File

@@ -15,7 +15,7 @@ https://vk.com/apps?act=manage
The **VK Provider** comes with a set of default options:
- [VK Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/vk.ts)
- [VK Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/vk.js)
You can override any of the options to suit your own use case.

View File

@@ -0,0 +1,75 @@
---
id: firebase
title: Firebase
---
:::warning
This adapter is still experimental and does not work with Auth.js 4 or newer. If you would like to help out upgrading it, please visit [this PR](https://github.com/nextauthjs/next-auth/pull/3873)
:::
This is the Firebase Adapter for [`next-auth`](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
## Getting Started
1. Install the necessary packages
```bash npm2yarn
npm install next-auth @next-auth/firebase-adapter@experimental
```
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import { FirebaseAdapter } from "@next-auth/firebase-adapter"
import firebase from "firebase/app"
import "firebase/firestore"
const firestore = (
firebase.apps[0] ?? firebase.initializeApp(/* your config */)
).firestore()
// For more information on each option (and a full list of options) go to
// https://authjs.dev/reference/configuration/auth-options
export default NextAuth({
// https://authjs.dev/reference/providers/
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
adapter: FirebaseAdapter(firestore),
...
})
```
## Options
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
An example firebase config looks like this:
```js
const firebaseConfig = {
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
authDomain: "myapp-project-123.firebaseapp.com",
databaseURL: "https://myapp-project-123.firebaseio.com",
projectId: "myapp-project-123",
storageBucket: "myapp-project-123.appspot.com",
messagingSenderId: "65211879809",
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
measurementId: "G-8GSGZQ44ST",
}
```
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
:::tip **From Firebase**
**Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
:::

View File

@@ -139,10 +139,9 @@ Prisma supports MongoDB, and so does Auth.js. Following the instructions of the
id String @id @default(auto()) @map("_id") @db.ObjectId
```
2. The Native database type attribute to `@db.String` from `@db.Text` and userId to `@db.ObjectId`.
2. The Native database type attribute to `@db.String` from `@db.Text`.
```prisma
user_id String @db.ObjectId
refresh_token String? @db.String
access_token String? @db.String
id_token String? @db.String

View File

@@ -32,6 +32,7 @@ Now that we're ready, let's create a new Xata project using our next-auth schema
```json title="schema.json"
{
"formatVersion": "",
"tables": [
{
"name": "nextauth_users",

View File

@@ -3,13 +3,57 @@ id: warnings
title: Warnings
---
A list of warnings from Auth.js that need your attention.
This is a list of warning output from Auth.js.
All warnings indicate things which you should take a look at, but do not inhibit normal operation.
## Debug enabled
---
The `debug` option was evaluated to `true`. It adds extra logs in the terminal which is useful in development, but since it can print sensitive information about users, make sure to set this to `false` in production. In Node.js environments, you can for example set `debug: process.env.NODE_ENV !== "production"`. Consult with your runtime/framework on how to set this value correctly.
## Client
## CSRF disabled
#### NEXTAUTH_URL
You were trying to get a CSRF response from Auth.js (eg.: by calling a `/csrf` endpoint), but in this setup, CSRF protection via Auth.js was turned off. This is likely if you are not directly using `@auth/core` but a framework library (like `@auth/sveltekit`) that already has CSRF protection built-in. You likely won't need the CSRF response.
Environment variable `NEXTAUTH_URL` missing. Please set it in your `.env` file.
:::note
On [Vercel](https://vercel.com) deployments, we will read the `VERCEL_URL` environment variable, so you won't need to define `NEXTAUTH_URL`.
:::
---
## Server
These warnings are displayed on the terminal.
#### NO_SECRET
In development, we generate a `secret` based on your configuration for convenience. This is volatile and will throw an error in production. [Read more](https://authjs.dev/reference/configuration/auth-config/#secret)
#### TWITTER_OAUTH_2_BETA
Twitter OAuth 2.0 is currently in beta as certain changes might still be necessary. This is not covered by semver. See the docs https://authjs.dev/reference/providers/twitter#oauth-2
#### EXPERIMENTAL_API
Some APIs are still experimental; they may be changed or removed in the future. Use at your own risk.
## Adapter
### ADAPTER_TYPEORM_UPDATING_ENTITIES
This warning occurs when typeorm finds that the provided entities differ from the database entities. By default while not in `production` the typeorm adapter will always synchronize changes made to the entities codefiles.
Disable this warning by setting `synchronize: false` in your typeorm config
Example:
```js title="/pages/api/auth/[...nextauth].js"
adapter: TypeORMLegacyAdapter({
type: 'mysql',
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
host: process.env.DATABASE_HOST,
database: process.env.DATABASE_DB,
synchronize: false
}),
```

View File

@@ -0,0 +1,25 @@
---
title: Overview
sidebar_label: Overview
sidebar_position: 0
---
## Core
## Providers
- OAuth/OIDC
- Email/Passwordless
- Credentials
## Database Adapters
## Frameworks
- Next.js
- SvelteKit
- SolidStart
- Remix
- Nuxt
- Gatsby
- etc.

View File

@@ -62,7 +62,7 @@ const docusaurusConfig = {
position: "left",
},
{
to: "/reference/core",
to: "/reference/core/modules/main",
// TODO: change to this when the overview page looks better.
// to: "/reference",
activeBasePath: "/reference",
@@ -101,7 +101,7 @@ const docusaurusConfig = {
announcementBar: {
id: "new-major-announcement",
content:
"<a target='_blank' rel='noopener noreferrer' href='https://next-auth.js.org'>NextAuth.js</a> is becoming Auth.js! 🎉 We're creating Authentication for the Web. Everyone included. Starting with SvelteKit, check out <a href='/reference/sveltekit'>the docs</a>. Note, this site is under active development.",
"<a target='_blank' rel='noopener noreferrer' href='https://next-auth.js.org'>NextAuth.js</a> is becoming Auth.js! 🎉 We're creating Authentication for the Web. Everyone included. Starting with SvelteKit, check out <a href='/reference/sveltekit'>the docs</a>.",
backgroundColor: "#000",
textColor: "#fff",
},
@@ -182,7 +182,10 @@ const docusaurusConfig = {
lastVersion: "current",
showLastUpdateAuthor: true,
showLastUpdateTime: true,
remarkPlugins: [require("@sapphire/docusaurus-plugin-npm2yarn2pnpm").npm2yarn2pnpm],
remarkPlugins: [
require("@sapphire/docusaurus-plugin-npm2yarn2pnpm").npm2yarn2pnpm,
require("remark-github"),
],
versions: {
current: {
label: "experimental",
@@ -201,14 +204,20 @@ const docusaurusConfig = {
{
...typedocConfig,
id: "core",
plugin: [require.resolve("./typedoc-mdn-links")],
watch: process.env.TYPEDOC_WATCH,
entryPoints: ["index.ts", "adapters.ts", "errors.ts", "jwt.ts", "types.ts"].map((e) => `${coreSrc}/${e}`).concat(providers),
plugin: ["./tyepdoc"],
entryPoints: [
"index.ts",
"adapters.ts",
"errors.ts",
"jwt.ts",
"types.ts",
]
.map((e) => `${coreSrc}/${e}`)
.concat(providers),
tsconfig: "../packages/core/tsconfig.json",
out: "reference/core",
sidebar: {
indexLabel: "index",
},
out: "reference/03-core",
watch: process.env.TYPEDOC_WATCH,
includeExtension: false,
},
],
[
@@ -216,29 +225,14 @@ const docusaurusConfig = {
{
...typedocConfig,
id: "sveltekit",
plugin: [require.resolve("./typedoc-mdn-links")],
watch: process.env.TYPEDOC_WATCH,
entryPoints: ["index.ts", "client.ts"].map((e) => `../packages/frameworks-sveltekit/src/lib/${e}`),
plugin: ["./tyepdoc"],
entryPoints: ["index.ts", "client.ts"].map(
(e) => `../packages/frameworks-sveltekit/src/lib/${e}`
),
tsconfig: "../packages/frameworks-sveltekit/tsconfig.json",
out: "reference/sveltekit",
sidebar: {
indexLabel: "index",
},
},
],
[
"docusaurus-plugin-typedoc",
{
...typedocConfig,
id: "firebase-adapter",
plugin: [require.resolve("./typedoc-mdn-links")],
out: "reference/04-sveltekit",
watch: process.env.TYPEDOC_WATCH,
entryPoints: ["../packages/adapter-firebase/src/index.ts"],
tsconfig: "../packages/adapter-firebase/tsconfig.json",
out: "reference/adapter/firebase",
sidebar: {
indexLabel: "Firebase",
},
includeExtension: false,
},
],
],

View File

@@ -3,7 +3,7 @@
"repository": "https://github.com/nextauthjs/next-auth",
"name": "docs",
"scripts": {
"start": "TYPEDOC_WATCH=true docusaurus start --no-open",
"start": "TYPEDOC_WATCH=true docusaurus start --no-open --port 8000",
"dev": "pnpm providers && pnpm snippets && pnpm start",
"build": "pnpm providers && docusaurus build",
"docusaurus": "docusaurus",
@@ -27,6 +27,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-marquee-slider": "^1.1.5",
"remark-github": "10.1.0",
"styled-components": "5.3.6"
},
"devDependencies": {
@@ -36,9 +37,7 @@
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-common": "2.2.0",
"@docusaurus/types": "2.2.0",
"docusaurus-plugin-typedoc": "1.0.0-next.2",
"typedoc": "^0.23.24",
"typedoc-plugin-markdown": "4.0.0-next.2"
"docusaurus-plugin-typedoc": "^0.18.0"
},
"browserslist": {
"production": [

View File

@@ -14,28 +14,61 @@ module.exports = {
},
],
referenceSidebar: [
"reference/index",
{
type: "category",
label: "@auth/core",
link: { type: "doc", id: "reference/core/index" },
items: [{ type: "autogenerated", dirName: "reference/core" }],
link: {
type: "doc",
id: "reference/core/modules/main",
},
items: [
{
type: "autogenerated",
dirName: "reference/03-core/modules",
// See: https://github.com/facebook/docusaurus/issues/5689
// exclude: ["index"],
},
{
type: "category",
label: "Reflections",
collapsed: true,
className: "reflection-category", // See src/index.css
items: [{ type: "autogenerated", dirName: "reference/03-core" }],
},
],
},
{
type: "category",
label: "@auth/sveltekit",
link: { type: "doc", id: "reference/sveltekit/index" },
items: [{ type: "autogenerated", dirName: "reference/sveltekit" }],
link: { type: "doc", id: "reference/sveltekit/modules/main" },
items: [
{ type: "autogenerated", dirName: "reference/04-sveltekit/modules" },
{
type: "category",
label: "Reflections",
collapsed: true,
className: "reflection-category", // See src/index.css
items: [{ type: "autogenerated", dirName: "reference/04-sveltekit" }],
},
],
},
{
type: "category",
label: "@auth/solid-start",
link: { type: "doc", id: "reference/solidstart/index" },
items: [{ type: "autogenerated", dirName: "reference/04-solidstart" }],
link: {
type: "doc",
id: "reference/solidstart/index",
},
items: ["reference/solidstart/client", "reference/solidstart/protected"],
},
{
type: "category",
label: "@auth/nextjs",
link: { type: "doc", id: "reference/nextjs/index" },
link: {
type: "doc",
id: "reference/nextjs/index",
},
items: [
"reference/nextjs/client",
{
@@ -50,8 +83,12 @@ module.exports = {
label: "Database Adapters",
link: { type: "doc", id: "reference/adapters/overview" },
items: [
{ type: "doc", id: "reference/adapter/firebase/index" },
{ type: "autogenerated", dirName: "reference/06-adapters" },
{
type: "autogenerated",
dirName: "reference/06-adapters",
// See: https://github.com/facebook/docusaurus/issues/5689
// exclude: ["index"],
},
],
},
{

View File

@@ -2,9 +2,11 @@ Add $1 login to your page.
## Example
```ts
import { Auth } from "@auth/core"
import $1 from "@auth/core/providers/$2"
@example
```js
import Auth from "@auth/core"
import { $1 } from "@auth/core/providers/$2"
const request = new Request("https://example.com")
const resposne = await AuthHandler(request, {
@@ -16,7 +18,7 @@ const resposne = await AuthHandler(request, {
## Resources
- [Link 1](https://example.com)
@see [Link 1](https://example.com)
---

View File

@@ -272,4 +272,27 @@ html[data-theme="dark"] #carbonads > span {
html[data-theme="dark"] #carbonads .carbon-poweredby {
color: #aaa;
background: #1e2021;
}
}
/*
This is a hack to hide the "Reflection" category and "main" module from the sidebar.
This is because:
1. opening any page under the "Reflection" category would hide the entire sidebar.
2. the "main" module would show up twice.
See sidebars.js
*/
.reflection-category,
.theme-doc-sidebar-item-link-level-2 [href="/reference/core/modules/main"],
.theme-doc-sidebar-item-link-level-2
[href="/reference/sveltekit/modules/main"] {
display: none;
}
/*
HACK: to hide the "Classes" header and duplicate items together with the "typedoc-plugin-markdown" patch.
See: https://github.com/TypeStrong/typedoc/issues/2006
*/
/* h3.anchor + p:has(code, strong), */ /** hack did not work as it hides property types elsewhere */
#classes {
display: none;
}

View File

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 99.881 86.449">
<g id="asgardeo-trifactor-logo-dark-16x40" transform="translate(-553.024 -388.98)">
<path id="Path_264" data-name="Path 264" d="M743.533,388.98l9.161,15.892-10.153,17.6h20.306l9.209,15.892H714.97Z" transform="translate(-119.151 0)" fill="#ff7300"/>
<path id="Path_265" data-name="Path 265" d="M705.95,438.364l9.209-15.892h20.306l-10.153-17.6,9.162-15.892,28.6,49.393Z" transform="translate(-152.926 0.009)" fill="#ff7300"/>
<path id="Path_266" data-name="Path 266" d="M749.175,446.183l-10.153-17.6-10.2,17.6H710.46l28.6-49.393,28.515,49.393Z" transform="translate(-136.043 29.246)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 711 B

View File

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 99.881 86.449">
<g id="asgardeo-trifactor-logo-light-16x40" transform="translate(-553.024 -388.98)">
<path id="Path_264" data-name="Path 264" d="M743.533,388.98l9.161,15.892-10.153,17.6h20.306l9.209,15.892H714.97Z" transform="translate(-119.151)" fill="#ff7300"/>
<path id="Path_265" data-name="Path 265" d="M705.95,438.364l9.209-15.892h20.306l-10.153-17.6,9.162-15.892,28.6,49.393Z" transform="translate(-152.926 0.009)" fill="#ff7300"/>
<path id="Path_266" data-name="Path 266" d="M749.175,446.183l-10.153-17.6-10.2,17.6H710.46l28.6-49.393,28.515,49.393Z" transform="translate(-136.043 29.246)" fill="#fff"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 722 B

23
docs/static/img/providers/bankid-no.svg vendored Executable file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="-1.5 -7 36 36" style="enable-background:new 0 0 36 36;" xml:space="preserve">
<style type="text/css">
.st1{fill:#fff;}
</style>
<g>
<g>
<path class="st1" d="M7.5,3h-6C0.7,3,0,2.3,0,1.5v0C0,0.7,0.7,0,1.5,0l6,0C8.3,0,9,0.7,9,1.5v0C9,2.3,8.3,3,7.5,3z"/>
<path class="st1" d="M7.5,15h-6C0.7,15,0,14.3,0,13.5v0C0,12.7,0.7,12,1.5,12h6C8.3,12,9,12.7,9,13.5v0C9,14.3,8.3,15,7.5,15z"/>
<path class="st1" d="M7.5,21h-6C0.7,21,0,20.3,0,19.5v0C0,18.7,0.7,18,1.5,18h6C8.3,18,9,18.7,9,19.5v0C9,20.3,8.3,21,7.5,21z"/>
<path class="st1" d="M19.5,9h-6C12.7,9,12,8.3,12,7.5v0C12,6.7,12.7,6,13.5,6h6C20.3,6,21,6.7,21,7.5v0C21,8.3,20.3,9,19.5,9z"/>
<path class="st1" d="M19.5,15h-6c-0.8,0-1.5-0.7-1.5-1.5v0c0-0.8,0.7-1.5,1.5-1.5h6c0.8,0,1.5,0.7,1.5,1.5v0
C21,14.3,20.3,15,19.5,15z"/>
<path class="st1" d="M31.5,3h-6C24.7,3,24,2.3,24,1.5v0C24,0.7,24.7,0,25.5,0l6,0C32.3,0,33,0.7,33,1.5v0C33,2.3,32.3,3,31.5,3z"
/>
<path class="st1" d="M31.5,9h-6C24.7,9,24,8.3,24,7.5v0C24,6.7,24.7,6,25.5,6h6C32.3,6,33,6.7,33,7.5v0C33,8.3,32.3,9,31.5,9z"/>
<path class="st1" d="M31.5,21h-6c-0.8,0-1.5-0.7-1.5-1.5v0c0-0.8,0.7-1.5,1.5-1.5h6c0.8,0,1.5,0.7,1.5,1.5v0
C33,20.3,32.3,21,31.5,21z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,5 +0,0 @@
<svg width="32" height="32" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>Notion icon</title>
<path d="M6.017 4.313l55.333 -4.087c6.797 -0.583 8.543 -0.19 12.817 2.917l17.663 12.443c2.913 2.14 3.883 2.723 3.883 5.053v68.243c0 4.277 -1.553 6.807 -6.99 7.193L24.467 99.967c-4.08 0.193 -6.023 -0.39 -8.16 -3.113L3.3 79.94c-2.333 -3.113 -3.3 -5.443 -3.3 -8.167V11.113c0 -3.497 1.553 -6.413 6.017 -6.8z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M61.35 0.227l-55.333 4.087C1.553 4.7 0 7.617 0 11.113v60.66c0 2.723 0.967 5.053 3.3 8.167l13.007 16.913c2.137 2.723 4.08 3.307 8.16 3.113l64.257 -3.89c5.433 -0.387 6.99 -2.917 6.99 -7.193V20.64c0 -2.21 -0.873 -2.847 -3.443 -4.733L74.167 3.143c-4.273 -3.107 -6.02 -3.5 -12.817 -2.917zM25.92 19.523c-5.247 0.353 -6.437 0.433 -9.417 -1.99L8.927 11.507c-0.77 -0.78 -0.383 -1.753 1.557 -1.947l53.193 -3.887c4.467 -0.39 6.793 1.167 8.54 2.527l9.123 6.61c0.39 0.197 1.36 1.36 0.193 1.36l-54.933 3.307 -0.68 0.047zM19.803 88.3V30.367c0 -2.53 0.777 -3.697 3.103 -3.893L86 22.78c2.14 -0.193 3.107 1.167 3.107 3.693v57.547c0 2.53 -0.39 4.67 -3.883 4.863l-60.377 3.5c-3.493 0.193 -5.043 -0.97 -5.043 -4.083zm59.6 -54.827c0.387 1.75 0 3.5 -1.75 3.7l-2.91 0.577v42.773c-2.527 1.36 -4.853 2.137 -6.797 2.137 -3.107 0 -3.883 -0.973 -6.21 -3.887l-19.03 -29.94v28.967l6.02 1.363s0 3.5 -4.857 3.5l-13.39 0.777c-0.39 -0.78 0 -2.723 1.357 -3.11l3.497 -0.97v-38.3L30.48 40.667c-0.39 -1.75 0.58 -4.277 3.3 -4.473l14.367 -0.967 19.8 30.327v-26.83l-5.047 -0.58c-0.39 -2.143 1.163 -3.7 3.103 -3.89l13.4 -0.78z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,16 +1,23 @@
{
"excludeNotDocumented": true,
"$schema": "https://typedoc.org/schema.json",
"cleanOutputDir": true,
"allReflectionsHaveOwnDocument": true,
"disableSources": true,
"hideBreadcrumbs": true,
"excludeExternals": true,
"excludeInternal": true,
"excludeNotDocumented": true,
"excludePrivate": true,
"cleanOutputDir": true,
"excludeProtected": true,
"hideHierarchy": true,
"gitRevision": "main",
"hideBreadcrumbs": true,
"hideGenerator": true,
"intentionallyNotExported": [
"ReturnTypes",
"CallbackParameters",
"JsonValue"
],
"readme": "none",
"sort": ["kind", "static-first", "required-first", "alphabetical"],
"kindSortOrder": [
"Function",
"TypeAlias",
@@ -34,13 +41,5 @@
"IndexSignature",
"GetSignature",
"SetSignature"
],
"readme": "none",
"sort": [
"kind",
"static-first",
"required-first",
"alphabetical"
],
"symbolsWithOwnFile": "none"
}
]
}

View File

@@ -18,8 +18,7 @@
"lint": "prettier --check .",
"format": "prettier --write .",
"release": "release",
"version:pr": "node ./config/version-pr",
"e2e": "turbo run e2e --filter=next-auth-app"
"version:pr": "node ./config/version-pr"
},
"devDependencies": {
"@actions/core": "^1.10.0",
@@ -41,12 +40,14 @@
"prettier": "2.8.1",
"prettier-plugin-svelte": "^2.8.1",
"turbo": "1.6.3",
"typedoc": "^0.23.22",
"typedoc-plugin-markdown": "^3.14.0",
"typescript": "4.9.4"
},
"engines": {
"node": "^16.13.0 || ^18.12.0"
},
"packageManager": "pnpm@7.23.0",
"packageManager": "pnpm@7.19.0",
"funding": [
{
"type": "github",
@@ -62,6 +63,7 @@
"undici": "5.11.0"
},
"patchedDependencies": {
"typedoc-plugin-markdown@3.14.0": "patches/typedoc-plugin-markdown@3.14.0.patch",
"@balazsorban/monorepo-release@0.1.8": "patches/@balazsorban__monorepo-release@0.1.8.patch"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@next-auth/dynamodb-adapter",
"repository": "https://github.com/nextauthjs/next-auth",
"version": "3.0.0",
"version": "1.0.6",
"description": "AWS DynamoDB adapter for next-auth.",
"keywords": [
"next-auth",
@@ -9,18 +9,11 @@
"oauth",
"dynamodb"
],
"type": "module",
"types": "./index.d.ts",
"homepage": "https://authjs.dev",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
}
},
"main": "dist/index.js",
"private": false,
"publishConfig": {
"access": "public"
@@ -33,10 +26,7 @@
},
"files": [
"README.md",
"index.js",
"index.d.ts",
"index.d.ts.map",
"src"
"dist"
],
"author": "Pol Marnette",
"license": "ISC",
@@ -51,11 +41,7 @@
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"@shelf/jest-dynamodb": "^2.1.0",
"@types/uuid": "^9.0.0",
"jest": "^27.4.3",
"next-auth": "workspace:*"
},
"dependencies": {
"uuid": "^9.0.0"
}
}

View File

@@ -1,4 +1,4 @@
import { v4 as uuid } from "uuid"
import { randomBytes } from "crypto"
import type {
BatchWriteCommandInput,
@@ -12,42 +12,32 @@ import type {
VerificationToken,
} from "next-auth/adapters"
export interface DynamoDBAdapterOptions {
tableName?: string
partitionKey?: string
sortKey?: string
indexName?: string
indexPartitionKey?: string
indexSortKey?: string
}
import { format, generateUpdateExpression } from "./utils"
export { format, generateUpdateExpression }
export function DynamoDBAdapter(
client: DynamoDBDocument,
options?: DynamoDBAdapterOptions
options?: { tableName: string }
): Adapter {
const TableName = options?.tableName ?? "next-auth"
const pk = options?.partitionKey ?? "pk"
const sk = options?.sortKey ?? "sk"
const IndexName = options?.indexName ?? "GSI1"
const GSI1PK = options?.indexPartitionKey ?? "GSI1PK"
const GSI1SK = options?.indexSortKey ?? "GSI1SK"
return {
async createUser(data) {
const user: AdapterUser = {
...(data as any),
id: uuid(),
id: randomBytes(16).toString("hex"),
}
await client.put({
TableName,
Item: format.to({
...user,
[pk]: `USER#${user.id}`,
[sk]: `USER#${user.id}`,
pk: `USER#${user.id}`,
sk: `USER#${user.id}`,
type: "USER",
[GSI1PK]: `USER#${user.email}`,
[GSI1SK]: `USER#${user.email}`,
GSI1PK: `USER#${user.email as string}`,
GSI1SK: `USER#${user.email as string}`,
}),
})
@@ -57,8 +47,8 @@ export function DynamoDBAdapter(
const data = await client.get({
TableName,
Key: {
[pk]: `USER#${userId}`,
[sk]: `USER#${userId}`,
pk: `USER#${userId}`,
sk: `USER#${userId}`,
},
})
return format.from<AdapterUser>(data.Item)
@@ -66,11 +56,11 @@ export function DynamoDBAdapter(
async getUserByEmail(email) {
const data = await client.query({
TableName,
IndexName,
IndexName: "GSI1",
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
ExpressionAttributeNames: {
"#gsi1pk": GSI1PK,
"#gsi1sk": GSI1SK,
"#gsi1pk": "GSI1PK",
"#gsi1sk": "GSI1SK",
},
ExpressionAttributeValues: {
":gsi1pk": `USER#${email}`,
@@ -83,11 +73,11 @@ export function DynamoDBAdapter(
async getUserByAccount({ provider, providerAccountId }) {
const data = await client.query({
TableName,
IndexName,
IndexName: "GSI1",
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
ExpressionAttributeNames: {
"#gsi1pk": GSI1PK,
"#gsi1sk": GSI1SK,
"#gsi1pk": "GSI1PK",
"#gsi1sk": "GSI1SK",
},
ExpressionAttributeValues: {
":gsi1pk": `ACCOUNT#${provider}`,
@@ -100,8 +90,8 @@ export function DynamoDBAdapter(
const res = await client.get({
TableName,
Key: {
[pk]: `USER#${accounts.userId}`,
[sk]: `USER#${accounts.userId}`,
pk: `USER#${accounts.userId}`,
sk: `USER#${accounts.userId}`,
},
})
return format.from<AdapterUser>(res.Item)
@@ -116,8 +106,8 @@ export function DynamoDBAdapter(
TableName,
Key: {
// next-auth type is incorrect it should be Partial<AdapterUser> & {id: string} instead of just Partial<AdapterUser>
[pk]: `USER#${user.id as string}`,
[sk]: `USER#${user.id as string}`,
pk: `USER#${user.id as string}`,
sk: `USER#${user.id as string}`,
},
UpdateExpression,
ExpressionAttributeNames,
@@ -133,7 +123,7 @@ export function DynamoDBAdapter(
const res = await client.query({
TableName,
KeyConditionExpression: "#pk = :pk",
ExpressionAttributeNames: { "#pk": pk },
ExpressionAttributeNames: { "#pk": "pk" },
ExpressionAttributeValues: { ":pk": `USER#${userId}` },
})
if (!res.Items) return null
@@ -144,8 +134,8 @@ export function DynamoDBAdapter(
return {
DeleteRequest: {
Key: {
[sk]: item.sk,
[pk]: item.pk,
sk: item.sk,
pk: item.pk,
},
},
}
@@ -161,11 +151,11 @@ export function DynamoDBAdapter(
async linkAccount(data) {
const item = {
...data,
id: uuid(),
[pk]: `USER#${data.userId}`,
[sk]: `ACCOUNT#${data.provider}#${data.providerAccountId}`,
[GSI1PK]: `ACCOUNT#${data.provider}`,
[GSI1SK]: `ACCOUNT#${data.providerAccountId}`,
id: randomBytes(16).toString("hex"),
pk: `USER#${data.userId}`,
sk: `ACCOUNT#${data.provider}#${data.providerAccountId}`,
GSI1PK: `ACCOUNT#${data.provider}`,
GSI1SK: `ACCOUNT#${data.providerAccountId}`,
}
await client.put({ TableName, Item: format.to(item) })
return data
@@ -173,11 +163,11 @@ export function DynamoDBAdapter(
async unlinkAccount({ provider, providerAccountId }) {
const data = await client.query({
TableName,
IndexName,
IndexName: "GSI1",
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
ExpressionAttributeNames: {
"#gsi1pk": GSI1PK,
"#gsi1sk": GSI1SK,
"#gsi1pk": "GSI1PK",
"#gsi1sk": "GSI1SK",
},
ExpressionAttributeValues: {
":gsi1pk": `ACCOUNT#${provider}`,
@@ -189,8 +179,8 @@ export function DynamoDBAdapter(
await client.delete({
TableName,
Key: {
[pk]: `USER#${account.userId}`,
[sk]: `ACCOUNT#${provider}#${providerAccountId}`,
pk: `USER#${account.userId}`,
sk: `ACCOUNT#${provider}#${providerAccountId}`,
},
ReturnValues: "ALL_OLD",
})
@@ -199,11 +189,11 @@ export function DynamoDBAdapter(
async getSessionAndUser(sessionToken) {
const data = await client.query({
TableName,
IndexName,
IndexName: "GSI1",
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
ExpressionAttributeNames: {
"#gsi1pk": GSI1PK,
"#gsi1sk": GSI1SK,
"#gsi1pk": "GSI1PK",
"#gsi1sk": "GSI1SK",
},
ExpressionAttributeValues: {
":gsi1pk": `SESSION#${sessionToken}`,
@@ -215,8 +205,8 @@ export function DynamoDBAdapter(
const res = await client.get({
TableName,
Key: {
[pk]: `USER#${session.userId}`,
[sk]: `USER#${session.userId}`,
pk: `USER#${session.userId}`,
sk: `USER#${session.userId}`,
},
})
const user = format.from<AdapterUser>(res.Item)
@@ -225,16 +215,16 @@ export function DynamoDBAdapter(
},
async createSession(data) {
const session = {
id: uuid(),
id: randomBytes(16).toString("hex"),
...data,
}
await client.put({
TableName,
Item: format.to({
[pk]: `USER#${data.userId}`,
[sk]: `SESSION#${data.sessionToken}`,
[GSI1SK]: `SESSION#${data.sessionToken}`,
[GSI1PK]: `SESSION#${data.sessionToken}`,
pk: `USER#${data.userId}`,
sk: `SESSION#${data.sessionToken}`,
GSI1SK: `SESSION#${data.sessionToken}`,
GSI1PK: `SESSION#${data.sessionToken}`,
type: "SESSION",
...data,
}),
@@ -245,11 +235,11 @@ export function DynamoDBAdapter(
const { sessionToken } = session
const data = await client.query({
TableName,
IndexName,
IndexName: "GSI1",
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
ExpressionAttributeNames: {
"#gsi1pk": GSI1PK,
"#gsi1sk": GSI1SK,
"#gsi1pk": "GSI1PK",
"#gsi1sk": "GSI1SK",
},
ExpressionAttributeValues: {
":gsi1pk": `SESSION#${sessionToken}`,
@@ -276,11 +266,11 @@ export function DynamoDBAdapter(
async deleteSession(sessionToken) {
const data = await client.query({
TableName,
IndexName,
IndexName: "GSI1",
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
ExpressionAttributeNames: {
"#gsi1pk": GSI1PK,
"#gsi1sk": GSI1SK,
"#gsi1pk": "GSI1PK",
"#gsi1sk": "GSI1SK",
},
ExpressionAttributeValues: {
":gsi1pk": `SESSION#${sessionToken}`,
@@ -302,8 +292,8 @@ export function DynamoDBAdapter(
await client.put({
TableName,
Item: format.to({
[pk]: `VT#${data.identifier}`,
[sk]: `VT#${data.token}`,
pk: `VT#${data.identifier}`,
sk: `VT#${data.token}`,
type: "VT",
...data,
}),
@@ -314,8 +304,8 @@ export function DynamoDBAdapter(
const data = await client.delete({
TableName,
Key: {
[pk]: `VT#${identifier}`,
[sk]: `VT#${token}`,
pk: `VT#${identifier}`,
sk: `VT#${token}`,
},
ReturnValues: "ALL_OLD",
})
@@ -323,73 +313,3 @@ export function DynamoDBAdapter(
},
}
}
// https://github.com/honeinc/is-iso-date/blob/master/index.js
const isoDateRE =
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
function isDate(value: any) {
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
}
const format = {
/** Takes a plain old JavaScript object and turns it into a Dynamodb object */
to(object: Record<string, any>) {
const newObject: Record<string, unknown> = {}
for (const key in object) {
const value = object[key]
if (value instanceof Date) {
// DynamoDB requires the TTL attribute be a UNIX timestamp (in secs).
if (key === "expires") newObject[key] = value.getTime() / 1000
else newObject[key] = value.toISOString()
} else newObject[key] = value
}
return newObject
},
/** Takes a Dynamo object and returns a plain old JavaScript object */
from<T = Record<string, unknown>>(object?: Record<string, any>): T | null {
if (!object) return null
const newObject: Record<string, unknown> = {}
for (const key in object) {
// Filter DynamoDB specific attributes so it doesn't get passed to core,
// to avoid revealing the type of database
if (["pk", "sk", "GSI1PK", "GSI1SK"].includes(key)) continue
const value = object[key]
if (isDate(value)) newObject[key] = new Date(value)
// hack to keep type property in account
else if (key === "type" && ["SESSION", "VT", "USER"].includes(value))
continue
// The expires property is stored as a UNIX timestamp in seconds, but
// JavaScript needs it in milliseconds, so multiply by 1000.
else if (key === "expires" && typeof value === "number")
newObject[key] = new Date(value * 1000)
else newObject[key] = value
}
return newObject as T
},
}
function generateUpdateExpression(object: Record<string, any>): {
UpdateExpression: string
ExpressionAttributeNames: Record<string, string>
ExpressionAttributeValues: Record<string, unknown>
} {
const formatedSession = format.to(object)
let UpdateExpression = "set"
const ExpressionAttributeNames: Record<string, string> = {}
const ExpressionAttributeValues: Record<string, unknown> = {}
for (const property in formatedSession) {
UpdateExpression += ` #${property} = :${property},`
ExpressionAttributeNames["#" + property] = property
ExpressionAttributeValues[":" + property] = formatedSession[property]
}
UpdateExpression = UpdateExpression.slice(0, -1)
return {
UpdateExpression,
ExpressionAttributeNames,
ExpressionAttributeValues,
}
}
export { format, generateUpdateExpression }

View File

@@ -0,0 +1,67 @@
// https://github.com/honeinc/is-iso-date/blob/master/index.js
const isoDateRE =
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
function isDate(value: any) {
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
}
export const format = {
/** Takes a plain old JavaScript object and turns it into a Dynamodb object */
to(object: Record<string, any>) {
const newObject: Record<string, unknown> = {}
for (const key in object) {
const value = object[key]
if (value instanceof Date) {
// DynamoDB requires the TTL attribute be a UNIX timestamp (in secs).
if (key === "expires") newObject[key] = value.getTime() / 1000
else newObject[key] = value.toISOString()
} else newObject[key] = value
}
return newObject
},
/** Takes a Dynamo object and returns a plain old JavaScript object */
from<T = Record<string, unknown>>(object?: Record<string, any>): T | null {
if (!object) return null
const newObject: Record<string, unknown> = {}
for (const key in object) {
// Filter DynamoDB specific attributes so it doesn't get passed to core,
// to avoid revealing the type of database
if (["pk", "sk", "GSI1PK", "GSI1SK"].includes(key)) continue
const value = object[key]
if (isDate(value)) newObject[key] = new Date(value)
// hack to keep type property in account
else if (key === "type" && ["SESSION", "VT", "USER"].includes(value))
continue
// The expires property is stored as a UNIX timestamp in seconds, but
// JavaScript needs it in milliseconds, so multiply by 1000.
else if (key === "expires" && typeof value === "number")
newObject[key] = new Date(value * 1000)
else newObject[key] = value
}
return newObject as T
},
}
export function generateUpdateExpression(object: Record<string, any>): {
UpdateExpression: string
ExpressionAttributeNames: Record<string, string>
ExpressionAttributeValues: Record<string, unknown>
} {
const formatedSession = format.to(object)
let UpdateExpression = "set"
const ExpressionAttributeNames: Record<string, string> = {}
const ExpressionAttributeValues: Record<string, unknown> = {}
for (const property in formatedSession) {
UpdateExpression += ` #${property} = :${property},`
ExpressionAttributeNames["#" + property] = property
ExpressionAttributeValues[":" + property] = formatedSession[property]
}
UpdateExpression = UpdateExpression.slice(0, -1)
return {
UpdateExpression,
ExpressionAttributeNames,
ExpressionAttributeValues,
}
}

View File

@@ -1,4 +1,4 @@
import { format } from "../src/"
import { format } from "../src/utils"
describe("dynamodb utils.format", () => {
it("format.to() preserves non-Date non-expires properties", () => {

View File

@@ -2,15 +2,7 @@
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"compilerOptions": {
"rootDir": "src",
"outDir": ".",
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"skipDefaultLibCheck": true,
"strictNullChecks": true,
"stripInternal": true,
"declarationMap": true,
"declaration": true
"outDir": "dist"
},
"exclude": ["tests", "dist", "jest.config.js", "jest-dynamodb-config.js"]
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,16 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.3](https://github.com/nextauthjs/adapters/compare/@next-auth/firebase-adapter@0.1.2...@next-auth/firebase-adapter@0.1.3) (2021-08-17)
**Note:** Version bump only for package @next-auth/firebase-adapter
## [0.1.2](https://github.com/nextauthjs/adapters/compare/@next-auth/firebase-adapter@0.1.1...@next-auth/firebase-adapter@0.1.2) (2021-07-02)
**Note:** Version bump only for package @next-auth/firebase-adapter
## [0.1.1](https://github.com/nextauthjs/adapters/compare/@next-auth/firebase-adapter@0.1.0...@next-auth/firebase-adapter@0.1.1) (2021-06-30)
**Note:** Version bump only for package @next-auth/firebase-adapter

View File

@@ -1,8 +1,8 @@
<p align="center">
<br/>
<a href="https://authjs.dev" target="_blank">
<img height="64px" src="https://authjs.dev/img/logo/logo-sm.png" /></a><img height="64px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/adapter-firebase/logo.svg" />
<h3 align="center"><b>Firebase Adapter</b> - Auth.js</h3>
<img height="64px" src="https://authjs.dev/img/logo/logo-sm.png" /></a><img height="64px" src="https://raw.githubusercontent.com/nextauthjs/adapters/main/packages/firebase/logo.svg" />
<h3 align="center"><b>Firebase Adapter</b> - NextAuth.js</h3>
<p align="center">
Open Source. Full Stack. Own Your Data.
</p>
@@ -13,12 +13,72 @@
</p>
</p>
## Overview
This is the official Firebase Adapter for [Auth.js](https://authjs.dev) / [NextAuth.js](https://next-auth.js.org/), using the [Firebase Admin SDK](https://firebase.google.com/docs/admin/setup) and [Firestore](https://firebase.google.com/docs/firestore).
This is the Firebase Adapter for [`auth.js`](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
## Documentation
You can find more Firebase information in the docs at [authjs.dev/reference/adapters/firebase](https://authjs.dev/reference/adapters/firebase).
Check out the [documentation](https://authjs.dev/reference/adapter/firebase) to learn how to use this adapter in your project.
## Getting Started
1. Install `next-auth` and `@next-auth/firebase-adapter`.
```js
npm install next-auth @next-auth/firebase-adapter
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
```js
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import { FirestoreAdapter } from "@next-auth/firebase-adapter"
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore"
const app = initializeApp({ projectId: "next-auth-test" });
const firestore = getFirestore(app);
// For more information on each option (and a full list of options) go to
// https://authjs.dev/reference/configuration/auth-options
export default NextAuth({
// https://authjs.dev/reference/providers/oauth-builtin
providers: [
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
adapter: FirestoreAdapter(firestore),
...
})
```
## Options
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
An example firebase config looks like this:
```js
const firebaseConfig = {
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
authDomain: "myapp-project-123.firebaseapp.com",
databaseURL: "https://myapp-project-123.firebaseio.com",
projectId: "myapp-project-123",
storageBucket: "myapp-project-123.appspot.com",
messagingSenderId: "65211879809",
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
measurementId: "G-8GSGZQ44ST",
}
```
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
> **From Firebase - Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
>
> For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
## Contributing

View File

@@ -1,8 +1,5 @@
{
"firestore": {
"rules": "firestore.rules"
},
"emulator": {
"emulators": {
"firestore": {
"port": 8080
}

View File

@@ -1,10 +0,0 @@
rules_version = '2';
// Deny read/write access to all users under any conditions
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}

View File

@@ -0,0 +1 @@
module.exports = require("@next-auth/adapter-test/jest/jest-preset")

View File

@@ -33,4 +33,4 @@
<circle cx="144" cy="144" r="40" fill="#757575"/>
<path d="M144 146l-18 8v-8l18-8 18 8v7-1.5 2.5zm0-22l18 8v8l-18-8-18 8v-8zm6.75 29l9 4-15.75 7v-8z" fill="#fff" fill-rule="evenodd"/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/firebase-adapter",
"version": "2.0.0",
"version": "1.0.3",
"description": "Firebase adapter for next-auth.",
"homepage": "https://authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -12,44 +12,35 @@
"Nico Domino <yo@ndo.dev>",
"Alex Meuer <github@alexmeuer.com>"
],
"type": "module",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
}
},
"main": "dist/index.js",
"files": [
"src",
"*.js",
"*.d.ts*"
"dist",
"index.d.ts"
],
"license": "ISC",
"keywords": [
"next-auth",
"next.js",
"firebase",
"firebase-admin"
"firebase"
],
"private": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "tsc -w",
"build": "tsc",
"test": "firebase emulators:exec --only firestore --project next-auth-test 'jest -c tests/jest.config.js'"
"test": "FIRESTORE_EMULATOR_HOST=localhost:8080 firebase --token '$FIREBASE_TOKEN' emulators:exec --only firestore --project next-auth-test jest"
},
"peerDependencies": {
"firebase-admin": "^11.4.1",
"firebase": "^9.7.0",
"next-auth": "^4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"firebase-admin": "^11.4.1",
"firebase": "^9.14.0",
"firebase-tools": "^11.16.1",
"jest": "^29.3.1",
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -0,0 +1,58 @@
import { Timestamp } from "firebase/firestore"
import type {
FirestoreDataConverter,
QueryDocumentSnapshot,
WithFieldValue,
} from "firebase/firestore"
const isTimestamp = (value: unknown): value is Timestamp =>
typeof value === "object" && value !== null && value instanceof Timestamp
interface GetConverterOptions {
excludeId?: boolean
}
export const getConverter = <Document extends Record<string, unknown>>(
options?: GetConverterOptions
): FirestoreDataConverter<Document> => ({
// `PartialWithFieldValue` implicitly types `object` as `any`, so we want to explicitly type it
toFirestore(object: WithFieldValue<Document>) {
const document: Record<string, unknown> = {}
Object.keys(object).forEach((key) => {
if (object[key] !== undefined) {
document[key] = object[key]
}
})
return document
},
// We need to explicitly type `snapshot` since it uses `DocumentData` for generic type
fromFirestore(snapshot: QueryDocumentSnapshot<Document>) {
if (!snapshot.exists()) {
return snapshot
}
let document: Document = snapshot.data()
if (!options?.excludeId) {
document = {
...document,
id: snapshot.id,
}
}
for (const key in document) {
const value = document[key]
if (isTimestamp(value)) {
document = {
...document,
[key]: value.toDate(),
}
}
}
return document
},
})

View File

@@ -1,302 +1,282 @@
/**
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
* <span>
* Official <b>Firebase</b> adapter for Auth.js / NextAuth.js,
* using the <a href="https://firebase.google.com/docs/admin/setup">Firebase Admin SDK</a>
* &nbsp;and <a href="https://firebase.google.com/docs/firestore">Firestore</a>.</span>
* <a href="https://firebase.google.com/">
* <img style={{display: "block"}} src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/adapter-firebase/logo.svg" height="48" width="48"/>
* </a>
* </div>
*
* ## Installation
*
* ```bash npm2yarn2pnpm
* npm install next-auth @next-auth/firebase-admin-adapter firebase-admin
* ```
*
* ## References
* - [`GOOGLE_APPLICATION_CREDENTIALS` environment variable](https://cloud.google.com/docs/authentication/application-default-credentials#GAC)
* - [Firebase Admin SDK setup](https://firebase.google.com/docs/admin/setup#initialize-sdk)
*
* @module @next-auth/firebase-adapter
*/
import { type AppOptions } from "firebase-admin"
import { Firestore } from "firebase-admin/firestore"
import type { Adapter, AdapterUser } from "next-auth/adapters"
import { initializeApp } from "firebase/app"
import type { FirebaseOptions } from "firebase/app"
import {
collestionsFactory,
deleteDocs,
initFirestore,
addDoc,
collection,
deleteDoc,
doc,
getDoc,
getOneDoc,
mapFieldsFactory,
} from "./utils"
getDocs,
getFirestore,
limit,
query,
runTransaction,
setDoc,
where,
connectFirestoreEmulator,
} from "firebase/firestore"
export { initFirestore } from "./utils"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"
/** Configure the Firebase Adapter. */
export interface FirebaseAdapterConfig extends AppOptions {
/**
* The name of the app passed to {@link https://firebase.google.com/docs/reference/admin/node/firebase-admin.md#initializeapp `initializeApp()`}.
*/
name?: string
firestore?: Firestore
/**
* Use this option if mixed `snake_case` and `camelCase` field names in the database is an issue for you.
* Passing `snake_case` will convert all field and collection names to `snake_case`.
* E.g. the collection `verificationTokens` will be `verification_tokens`,
* and fields like `emailVerified` will be `email_verified` instead.
*
*
* @example
* ```ts title="pages/api/auth/[...nextauth].ts"
* import NextAuth from "next-auth"
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
*
* export default NextAuth({
* adapter: FirestoreAdapter({ namingStrategy: "snake_case" })
* // ...
* })
* ```
*/
namingStrategy?: "snake_case"
import { getConverter } from "./converter"
export type IndexableObject = Record<string, unknown>
export interface FirestoreAdapterOptions {
emulator?: {
host?: string
port?: number
}
}
/**
* #### Usage
*
* First, create a Firebase project and generate a service account key.
* Visit: `https://console.firebase.google.com/u/0/project/{project-id}/settings/serviceaccounts/adminsdk` (replace `{project-id}` with your project's id)
*
* Now you have a few options to authenticate with the Firebase Admin SDK in your app:
*
* ##### 1. `GOOGLE_APPLICATION_CREDENTIALS` environment variable:
* - Download the service account key and save it in your project. (Make sure to add the file to your `.gitignore`!)
* - Add [`GOOGLE_APPLICATION_CREDENTIALS`](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) to your environment variables and point it to the service account key file.
* - The adapter will automatically pick up the environment variable and use it to authenticate with the Firebase Admin SDK.
*
* @example
* ```ts title="pages/api/auth/[...nextauth].ts"
* import NextAuth from "next-auth"
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
*
* export default NextAuth({
* adapter: FirestoreAdapter(),
* // ...
* })
* ```
*
* ##### 2. Service account values as environment variables
*
* - Download the service account key to a temporary location. (Make sure to not commit this file to your repository!)
* - Add the following environment variables to your project: `FIREBASE_PROJECT_ID`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_PRIVATE_KEY`.
* - Pass the config to the adapter, using the environment variables as shown in the example below.
*
* @example
* ```ts title="pages/api/auth/[...nextauth].ts"
* import NextAuth from "next-auth"
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
* import { cert } from "firebase-admin/app"
*
* export default NextAuth({
* adapter: FirestoreAdapter({
* credential: cert({
* projectId: process.env.FIREBASE_PROJECT_ID,
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
* })
* })
* // ...
* })
* ```
*
* ##### 3. Use an existing Firestore instance
*
* If you already have a Firestore instance, you can pass that to the adapter directly instead.
*
* :::note
* When passing an instance and in a serverless environment, remember to handle duplicate app initialization.
* :::
*
* :::tip
* You can use the {@link initFirestore} utility to initialize the app and get an instance safely.
* :::
*
* @example
* ```ts title="pages/api/auth/[...nextauth].ts"
* import NextAuth from "next-auth"
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
* import { firestore } from "lib/firestore"
*
* export default NextAuth({
* adapter: FirestoreAdapter(firestore),
* // ...
* })
* ```
*/
export function FirestoreAdapter(
config?: FirebaseAdapterConfig | Firestore
): Adapter {
const { db, namingStrategy = "default" } =
config instanceof Firestore
? { db: config }
: { ...config, db: config?.firestore ?? initFirestore(config) }
export function FirestoreAdapter({
emulator,
...firebaseOptions
}: FirebaseOptions & FirestoreAdapterOptions): Adapter {
const firebaseApp = initializeApp(firebaseOptions)
const db = getFirestore(firebaseApp)
const preferSnakeCase = namingStrategy === "snake_case"
const C = collestionsFactory(db, preferSnakeCase)
const mapper = mapFieldsFactory(preferSnakeCase)
if (emulator) {
connectFirestoreEmulator(
db,
emulator?.host ?? "localhost",
emulator?.port ?? 3001
)
}
const Users = collection(db, "users").withConverter(
getConverter<AdapterUser & IndexableObject>()
)
const Sessions = collection(db, "sessions").withConverter(
getConverter<AdapterSession & IndexableObject>()
)
const Accounts = collection(db, "accounts").withConverter(
getConverter<AdapterAccount>()
)
const VerificationTokens = collection(db, "verificationTokens").withConverter(
getConverter<VerificationToken & IndexableObject>({ excludeId: true })
)
return {
async createUser(userInit) {
const { id: userId } = await C.users.add(userInit as AdapterUser)
async createUser(newUser) {
const userRef = await addDoc(Users, newUser)
const userSnapshot = await getDoc(userRef)
const user = await getDoc(C.users.doc(userId))
if (!user) throw new Error("[createUser] Failed to fetch created user")
if (userSnapshot.exists() && Users.converter) {
return Users.converter.fromFirestore(userSnapshot)
}
return user
throw new Error("[createUser] Failed to create user")
},
async getUser(id) {
return await getDoc(C.users.doc(id))
},
const userSnapshot = await getDoc(doc(Users, id))
if (userSnapshot.exists() && Users.converter) {
return Users.converter.fromFirestore(userSnapshot)
}
return null
},
async getUserByEmail(email) {
return await getOneDoc(C.users.where("email", "==", email))
const userQuery = query(Users, where("email", "==", email), limit(1))
const userSnapshots = await getDocs(userQuery)
const userSnapshot = userSnapshots.docs[0]
if (userSnapshot?.exists() && Users.converter) {
return Users.converter.fromFirestore(userSnapshot)
}
return null
},
async getUserByAccount({ provider, providerAccountId }) {
const account = await getOneDoc(
C.accounts
.where("provider", "==", provider)
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
const accountQuery = query(
Accounts,
where("provider", "==", provider),
where("providerAccountId", "==", providerAccountId),
limit(1)
)
if (!account) return null
const accountSnapshots = await getDocs(accountQuery)
const accountSnapshot = accountSnapshots.docs[0]
return await getDoc(C.users.doc(account.userId))
if (accountSnapshot?.exists()) {
const { userId } = accountSnapshot.data()
const userDoc = await getDoc(doc(Users, userId))
if (userDoc.exists() && Users.converter) {
return Users.converter.fromFirestore(userDoc)
}
}
return null
},
async updateUser(partialUser) {
if (!partialUser.id) throw new Error("[updateUser] Missing id")
const userRef = doc(Users, partialUser.id)
const userRef = C.users.doc(partialUser.id)
await setDoc(userRef, partialUser, { merge: true })
await userRef.set(partialUser, { merge: true })
const userSnapshot = await getDoc(userRef)
const user = await getDoc(userRef)
if (!user) throw new Error("[updateUser] Failed to fetch updated user")
if (userSnapshot.exists() && Users.converter) {
return Users.converter.fromFirestore(userSnapshot)
}
return user
throw new Error("[updateUser] Failed to update user")
},
async deleteUser(userId) {
await db.runTransaction(async (transaction) => {
const accounts = await C.accounts
.where(mapper.toDb("userId"), "==", userId)
.get()
const sessions = await C.sessions
.where(mapper.toDb("userId"), "==", userId)
.get()
const userRef = doc(Users, userId)
const accountsQuery = query(Accounts, where("userId", "==", userId))
const sessionsQuery = query(Sessions, where("userId", "==", userId))
transaction.delete(C.users.doc(userId))
// TODO: May be better to use events instead of transactions?
await runTransaction(db, async (transaction) => {
const accounts = await getDocs(accountsQuery)
const sessions = await getDocs(sessionsQuery)
transaction.delete(userRef)
accounts.forEach((account) => transaction.delete(account.ref))
sessions.forEach((session) => transaction.delete(session.ref))
})
},
async linkAccount(accountInit) {
const ref = await C.accounts.add(accountInit)
const account = await ref.get().then((doc) => doc.data())
return account ?? null
async linkAccount(account) {
const accountRef = await addDoc(Accounts, account)
const accountSnapshot = await getDoc(accountRef)
if (accountSnapshot.exists() && Accounts.converter) {
return Accounts.converter.fromFirestore(accountSnapshot)
}
},
async unlinkAccount({ provider, providerAccountId }) {
await deleteDocs(
C.accounts
.where("provider", "==", provider)
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
.limit(1)
const accountQuery = query(
Accounts,
where("provider", "==", provider),
where("providerAccountId", "==", providerAccountId),
limit(1)
)
const accountSnapshots = await getDocs(accountQuery)
const accountSnapshot = accountSnapshots.docs[0]
if (accountSnapshot?.exists()) {
await deleteDoc(accountSnapshot.ref)
}
},
async createSession(sessionInit) {
const ref = await C.sessions.add(sessionInit)
const session = await ref.get().then((doc) => doc.data())
async createSession(session) {
const sessionRef = await addDoc(Sessions, session)
const sessionSnapshot = await getDoc(sessionRef)
if (session) return session ?? null
if (sessionSnapshot.exists() && Sessions.converter) {
return Sessions.converter.fromFirestore(sessionSnapshot)
}
throw new Error("[createSession] Failed to fetch created session")
throw new Error("[createSession] Failed to create session")
},
async getSessionAndUser(sessionToken) {
const session = await getOneDoc(
C.sessions.where(mapper.toDb("sessionToken"), "==", sessionToken)
const sessionQuery = query(
Sessions,
where("sessionToken", "==", sessionToken),
limit(1)
)
if (!session) return null
const sessionSnapshots = await getDocs(sessionQuery)
const sessionSnapshot = sessionSnapshots.docs[0]
const user = await getDoc(C.users.doc(session.userId))
if (!user) return null
if (sessionSnapshot?.exists() && Sessions.converter) {
const session = Sessions.converter.fromFirestore(sessionSnapshot)
const userDoc = await getDoc(doc(Users, session.userId))
return { session, user }
if (userDoc.exists() && Users.converter) {
const user = Users.converter.fromFirestore(userDoc)
return { session, user }
}
}
return null
},
async updateSession(partialSession) {
const sessionId = await db.runTransaction(async (transaction) => {
const sessionSnapshot = (
await transaction.get(
C.sessions
.where(
mapper.toDb("sessionToken"),
"==",
partialSession.sessionToken
)
.limit(1)
)
).docs[0]
if (!sessionSnapshot?.exists) return null
const sessionQuery = query(
Sessions,
where("sessionToken", "==", partialSession.sessionToken),
limit(1)
)
const sessionSnapshots = await getDocs(sessionQuery)
const sessionSnapshot = sessionSnapshots.docs[0]
transaction.set(sessionSnapshot.ref, partialSession, { merge: true })
if (sessionSnapshot?.exists()) {
await setDoc(sessionSnapshot.ref, partialSession, { merge: true })
return sessionSnapshot.id
})
const sessionDoc = await getDoc(sessionSnapshot.ref)
if (!sessionId) return null
if (sessionDoc?.exists() && Sessions.converter) {
const session = Sessions.converter.fromFirestore(sessionDoc)
const session = await getDoc(C.sessions.doc(sessionId))
if (session) return session
throw new Error("[updateSession] Failed to fetch updated session")
return session
}
}
return null
},
async deleteSession(sessionToken) {
await deleteDocs(
C.sessions
.where(mapper.toDb("sessionToken"), "==", sessionToken)
.limit(1)
const sessionQuery = query(
Sessions,
where("sessionToken", "==", sessionToken),
limit(1)
)
const sessionSnapshots = await getDocs(sessionQuery)
const sessionSnapshot = sessionSnapshots.docs[0]
if (sessionSnapshot?.exists()) {
await deleteDoc(sessionSnapshot.ref)
}
},
async createVerificationToken(verificationToken) {
await C.verification_tokens.add(verificationToken)
return verificationToken
const verificationTokenRef = await addDoc(
VerificationTokens,
verificationToken
)
const verificationTokenSnapshot = await getDoc(verificationTokenRef)
if (verificationTokenSnapshot.exists() && VerificationTokens.converter) {
const { id, ...verificationToken } =
VerificationTokens.converter.fromFirestore(verificationTokenSnapshot)
return verificationToken
}
},
async useVerificationToken({ identifier, token }) {
const verificationTokenSnapshot = (
await C.verification_tokens
.where("identifier", "==", identifier)
.where("token", "==", token)
.limit(1)
.get()
).docs[0]
const verificationTokensQuery = query(
VerificationTokens,
where("identifier", "==", identifier),
where("token", "==", token),
limit(1)
)
const verificationTokenSnapshots = await getDocs(verificationTokensQuery)
const verificationTokenSnapshot = verificationTokenSnapshots.docs[0]
if (!verificationTokenSnapshot) return null
if (verificationTokenSnapshot?.exists() && VerificationTokens.converter) {
await deleteDoc(verificationTokenSnapshot.ref)
const data = verificationTokenSnapshot.data()
await verificationTokenSnapshot.ref.delete()
return data
const { id, ...verificationToken } =
VerificationTokens.converter.fromFirestore(verificationTokenSnapshot)
return verificationToken
}
return null
},
}
}

View File

@@ -1,168 +0,0 @@
import { AppOptions, getApps, initializeApp } from "firebase-admin/app"
import {
getFirestore,
initializeFirestore,
Timestamp,
} from "firebase-admin/firestore"
import type {
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"
import { FirebaseAdapterConfig } from "."
// for consistency, store all fields as snake_case in the database
const MAP_TO_FIRESTORE: Record<string, string | undefined> = {
userId: "user_id",
sessionToken: "session_token",
providerAccountId: "provider_account_id",
emailVerified: "email_verified",
}
const MAP_FROM_FIRESTORE: Record<string, string | undefined> = {}
for (const key in MAP_TO_FIRESTORE) {
MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]!] = key
}
const identity = <T>(x: T) => x
/** @internal */
export function mapFieldsFactory(preferSnakeCase?: boolean) {
if (preferSnakeCase) {
return {
toDb: (field: string) => MAP_TO_FIRESTORE[field] ?? field,
fromDb: (field: string) => MAP_FROM_FIRESTORE[field] ?? field,
}
}
return { toDb: identity, fromDb: identity }
}
/** @internal */
export function getConverter<Document extends Record<string, any>>(options: {
excludeId?: boolean
preferSnakeCase?: boolean
}): FirebaseFirestore.FirestoreDataConverter<Document> {
const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false)
return {
toFirestore(object) {
const document: Record<string, unknown> = {}
for (const key in object) {
if (key === "id") continue
const value = object[key]
if (value !== undefined) {
document[mapper.toDb(key)] = value
} else {
console.warn(`FirebaseAdapter: value for key "${key}" is undefined`)
}
}
return document
},
fromFirestore(
snapshot: FirebaseFirestore.QueryDocumentSnapshot<Document>
): Document {
const document = snapshot.data()! // we can guarentee it exists
const object: Record<string, unknown> = {}
if (!options?.excludeId) {
object.id = snapshot.id
}
for (const key in document) {
let value: any = document[key]
if (value instanceof Timestamp) value = value.toDate()
object[mapper.fromDb(key)] = value
}
return object as Document
},
}
}
/** @internal */
export async function getOneDoc<T>(
querySnapshot: FirebaseFirestore.Query<T>
): Promise<T | null> {
const querySnap = await querySnapshot.limit(1).get()
return querySnap.docs[0]?.data() ?? null
}
/** @internal */
export async function deleteDocs<T>(
querySnapshot: FirebaseFirestore.Query<T>
): Promise<void> {
const querySnap = await querySnapshot.get()
for (const doc of querySnap.docs) {
await doc.ref.delete()
}
}
/** @internal */
export async function getDoc<T>(
docRef: FirebaseFirestore.DocumentReference<T>
): Promise<T | null> {
const docSnap = await docRef.get()
return docSnap.data() ?? null
}
/** @internal */
export function collestionsFactory(
db: FirebaseFirestore.Firestore,
preferSnakeCase = false
) {
return {
users: db
.collection("users")
.withConverter(getConverter<AdapterUser>({ preferSnakeCase })),
sessions: db
.collection("sessions")
.withConverter(getConverter<AdapterSession>({ preferSnakeCase })),
accounts: db
.collection("accounts")
.withConverter(getConverter<AdapterAccount>({ preferSnakeCase })),
verification_tokens: db
.collection(
preferSnakeCase ? "verification_tokens" : "verificationTokens"
)
.withConverter(
getConverter<VerificationToken>({ preferSnakeCase, excludeId: true })
),
}
}
/**
* Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
* If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
*
* @example
* ```ts title="lib/firestore.ts"
* import { initFirestore } from "@next-auth/firebase-adapter"
* import { cert } from "firebase-admin/app"
*
* export const firestore = initFirestore({
* credential: cert({
* projectId: process.env.FIREBASE_PROJECT_ID,
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
* })
* })
* ```
*/
export function initFirestore(
options: AppOptions & { name?: FirebaseAdapterConfig["name"] } = {}
) {
const apps = getApps()
const app = options.name ? apps.find((a) => a.name === options.name) : apps[0]
if (app) return getFirestore(app)
return initializeFirestore(initializeApp(options, options.name))
}

View File

@@ -1,57 +1,118 @@
import { runBasicTests } from "@next-auth/adapter-test"
import { FirestoreAdapter } from "../src"
import { FirestoreAdapter, type FirebaseAdapterConfig } from "../src"
import {
collestionsFactory,
initFirestore,
getFirestore,
connectFirestoreEmulator,
terminate,
collection,
query,
where,
limit,
getDocs,
getDoc,
getOneDoc,
mapFieldsFactory,
} from "../src/utils"
doc,
} from "firebase/firestore"
import { initializeApp } from "firebase/app"
import { getConverter } from "../src/converter"
import type {
AdapterSession,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
import type { Account } from "next-auth"
describe.each([
{ namingStrategy: "snake_case" },
{ namingStrategy: "default" },
] as Partial<FirebaseAdapterConfig>[])(
"FirebaseAdapter with config: %s",
(config) => {
config.name = `next-auth-test-${config.namingStrategy}`
config.projectId = "next-auth-test"
config.databaseURL = "http://localhost:8080"
const app = initializeApp({ projectId: "next-auth-test" })
const firestore = getFirestore(app)
const db = initFirestore(config)
const preferSnakeCase = config.namingStrategy === "snake_case"
const mapper = mapFieldsFactory(preferSnakeCase)
const C = collestionsFactory(db, preferSnakeCase)
connectFirestoreEmulator(firestore, "localhost", 8080)
for (const [name, collection] of Object.entries(C)) {
test(`collection "${name}" should be empty`, async () => {
expect((await collection.count().get()).data().count).toBe(0)
})
}
type IndexableObject = Record<string, unknown>
runBasicTests({
adapter: FirestoreAdapter(config),
db: {
disconnect: async () => await db.terminate(),
session: (sessionToken) =>
getOneDoc(
C.sessions.where(mapper.toDb("sessionToken"), "==", sessionToken)
),
user: (userId) => getDoc(C.users.doc(userId)),
account: ({ provider, providerAccountId }) =>
getOneDoc(
C.accounts
.where("provider", "==", provider)
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
),
verificationToken: ({ identifier, token }) =>
getOneDoc(
C.verification_tokens
.where("identifier", "==", identifier)
.where("token", "==", token)
),
},
})
}
const Users = collection(firestore, "users").withConverter(
getConverter<AdapterUser & IndexableObject>()
)
const Sessions = collection(firestore, "sessions").withConverter(
getConverter<AdapterSession & IndexableObject>()
)
const Accounts = collection(firestore, "accounts").withConverter(
getConverter<Account>()
)
const VerificationTokens = collection(
firestore,
"verificationTokens"
).withConverter(
getConverter<VerificationToken & IndexableObject>({ excludeId: true })
)
runBasicTests({
adapter: FirestoreAdapter({ projectId: "next-auth-test" }),
db: {
async disconnect() {
await terminate(firestore)
},
async session(sessionToken) {
const snapshotQuery = query(
Sessions,
where("sessionToken", "==", sessionToken),
limit(1)
)
const snapshots = await getDocs(snapshotQuery)
const snapshot = snapshots.docs[0]
if (snapshot?.exists() && Sessions.converter) {
const session = Sessions.converter.fromFirestore(snapshot)
return session
}
return null
},
async user(id) {
const snapshot = await getDoc(doc(Users, id))
if (snapshot?.exists() && Users.converter) {
const user = Users.converter.fromFirestore(snapshot)
return user
}
return null
},
async account({ provider, providerAccountId }) {
const snapshotQuery = query(
Accounts,
where("provider", "==", provider),
where("providerAccountId", "==", providerAccountId),
limit(1)
)
const snapshots = await getDocs(snapshotQuery)
const snapshot = snapshots.docs[0]
if (snapshot?.exists() && Accounts.converter) {
const account = Accounts.converter.fromFirestore(snapshot)
return account
}
return null
},
async verificationToken({ identifier, token }) {
const snapshotQuery = query(
VerificationTokens,
where("identifier", "==", identifier),
where("token", "==", token),
limit(1)
)
const snapshots = await getDocs(snapshotQuery)
const snapshot = snapshots.docs[0]
if (snapshot?.exists() && VerificationTokens.converter) {
const verificationToken =
VerificationTokens.converter.fromFirestore(snapshot)
return verificationToken
}
},
},
})

View File

@@ -1,11 +0,0 @@
import config from "@next-auth/adapter-test/jest/jest-preset.js"
//TODO: update rest of the packages to Jest 29+
const {testURL, ...rest} = config
export default {
...rest,
testEnvironmentOptions: {
url: testURL
},
rootDir: ".."
}

View File

@@ -1,23 +1,11 @@
{
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"strict": true,
"noUncheckedIndexedAccess": true,
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"outDir": ".",
"rootDir": "src",
"skipDefaultLibCheck": true,
"strictNullChecks": true,
"stripInternal": true,
"declarationMap": true,
"declaration": true
"moduleResolution": "node"
},
"include": [
"src/**/*"
],
"exclude": [
"tests"
]
}
"exclude": ["tests", "dist", "jest.config.js"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/xata-adapter",
"version": "0.2.2",
"version": "0.2.0",
"description": "Xata adapter for next-auth.",
"homepage": "https://authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -43,4 +43,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@auth/core",
"version": "0.4.0",
"version": "0.2.5",
"description": "Authentication for the Web.",
"keywords": [
"authentication",
@@ -27,7 +27,7 @@
"types": "./index.d.ts",
"files": [
"*.js",
"*.d.ts*",
"*.d.ts",
"lib",
"providers",
"src"
@@ -69,7 +69,7 @@
"preact-render-to-string": "5.2.3"
},
"peerDependencies": {
"nodemailer": "^6.8.0"
"nodemailer": "6.8.0"
},
"peerDependenciesMeta": {
"nodemailer": {
@@ -77,11 +77,10 @@
}
},
"scripts": {
"build": "pnpm css && pnpm providers && tsc",
"build": "pnpm css && tsc",
"clean": "rm -rf *.js *.d.ts* lib providers",
"css": "node scripts/generate-css",
"dev": "pnpm css && pnpm providers && tsc -w",
"providers": "node scripts/generate-providers"
"dev": "pnpm css && tsc -w"
},
"devDependencies": {
"@next-auth/tsconfig": "workspace:*",
@@ -93,4 +92,4 @@
"postcss": "8.4.19",
"postcss-nested": "6.0.0"
}
}
}

View File

@@ -1,18 +0,0 @@
import { join } from "path"
import { readdirSync, writeFileSync } from "fs"
const providersPath = join(process.cwd(), "src/providers")
const files = readdirSync(providersPath, "utf8")
const providers = files.map((file) => {
const strippedProviderName = file.substring(0, file.indexOf("."))
return `"${strippedProviderName}"`
}).filter((provider) => provider !== '"oauth-types"' && provider !== '"index"')
const result = `
// THIS FILE IS AUTOGENERATED. DO NOT EDIT.
export type OAuthProviderType =
| ${providers.join("\n | ")}`
writeFileSync(join(providersPath, "oauth-types.ts"), result)

View File

@@ -1,8 +1,4 @@
/**
*
* :::warning Experimental
* `@auth/core` is under active development.
* :::
*
* This is the main entry point to the Auth.js library.
*
@@ -31,14 +27,14 @@
* ## Resources
*
* - [Getting started](https://authjs.dev/getting-started/introduction)
* - [Most common use case guides](https://authjs.dev/guides)
* - [Most common use case guides](https://authjs.dev/guides/overview)
*
* @module index
* @module main
*/
import { assertConfig } from "./lib/assert.js"
import { ErrorPageLoop } from "./errors.js"
import { AuthInternal, skipCSRFCheck } from "./lib/index.js"
import { AuthInternal } from "./lib/index.js"
import renderPage from "./lib/pages/index.js"
import { logger, setLogger, type LoggerInstance } from "./lib/utils/logger.js"
import { toInternalRequest, toResponse } from "./lib/web.js"
@@ -55,8 +51,6 @@ import type {
import type { Provider } from "./providers/index.js"
import { JWTOptions } from "./jwt.js"
export { skipCSRFCheck }
/**
* Core functionality provided by Auth.js.
*
@@ -302,5 +296,4 @@ export interface AuthConfig {
cookies?: Partial<CookiesOptions>
/** @todo */
trustHost?: boolean
skipCSRFCheck?: typeof skipCSRFCheck
}

View File

@@ -45,7 +45,7 @@ export function assertConfig(
const { url } = request
const warnings: WarningCode[] = []
if (!warned && options.debug) warnings.push("debug-enabled")
if (!warned && options.debug) warnings.push("debug_enabled")
if (!options.trustHost) {
return new UntrustedHost(`Host must be trusted. URL was: ${request.url}`)

View File

@@ -1,14 +1,14 @@
import { UnknownAction } from "../errors.js"
import { SessionStore } from "./cookie.js"
import { UnknownAction } from "../errors.js"
import { init } from "./init.js"
import renderPage from "./pages/index.js"
import * as routes from "./routes/index.js"
import type {
AuthConfig,
ErrorPageParam,
RequestInternal,
ResponseInternal,
AuthConfig,
ErrorPageParam,
} from "../types.js"
export async function AuthInternal<
@@ -19,8 +19,6 @@ export async function AuthInternal<
): Promise<ResponseInternal<Body>> {
const { action, providerId, error, method } = request
const csrfDisabled = authOptions.skipCSRFCheck === skipCSRFCheck
const { options, cookies } = await init({
authOptions,
action,
@@ -30,7 +28,6 @@ export async function AuthInternal<
csrfToken: request.body?.csrfToken,
cookies: request.cookies,
isPost: method === "POST",
csrfDisabled,
})
const sessionStore = new SessionStore(
@@ -51,29 +48,19 @@ export async function AuthInternal<
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return { ...session, cookies } as any
}
case "csrf": {
if (csrfDisabled) {
options.logger.warn("csrf-disabled")
cookies.push({
name: options.cookies.csrfToken.name,
value: "",
options: { ...options.cookies.csrfToken.options, maxAge: 0 },
})
return { status: 404, cookies }
}
case "csrf":
return {
headers: { "Content-Type": "application/json" },
body: { csrfToken: options.csrfToken } as any,
cookies,
}
}
case "signin":
if (pages.signIn) {
let signinUrl = `${pages.signIn}${
pages.signIn.includes("?") ? "&" : "?"
}${new URLSearchParams({ callbackUrl: options.callbackUrl })}`
}callbackUrl=${encodeURIComponent(options.callbackUrl)}`
if (error)
signinUrl = `${signinUrl}&${new URLSearchParams({ error })}`
signinUrl = `${signinUrl}&error=${encodeURIComponent(error)}`
return { redirect: signinUrl, cookies }
}
@@ -138,7 +125,8 @@ export async function AuthInternal<
} else {
switch (action) {
case "signin":
if ((csrfDisabled || options.csrfTokenVerified) && options.provider) {
// Verified CSRF Token required for all sign in routes
if (options.csrfTokenVerified && options.provider) {
const signin = await routes.signin(
request.query,
request.body,
@@ -150,7 +138,8 @@ export async function AuthInternal<
return { redirect: `${options.url}/signin?csrf=true`, cookies }
case "signout":
if (csrfDisabled || options.csrfTokenVerified) {
// Verified CSRF Token required for signout
if (options.csrfTokenVerified) {
const signout = await routes.signout(sessionStore, options)
if (signout.cookies) cookies.push(...signout.cookies)
return { ...signout, cookies }
@@ -161,7 +150,6 @@ export async function AuthInternal<
// Verified CSRF Token required for credentials providers only
if (
options.provider.type === "credentials" &&
!csrfDisabled &&
!options.csrfTokenVerified
) {
return { redirect: `${options.url}/signin?csrf=true`, cookies }
@@ -185,14 +173,3 @@ export async function AuthInternal<
}
throw new UnknownAction(`Cannot handle action: ${action}`)
}
/**
* :::danger
* This option is intended for framework authors.
* :::
*
* Auth.js comes with built-in {@link https://authjs.dev/concepts/security#csrf CSRF} protection, but
* if you are implementing a framework that is already protected against CSRF attacks, you can skip this check by
* passing this value to {@link AuthConfig.skipCSRFCheck}.
*/
export const skipCSRFCheck = Symbol("skip-csrf-check")

View File

@@ -25,7 +25,6 @@ interface InitParams {
/** CSRF token value extracted from the incoming request. From body if POST, from query if GET */
csrfToken?: string
/** Is the incoming request a POST request? */
csrfDisabled: boolean
isPost: boolean
cookies: RequestInternal["cookies"]
}
@@ -39,7 +38,6 @@ export async function init({
cookies: reqCookies,
callbackUrl: reqCallbackUrl,
csrfToken: reqCsrfToken,
csrfDisabled,
isPost,
}: InitParams): Promise<{
options: InternalOptions
@@ -119,28 +117,26 @@ export async function init({
const cookies: cookie.Cookie[] = []
if (!csrfDisabled) {
const {
csrfToken,
cookie: csrfCookie,
csrfTokenVerified,
} = await createCSRFToken({
options,
cookieValue: reqCookies?.[options.cookies.csrfToken.name],
isPost,
bodyValue: reqCsrfToken,
const {
csrfToken,
cookie: csrfCookie,
csrfTokenVerified,
} = await createCSRFToken({
options,
cookieValue: reqCookies?.[options.cookies.csrfToken.name],
isPost,
bodyValue: reqCsrfToken,
})
options.csrfToken = csrfToken
options.csrfTokenVerified = csrfTokenVerified
if (csrfCookie) {
cookies.push({
name: options.cookies.csrfToken.name,
value: csrfCookie,
options: options.cookies.csrfToken.options,
})
options.csrfToken = csrfToken
options.csrfTokenVerified = csrfTokenVerified
if (csrfCookie) {
cookies.push({
name: options.cookies.csrfToken.name,
value: csrfCookie,
options: options.cookies.csrfToken.options,
})
}
}
const { callbackUrl, callbackUrlCookie } = await createCallbackUrl({

View File

@@ -1,7 +1,7 @@
import * as checks from "./checks.js"
import * as o from "oauth4webapi"
import type {
CookiesOptions,
InternalOptions,
RequestInternal,
ResponseInternal,
@@ -58,10 +58,10 @@ export async function getAuthorizationUrl(
const cookies: Cookie[] = []
const state = await checks.state.create(options)
if (state) {
authParams.set("state", state.value)
cookies.push(state.cookie)
if (provider.checks?.includes("state")) {
const { value, raw } = await createState(options)
authParams.set("state", raw)
cookies.push(value)
}
if (provider.checks?.includes("pkce")) {
@@ -70,17 +70,17 @@ export async function getAuthorizationUrl(
// a random `nonce` must be used for CSRF protection.
provider.checks = ["nonce"]
} else {
const { value, cookie } = await checks.pkce.create(options)
authParams.set("code_challenge", value)
const { code_challenge, pkce } = await createPKCE(options)
authParams.set("code_challenge", code_challenge)
authParams.set("code_challenge_method", "S256")
cookies.push(cookie)
cookies.push(pkce)
}
}
const nonce = await checks.nonce.create(options)
if (nonce) {
if (provider.checks?.includes("nonce")) {
const nonce = await createNonce(options)
authParams.set("nonce", nonce.value)
cookies.push(nonce.cookie)
cookies.push(nonce)
}
// TODO: This does not work in normalizeOAuth because authorization endpoint can come from discovery
@@ -90,5 +90,54 @@ export async function getAuthorizationUrl(
}
logger.debug("authorization url is ready", { url, cookies, provider })
return { redirect: url.toString(), cookies }
return { redirect: url, cookies }
}
/** Returns a signed cookie. */
export async function signCookie(
type: keyof CookiesOptions,
value: string,
maxAge: number,
options: InternalOptions<"oauth">
): Promise<Cookie> {
const { cookies, jwt, logger } = options
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge })
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
return {
name: cookies[type].name,
value: await jwt.encode({ ...jwt, maxAge, token: { value } }),
options: { ...cookies[type].options, expires },
}
}
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
async function createState(options: InternalOptions<"oauth">) {
const raw = o.generateRandomState()
const maxAge = STATE_MAX_AGE
const value = await signCookie("state", raw, maxAge, options)
return { value, raw }
}
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
async function createPKCE(options: InternalOptions<"oauth">) {
const code_verifier = o.generateRandomCodeVerifier()
const code_challenge = await o.calculatePKCECodeChallenge(code_verifier)
const maxAge = PKCE_MAX_AGE
const pkce = await signCookie(
"pkceCodeVerifier",
code_verifier,
maxAge,
options
)
return { code_challenge, pkce }
}
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
async function createNonce(options: InternalOptions<"oauth">) {
const raw = o.generateRandomNonce()
const maxAge = NONCE_MAX_AGE
return await signCookie("nonce", raw, maxAge, options)
}

View File

@@ -1,6 +1,8 @@
import * as checks from "./checks.js"
import * as o from "oauth4webapi"
import { OAuthCallbackError, OAuthProfileParseError } from "../../errors.js"
import { useNonce } from "./nonce-handler.js"
import { usePKCECodeVerifier } from "./pkce-handler.js"
import { useState } from "./state-handler.js"
import type {
InternalOptions,
@@ -71,7 +73,7 @@ export async function handleOAuth(
const resCookies: Cookie[] = []
const state = await checks.state.use(cookies, resCookies, options)
const state = await useState(cookies, resCookies, options)
const parameters = o.validateAuthResponse(
as,
@@ -89,7 +91,7 @@ export async function handleOAuth(
throw new OAuthCallbackError(parameters.error)
}
const codeVerifier = await checks.pkce.use(
const codeVerifier = await usePKCECodeVerifier(
cookies?.[options.cookies.pkceCodeVerifier.name],
options
)
@@ -97,15 +99,12 @@ export async function handleOAuth(
if (codeVerifier) resCookies.push(codeVerifier.cookie)
// TODO:
const nonce = await checks.nonce.use(
cookies?.[options.cookies.nonce.name],
options
)
const nonce = await useNonce(cookies?.[options.cookies.nonce.name], options)
if (nonce && provider.type === "oidc") {
resCookies.push(nonce.cookie)
}
let codeGrantResponse = await o.authorizationCodeGrantRequest(
const codeGrantResponse = await o.authorizationCodeGrantRequest(
as,
client,
parameters,
@@ -113,12 +112,6 @@ export async function handleOAuth(
codeVerifier?.codeVerifier ?? "auth" // TODO: review fallback code verifier
)
if (provider.token?.conform) {
codeGrantResponse =
(await provider.token.conform(codeGrantResponse.clone())) ??
codeGrantResponse
}
let challenges: o.WWWAuthenticateChallenge[] | undefined
if ((challenges = o.parseWwwAuthenticateChallenges(codeGrantResponse))) {
for (const challenge of challenges) {
@@ -142,8 +135,13 @@ export async function handleOAuth(
throw new Error("TODO: Handle OIDC response body error")
}
profile = o.getValidatedIdTokenClaims(result)
tokens = result
if (provider.userinfo?.request) {
profile = await provider.userinfo.request({ tokens, provider })
} else {
profile = o.getValidatedIdTokenClaims(result)
}
} else {
tokens = await o.processAuthorizationCodeOAuth2Response(
as,
@@ -203,7 +201,7 @@ async function getProfile(
// If we didn't get a response either there was a problem with the provider
// response *or* the user cancelled the action with the provider.
//
// Unfortunately, we can't tell which - at least not in a way that works for
// Unfortuately, we can't tell which - at least not in a way that works for
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.

View File

@@ -1,155 +0,0 @@
import * as o from "oauth4webapi"
import * as jwt from "../../jwt.js"
import type {
InternalOptions,
RequestInternal,
CookiesOptions,
} from "../../types.js"
import type { Cookie } from "../cookie.js"
import { InvalidState } from "../../errors.js"
/** Returns a signed cookie. */
export async function signCookie(
type: keyof CookiesOptions,
value: string,
maxAge: number,
options: InternalOptions<"oauth">
): Promise<Cookie> {
const { cookies, logger } = options
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge })
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
return {
name: cookies[type].name,
value: await jwt.encode({ ...options.jwt, maxAge, token: { value } }),
options: { ...cookies[type].options, expires },
}
}
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const pkce = {
async create(options: InternalOptions<"oauth">) {
const code_verifier = o.generateRandomCodeVerifier()
const value = await o.calculatePKCECodeChallenge(code_verifier)
const maxAge = PKCE_MAX_AGE
const cookie = await signCookie(
"pkceCodeVerifier",
code_verifier,
maxAge,
options
)
return { cookie, value }
},
/**
* Returns code_verifier if provider uses PKCE,
* and clears the container cookie afterwards.
*/
async use(
codeVerifier: string | undefined,
options: InternalOptions<"oauth">
): Promise<{ codeVerifier: string; cookie: Cookie } | undefined> {
const { cookies, provider } = options
if (!provider?.checks?.includes("pkce") || !codeVerifier) {
return
}
const pkce = (await jwt.decode({
...options.jwt,
token: codeVerifier,
})) as any
return {
codeVerifier: pkce?.value ?? undefined,
cookie: {
name: cookies.pkceCodeVerifier.name,
value: "",
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
},
}
},
}
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const state = {
async create(options: InternalOptions<"oauth">) {
if (!options.provider.checks.includes("state")) return
// TODO: support customizing the state
const value = o.generateRandomState()
const maxAge = STATE_MAX_AGE
const cookie = await signCookie("state", value, maxAge, options)
return { cookie, value }
},
/**
* Returns state from the saved cookie
* if the provider supports states,
* and clears the container cookie afterwards.
*/
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">
): Promise<string | undefined> {
const { provider, jwt } = options
if (!provider.checks.includes("state")) return
const state = cookies?.[options.cookies.state.name]
if (!state) throw new InvalidState("State was missing from the cookies.")
// IDEA: Let the user do something with the returned state
const value = (await jwt.decode({ ...options.jwt, token: state })) as any
if (!value?.value) throw new InvalidState("Could not parse state cookie.")
// Clear the state cookie after use
resCookies.push({
name: options.cookies.state.name,
value: "",
options: { ...options.cookies.state.options, maxAge: 0 },
})
return value.value
},
}
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const nonce = {
async create(options: InternalOptions<"oauth">) {
if (!options.provider.checks.includes("nonce")) return
const value = o.generateRandomNonce()
const maxAge = NONCE_MAX_AGE
const cookie = await signCookie("nonce", value, maxAge, options)
return { cookie, value }
},
/**
* Returns nonce from if the provider supports nonce,
* and clears the container cookie afterwards.
*/
async use(
nonce: string | undefined,
options: InternalOptions<"oauth">
): Promise<{ value: string; cookie: Cookie } | undefined> {
const { cookies, provider } = options
if (!provider?.checks?.includes("nonce") || !nonce) {
return
}
const value = (await jwt.decode({ ...options.jwt, token: nonce })) as any
return {
value: value?.value ?? undefined,
cookie: {
name: cookies.nonce.name,
value: "",
options: { ...cookies.nonce.options, maxAge: 0 },
},
}
},
}

View File

@@ -0,0 +1,77 @@
import * as o from "oauth4webapi"
import * as jwt from "../../jwt.js"
import type { InternalOptions } from "../../types.js"
import type { Cookie } from "../cookie.js"
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
/**
* Returns nonce if the provider supports it
* and saves it in a cookie
*/
export async function createNonce(options: InternalOptions<"oauth">): Promise<
| undefined
| {
value: string
cookie: Cookie
}
> {
const { cookies, logger, provider } = options
if (!provider.checks?.includes("nonce")) {
// Provider does not support nonce, return nothing.
return
}
const nonce = o.generateRandomNonce()
const expires = new Date()
expires.setTime(expires.getTime() + NONCE_MAX_AGE * 1000)
// Encrypt nonce and save it to an encrypted cookie
const encryptedNonce = await jwt.encode({
...options.jwt,
maxAge: NONCE_MAX_AGE,
token: { nonce },
})
logger.debug("CREATE_ENCRYPTED_NONCE", {
nonce,
maxAge: NONCE_MAX_AGE,
})
return {
cookie: {
name: cookies.nonce.name,
value: encryptedNonce,
options: { ...cookies.nonce.options, expires },
},
value: nonce,
}
}
/**
* Returns nonce from if the provider supports nonce,
* and clears the container cookie afterwards.
*/
export async function useNonce(
nonce: string | undefined,
options: InternalOptions<"oauth">
): Promise<{ value: string; cookie: Cookie } | undefined> {
const { cookies, provider } = options
if (!provider?.checks?.includes("nonce") || !nonce) {
return
}
const value = (await jwt.decode({ ...options.jwt, token: nonce })) as any
return {
value: value?.value ?? undefined,
cookie: {
name: cookies.nonce.name,
value: "",
options: { ...cookies.nonce.options, maxAge: 0 },
},
}
}

View File

@@ -0,0 +1,87 @@
import * as o from "oauth4webapi"
import * as jwt from "../../jwt.js"
import type { InternalOptions } from "../../types.js"
import type { Cookie } from "../cookie.js"
const PKCE_CODE_CHALLENGE_METHOD = "S256"
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
/**
* Returns `code_challenge` and `code_challenge_method`
* and saves them in a cookie.
*/
export async function createPKCE(options: InternalOptions<"oauth">): Promise<
| undefined
| {
code_challenge: string
code_challenge_method: "S256"
cookie: Cookie
}
> {
const { cookies, logger, provider } = options
if (!provider.checks?.includes("pkce")) {
// Provider does not support PKCE, return nothing.
return
}
const code_verifier = o.generateRandomCodeVerifier()
const code_challenge = await o.calculatePKCECodeChallenge(code_verifier)
const maxAge = cookies.pkceCodeVerifier.options.maxAge ?? PKCE_MAX_AGE
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
// Encrypt code_verifier and save it to an encrypted cookie
const encryptedCodeVerifier = await jwt.encode({
...options.jwt,
maxAge,
token: { code_verifier },
})
logger.debug("CREATE_PKCE_CHALLENGE_VERIFIER", {
code_challenge,
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
code_verifier,
maxAge,
})
return {
code_challenge,
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
cookie: {
name: cookies.pkceCodeVerifier.name,
value: encryptedCodeVerifier,
options: { ...cookies.pkceCodeVerifier.options, expires },
},
}
}
/**
* Returns code_verifier if provider uses PKCE,
* and clears the container cookie afterwards.
*/
export async function usePKCECodeVerifier(
codeVerifier: string | undefined,
options: InternalOptions<"oauth">
): Promise<{ codeVerifier: string; cookie: Cookie } | undefined> {
const { cookies, provider } = options
if (!provider?.checks?.includes("pkce") || !codeVerifier) {
return
}
const pkce = (await jwt.decode({
...options.jwt,
token: codeVerifier,
})) as any
return {
codeVerifier: pkce?.value ?? undefined,
cookie: {
name: cookies.pkceCodeVerifier.name,
value: "",
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
},
}
}

View File

@@ -0,0 +1,72 @@
import * as o from "oauth4webapi"
import type { InternalOptions, RequestInternal } from "../../types.js"
import type { Cookie } from "../cookie.js"
import { InvalidState } from "../../errors.js"
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
/** Returns state if the provider supports it */
export async function createState(
options: InternalOptions<"oauth">
): Promise<{ cookie: Cookie; value: string } | undefined> {
const { logger, provider, jwt, cookies } = options
if (!provider.checks?.includes("state")) {
// Provider does not support state, return nothing
return
}
const state = o.generateRandomState()
const maxAge = cookies.state.options.maxAge ?? STATE_MAX_AGE
const encodedState = await jwt.encode({
...jwt,
maxAge,
token: { state },
})
logger.debug("CREATE_STATE", { state, maxAge })
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
return {
value: state,
cookie: {
name: cookies.state.name,
value: encodedState,
options: { ...cookies.state.options, expires },
},
}
}
/**
* Returns state from the saved cookie
* if the provider supports states,
* and clears the container cookie afterwards.
*/
export async function useState(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">
): Promise<string | undefined> {
const { provider, jwt } = options
if (!provider.checks.includes("state")) return
const state = cookies?.[options.cookies.state.name]
if (!state) throw new InvalidState("State was missing from the cookies.")
// IDEA: Let the user do something with the returned state
const value = (await jwt.decode({ ...options.jwt, token: state })) as any
if (!value?.value) throw new InvalidState("Could not parse state cookie.")
// Clear the state cookie after use
resCookies.push({
name: options.cookies.state.name,
value: "",
options: { ...options.cookies.state.options, maxAge: 0 },
})
return value.value
}

View File

@@ -96,9 +96,6 @@ function normalizeEndpoint(
// NOTE: This need to be checked when constructing the URL
// for the authorization, token and userinfo endpoints.
const url = new URL(e?.url ?? "https://authjs.dev")
for (const k in e?.params) {
if (e?.params && k === "claims") e.params[k] = JSON.stringify(e.params[k])
url.searchParams.set(k, e?.params[k])
}
return { url, request: e?.request, conform: e?.conform }
for (const k in e?.params) url.searchParams.set(k, e?.params[k])
return { url, request: e?.request }
}

View File

@@ -53,7 +53,7 @@ export async function callback(params: {
cookies.push(...authorizationResult.cookies)
}
logger.debug("authorization result", authorizationResult)
logger.debug("authroization result", authorizationResult)
const { profile, account, OAuthProfile } = authorizationResult
@@ -111,22 +111,17 @@ export async function callback(params: {
isNewUser,
})
// Clear cookies if token is null
if (token === null) {
cookies.push(...sessionStore.clean())
} else {
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
}
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
} else {
// Save Session Token in cookie
cookies.push({
@@ -149,7 +144,7 @@ export async function callback(params: {
return {
redirect: `${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}${new URLSearchParams({ callbackUrl })}`,
}callbackUrl=${encodeURIComponent(callbackUrl)}`,
cookies,
}
}
@@ -219,22 +214,17 @@ export async function callback(params: {
isNewUser,
})
// Clear cookies if token is null
if (token === null) {
cookies.push(...sessionStore.clean())
} else {
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
}
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
} else {
// Save Session Token in cookie
cookies.push({
@@ -256,7 +246,7 @@ export async function callback(params: {
return {
redirect: `${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}${new URLSearchParams({ callbackUrl })}`,
}callbackUrl=${encodeURIComponent(callbackUrl)}`,
cookies,
}
}
@@ -315,23 +305,18 @@ export async function callback(params: {
isNewUser: false,
})
// Clear cookies if token is null
if (token === null) {
cookies.push(...sessionStore.clean())
} else {
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
}
cookies.push(...sessionCookies)
// @ts-expect-error
await events.signIn?.({ user, account })
@@ -350,6 +335,6 @@ export async function callback(params: {
logger.error(error)
url.searchParams.set("error", CallbackRouteError.name)
url.pathname += "/error"
return { redirect: url.toString(), cookies }
return { redirect: url, cookies }
}
}

View File

@@ -48,32 +48,27 @@ export async function session(
// @ts-expect-error
const token = await callbacks.jwt({ token: decodedToken })
// @ts-expect-error
const newSession = await callbacks.session({ session, token })
if (token !== null) {
// @ts-expect-error
const newSession = await callbacks.session({ session, token })
// Return session payload as response
response.body = newSession
// Return session payload as response
response.body = newSession
// Refresh JWT expiry by re-signing it, with an updated expiry date
const newToken = await jwt.encode({
...jwt,
token,
maxAge: options.session.maxAge,
})
// Refresh JWT expiry by re-signing it, with an updated expiry date
const newToken = await jwt.encode({
...jwt,
token,
maxAge: options.session.maxAge,
})
// Set cookie, to also update expiry date on cookie
const sessionCookies = sessionStore.chunk(newToken, {
expires: newExpires,
})
// Set cookie, to also update expiry date on cookie
const sessionCookies = sessionStore.chunk(newToken, {
expires: newExpires,
})
response.cookies?.push(...sessionCookies)
response.cookies?.push(...sessionCookies)
await events.session?.({ session: newSession, token })
} else {
response.cookies?.push(...sessionStore.clean())
}
await events.session?.({ session: newSession, token })
} catch (e) {
logger.error(new JWTSessionError(e as Error))
// If the JWT is not verifiable remove the broken session cookie(s).

View File

@@ -7,20 +7,19 @@ export async function handleAuthorized(
params: any,
{ url, logger, callbacks: { signIn } }: InternalOptions
) {
url.pathname += "/error"
try {
const authorized = await signIn(params)
if (!authorized) {
url.pathname += "/error"
logger.debug("User not authorized", params)
url.searchParams.set("error", "AccessDenied")
return { status: 403 as const, redirect: url.toString() }
return { status: 403 as const, redirect: url }
}
} catch (e) {
url.pathname += "/error"
const error = new AuthorizedCallbackError(e as Error)
logger.error(error)
url.searchParams.set("error", "Configuration")
return { status: 500 as const, redirect: url.toString() }
return { status: 500 as const, redirect: url }
}
}

View File

@@ -54,7 +54,7 @@ export async function signin(
logger.error(error)
url.searchParams.set("error", error.name)
url.pathname += "/error"
return { redirect: url.toString() }
return { redirect: url }
}
}

View File

@@ -8,7 +8,7 @@ import type { SessionStore } from "../cookie.js"
* If the session strategy is database,
* The session is also deleted from the database.
* In any case, the session cookie is cleared and
* {@link EventCallbacks.signOut} is emitted.
* an `events.signOut` is emitted.
*/
export async function signout(
sessionStore: SessionStore,

View File

@@ -1,6 +1,6 @@
import { AuthError } from "../../errors.js"
export type WarningCode = "debug-enabled" | "csrf-disabled"
export type WarningCode = "debug_enabled"
/**
* Override any of the methods, and the rest will use the default logger.
@@ -38,7 +38,7 @@ export const logger: LoggerInstance = {
}
},
warn(code) {
const url = `https://warnings.authjs.dev#${code}`
const url = `https://errors.authjs.dev#${code}`
console.warn(`${yellow}[auth][warn][${code}]${reset}`, `Read more: ${url}`)
},
debug(message, metadata) {

View File

@@ -76,22 +76,27 @@ export function toResponse(res: ResponseInternal): Response {
res.cookies?.forEach((cookie) => {
const { name, value, options } = cookie
const cookieHeader = serialize(name, value, options)
if (headers.has("Set-Cookie")) headers.append("Set-Cookie", cookieHeader)
else headers.set("Set-Cookie", cookieHeader)
if (headers.has("Set-Cookie")) {
headers.append("Set-Cookie", cookieHeader)
} else {
headers.set("Set-Cookie", cookieHeader)
}
// headers.set("Set-Cookie", cookieHeader) // TODO: Remove. Seems to be a bug with Headers in the runtime
})
let body = res.body
const body =
headers.get("content-type") === "application/json"
? JSON.stringify(res.body)
: res.body
if (headers.get("content-type") === "application/json")
body = JSON.stringify(res.body)
else if (headers.get("content-type") === "application/x-www-form-urlencoded")
body = new URLSearchParams(res.body).toString()
const response = new Response(body, {
headers,
status: res.redirect ? 302 : res.status ?? 200,
})
const status = res.redirect ? 302 : res.status ?? 200
const response = new Response(body, { headers, status })
if (res.redirect) response.headers.set("Location", res.redirect)
if (res.redirect) {
response.headers.set("Location", res.redirect.toString())
}
return response
}

View File

@@ -1,15 +1,3 @@
/**
* <div style={{backgroundColor: "#000", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}>
* <span>Built-in <b>Apple</b> integration.</span>
* <a href="https://apple.com">
* <img style={{display: "block"}} src="https://authjs.dev/img/providers/apple-dark.svg" height="48" width="48"/>
* </a>
* </div>
*
* ---
* @module providers/apple
*/
import type { OAuthConfig, OAuthUserConfig } from "./index.js"
/**

View File

@@ -1,112 +0,0 @@
/**
* <div style={{backgroundColor: "#24292f", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}>
* <span>Built-in <b>Asgardeo</b> integration.</span>
* <a href="https://wso2.com/asgardeo/">
* <img style={{display: "block"}} src="https://authjs.dev/img/providers/asgardeo-dark.svg" height="48" width="48"/>
* </a>
* </div>
*
* ---
* @module providers/asgardeo
*/
import type { OIDCConfig, OIDCUserConfig } from "./index.js"
export interface AsgardeoProfile {
sub: string
given_name: string
email: string
picture: string
}
/**
* Add Asgardeo login to your page.
* ## Documentation
*
* https://wso2.com/asgardeo/docs/guides/authentication
*
*
* ## Instructions
*
* - Log into https://console.asgardeo.io.
* - Next, go to "Application" tab (More info: https://wso2.com/asgardeo/docs/guides/applications/register-oidc-web-app/).
* - Register standard based - Open id connect, application.
* - Add callback URL: http://localhost:3000/api/auth/callback/asgardeo and https://your-domain.com/api/auth/callback/asgardeo
* - After registering the application, go to protocol tab.
* - Check `code` grant type.
* - Add Authorized redirect URLs & Allowed origins fields.
* - Make Email, First Name, Photo URL user attributes mandatory from the console.
*
* Create a `.env` file in the project root add the following entries:
*
* These values can be collected from the application created.
*
* ```
* ASGARDEO_CLIENT_ID=<Copy client ID from protocol tab here>
* ASGARDEO_CLIENT_SECRET=<Copy client from protocol tab here>
* ASGARDEO_ISSUER=<Copy the issuer url from the info tab here>
* ```
*
* In `pages/api/auth/[...nextauth].js` find or add the `Asgardeo` entries:
*
* ```js
* import Asgardeo from "next-auth/providers/asgardeo";
* ...
* providers: [
* Asgardeo({
* clientId: process.env.ASGARDEO_CLIENT_ID,
* clientSecret: process.env.ASGARDEO_CLIENT_SECRET,
* issuer: process.env.ASGARDEO_ISSUER
* }),
* ],
*
* ...
* ```
*
* ## Resources
*
* @see [Asgardeo - Authentication Guide](https://wso2.com/asgardeo/docs/guides/authentication)
* @see [Learn more about OAuth](https://authjs.dev/concepts/oauth)
* @see [Source code](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/asgardeo.ts)
*
* ## Notes
*
* By default, Auth.js assumes that the Asgardeo provider is
* based on the [OAuth 2](https://www.rfc-editor.org/rfc/rfc6749.html) specification.
*
* :::tip
*
* The Asgardeo provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/asgardeo.ts).
* To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/providers/custom-provider#override-default-options).
*
* :::
*
* :::info **Disclaimer**
*
* If you think you found a bug in the default configuration, you can [open an issue](https://authjs.dev/new/provider-issue).
*
* Auth.js strictly adheres to the specification and it cannot take responsibility for any deviation from
* the spec by the provider. You can open an issue, but if the problem is non-compliance with the spec,
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions).
*
* :::
*/
export default function Asgardeo(
config: OIDCUserConfig<AsgardeoProfile>
): OIDCConfig<AsgardeoProfile> {
return {
id: "asgardeo",
name: "Asgardeo",
type: "oidc",
wellKnown: `${config?.issuer}/oauth2/token/.well-known/openid-configuration`,
style: {
logo: "/asgardeo.svg",
logoDark: "/asgardeo-dark.svg",
bg: "#fff",
text: "#000",
bgDark: "#000",
textDark: "#fff",
},
options: config,
}
}

View File

@@ -1,128 +1,15 @@
/**
* <div style={{backgroundColor: "#EB5424", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}>
* <span>Built-in <b>Auth0</b> integration.</span>
* <a href="https://auth0.com">
* <img style={{display: "block"}} src="https://authjs.dev/img/providers/auth0-dark.svg" height="48" width="48"/>
* </a>
* </div>
*
* ---
* @module providers/auth0
*/
import type { OAuthConfig, OAuthUserConfig } from "./index.js"
import type { OIDCConfig, OIDCUserConfig } from "./index.js"
/** @see [User Profile Structure](https://auth0.com/docs/manage-users/user-accounts/user-profiles/user-profile-structure) */
export interface Auth0Profile {
/** The user's unique identifier. */
export interface Auth0Profile extends Record<string, any> {
sub: string
/** Custom fields that store info about a user that influences the user's access, such as support plan, security roles (if not using the Authorization Core feature set), or access control groups. To learn more, read Metadata Overview. */
app_metadata: object
/** Indicates whether the user has been blocked. Importing enables subscribers to ensure that users remain blocked when migrating to Auth0. */
blocked: boolean
/** Timestamp indicating when the user profile was first created. */
created_at: Date
/** (unique) The user's email address. */
email: string
/** Indicates whether the user has verified their email address. */
email_verified: boolean
/** The user's family name. */
family_name: string
/** The user's given name. */
given_name: string
/** Custom fields that store info about a user that does not impact what they can or cannot access, such as work address, home address, or user preferences. To learn more, read Metadata Overview. */
user_metadata: object
/** (unique) The user's username. */
username: string
/** Contains info retrieved from the identity provider with which the user originally authenticates. Users may also link their profile to multiple identity providers; those identities will then also appear in this array. The contents of an individual identity provider object varies by provider. In some cases, it will also include an API Access Token to be used with the provider. */
identities: Array<{
/** Name of the Auth0 connection used to authenticate the user. */
connection: string
/** Indicates whether the connection is a social one. */
isSocial: boolean
/** Name of the entity that is authenticating the user, such as Facebook, Google, SAML, or your own provider. */
provider: string
/** User's unique identifier for this connection/provider. */
user_id: string
/** User info associated with the connection. When profiles are linked, it is populated with the associated user info for secondary accounts. */
profileData: object
[key: string]: any
}>
/** IP address associated with the user's last login. */
last_ip: string
/** Timestamp indicating when the user last logged in. If a user is blocked and logs in, the blocked session updates last_login. If you are using this property from inside a Rule using the user< object, its value will be associated with the login that triggered the rule; this is because rules execute after login. */
last_login: Date
/** Timestamp indicating the last time the user's password was reset/changed. At user creation, this field does not exist. This property is only available for Database connections. */
last_password_reset: Date
/** Number of times the user has logged in. If a user is blocked and logs in, the blocked session is counted in logins_count. */
logins_count: number
/** List of multi-factor providers with which the user is enrolled. */
multifactor: string
/** The user's full name. */
name: string
/** The user's nickname. */
nickname: string
/** The user's phone number. Only valid for users with SMS connections. */
phone_number: string
/** Indicates whether the user has been verified their phone number. Only valid for users with SMS connections. */
phone_verified: boolean
/** URL pointing to the user's profile picture. */
email: string
picture: string
/** Timestamp indicating when the user's profile was last updated/modified. Changes to last_login are considered updates, so most of the time, updated_at will match last_login. */
updated_at: Date
/** (unique) The user's identifier. Importing allows user records to be synchronized across multiple systems without using mapping tables. */
user_id: string
}
/**
* Add Auth0 login to your page.
*
* ## Example
*
* ```ts
* import { Auth } from "@auth/core"
* import Auth0 from "@auth/core/providers/auth0"
*
* const request = new Request("https://example.com")
* const resposne = await Auth(request, {
* providers: [Auth0({ clientId: "", clientSecret: "", issuer: "" })],
* })
* ```
*
* ---
*
* ## Resources
*
* - [Authenticate - Auth0 docs](https://auth0.com/docs/authenticate)
*
* ---
*
* ## Notes
*
* By default, Auth.js assumes that the Auth0 provider is
* based on the [OIDC](https://openid.net/specs/openid-connect-core-1_0.html) specification.
*
* :::tip
*
* The Auth0 provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/auth0.ts).
* To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/providers/custom-provider#override-default-options).
*
* :::
*
* :::info **Disclaimer**
*
* If you think you found a bug in the default configuration, you can [open an issue](https://authjs.dev/new/provider-issue).
*
* Auth.js strictly adheres to the specification and it cannot take responsibility for any deviation from
* the spec by the provider. You can open an issue, but if the problem is non-compliance with the spec,
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions).
*
* :::
*/
export default function Auth0(
config: OIDCUserConfig<Auth0Profile>
): OIDCConfig<Auth0Profile> {
export default function Auth0<P extends Auth0Profile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "auth0",
name: "Auth0",
@@ -135,6 +22,6 @@ export default function Auth0(
bgDark: "#EB5424",
textDark: "#fff",
},
options: config,
options,
}
}

View File

@@ -34,21 +34,22 @@ export default function AzureAD<P extends AzureADProfile>(
)
// Confirm that profile photo was returned
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,
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,
}
}
},
style: {

View File

@@ -0,0 +1,185 @@
/**
* <div style={{backgroundColor: "#39134c", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}>
* <span>Built-in <b>BankID Norway</b> integration.</span>
* <a href="https://www.bankid.no/en">
* <img style={{display: "block"}} src="https://raw.githubusercontent.com/nextauthjs/next-auth/b2b0c05da8e2f61435d14fbce6afa6e8b607e971/docs/static/img/providers/bankid-no.svg" height="48" width="48"/>
* </a>
* </div>
*
* ---
* @module providers/bankid-no
*/
import { decodeJwt } from "jose"
import type { OAuthUserConfig, OIDCConfig } from "./index.js"
/**
* @see [Core conepts - ID Token](https://confluence.bankidnorge.no/confluence/pdoidcl/technical-documentation/core-concepts/id-token)
* @see [userinfo](https://confluence.bankidnorge.no/confluence/pdoidcl/technical-documentation/api/userinfo)
*/
export interface BankIDNorwayProfile {
exp: number
iat: number
/** Epoc time */
auth_time: number
jti: string
iss: string
/** Always client_id */
aud: string
sub: string
typ: "ID"
/** Equals client_id */
azp: string
session_state: string
at_hash: string
name: string
given_name: string
family_name: string
birthdate: string
updated_at: number
/**
* Uniform Resource Name for [IDP option](https://confluence.bankidnorge.no/confluence/pdoidcl/technical-documentation/core-concepts/identity-providers) being used,
* including Level of Assurance (LoA).
* @example
* ```
* urn:bankid:bid;LOA=4
* ```
*/
acr: string
sid: string
/**
* Name of [IDP option](https://confluence.bankidnorge.no/confluence/pdoidcl/technical-documentation/core-concepts/identity-providers) being used to authenticate the end-user.
* If the end-user is subject to authentication step-up,
* note that this value may differ from any `amr` value specified
* in the `login_hint` parameter of the [authorize](https://confluence.bankidnorge.no/confluence/pdoidcl/technical-documentation/api/authorize) endpoint.
*/
amr: "BID" | "BIM" | "BIS"
/** Personal Identifier (PID) / Serial Number) from associated BankID certificate. */
bankid_altsub: string
/**
* In case of BID or BIM, the issuer of the end user certificate is returned.
* @example
* ```
* CN=BankID Bankenes ID-tjeneste Bank CA 2,
* OU=988477052,
* O=Bankenes ID-tjeneste AS,*
* C=NO;OrginatorId=9775;OriginatorName=Gjensidige Bank RA 1
* ```
*/
originator: string
additionalCertInfo: {
certValidFrom: number
serialNumber: string
keyAlgorithm: string
keySize: string
policyOid: string
certQualified: boolean
certValidTo: number
versionNumber: string
subjectName: string
}
/** Currently used as an input parameter for the [securityData](https://confluence.bankidnorge.no/confluence/pdoidcl/technical-documentation/api/securitydata) endpoint of the [Fraud Data](https://confluence.bankidnorge.no/confluence/pdoidcl/technical-documentation/advanced-topics/fraud-data) service */
tid: string
/** Only returned from the `userinfo_endpoint` */
email?: string
/**
* [Norwegian National Identity Number (fødselsnummer)](https://www.skatteetaten.no/en/person/foreign/norwegian-identification-number/national-identity-number). It can be an alternative to `sub`.
* Requires `nnin_altsub` scope at the [authorize](https://confluence.bankidnorge.no/confluence/pdoidcl/technical-documentation/api/authorize) endpoint.
* @example
* ```
* 181266*****
* ```
*/
nnin_altsub?: string
}
/**
* Add BankID Norge login to your page.
*
* ## Example
*
* ```ts
* import { Auth } from "@auth/core"
* import BankIDNorway from "@auth/core/providers/bankid-no"
*
* const request = new Request("https://example.com")
* const resposne = await Auth(request, {
* providers: [BankIDNorway({ clientId: "", clientSecret: "", issuer: "" })],
* })
* ```
*
* ---
*
* ## Resources
*
* - [OpenID Connect Provider from BankID](https://confluence.bankidnorge.no/confluence/pdoidcl)
*
* ---
*
* ## Notes
*
* By default, Auth.js assumes that the BankIDNorway provider is
* based on the [OIDC](https://openid.net/specs/openid-connect-core-1_0.html) specification.
*
* :::tip
*
* The BankIDNorway provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/bankid-no.ts).
* To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/providers/custom-provider#override-default-options).
*
* :::
*
* :::info **Disclaimer**
*
* If you think you found a bug in the default configuration, you can [open an issue](https://authjs.dev/new/provider-issue).
*
* Auth.js strictly adheres to the specification and it cannot take responsibility for any deviation from
* the spec by the provider. You can open an issue, but if the problem is non-compliance with the spec,
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions).
*
* :::
*/
export default function BankIDNorway<P extends BankIDNorwayProfile>(
config: OAuthUserConfig<P>
): OIDCConfig<P> {
config.issuer ??= "https://auth.current.bankid.no/auth/realms/current"
return {
id: "bankid-no",
name: "BankID Norge",
type: "oidc",
style: {
text: "#fff",
textDark: "#fff",
bg: "#39134c",
bgDark: "#39134c",
logo: "/bankid-no.svg",
logoDark: "/bankid-no-dark.svg",
},
client: { token_endpoint_auth_method: "client_secret_post" },
authorization: { params: { ui_locales: "no" } },
userinfo: {
// BankID Norway does not return the email in id_token, only from the userinfo endpoint
// https://confluence.bankidnorge.no/confluence/pdoidcl/technical-documentation/core-concepts/scopes-and-claims#:~:text=is%20not%20optimal.-,email,-Used%20to%20request
async request({ tokens }) {
const res = await fetch(
`${config.issuer}/.well-known/openid-configuration`
)
const meta = await res.json()
const response = await fetch(meta.userinfo_endpoint, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
})
const token = await response.text()
// https://confluence.bankidnorge.no/confluence/pdoidcl/technical-documentation/api/userinfo#apiuserinfo-response-paramsResponseelements
return decodeJwt(token)
},
},
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email ?? null,
image: null,
}
},
options: config,
}
}

View File

@@ -49,6 +49,10 @@ export interface CredentialsConfig<
export type CredentialsProviderType = "Credentials"
export type CredentialsConfigInternal<
C extends Record<string, CredentialInput> = Record<string, CredentialInput>
> = CredentialsConfig<C> & { options: CredentialsConfig<C> }
/**
* The Credentials provider allows you to handle signing in with arbitrary credentials,
* such as a username and password, domain, or two factor authentication or hardware device (e.g. YubiKey U2F / FIDO).

View File

@@ -1,77 +1,21 @@
import type { OAuthConfig, OAuthUserConfig } from "./index.js"
/**
* Corresponds to the user structure documented here:
* https://discord.com/developers/docs/resources/user#user-object-user-structure
*/
export interface DiscordProfile extends Record<string, any> {
/** the user's id (i.e. the numerical snowflake) */
id: string
/** the user's username, not unique across the platform */
username: string
/** the user's 4-digit discord-tag */
accent_color: number
avatar: string
banner: string
banner_color: string
discriminator: string
/**
* the user's avatar hash:
* https://discord.com/developers/docs/reference#image-formatting
*/
avatar: string | null
/** whether the user belongs to an OAuth2 application */
bot?: boolean
/**
* whether the user is an Official Discord System user (part of the urgent
* message system)
*/
system?: boolean
/** whether the user has two factor enabled on their account */
mfa_enabled: boolean
/**
* the user's banner hash:
* https://discord.com/developers/docs/reference#image-formatting
*/
banner: string | null
/** the user's banner color encoded as an integer representation of hexadecimal color code */
accent_color: number | null
/**
* the user's chosen language option:
* https://discord.com/developers/docs/reference#locales
*/
locale: string
/** whether the email on this account has been verified */
verified: boolean
/** the user's email */
email: string | null
/**
* the flags on a user's account:
* https://discord.com/developers/docs/resources/user#user-object-user-flags
*/
email: string
flags: number
/**
* the type of Nitro subscription on a user's account:
* https://discord.com/developers/docs/resources/user#user-object-premium-types
*/
premium_type: number
/**
* the public flags on a user's account:
* https://discord.com/developers/docs/resources/user#user-object-user-flags
*/
public_flags: number
/** undocumented field; corresponds to the user's custom nickname */
display_name: string | null
/**
* undocumented field; corresponds to the Discord feature where you can e.g.
* put your avatar inside of an ice cube
*/
avatar_decoration: string | null
/**
* undocumented field; corresponds to the premium feature where you can
* select a custom banner color
*/
banner_color: string | null
/** undocumented field; the CDN URL of their profile picture */
id: string
image_url: string
locale: string
mfa_enabled: boolean
premium_type: number
public_flags: number
username: string
verified: boolean
}
export default function Discord<P extends DiscordProfile>(

View File

@@ -1,18 +1,6 @@
/**
* <div style={{backgroundColor: "#24292f", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}>
* <span>Built-in <b>GitHub</b> integration.</span>
* <a href="https://github.com">
* <img style={{display: "block"}} src="https://authjs.dev/img/providers/github-dark.svg" height="48" width="48"/>
* </a>
* </div>
*
* ---
* @module providers/github
*/
import type { OAuthConfig, OAuthUserConfig } from "./index.js"
export interface GitHubEmail {
export interface GithubEmail extends Record<string, any> {
email: string
primary: boolean
verified: boolean
@@ -20,7 +8,7 @@ export interface GitHubEmail {
}
/** @see [Get the authenticated user](https://docs.github.com/en/rest/users/users#get-the-authenticated-user) */
export interface GitHubProfile {
export interface GithubProfile extends Record<string, any> {
login: string
id: number
node_id: string
@@ -73,12 +61,14 @@ export interface GitHubProfile {
*
* ## Example
*
* @example
*
* ```ts
* import { Auth } from "@auth/core"
* import GitHub from "@auth/core/providers/github"
* import Auth from "@auth/core"
* import { GitHub } from "@auth/core/providers/github"
*
* const request = new Request("https://example.com")
* const resposne = await Auth(request, {
* const resposne = await AuthHandler(request, {
* providers: [GitHub({ clientId: "", clientSecret: "" })],
* })
* ```
@@ -113,9 +103,9 @@ export interface GitHubProfile {
*
* :::
*/
export default function GitHub(
config: OAuthUserConfig<GitHubProfile>
): OAuthConfig<GitHubProfile> {
export default function GitHub<Profile extends GithubProfile>(
options: OAuthUserConfig<Profile>
): OAuthConfig<Profile> {
return {
id: "github",
name: "GitHub",
@@ -140,7 +130,7 @@ export default function GitHub(
})
if (res.ok) {
const emails: GitHubEmail[] = await res.json()
const emails: GithubEmail[] = await res.json()
profile.email = (emails.find((e) => e.primary) ?? emails[0]).email
}
}
@@ -160,10 +150,10 @@ export default function GitHub(
logo: "/github.svg",
logoDark: "/github-dark.svg",
bg: "#fff",
bgDark: "#24292f",
bgDark: "#000",
text: "#000",
textDark: "#fff",
},
options: config,
options,
}
}

View File

@@ -65,16 +65,12 @@ export type Provider<P extends Profile = Profile> = (
| EmailConfig
| CredentialsConfig
) & {
/**
* Used to deep merge user-provided config with the default config
* @internal
*/
options: Record<string, unknown>
}
export type BuiltInProviders = Record<
OAuthProviderType,
(config: Partial<OAuthConfig<any>>) => OAuthConfig<any>
(options: Partial<OAuthConfig<any>>) => OAuthConfig<any>
> &
Record<CredentialsProviderType, typeof CredentialsProvider> &
Record<EmailProviderType, typeof EmailProvider>

View File

@@ -1,166 +0,0 @@
/**
* <div style={{backgroundColor: "#000", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}>
* <span>Built-in <b>Notion</b> integration.</span>
* <a href="https://notion.so">
* <img style={{display: "block"}} src="https://authjs.dev/img/providers/notion.svg" height="48" width="48"/>
* </a>
* </div>
*
* ---
* @module providers/notion
*/
import type { OAuthConfig, OAuthUserConfig } from "."
export interface Person extends Record<string, any> {
email: string
}
// https://developers.notion.com/reference/user
export interface User extends Record<string, any> {
object: "user" | "bot"
id: string
type: string
name: string
avatar_url: null | string
person: Person
owner?: {
type: "workspace" | "user"
workspace: string
}
workspace_name?: string | null
}
export interface Owner {
type: string
user: User
}
// Notion responds with an access_token + some additional information, which we define here
// More info - https://developers.notion.com/docs/authorization#step-4-notion-responds-with-an-access_token-and-some-additional-information
export interface NotionProfile extends Record<string, any> {
access_token: string
bot_id: string
duplicated_template_id: string
owner?: Owner
workspace_icon: string
workspace_id: number
workspace_name: string
}
// Any config required that isn't part of the `OAuthUserConfig` spec should belong here
// For example, we must pass a `redirectUri` to the Notion API when requesting tokens, therefore we add it here
interface AdditionalConfig {
redirectUri: string
}
const NOTION_HOST = "https://api.notion.com"
const NOTION_API_VERSION = "2022-06-28"
/**
* Add Notion login to your page.
*
* ## Example
*
* ```ts
* import { Auth } from "@auth/core"
* import Notion from "@auth/core/providers/notion"
*
* const request = new Request("https://example.com")
* const response = await Auth(request, {
* providers: [Notion({ clientId: "", clientSecret: "", redirectUri: "" })],
* })
* ```
*
* ---
*
* ## Resources
* - [Notion Docs](https://developers.notion.com/docs)
* - [Notion Authorization Docs](https://developers.notion.com/docs/authorization)
* - [Notion Integrations](https://www.notion.so/my-integrations)
*
* ---
*
* ## Notes
* You need to select "Public Integration" on the configuration page to get an `oauth_id` and `oauth_secret`. Private integrations do not provide these details.
* You must provide a `clientId` and `clientSecret` to use this provider, as-well as a redirect URI (due to this being required by Notion endpoint to fetch tokens).
*
* :::tip
*
* The Notion provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/notion.ts).
* To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/providers/custom-provider#override-default-options).
*
* :::
*
* :::info **Disclaimer**
*
* If you think you found a bug in the default configuration, you can [open an issue](https://authjs.dev/new/provider-issue).
*
* Auth.js strictly adheres to the specification and it cannot take responsibility for any deviation from
* the spec by the provider. You can open an issue, but if the problem is non-compliance with the spec,
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions).
*
* :::
*/
export default function NotionProvider<P extends NotionProfile>(
options: OAuthUserConfig<P> & AdditionalConfig
): OAuthConfig<P> {
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,
}
}

View File

@@ -0,0 +1,69 @@
// 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"

View File

@@ -42,8 +42,6 @@ interface AdvancedEndpointHandler<P extends UrlParams, C, R> {
* You should **try to avoid using advanced options** unless you are very comfortable using them.
*/
request?: EndpointRequest<C, R, P>
/** @internal */
conform?: (response: Response) => Awaitable<Response | undefined>
}
/** Either an URL (containing all the parameters) or an object with more granular control. */
@@ -66,7 +64,7 @@ export type TokenEndpointHandler = EndpointHandler<
params: CallbackParamsType
/**
* When using this custom flow, make sure to do all the necessary security checks.
* This object contains parameters you have to match against the request to make sure it is valid.
* Thist object contains parameters you have to match against the request to make sure it is valid.
*/
checks: OAuthChecks
},
@@ -81,8 +79,8 @@ export type UserinfoEndpointHandler = EndpointHandler<
Profile
>
export type ProfileCallback<Profile> = (
profile: Profile,
export type ProfileCallback<P> = (
profile: P,
tokens: TokenSet
) => Awaitable<User>
@@ -96,9 +94,7 @@ export interface OAuthProviderButtonStyles {
}
/** TODO: */
export interface OAuth2Config<Profile>
extends CommonProviderOptions,
PartialIssuer {
export interface OAuth2Config<P> extends CommonProviderOptions, PartialIssuer {
/**
* Identifies the provider when you want to sign in to
* a specific provider.
@@ -138,7 +134,7 @@ export interface OAuth2Config<Profile>
*
* [Documentation](https://authjs.dev/reference/adapters/models#user)
*/
profile?: ProfileCallback<Profile>
profile?: ProfileCallback<P>
/**
* The CSRF protection performed on the callback endpoint.
* @default ["pkce"]
@@ -163,45 +159,30 @@ export interface OAuth2Config<Profile>
*
* @internal
*/
options?: OAuthUserConfig<Profile>
options?: OAuthUserConfig<P>
}
/** TODO: */
export interface OIDCConfig<Profile>
extends Omit<OAuth2Config<Profile>, "type"> {
export interface OIDCConfig<P> extends Omit<OAuth2Config<P>, "type"> {
type: "oidc"
}
export type OAuthConfig<Profile> = OIDCConfig<Profile> | OAuth2Config<Profile>
export type OAuthConfig<P> = OIDCConfig<P> | OAuth2Config<P>
export type OAuthEndpointType = "authorization" | "token" | "userinfo"
/**
* We parsed `authorization`, `token` and `userinfo`
* We parsesd `authorization`, `token` and `userinfo`
* to always contain a valid `URL`, with the params
* @internal
*/
export type OAuthConfigInternal<Profile> = Omit<
OAuthConfig<Profile>,
OAuthEndpointType
> & {
export type OAuthConfigInternal<P> = Omit<OAuthConfig<P>, OAuthEndpointType> & {
authorization?: { url: URL }
token?: {
url: URL
request?: TokenEndpointHandler["request"]
conform?: TokenEndpointHandler["conform"]
}
token?: { url: URL; request?: TokenEndpointHandler["request"] }
userinfo?: { url: URL; request?: UserinfoEndpointHandler["request"] }
} & Pick<Required<OAuthConfig<Profile>>, "clientId" | "checks" | "profile">
} & Pick<Required<OAuthConfig<P>>, "clientId" | "checks" | "profile">
export type OAuthUserConfig<Profile> = Omit<
Partial<OAuthConfig<Profile>>,
export type OAuthUserConfig<P> = Omit<
Partial<OAuthConfig<P>>,
"options" | "type"
> &
Required<Pick<OAuthConfig<Profile>, "clientId" | "clientSecret">>
export type OIDCUserConfig<Profile> = Omit<
Partial<OIDCConfig<Profile>>,
"options" | "type"
> &
Required<Pick<OIDCConfig<Profile>, "clientId" | "clientSecret">>
Required<Pick<OAuthConfig<P>, "clientId" | "clientSecret">>

Some files were not shown because too many files have changed in this diff Show More