mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
34 Commits
feat/banki
...
@auth/core
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9a84350b5 | ||
|
|
44c38247da | ||
|
|
9b9af4d5e5 | ||
|
|
fd2179bdca | ||
|
|
7bb037bb9d | ||
|
|
52f70e9f4f | ||
|
|
505f69b519 | ||
|
|
b21709db40 | ||
|
|
aff7b37ef9 | ||
|
|
daa85be1ad | ||
|
|
c31718ca10 | ||
|
|
fbcfedf0e8 | ||
|
|
bd032335eb | ||
|
|
128e0f3a10 | ||
|
|
557fb9d741 | ||
|
|
b4d6ed5f5f | ||
|
|
035836da98 | ||
|
|
294039a497 | ||
|
|
b2450ef625 | ||
|
|
a81bb3e51e | ||
|
|
bb506f7eb9 | ||
|
|
87d9cc4244 | ||
|
|
d2e3b76031 | ||
|
|
c36834b3b0 | ||
|
|
8f7145801a | ||
|
|
fdce27b8ca | ||
|
|
4056dafa7a | ||
|
|
f0b61bd5fd | ||
|
|
866e42b343 | ||
|
|
6d4cde4b02 | ||
|
|
2377596bb6 | ||
|
|
3c7c25cefa | ||
|
|
c441f681af | ||
|
|
c05951f0f9 |
1
.github/ISSUE_TEMPLATE/2_bug_provider.yml
vendored
1
.github/ISSUE_TEMPLATE/2_bug_provider.yml
vendored
@@ -25,6 +25,7 @@ body:
|
||||
- "Custom provider"
|
||||
- "42 School"
|
||||
- "Apple"
|
||||
- "Asgardeo"
|
||||
- "Atlassian"
|
||||
- "Auth0"
|
||||
- "Authentik"
|
||||
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -35,6 +35,22 @@ 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:
|
||||
|
||||
@@ -9,6 +9,10 @@ NEXTAUTH_URL=http://localhost:3000
|
||||
# and/or verification tokens.
|
||||
NEXTAUTH_SECRET=secret
|
||||
|
||||
ASGARDEO_CLIENT_ID=
|
||||
ASGARDEO_CLIENT_SECRET=
|
||||
ASGARDEO_ISSUER=
|
||||
|
||||
AUTH0_ID=
|
||||
AUTH0_SECRET=
|
||||
AUTH0_ISSUER=
|
||||
|
||||
4
apps/dev/nextjs/.gitignore
vendored
Normal file
4
apps/dev/nextjs/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
@@ -9,10 +9,12 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"email": "fake-smtp-server",
|
||||
"start:email": "pnpm email"
|
||||
"start:email": "pnpm email",
|
||||
"e2e": "pnpm dlx playwright test"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@auth/core": "workspace:*",
|
||||
"@next-auth/fauna-adapter": "workspace:*",
|
||||
"@next-auth/prisma-adapter": "workspace:*",
|
||||
"@next-auth/supabase-adapter": "workspace:*",
|
||||
@@ -22,15 +24,16 @@
|
||||
"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",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Auth, type AuthConfig } from "@auth/core"
|
||||
|
||||
// Providers
|
||||
import Apple from "@auth/core/providers/apple"
|
||||
import Asgardeo from "@auth/core/providers/asgardeo"
|
||||
import Auth0 from "@auth/core/providers/auth0"
|
||||
import AzureAD from "@auth/core/providers/azure-ad"
|
||||
import AzureB2C from "@auth/core/providers/azure-ad-b2c"
|
||||
@@ -82,6 +83,7 @@ export const authConfig: AuthConfig = {
|
||||
},
|
||||
}),
|
||||
Apple({ clientId: process.env.APPLE_ID, clientSecret: process.env.APPLE_SECRET }),
|
||||
Asgardeo({ clientId: process.env.ASGARDEO_CLIENT_ID, clientSecret: process.env.ASGARDEO_CLIENT_SECRET, issuer: process.env.ASGARDEO_ISSUER }),
|
||||
Auth0({ clientId: process.env.AUTH0_ID, clientSecret: process.env.AUTH0_SECRET, issuer: process.env.AUTH0_ISSUER }),
|
||||
AzureAD({
|
||||
clientId: process.env.AZURE_AD_CLIENT_ID,
|
||||
|
||||
107
apps/dev/nextjs/playwright.config.ts
Normal file
107
apps/dev/nextjs/playwright.config.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
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;
|
||||
39
apps/dev/nextjs/tests/signin.spec.ts
Normal file
39
apps/dev/nextjs/tests/signin.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
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()
|
||||
})
|
||||
@@ -19,8 +19,8 @@
|
||||
"vite": "4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "latest",
|
||||
"@auth/sveltekit": "latest"
|
||||
"@auth/core": "0.2.5",
|
||||
"@auth/sveltekit": "0.1.12"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"vite": "^3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.1.4",
|
||||
"@auth/core": "latest",
|
||||
"@solid-auth/next": "^0.0.19",
|
||||
"@solidjs/meta": "^0.28.0",
|
||||
"@solidjs/router": "^0.6.0",
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
$page.data.session.user?.name}</strong
|
||||
>
|
||||
</span>
|
||||
<a href="/auth/signout" class="button">Sign out</a>
|
||||
<a href="/auth/signout" class="button" data-sveltekit-preload-data="off">Sign out</a>
|
||||
{:else}
|
||||
<span class="notSignedInText">You are not signed in</span>
|
||||
<a href="/auth/signin" class="buttonPrimary">Sign in</a>
|
||||
<a href="/auth/signin" class="buttonPrimary" data-sveltekit-preload-data="off">Sign in</a>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.0",
|
||||
"gatsby": "next",
|
||||
"next-auth": "latest",
|
||||
"next-auth": "workspace:*",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
|
||||
@@ -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.ts"
|
||||
```ts title="pages/_app.tsx"
|
||||
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, let's 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, 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 tem 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 them, 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:
|
||||
|
||||
@@ -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 gonna be using [Sendgrid](https://sendgrid.com/), but any of the services linked above should work the same
|
||||
For this tutorial, we're going to 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 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.
|
||||
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.
|
||||
|
||||
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 gonna 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 going to 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" />
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ title: TypeScript
|
||||
Auth.js has its own type definitions to use in your TypeScript projects safely. Even if you don't use TypeScript, IDEs like VSCode will pick this up to provide you with a better developer experience. While you are typing, you will get suggestions about what certain objects/functions look like, and sometimes links to documentation, examples, and other valuable resources.
|
||||
|
||||
Check out the example repository showcasing how to use `next-auth` on a Next.js application with TypeScript:
|
||||
https://github.com/nextauthjs/next-auth-typescript-example
|
||||
https://github.com/nextauthjs/next-auth-example
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
153
docs/docs/guides/03-basics/role-based-authentication.md
Normal file
153
docs/docs/guides/03-basics/role-based-authentication.md
Normal file
@@ -0,0 +1,153 @@
|
||||
---
|
||||
title: Role-based authentication
|
||||
---
|
||||
|
||||
There are two ways to add role-based authentication (RBAC) to your application, based on the [session strategy](/concepts/session-strategies) you choose. Let's see an example for each of these.
|
||||
|
||||
## Getting the role
|
||||
|
||||
We are going to start by adding a `profile()` callback to the providers' config to determine the user role:
|
||||
|
||||
```ts title="/pages/api/auth/[...nextauth].ts"
|
||||
import NextAuth from "next-auth"
|
||||
import Google from "next-auth/providers/google"
|
||||
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
Google({
|
||||
profile(profile) {
|
||||
return { role: profile.role ?? "user", ... }
|
||||
},
|
||||
...
|
||||
})
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
:::tip
|
||||
To determine the user's role, you can either add your logic or if your provider assigns roles already, use that instead.
|
||||
:::
|
||||
|
||||
## Persisting the role
|
||||
### With JWT
|
||||
|
||||
When you don't have a database configured, the role will be persisted in a cookie, by using the `jwt()` callback. On sign-in, the `role` property is exposed from the `profile` callback on the `user` object. Persist the `user.role` value by assigning it to `token.role`. That's it!
|
||||
|
||||
If you also want to use the role on the client, you can expose it via the `session` callback.
|
||||
|
||||
```ts title="/pages/api/auth/[...nextauth].ts"
|
||||
import NextAuth from "next-auth"
|
||||
import Google from "next-auth/providers/google"
|
||||
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
Google({
|
||||
profile(profile) {
|
||||
return { role: profile.role ?? "user", ... }
|
||||
},
|
||||
...
|
||||
})
|
||||
],
|
||||
// highlight-start
|
||||
callbacks: {
|
||||
jwt({ token, user }) {
|
||||
if(user) token.role = user.role
|
||||
return token
|
||||
},
|
||||
session({ session, token }) {
|
||||
session.user.role = token.role
|
||||
return session
|
||||
}
|
||||
}
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
:::info
|
||||
With this strategy, if you want to update the role, the user needs to be forced to sign in again.
|
||||
:::
|
||||
|
||||
### With Database
|
||||
|
||||
When you have a database, you can save the user role on the [User model](/reference/adapters/models#user). The below example is showing you how to do this with Prisma, but the idea is the same for all adapters.
|
||||
|
||||
First, add a `role` column to the User model.
|
||||
|
||||
```ts title="/prisma/schema.prisma"
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
role String? // New column
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
```
|
||||
|
||||
The `profile()` callback's return value is used to create users in the database. That's it! Your newly created users will now have an assigned role.
|
||||
|
||||
If you also want to use the role on the client, you can expose it via the `session` callback.
|
||||
|
||||
```ts title="/pages/api/auth/[...nextauth].ts"
|
||||
import NextAuth from "next-auth"
|
||||
import Google from "next-auth/providers/google"
|
||||
// highlight-next-line
|
||||
import prisma from "lib/prisma"
|
||||
|
||||
export default NextAuth({
|
||||
// highlight-next-line
|
||||
adapter: PrismaAdapter(prisma),
|
||||
providers: [
|
||||
Google({
|
||||
profile(profile) {
|
||||
return { role: profile.role ?? "user", ... }
|
||||
}
|
||||
...
|
||||
})
|
||||
],
|
||||
// highlight-start
|
||||
callbacks: {
|
||||
session({ session, user }) {
|
||||
session.user.role = user.role
|
||||
return session
|
||||
}
|
||||
}
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
:::info
|
||||
It is up to you how you want to manage to update the roles, either through direct database access or building your role update API.
|
||||
:::
|
||||
|
||||
## Using the role
|
||||
|
||||
If you want to use the role in the client, for both cases above, when using the `useSession` hook, `session.user.role` will have the required role if you exposed it via the `session` callback. You can use this to render a different UI for different users.
|
||||
|
||||
```ts title="/pages/admin.tsx"
|
||||
import { useSession } from "next-auth/react"
|
||||
|
||||
export default function Page() {
|
||||
const session = await useSession()
|
||||
|
||||
if (session?.user.role === "admin") {
|
||||
return <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)
|
||||
@@ -1,64 +0,0 @@
|
||||
---
|
||||
title: Role based logins
|
||||
---
|
||||
|
||||
To add role based authentication to your application, you must do three things.
|
||||
|
||||
1. Update your database schema
|
||||
2. Add the `role` to the session object
|
||||
3. Check for `role` in your pages/components
|
||||
|
||||
First modify the `user` table and add a `role` column with the type of `String?`.
|
||||
|
||||
Below is an example Prisma schema file.
|
||||
|
||||
```javascript title="/prisma/schema.prisma"
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
role String? // New Column
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Next, implement a custom session callback in the `[...nextauth].js` file, as shown below.
|
||||
|
||||
```javascript title="/pages/api/auth/[...nextauth].js"
|
||||
callbacks: {
|
||||
async session({ session, token, user }) {
|
||||
session.user.role = user.role; // Add role value to user object so it is passed along with session
|
||||
return session;
|
||||
},
|
||||
```
|
||||
|
||||
Going forward, when using the `getSession` hook, check that `session.user.role` matches the required role. The example below assumes the role `'admin'` is required.
|
||||
|
||||
```javascript title="/pages/admin.js"
|
||||
import { getSession } from "next-auth/react"
|
||||
|
||||
export default function Page() {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (session && session.user.role === "admin") {
|
||||
return (
|
||||
<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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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.js)
|
||||
- [Battle.net Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/battlenet.ts)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
|
||||
@@ -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.js)
|
||||
- [GitHub Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/github.ts)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
|
||||
@@ -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.js)
|
||||
- [Gitlab Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/gitlab.ts)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
|
||||
@@ -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.js)
|
||||
- [Kakao Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/kakao.ts)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
|
||||
@@ -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.js)
|
||||
- [Salesforce Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/salesforce.ts)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
|
||||
@@ -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.js)
|
||||
- [VK Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/vk.ts)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ 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",
|
||||
|
||||
@@ -3,57 +3,13 @@ id: warnings
|
||||
title: Warnings
|
||||
---
|
||||
|
||||
This is a list of warning output from Auth.js.
|
||||
A list of warnings from Auth.js that need your attention.
|
||||
|
||||
All warnings indicate things which you should take a look at, but do not inhibit normal operation.
|
||||
|
||||
---
|
||||
## Debug enabled
|
||||
|
||||
## Client
|
||||
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.
|
||||
|
||||
#### NEXTAUTH_URL
|
||||
## CSRF disabled
|
||||
|
||||
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
|
||||
}),
|
||||
```
|
||||
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.
|
||||
@@ -2,11 +2,9 @@ Add $1 login to your page.
|
||||
|
||||
## Example
|
||||
|
||||
@example
|
||||
|
||||
```js
|
||||
import Auth from "@auth/core"
|
||||
import { $1 } from "@auth/core/providers/$2"
|
||||
```ts
|
||||
import { Auth } from "@auth/core"
|
||||
import $1 from "@auth/core/providers/$2"
|
||||
|
||||
const request = new Request("https://example.com")
|
||||
const resposne = await AuthHandler(request, {
|
||||
@@ -18,7 +16,7 @@ const resposne = await AuthHandler(request, {
|
||||
|
||||
## Resources
|
||||
|
||||
@see [Link 1](https://example.com)
|
||||
- [Link 1](https://example.com)
|
||||
|
||||
---
|
||||
|
||||
|
||||
7
docs/static/img/providers/asgardeo-dark.svg
vendored
Normal file
7
docs/static/img/providers/asgardeo-dark.svg
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 711 B |
7
docs/static/img/providers/asgardeo.svg
vendored
Normal file
7
docs/static/img/providers/asgardeo.svg
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 722 B |
@@ -18,7 +18,8 @@
|
||||
"lint": "prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"release": "release",
|
||||
"version:pr": "node ./config/version-pr"
|
||||
"version:pr": "node ./config/version-pr",
|
||||
"e2e": "turbo run e2e --filter=next-auth-app"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
@@ -47,7 +48,7 @@
|
||||
"engines": {
|
||||
"node": "^16.13.0 || ^18.12.0"
|
||||
},
|
||||
"packageManager": "pnpm@7.19.0",
|
||||
"packageManager": "pnpm@7.23.0",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@next-auth/dynamodb-adapter",
|
||||
"repository": "https://github.com/nextauthjs/next-auth",
|
||||
"version": "1.0.6",
|
||||
"version": "1.2.0",
|
||||
"description": "AWS DynamoDB adapter for next-auth.",
|
||||
"keywords": [
|
||||
"next-auth",
|
||||
|
||||
@@ -16,11 +16,25 @@ import { format, generateUpdateExpression } from "./utils"
|
||||
|
||||
export { format, generateUpdateExpression }
|
||||
|
||||
export interface DynamoDBAdapterOptions {
|
||||
tableName?: string,
|
||||
partitionKey?: string,
|
||||
sortKey?: string,
|
||||
indexName?: string,
|
||||
indexPartitionKey?: string,
|
||||
indexSortKey?: string
|
||||
}
|
||||
|
||||
export function DynamoDBAdapter(
|
||||
client: DynamoDBDocument,
|
||||
options?: { tableName: string }
|
||||
options?: DynamoDBAdapterOptions
|
||||
): 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) {
|
||||
@@ -33,11 +47,11 @@ export function DynamoDBAdapter(
|
||||
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 as string}`,
|
||||
GSI1SK: `USER#${user.email as string}`,
|
||||
[GSI1PK]: `USER#${user.email as string}`,
|
||||
[GSI1SK]: `USER#${user.email as string}`,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -47,8 +61,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)
|
||||
@@ -56,11 +70,11 @@ export function DynamoDBAdapter(
|
||||
async getUserByEmail(email) {
|
||||
const data = await client.query({
|
||||
TableName,
|
||||
IndexName: "GSI1",
|
||||
IndexName,
|
||||
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
|
||||
ExpressionAttributeNames: {
|
||||
"#gsi1pk": "GSI1PK",
|
||||
"#gsi1sk": "GSI1SK",
|
||||
"#gsi1pk": GSI1PK,
|
||||
"#gsi1sk": GSI1SK,
|
||||
},
|
||||
ExpressionAttributeValues: {
|
||||
":gsi1pk": `USER#${email}`,
|
||||
@@ -73,11 +87,11 @@ export function DynamoDBAdapter(
|
||||
async getUserByAccount({ provider, providerAccountId }) {
|
||||
const data = await client.query({
|
||||
TableName,
|
||||
IndexName: "GSI1",
|
||||
IndexName,
|
||||
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
|
||||
ExpressionAttributeNames: {
|
||||
"#gsi1pk": "GSI1PK",
|
||||
"#gsi1sk": "GSI1SK",
|
||||
"#gsi1pk": GSI1PK,
|
||||
"#gsi1sk": GSI1SK,
|
||||
},
|
||||
ExpressionAttributeValues: {
|
||||
":gsi1pk": `ACCOUNT#${provider}`,
|
||||
@@ -90,8 +104,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)
|
||||
@@ -106,8 +120,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,
|
||||
@@ -123,7 +137,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
|
||||
@@ -134,8 +148,8 @@ export function DynamoDBAdapter(
|
||||
return {
|
||||
DeleteRequest: {
|
||||
Key: {
|
||||
sk: item.sk,
|
||||
pk: item.pk,
|
||||
[sk]: item.sk,
|
||||
[pk]: item.pk,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -152,10 +166,10 @@ export function DynamoDBAdapter(
|
||||
const item = {
|
||||
...data,
|
||||
id: randomBytes(16).toString("hex"),
|
||||
pk: `USER#${data.userId}`,
|
||||
sk: `ACCOUNT#${data.provider}#${data.providerAccountId}`,
|
||||
GSI1PK: `ACCOUNT#${data.provider}`,
|
||||
GSI1SK: `ACCOUNT#${data.providerAccountId}`,
|
||||
[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
|
||||
@@ -163,11 +177,11 @@ export function DynamoDBAdapter(
|
||||
async unlinkAccount({ provider, providerAccountId }) {
|
||||
const data = await client.query({
|
||||
TableName,
|
||||
IndexName: "GSI1",
|
||||
IndexName,
|
||||
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
|
||||
ExpressionAttributeNames: {
|
||||
"#gsi1pk": "GSI1PK",
|
||||
"#gsi1sk": "GSI1SK",
|
||||
"#gsi1pk": GSI1PK,
|
||||
"#gsi1sk": GSI1SK,
|
||||
},
|
||||
ExpressionAttributeValues: {
|
||||
":gsi1pk": `ACCOUNT#${provider}`,
|
||||
@@ -179,8 +193,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",
|
||||
})
|
||||
@@ -189,11 +203,11 @@ export function DynamoDBAdapter(
|
||||
async getSessionAndUser(sessionToken) {
|
||||
const data = await client.query({
|
||||
TableName,
|
||||
IndexName: "GSI1",
|
||||
IndexName,
|
||||
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
|
||||
ExpressionAttributeNames: {
|
||||
"#gsi1pk": "GSI1PK",
|
||||
"#gsi1sk": "GSI1SK",
|
||||
"#gsi1pk": GSI1PK,
|
||||
"#gsi1sk": GSI1SK,
|
||||
},
|
||||
ExpressionAttributeValues: {
|
||||
":gsi1pk": `SESSION#${sessionToken}`,
|
||||
@@ -205,8 +219,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)
|
||||
@@ -221,10 +235,10 @@ export function DynamoDBAdapter(
|
||||
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,
|
||||
}),
|
||||
@@ -235,11 +249,11 @@ export function DynamoDBAdapter(
|
||||
const { sessionToken } = session
|
||||
const data = await client.query({
|
||||
TableName,
|
||||
IndexName: "GSI1",
|
||||
IndexName,
|
||||
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
|
||||
ExpressionAttributeNames: {
|
||||
"#gsi1pk": "GSI1PK",
|
||||
"#gsi1sk": "GSI1SK",
|
||||
"#gsi1pk": GSI1PK,
|
||||
"#gsi1sk": GSI1SK,
|
||||
},
|
||||
ExpressionAttributeValues: {
|
||||
":gsi1pk": `SESSION#${sessionToken}`,
|
||||
@@ -266,11 +280,11 @@ export function DynamoDBAdapter(
|
||||
async deleteSession(sessionToken) {
|
||||
const data = await client.query({
|
||||
TableName,
|
||||
IndexName: "GSI1",
|
||||
IndexName,
|
||||
KeyConditionExpression: "#gsi1pk = :gsi1pk AND #gsi1sk = :gsi1sk",
|
||||
ExpressionAttributeNames: {
|
||||
"#gsi1pk": "GSI1PK",
|
||||
"#gsi1sk": "GSI1SK",
|
||||
"#gsi1pk": GSI1PK,
|
||||
"#gsi1sk": GSI1SK,
|
||||
},
|
||||
ExpressionAttributeValues: {
|
||||
":gsi1pk": `SESSION#${sessionToken}`,
|
||||
@@ -292,8 +306,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,
|
||||
}),
|
||||
@@ -304,8 +318,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",
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/xata-adapter",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@auth/core",
|
||||
"version": "0.2.5",
|
||||
"version": "0.3.0",
|
||||
"description": "Authentication for the Web.",
|
||||
"keywords": [
|
||||
"authentication",
|
||||
@@ -92,4 +92,4 @@
|
||||
"postcss": "8.4.19",
|
||||
"postcss-nested": "6.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,4 +296,16 @@ export interface AuthConfig {
|
||||
cookies?: Partial<CookiesOptions>
|
||||
/** @todo */
|
||||
trustHost?: boolean
|
||||
skipCSRFCheck?: typeof skipCSRFCheck
|
||||
}
|
||||
|
||||
/**
|
||||
* :::danger
|
||||
* This option is inteded 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")
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { SessionStore } from "./cookie.js"
|
||||
import { UnknownAction } from "../errors.js"
|
||||
import { skipCSRFCheck } from "../index.js"
|
||||
import { SessionStore } from "./cookie.js"
|
||||
import { init } from "./init.js"
|
||||
import renderPage from "./pages/index.js"
|
||||
import * as routes from "./routes/index.js"
|
||||
|
||||
import type {
|
||||
RequestInternal,
|
||||
ResponseInternal,
|
||||
AuthConfig,
|
||||
ErrorPageParam,
|
||||
RequestInternal,
|
||||
ResponseInternal,
|
||||
} from "../types.js"
|
||||
|
||||
export async function AuthInternal<
|
||||
@@ -19,6 +20,8 @@ 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,
|
||||
@@ -28,6 +31,7 @@ export async function AuthInternal<
|
||||
csrfToken: request.body?.csrfToken,
|
||||
cookies: request.cookies,
|
||||
isPost: method === "POST",
|
||||
csrfDisabled,
|
||||
})
|
||||
|
||||
const sessionStore = new SessionStore(
|
||||
@@ -48,12 +52,22 @@ export async function AuthInternal<
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
return { ...session, cookies } as any
|
||||
}
|
||||
case "csrf":
|
||||
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 }
|
||||
}
|
||||
return {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: { csrfToken: options.csrfToken } as any,
|
||||
cookies,
|
||||
}
|
||||
}
|
||||
case "signin":
|
||||
if (pages.signIn) {
|
||||
let signinUrl = `${pages.signIn}${
|
||||
@@ -125,8 +139,7 @@ export async function AuthInternal<
|
||||
} else {
|
||||
switch (action) {
|
||||
case "signin":
|
||||
// Verified CSRF Token required for all sign in routes
|
||||
if (options.csrfTokenVerified && options.provider) {
|
||||
if ((csrfDisabled || options.csrfTokenVerified) && options.provider) {
|
||||
const signin = await routes.signin(
|
||||
request.query,
|
||||
request.body,
|
||||
@@ -138,8 +151,7 @@ export async function AuthInternal<
|
||||
|
||||
return { redirect: `${options.url}/signin?csrf=true`, cookies }
|
||||
case "signout":
|
||||
// Verified CSRF Token required for signout
|
||||
if (options.csrfTokenVerified) {
|
||||
if (csrfDisabled || options.csrfTokenVerified) {
|
||||
const signout = await routes.signout(sessionStore, options)
|
||||
if (signout.cookies) cookies.push(...signout.cookies)
|
||||
return { ...signout, cookies }
|
||||
@@ -150,6 +162,7 @@ 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 }
|
||||
|
||||
@@ -25,6 +25,7 @@ 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"]
|
||||
}
|
||||
@@ -38,6 +39,7 @@ export async function init({
|
||||
cookies: reqCookies,
|
||||
callbackUrl: reqCallbackUrl,
|
||||
csrfToken: reqCsrfToken,
|
||||
csrfDisabled,
|
||||
isPost,
|
||||
}: InitParams): Promise<{
|
||||
options: InternalOptions
|
||||
@@ -117,26 +119,28 @@ export async function init({
|
||||
|
||||
const cookies: cookie.Cookie[] = []
|
||||
|
||||
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,
|
||||
if (!csrfDisabled) {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { callbackUrl, callbackUrlCookie } = await createCallbackUrl({
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
if (provider.checks?.includes("state")) {
|
||||
const { value, raw } = await createState(options)
|
||||
authParams.set("state", raw)
|
||||
cookies.push(value)
|
||||
const state = await checks.state.create(options)
|
||||
if (state) {
|
||||
authParams.set("state", state.value)
|
||||
cookies.push(state.cookie)
|
||||
}
|
||||
|
||||
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 { code_challenge, pkce } = await createPKCE(options)
|
||||
authParams.set("code_challenge", code_challenge)
|
||||
const { value, cookie } = await checks.pkce.create(options)
|
||||
authParams.set("code_challenge", value)
|
||||
authParams.set("code_challenge_method", "S256")
|
||||
cookies.push(pkce)
|
||||
cookies.push(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
if (provider.checks?.includes("nonce")) {
|
||||
const nonce = await createNonce(options)
|
||||
const nonce = await checks.nonce.create(options)
|
||||
if (nonce) {
|
||||
authParams.set("nonce", nonce.value)
|
||||
cookies.push(nonce)
|
||||
cookies.push(nonce.cookie)
|
||||
}
|
||||
|
||||
// TODO: This does not work in normalizeOAuth because authorization endpoint can come from discovery
|
||||
@@ -92,52 +92,3 @@ export async function getAuthorizationUrl(
|
||||
logger.debug("authorization url is ready", { url, cookies, provider })
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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,
|
||||
@@ -73,7 +71,7 @@ export async function handleOAuth(
|
||||
|
||||
const resCookies: Cookie[] = []
|
||||
|
||||
const state = await useState(cookies, resCookies, options)
|
||||
const state = await checks.state.use(cookies, resCookies, options)
|
||||
|
||||
const parameters = o.validateAuthResponse(
|
||||
as,
|
||||
@@ -91,7 +89,7 @@ export async function handleOAuth(
|
||||
throw new OAuthCallbackError(parameters.error)
|
||||
}
|
||||
|
||||
const codeVerifier = await usePKCECodeVerifier(
|
||||
const codeVerifier = await checks.pkce.use(
|
||||
cookies?.[options.cookies.pkceCodeVerifier.name],
|
||||
options
|
||||
)
|
||||
@@ -99,12 +97,15 @@ export async function handleOAuth(
|
||||
if (codeVerifier) resCookies.push(codeVerifier.cookie)
|
||||
|
||||
// TODO:
|
||||
const nonce = await useNonce(cookies?.[options.cookies.nonce.name], options)
|
||||
const nonce = await checks.nonce.use(
|
||||
cookies?.[options.cookies.nonce.name],
|
||||
options
|
||||
)
|
||||
if (nonce && provider.type === "oidc") {
|
||||
resCookies.push(nonce.cookie)
|
||||
}
|
||||
|
||||
const codeGrantResponse = await o.authorizationCodeGrantRequest(
|
||||
let codeGrantResponse = await o.authorizationCodeGrantRequest(
|
||||
as,
|
||||
client,
|
||||
parameters,
|
||||
@@ -112,6 +113,12 @@ 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) {
|
||||
|
||||
155
packages/core/src/lib/oauth/checks.ts
Normal file
155
packages/core/src/lib/oauth/checks.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
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 },
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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 },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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 },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -96,6 +96,9 @@ 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) url.searchParams.set(k, e?.params[k])
|
||||
return { url, request: e?.request }
|
||||
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 }
|
||||
}
|
||||
|
||||
@@ -111,17 +111,22 @@ export async function callback(params: {
|
||||
isNewUser,
|
||||
})
|
||||
|
||||
// Encode token
|
||||
const newToken = await jwt.encode({ ...jwt, token })
|
||||
// Clear cookies if token is null
|
||||
if (token === null) {
|
||||
cookies.push(...sessionStore.clean())
|
||||
} else {
|
||||
// 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({
|
||||
@@ -214,17 +219,22 @@ export async function callback(params: {
|
||||
isNewUser,
|
||||
})
|
||||
|
||||
// Encode token
|
||||
const newToken = await jwt.encode({ ...jwt, token })
|
||||
// Clear cookies if token is null
|
||||
if (token === null) {
|
||||
cookies.push(...sessionStore.clean())
|
||||
} else {
|
||||
// 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({
|
||||
@@ -305,18 +315,23 @@ export async function callback(params: {
|
||||
isNewUser: false,
|
||||
})
|
||||
|
||||
// Encode token
|
||||
const newToken = await jwt.encode({ ...jwt, token })
|
||||
// Clear cookies if token is null
|
||||
if (token === null) {
|
||||
cookies.push(...sessionStore.clean())
|
||||
} else {
|
||||
// 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 })
|
||||
|
||||
@@ -48,27 +48,32 @@ export async function session(
|
||||
|
||||
// @ts-expect-error
|
||||
const token = await callbacks.jwt({ token: decodedToken })
|
||||
// @ts-expect-error
|
||||
const newSession = await callbacks.session({ session, token })
|
||||
|
||||
// Return session payload as response
|
||||
response.body = newSession
|
||||
if (token !== null) {
|
||||
// @ts-expect-error
|
||||
const newSession = await callbacks.session({ session, token })
|
||||
|
||||
// Refresh JWT expiry by re-signing it, with an updated expiry date
|
||||
const newToken = await jwt.encode({
|
||||
...jwt,
|
||||
token,
|
||||
maxAge: options.session.maxAge,
|
||||
})
|
||||
// Return session payload as response
|
||||
response.body = newSession
|
||||
|
||||
// Set cookie, to also update expiry date on cookie
|
||||
const sessionCookies = sessionStore.chunk(newToken, {
|
||||
expires: newExpires,
|
||||
})
|
||||
// Refresh JWT expiry by re-signing it, with an updated expiry date
|
||||
const newToken = await jwt.encode({
|
||||
...jwt,
|
||||
token,
|
||||
maxAge: options.session.maxAge,
|
||||
})
|
||||
|
||||
response.cookies?.push(...sessionCookies)
|
||||
// Set cookie, to also update expiry date on cookie
|
||||
const sessionCookies = sessionStore.chunk(newToken, {
|
||||
expires: newExpires,
|
||||
})
|
||||
|
||||
await events.session?.({ session: newSession, token })
|
||||
response.cookies?.push(...sessionCookies)
|
||||
|
||||
await events.session?.({ session: newSession, token })
|
||||
} else {
|
||||
response.cookies?.push(...sessionStore.clean())
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(new JWTSessionError(e as Error))
|
||||
// If the JWT is not verifiable remove the broken session cookie(s).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AuthError } from "../../errors.js"
|
||||
|
||||
export type WarningCode = "debug_enabled"
|
||||
export type WarningCode = "debug-enabled" | "csrf-disabled"
|
||||
|
||||
/**
|
||||
* 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://errors.authjs.dev#${code}`
|
||||
const url = `https://warnings.authjs.dev#${code}`
|
||||
console.warn(`${yellow}[auth][warn][${code}]${reset}`, `Read more: ${url}`)
|
||||
},
|
||||
debug(message, metadata) {
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
/**
|
||||
* <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"
|
||||
|
||||
/**
|
||||
|
||||
112
packages/core/src/providers/asgardeo.ts
Normal file
112
packages/core/src/providers/asgardeo.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* <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,
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,128 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "./index.js"
|
||||
/**
|
||||
* <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
|
||||
*/
|
||||
|
||||
export interface Auth0Profile extends Record<string, any> {
|
||||
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. */
|
||||
sub: string
|
||||
nickname: 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. */
|
||||
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
|
||||
}
|
||||
|
||||
export default function Auth0<P extends Auth0Profile>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
/**
|
||||
* 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> {
|
||||
return {
|
||||
id: "auth0",
|
||||
name: "Auth0",
|
||||
@@ -22,6 +135,6 @@ export default function Auth0<P extends Auth0Profile>(
|
||||
bgDark: "#EB5424",
|
||||
textDark: "#fff",
|
||||
},
|
||||
options,
|
||||
options: config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,22 +34,21 @@ export default function AzureAD<P extends AzureADProfile>(
|
||||
)
|
||||
|
||||
// Confirm that profile photo was returned
|
||||
if (response.ok) {
|
||||
const pictureBuffer = await response.arrayBuffer()
|
||||
const pictureBase64 = Buffer.from(pictureBuffer).toString("base64")
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: `data:image/jpeg;base64, ${pictureBase64}`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
}
|
||||
let image
|
||||
// TODO: Do this without Buffer
|
||||
if (response.ok && typeof Buffer !== "undefined") {
|
||||
try {
|
||||
const pictureBuffer = await response.arrayBuffer()
|
||||
const pictureBase64 = Buffer.from(pictureBuffer).toString("base64")
|
||||
image = `data:image/jpeg;base64, ${pictureBase64}`
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: image ?? null,
|
||||
}
|
||||
},
|
||||
style: {
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
/**
|
||||
* <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 extends Record<string, any> {
|
||||
export interface GitHubEmail {
|
||||
email: string
|
||||
primary: boolean
|
||||
verified: boolean
|
||||
@@ -8,7 +20,7 @@ export interface GithubEmail extends Record<string, any> {
|
||||
}
|
||||
|
||||
/** @see [Get the authenticated user](https://docs.github.com/en/rest/users/users#get-the-authenticated-user) */
|
||||
export interface GithubProfile extends Record<string, any> {
|
||||
export interface GitHubProfile {
|
||||
login: string
|
||||
id: number
|
||||
node_id: string
|
||||
@@ -61,14 +73,12 @@ export interface GithubProfile extends Record<string, any> {
|
||||
*
|
||||
* ## 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 AuthHandler(request, {
|
||||
* const resposne = await Auth(request, {
|
||||
* providers: [GitHub({ clientId: "", clientSecret: "" })],
|
||||
* })
|
||||
* ```
|
||||
@@ -103,9 +113,9 @@ export interface GithubProfile extends Record<string, any> {
|
||||
*
|
||||
* :::
|
||||
*/
|
||||
export default function GitHub<Profile extends GithubProfile>(
|
||||
options: OAuthUserConfig<Profile>
|
||||
): OAuthConfig<Profile> {
|
||||
export default function GitHub(
|
||||
config: OAuthUserConfig<GitHubProfile>
|
||||
): OAuthConfig<GitHubProfile> {
|
||||
return {
|
||||
id: "github",
|
||||
name: "GitHub",
|
||||
@@ -130,7 +140,7 @@ export default function GitHub<Profile extends GithubProfile>(
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -150,10 +160,10 @@ export default function GitHub<Profile extends GithubProfile>(
|
||||
logo: "/github.svg",
|
||||
logoDark: "/github-dark.svg",
|
||||
bg: "#fff",
|
||||
bgDark: "#000",
|
||||
bgDark: "#24292f",
|
||||
text: "#000",
|
||||
textDark: "#fff",
|
||||
},
|
||||
options,
|
||||
options: config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ 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. */
|
||||
@@ -79,8 +81,8 @@ export type UserinfoEndpointHandler = EndpointHandler<
|
||||
Profile
|
||||
>
|
||||
|
||||
export type ProfileCallback<P> = (
|
||||
profile: P,
|
||||
export type ProfileCallback<Profile> = (
|
||||
profile: Profile,
|
||||
tokens: TokenSet
|
||||
) => Awaitable<User>
|
||||
|
||||
@@ -94,7 +96,9 @@ export interface OAuthProviderButtonStyles {
|
||||
}
|
||||
|
||||
/** TODO: */
|
||||
export interface OAuth2Config<P> extends CommonProviderOptions, PartialIssuer {
|
||||
export interface OAuth2Config<Profile>
|
||||
extends CommonProviderOptions,
|
||||
PartialIssuer {
|
||||
/**
|
||||
* Identifies the provider when you want to sign in to
|
||||
* a specific provider.
|
||||
@@ -134,7 +138,7 @@ export interface OAuth2Config<P> extends CommonProviderOptions, PartialIssuer {
|
||||
*
|
||||
* [Documentation](https://authjs.dev/reference/adapters/models#user)
|
||||
*/
|
||||
profile?: ProfileCallback<P>
|
||||
profile?: ProfileCallback<Profile>
|
||||
/**
|
||||
* The CSRF protection performed on the callback endpoint.
|
||||
* @default ["pkce"]
|
||||
@@ -159,30 +163,45 @@ export interface OAuth2Config<P> extends CommonProviderOptions, PartialIssuer {
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
options?: OAuthUserConfig<P>
|
||||
options?: OAuthUserConfig<Profile>
|
||||
}
|
||||
|
||||
/** TODO: */
|
||||
export interface OIDCConfig<P> extends Omit<OAuth2Config<P>, "type"> {
|
||||
export interface OIDCConfig<Profile>
|
||||
extends Omit<OAuth2Config<Profile>, "type"> {
|
||||
type: "oidc"
|
||||
}
|
||||
|
||||
export type OAuthConfig<P> = OIDCConfig<P> | OAuth2Config<P>
|
||||
export type OAuthConfig<Profile> = OIDCConfig<Profile> | OAuth2Config<Profile>
|
||||
|
||||
export type OAuthEndpointType = "authorization" | "token" | "userinfo"
|
||||
|
||||
/**
|
||||
* We parsesd `authorization`, `token` and `userinfo`
|
||||
* We parsed `authorization`, `token` and `userinfo`
|
||||
* to always contain a valid `URL`, with the params
|
||||
* @internal
|
||||
*/
|
||||
export type OAuthConfigInternal<P> = Omit<OAuthConfig<P>, OAuthEndpointType> & {
|
||||
export type OAuthConfigInternal<Profile> = Omit<
|
||||
OAuthConfig<Profile>,
|
||||
OAuthEndpointType
|
||||
> & {
|
||||
authorization?: { url: URL }
|
||||
token?: { url: URL; request?: TokenEndpointHandler["request"] }
|
||||
token?: {
|
||||
url: URL
|
||||
request?: TokenEndpointHandler["request"]
|
||||
conform?: TokenEndpointHandler["conform"]
|
||||
}
|
||||
userinfo?: { url: URL; request?: UserinfoEndpointHandler["request"] }
|
||||
} & Pick<Required<OAuthConfig<P>>, "clientId" | "checks" | "profile">
|
||||
} & Pick<Required<OAuthConfig<Profile>>, "clientId" | "checks" | "profile">
|
||||
|
||||
export type OAuthUserConfig<P> = Omit<
|
||||
Partial<OAuthConfig<P>>,
|
||||
export type OAuthUserConfig<Profile> = Omit<
|
||||
Partial<OAuthConfig<Profile>>,
|
||||
"options" | "type"
|
||||
> &
|
||||
Required<Pick<OAuthConfig<P>, "clientId" | "clientSecret">>
|
||||
Required<Pick<OAuthConfig<Profile>, "clientId" | "clientSecret">>
|
||||
|
||||
export type OIDCUserConfig<Profile> = Omit<
|
||||
Partial<OIDCConfig<Profile>>,
|
||||
"options" | "type"
|
||||
> &
|
||||
Required<Pick<OIDCConfig<Profile>, "clientId" | "clientSecret">>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "./index.js"
|
||||
import type { OIDCConfig, OIDCUserConfig } from "./index.js"
|
||||
|
||||
export interface TwitchProfile extends Record<string, any> {
|
||||
sub: string
|
||||
@@ -7,26 +7,52 @@ export interface TwitchProfile extends Record<string, any> {
|
||||
picture: string
|
||||
}
|
||||
|
||||
export default function Twitch<P extends TwitchProfile>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
export default function Twitch(
|
||||
config: OIDCUserConfig<TwitchProfile>
|
||||
): OIDCConfig<TwitchProfile> {
|
||||
return {
|
||||
issuer: "https://id.twitch.tv/oauth2",
|
||||
id: "twitch",
|
||||
name: "Twitch",
|
||||
type: "oidc",
|
||||
client: { token_endpoint_auth_method: "client_secret_post" },
|
||||
authorization: {
|
||||
params: {
|
||||
scope: "openid user:read:email",
|
||||
claims: {
|
||||
id_token: {
|
||||
email: null,
|
||||
picture: null,
|
||||
preferred_username: null,
|
||||
},
|
||||
id_token: { email: null, picture: null, preferred_username: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
token: {
|
||||
async conform(response) {
|
||||
const body = await response.json()
|
||||
if (response.ok) {
|
||||
if (typeof body.scope === "string") {
|
||||
console.warn(
|
||||
"'scope' is a string. Redundant workaround, please open an issue."
|
||||
)
|
||||
} else if (Array.isArray(body.scope)) {
|
||||
body.scope = body.scope.join(" ")
|
||||
return new Response(JSON.stringify(body), response)
|
||||
} else if ("scope" in body) {
|
||||
delete body.scope
|
||||
return new Response(JSON.stringify(body), response)
|
||||
}
|
||||
} else {
|
||||
const { message: error_description, error } = body
|
||||
if (typeof error !== "string") {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "invalid_request", error_description }),
|
||||
response
|
||||
)
|
||||
}
|
||||
console.warn(
|
||||
"Response has 'error'. Redundant workaround, please open an issue."
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
style: {
|
||||
logo: "/twitch.svg",
|
||||
logoDark: "/twitch-dark.svg",
|
||||
@@ -35,6 +61,6 @@ export default function Twitch<P extends TwitchProfile>(
|
||||
bgDark: "#65459B",
|
||||
textDark: "#fff",
|
||||
},
|
||||
options,
|
||||
options: config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,120 +1,51 @@
|
||||
// TODO: move OAuth 1.0 support or remove it?
|
||||
import type { OAuthConfig, OAuthUserConfig } from "./index.js"
|
||||
|
||||
export interface TwitterLegacyProfile {
|
||||
id: number
|
||||
id_str: string
|
||||
name: string
|
||||
screen_name: string
|
||||
location: string
|
||||
description: string
|
||||
url: string
|
||||
entities: {
|
||||
url: {
|
||||
urls: Array<{
|
||||
url: string
|
||||
expanded_url: string
|
||||
display_url: string
|
||||
indices: number[]
|
||||
}>
|
||||
}
|
||||
description: {
|
||||
urls: any[]
|
||||
}
|
||||
}
|
||||
protected: boolean
|
||||
followers_count: number
|
||||
friends_count: number
|
||||
listed_count: number
|
||||
created_at: string
|
||||
favourites_count: number
|
||||
utc_offset?: any
|
||||
time_zone?: any
|
||||
geo_enabled: boolean
|
||||
verified: boolean
|
||||
statuses_count: number
|
||||
lang?: any
|
||||
status: {
|
||||
created_at: string
|
||||
id: number
|
||||
id_str: string
|
||||
text: string
|
||||
truncated: boolean
|
||||
entities: {
|
||||
hashtags: any[]
|
||||
symbols: any[]
|
||||
user_mentions: Array<{
|
||||
screen_name: string
|
||||
name: string
|
||||
id: number
|
||||
id_str: string
|
||||
indices: number[]
|
||||
}>
|
||||
urls: any[]
|
||||
}
|
||||
source: string
|
||||
in_reply_to_status_id: number
|
||||
in_reply_to_status_id_str: string
|
||||
in_reply_to_user_id: number
|
||||
in_reply_to_user_id_str: string
|
||||
in_reply_to_screen_name: string
|
||||
geo?: any
|
||||
coordinates?: any
|
||||
place?: any
|
||||
contributors?: any
|
||||
is_quote_status: boolean
|
||||
retweet_count: number
|
||||
favorite_count: number
|
||||
favorited: boolean
|
||||
retweeted: boolean
|
||||
lang: string
|
||||
}
|
||||
contributors_enabled: boolean
|
||||
is_translator: boolean
|
||||
is_translation_enabled: boolean
|
||||
profile_background_color: string
|
||||
profile_background_image_url: string
|
||||
profile_background_image_url_https: string
|
||||
profile_background_tile: boolean
|
||||
profile_image_url: string
|
||||
profile_image_url_https: string
|
||||
profile_banner_url: string
|
||||
profile_link_color: string
|
||||
profile_sidebar_border_color: string
|
||||
profile_sidebar_fill_color: string
|
||||
profile_text_color: string
|
||||
profile_use_background_image: boolean
|
||||
has_extended_profile: boolean
|
||||
default_profile: boolean
|
||||
default_profile_image: boolean
|
||||
following: boolean
|
||||
follow_request_sent: boolean
|
||||
notifications: boolean
|
||||
translator_type: string
|
||||
withheld_in_countries: any[]
|
||||
suspended: boolean
|
||||
needs_phone_verification: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* [Documentation](https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me)
|
||||
* [Users lookup](https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me)
|
||||
*/
|
||||
export interface TwitterProfile {
|
||||
data: {
|
||||
/**
|
||||
* Unique identifier of this user. This is returned as a string in order to avoid complications with languages and tools
|
||||
* that cannot handle large integers.
|
||||
*/
|
||||
id: string
|
||||
/** The friendly name of this user, as shown on their profile. */
|
||||
name: string
|
||||
/** @note Email is currently unsupported by Twitter. */
|
||||
email?: string
|
||||
/** The Twitter handle (screen name) of this user. */
|
||||
username: string
|
||||
/**
|
||||
* The location specified in the user's profile, if the user provided one.
|
||||
* As this is a freeform value, it may not indicate a valid location, but it may be fuzzily evaluated when performing searches with location queries.
|
||||
*
|
||||
* To return this field, add `user.fields=location` in the authorization request's query parameter.
|
||||
*/
|
||||
location?: string
|
||||
/**
|
||||
* This object and its children fields contain details about text that has a special meaning in the user's description.
|
||||
*
|
||||
*To return this field, add `user.fields=entities` in the authorization request's query parameter.
|
||||
*/
|
||||
entities?: {
|
||||
/** Contains details about the user's profile website. */
|
||||
url: {
|
||||
/** Contains details about the user's profile website. */
|
||||
urls: Array<{
|
||||
/** The start position (zero-based) of the recognized user's profile website. All start indices are inclusive. */
|
||||
start: number
|
||||
/** The end position (zero-based) of the recognized user's profile website. This end index is exclusive. */
|
||||
end: number
|
||||
/** The URL in the format entered by the user. */
|
||||
url: string
|
||||
/** The fully resolved URL. */
|
||||
expanded_url: string
|
||||
/** The URL as displayed in the user's profile. */
|
||||
display_url: string
|
||||
}>
|
||||
}
|
||||
/** Contains details about URLs, Hashtags, Cashtags, or mentions located within a user's description. */
|
||||
description: {
|
||||
hashtags: Array<{
|
||||
start: number
|
||||
@@ -123,11 +54,32 @@ export interface TwitterProfile {
|
||||
}>
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Indicate if this user is a verified Twitter user.
|
||||
*
|
||||
* To return this field, add `user.fields=verified` in the authorization request's query parameter.
|
||||
*/
|
||||
verified?: boolean
|
||||
/**
|
||||
* The text of this user's profile description (also known as bio), if the user provided one.
|
||||
*
|
||||
* To return this field, add `user.fields=description` in the authorization request's query parameter.
|
||||
*/
|
||||
description?: string
|
||||
/**
|
||||
* The URL specified in the user's profile, if present.
|
||||
*
|
||||
* To return this field, add `user.fields=url` in the authorization request's query parameter.
|
||||
*/
|
||||
url?: string
|
||||
/** The URL to the profile image for this user, as shown on the user's profile. */
|
||||
profile_image_url?: string
|
||||
protected?: boolean
|
||||
/**
|
||||
* Unique identifier of this user's pinned Tweet.
|
||||
*
|
||||
* You can obtain the expanded object in `includes.tweets` by adding `expansions=pinned_tweet_id` in the authorization request's query parameter.
|
||||
*/
|
||||
pinned_tweet_id?: string
|
||||
created_at?: string
|
||||
}
|
||||
@@ -139,41 +91,24 @@ export interface TwitterProfile {
|
||||
}
|
||||
}
|
||||
|
||||
export default function Twitter<
|
||||
P extends Record<string, any> = TwitterLegacyProfile | TwitterProfile
|
||||
>(options: OAuthUserConfig<P> & { version?: "2.0" }): OAuthConfig<P> {
|
||||
export default function Twitter(
|
||||
config: OAuthUserConfig<TwitterProfile>
|
||||
): OAuthConfig<TwitterProfile> {
|
||||
return {
|
||||
id: "twitter",
|
||||
name: "Twitter",
|
||||
type: "oauth",
|
||||
checks: ["pkce", "state"],
|
||||
authorization: {
|
||||
url: "https://twitter.com/i/oauth2/authorize",
|
||||
params: { scope: "users.read tweet.read offline.access" },
|
||||
},
|
||||
token: {
|
||||
url: "https://api.twitter.com/2/oauth2/token",
|
||||
// @ts-expect-error TODO: Remove this
|
||||
async request({ client, params, checks, provider }) {
|
||||
const response = await client.oauthCallback(
|
||||
provider.callbackUrl,
|
||||
params,
|
||||
checks,
|
||||
{ exchangeBody: { client_id: options.clientId } }
|
||||
)
|
||||
return { tokens: response }
|
||||
},
|
||||
},
|
||||
userinfo: {
|
||||
url: "https://api.twitter.com/2/users/me",
|
||||
params: { "user.fields": "profile_image_url" },
|
||||
},
|
||||
authorization:
|
||||
"https://twitter.com/i/oauth2/authorize?scope=users.read tweet.read offline.access",
|
||||
token: "https://api.twitter.com/2/oauth2/token",
|
||||
userinfo:
|
||||
"https://api.twitter.com/2/users/me?user.fields=profile_image_url",
|
||||
profile({ data }) {
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
// NOTE: E-mail is currently unsupported by OAuth 2 Twitter.
|
||||
email: null,
|
||||
email: data.email ?? null,
|
||||
image: data.profile_image_url,
|
||||
}
|
||||
},
|
||||
@@ -185,6 +120,6 @@ export default function Twitter<
|
||||
bgDark: "#1da1f2",
|
||||
textDark: "#fff",
|
||||
},
|
||||
options,
|
||||
options: config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,11 @@ export interface CallbacksOptions<P = Profile, A = Account> {
|
||||
* Its content is forwarded to the `session` callback,
|
||||
* where you can control what should be returned to the client.
|
||||
* Anything else will be kept inaccessible from the client.
|
||||
*
|
||||
* Returning `null` will invalidate the JWT session by clearing
|
||||
* the user's cookies. You'll still have to monitor and invalidate
|
||||
* unexpired tokens from future requests yourself to prevent
|
||||
* unauthorized access.
|
||||
*
|
||||
* By default the JWT is encrypted.
|
||||
*
|
||||
@@ -215,7 +220,7 @@ export interface CallbacksOptions<P = Profile, A = Account> {
|
||||
account?: A | null
|
||||
profile?: P
|
||||
isNewUser?: boolean
|
||||
}) => Awaitable<JWT>
|
||||
}) => Awaitable<JWT|null>
|
||||
}
|
||||
|
||||
/** [Documentation](https://authjs.dev/reference/configuration/auth-config#cookies) */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@auth/sveltekit",
|
||||
"version": "0.1.12",
|
||||
"version": "0.2.0",
|
||||
"description": "Authentication for SvelteKit.",
|
||||
"keywords": [
|
||||
"authentication",
|
||||
@@ -32,7 +32,7 @@
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@playwright/test": "1.29.2",
|
||||
"@sveltejs/adapter-auto": "^1.0.0",
|
||||
"@sveltejs/kit": "^1.0.0",
|
||||
"@sveltejs/package": "^1.0.0",
|
||||
|
||||
1383
pnpm-lock.yaml
generated
1383
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "apps/**/**"
|
||||
- "packages/*"
|
||||
- "apps/dev/*"
|
||||
- "apps/playgrounds/*"
|
||||
- "docs"
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
"test": {
|
||||
"outputs": []
|
||||
},
|
||||
"e2e": {
|
||||
"outputs": ["playwright-report/**"]
|
||||
},
|
||||
"@next-auth/upstash-redis-adapter#test": {
|
||||
"env": ["UPSTASH_REDIS_KEY", "UPSTASH_REDIS_URL"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user