Compare commits

...

10 Commits

Author SHA1 Message Date
GitHub Actions
0ddd47cc0a chore(release): bump package version(s) [skip ci] 2023-04-20 09:38:01 +00:00
Balázs Orbán
0100888d9b fix: consume nonce exactly once (#7327)
* fix: consume nonce exactly once

* tweak state handling
2023-04-20 10:25:41 +01:00
Balázs Orbán
9eeea02fe2 feat: redirect proxy (#7326)
* types

* add `redirectProxy` option

* ignore if no state

* empty commit

* tweak proxy detection

* add origin proxy check to checks

* run randomstate decode

* don't generate state data when no proxy

* ignore next-2

* update dev app

* clarify `UnknownAction` error

* rename to `AUTH_REDIRECT_PROXY_URL`

* simplify state

* clear todos

* cleanup

* clarify comment

* use `InalidChecks` error

* simplify

* clarify errors

* add debug logger to redirect proxy

* add proxy redirect logger

* don't throw error when no origin on proxy

* fix redirect_uri in callback

* add docs/guide

* sort imports

* docs: rephrase
2023-04-20 09:53:44 +01:00
GitHub Actions
0a57fea430 chore(release): bump package version(s) [skip ci] 2023-04-20 08:41:41 +00:00
Tim Schneider
51750e1a06 fix(adapters): correct peer dependency (#7310)
Typo in package.json

Missing | in package.json causing ETARGET and peer dependency errors
2023-04-20 09:23:30 +01:00
Balázs Orbán
039a14d992 fix: clarify unknown action error 2023-04-19 10:40:51 +02:00
Balázs Orbán
da821d2789 chore: cleanup todos, format 2023-04-19 10:40:42 +02:00
Balázs Orbán
be5c42e350 Merge branch 'main' of github.com:nextauthjs/next-auth 2023-04-19 10:36:50 +02:00
Balázs Orbán
b68f461f8b chore: upgrade next 2023-04-19 10:35:34 +02:00
Nick Parsons
95c5ba0b5d docs: Update Clerk sponsorship URL (#7305)
- Change Clerk URL from `https://clerk.dev` to `https://clerk.com`

- Fix alt from copy/paste
2023-04-18 20:13:19 +01:00
24 changed files with 317 additions and 280 deletions

View File

@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -22,7 +22,7 @@
"@prisma/client": "^3",
"@supabase/supabase-js": "^2.0.5",
"faunadb": "^4",
"next": "13.1.1",
"next": "13.3.0",
"next-auth": "workspace:*",
"nodemailer": "^6",
"react": "^18",

View File

@@ -102,7 +102,7 @@ export const authConfig: AuthConfig = {
Facebook({ clientId: process.env.FACEBOOK_ID, clientSecret: process.env.FACEBOOK_SECRET }),
Foursquare({ clientId: process.env.FOURSQUARE_ID, clientSecret: process.env.FOURSQUARE_SECRET }),
Freshbooks({ clientId: process.env.FRESHBOOKS_ID, clientSecret: process.env.FRESHBOOKS_SECRET }),
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, redirectProxy: process.env.AUTH_REDIRECT_PROXY_URL }),
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }),
// IDS4({ clientId: process.env.IDS4_ID, clientSecret: process.env.IDS4_SECRET, issuer: process.env.IDS4_ISSUER }),

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
@@ -19,8 +23,17 @@
{
"name": "next"
}
]
],
"strictNullChecks": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", "jest.config.js"]
}
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules",
"jest.config.js"
]
}

View File

@@ -18,77 +18,55 @@ See below for more detailed provider settings.
## Vercel
1. Make sure to expose the Vercel [System Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables) in your project settings.
2. Create a `NEXTAUTH_SECRET` environment variable for all environments.
1. Make sure to expose the Vercel [System Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables) in your project settings. This way, we can detect the environment. (Setting `NEXTAUTH_URL` environment variable on Vercel is **unnecessary**).
2. Create a `NEXTAUTH_SECRET` environment variable for both Production and Preview environments.
a. You can use `openssl rand -base64 32` or https://generate-secret.vercel.app/32 to generate a random value.
b. You **do not** need the `NEXTAUTH_URL` environment variable in Vercel.
3. Add your provider's client ID and client secret to environment variables. _(Skip this step if not using an [OAuth Provider](/reference/providers/index))_
4. Deploy!
Example repository: https://github.com/nextauthjs/next-auth-example
A few notes about deploying to Vercel. The environment variables are read server-side, so you do not need to prefix them with `NEXT_PUBLIC_`. When deploying here, you do not need to explicitly set the `NEXTAUTH_URL` environment variable. With other providers **you will** need to also set this environment variable.
A few notes about deploying to Vercel. The environment variables are read server-side, so you **should not** prefix them with `NEXT_PUBLIC_` to avoid accidentally bundling a secret in the client-side JavaScript code.
### Securing a preview deployment
Securing a preview deployment (with an OAuth provider) comes with some critical obstacles. Most OAuth providers only allow a single redirect/callback URL, or at least a set of full static URLs. Meaning you cannot set the value before publishing the site and you cannot use wildcard subdomains in the callback URL settings of your OAuth provider. Here are a few ways you can still use Auth.js to secure your Preview Deployments.
Most OAuth providers cannot be configured with multiple callback URLs or using a wildcard.
#### Using the Credentials Provider
However, Auth.js **supports Preview deployments**, even **with OAuth providers**:
You could check in your `/pages/api/auth/[...nextauth].js` API route / configuration file to see if you're currently in a Vercel preview environment, and if so, enable a simple "credential provider", meaning username/password. Vercel offers a few built-in [system environment variables](https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables) which you could check against, like `VERCEL_ENV`. This would allow you to use this basic, for testing only, authentication strategy in your preview deployments.
1. Determine a stable deployment URL. Eg.: A deployment whose URL does not change between builds, for example. `auth.yourdomain.com`),
2. Set `AUTH_REDIRECT_PROXY_URL` to that URL, adding the path up until your `[...nextauth]` route. Eg.: (`https://auth.yourdomain.com/api/auth`)
3. For your OAuth provider, set the callback URL using the stable deployment URL. Eg.: For GitHub `https://auth.yourdomain.com/api/auth/callback/github`)
Some things to be aware of here, include:
:::info
To support preview deployments, the `AUTH_SECRET` value needs to be the same for the stable deployment and deployments that will need OAuth support.
:::
- Do not let this potential testing-only user have access to any critical data
- If possible, maybe do not even connect this preview deployment to your production database
##### Example
<details>
<summary>
<b>How does this work?</b>
</summary>
To support preview deployments, Auth.js uses the stable deployment URL as a redirect proxy server.
```js title="/pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import CredentialsProvider from "next-auth/providers/credentials"
It will redirect the OAuth callback request to the preview deployment URL, but only when the `AUTH_REDIRECT_PROXY_URL` environment variable is set. The stable deployment can still act as a regular app.
export default NextAuth({
providers: [
process.env.VERCEL_ENV === "preview"
? CredentialsProvider({
name: "Credentials",
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "jsmith",
},
password: { label: "Password", type: "password" },
},
async authorize() {
return {
id: 1,
name: "J Smith",
email: "jsmith@example.com",
image: "https://i.pravatar.cc/150?u=jsmith@example.com",
}
},
})
: GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
})
```
When a user initiates an OAuth sign-in flow on a preview deployment, we save its URL in the `state` query parameter but set the `redirect_uri` to the stable deployment.
#### Using the branch based preview URL
Then, the OAuth provider will redirect the user to the stable deployment, which then will verify the `state` parameter and redirect the user to the preview deployment URL if the `state` is valid. This is secured by relying on the same server-side `AUTH_SECRET` for the stable deployment and the preview deployment.
Preview deployments at Vercel are often available via multiple URLs. For example, PR's merged to `master` or `main`, will be available the commit and PR specific preview URLs, but also the branch specific preview URLs. This branch specific URL will obviously not change as long as you work with that same branch. Therefore, you could add to your OAuth provider your `{project}-git-main-{user}.vercel.app` preview URL. As this will stay constant for that branch, you can reuse that preview deployment / URL for testing any authentication related deployments.
See also:
<ul>
<li><a href="https://www.ietf.org/rfc/rfc6749.html#section-4.1.1">OAuth 2.0 specification: `state` query parameter</a></li>
</ul>
</details>
## Netlify
Netlify is very similar to Vercel in that you can deploy a Next.js project without almost any extra work.
In order to setup Auth.js correctly here, you will want to make sure you add your `NEXTAUTH_SECRET` environment variable in the project settings. If you are using the [Essential Next.js Build Plugin](https://github.com/netlify/netlify-plugin-nextjs) within your project, you **do not** need to set the `NEXTAUTH_URL` environment variable as it is set automatically as part of the build process.
To set up Auth.js correctly here, you will want to make sure you add your `NEXTAUTH_SECRET` environment variable in the project settings. If you are using the [Essential Next.js Build Plugin](https://github.com/netlify/netlify-plugin-nextjs) within your project, you **do not** need to set the `NEXTAUTH_URL` environment variable as it is set automatically as part of the build process.
Netlify also exposes some [system environment variables](https://docs.netlify.com/configure-builds/environment-variables/) from which you can check which `NODE_ENV` you are currently in and much more.
After this, just make sure you either have your OAuth provider setup correctly with `clientId` / `clientSecret`'s and callback URLs.
After this, make sure you either have your OAuth provider set up correctly with `clientId` / `clientSecret`'s and callback URLs.

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/mongodb-adapter",
"version": "1.1.2",
"version": "1.1.3",
"description": "mongoDB adapter for next-auth.",
"homepage": "https://authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -31,7 +31,7 @@
"dist"
],
"peerDependencies": {
"mongodb": "^5 | ^4",
"mongodb": "^5 || ^4",
"next-auth": "^4"
},
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@auth/core",
"version": "0.6.0",
"version": "0.7.0",
"description": "Authentication for the Web.",
"keywords": [
"authentication",

View File

@@ -337,4 +337,36 @@ export interface AuthConfig {
/** @todo */
trustHost?: boolean
skipCSRFCheck?: typeof skipCSRFCheck
/**
* When set, during an OAuth sign-in flow,
* the `redirect_uri` of the authorization request
* will be set based on this value.
*
* This is useful if your OAuth Provider only supports a single `redirect_uri`
* or you want to use OAuth on preview URLs (like Vercel), where you don't know the final deployment URL beforehand.
*
* The url needs to include the full path up to where Auth.js is initialized.
*
* @note This will auto-enable the `state` {@link OAuth2Config.checks} on the provider.
*
* @example
* ```
* "https://authjs.example.com/api/auth"
* ```
*
* You can also override this individually for each provider.
*
* @example
* ```ts
* GitHub({
* ...
* redirectProxyUrl: "https://github.example.com/api/auth"
* })
* ```
*
* @default `AUTH_REDIRECT_PROXY_URL` environment variable
*
* See also: [Guide: Securing a Preview Deployment](https://authjs.dev/guides/basics/deployment#securing-a-preview-deployment)
*/
redirectProxyUrl?: string
}

View File

@@ -56,10 +56,26 @@ export async function init({
providers: authOptions.providers,
url,
providerId,
options: authOptions,
})
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default
let isOnRedirectProxy = false
if (
(provider?.type === "oauth" || provider?.type === "oidc") &&
provider.redirectProxyUrl
) {
try {
isOnRedirectProxy =
new URL(provider.redirectProxyUrl).origin === url.origin
} catch {
throw new TypeError(
`redirectProxyUrl must be a valid URL. Received: ${provider.redirectProxyUrl}`
)
}
}
// User provided options are overridden by other options,
// except for the options with special handling above
const options: InternalOptions = {
@@ -113,6 +129,7 @@ export async function init({
callbacks: { ...defaultCallbacks, ...authOptions.callbacks },
logger,
callbackUrl: url.origin,
isOnRedirectProxy,
}
// Init cookies

View File

@@ -15,7 +15,7 @@ import type { Cookie } from "../cookie.js"
*/
export async function getAuthorizationUrl(
query: RequestInternal["query"],
options: InternalOptions<"oauth">
options: InternalOptions<"oauth" | "oidc">
): Promise<ResponseInternal> {
const { logger, provider } = options
@@ -41,12 +41,21 @@ export async function getAuthorizationUrl(
}
const authParams = url.searchParams
let redirect_uri: string = provider.callbackUrl
let data: object | undefined
if (!options.isOnRedirectProxy && provider.redirectProxyUrl) {
redirect_uri = provider.redirectProxyUrl
data = { origin: provider.callbackUrl }
logger.debug("using redirect proxy", { redirect_uri, data })
}
const params = Object.assign(
{
response_type: "code",
// clientId can technically be undefined, should we check this in assert.ts or rely on the Authorization Server to do it?
client_id: provider.clientId,
redirect_uri: provider.callbackUrl,
redirect_uri,
// @ts-expect-error TODO:
...provider.authorization?.params,
},
@@ -58,7 +67,7 @@ export async function getAuthorizationUrl(
const cookies: Cookie[] = []
const state = await checks.state.create(options)
const state = await checks.state.create(options, data)
if (state) {
authParams.set("state", state.value)
cookies.push(state.cookie)
@@ -68,7 +77,7 @@ export async function getAuthorizationUrl(
if (as && !as.code_challenge_methods_supported?.includes("S256")) {
// We assume S256 PKCE support, if the server does not advertise that,
// a random `nonce` must be used for CSRF protection.
provider.checks = ["nonce"]
if (provider.type === "oidc") provider.checks = ["nonce"] as any
} else {
const { value, cookie } = await checks.pkce.create(options)
authParams.set("code_challenge", value)

View File

@@ -24,7 +24,8 @@ import type { Cookie } from "../cookie.js"
export async function handleOAuth(
query: RequestInternal["query"],
cookies: RequestInternal["cookies"],
options: InternalOptions<"oauth">
options: InternalOptions<"oauth" | "oidc">,
randomState?: string
) {
const { logger, provider } = options
let as: o.AuthorizationServer
@@ -71,7 +72,12 @@ export async function handleOAuth(
const resCookies: Cookie[] = []
const state = await checks.state.use(cookies, resCookies, options)
const state = await checks.state.use(
cookies,
resCookies,
options,
randomState
)
const codeGrantParams = o.validateAuthResponse(
as,
@@ -91,11 +97,15 @@ export async function handleOAuth(
const codeVerifier = await checks.pkce.use(cookies, resCookies, options)
let redirect_uri = provider.callbackUrl
if (!options.isOnRedirectProxy && provider.redirectProxyUrl) {
redirect_uri = provider.redirectProxyUrl
}
let codeGrantResponse = await o.authorizationCodeGrantRequest(
as,
client,
codeGrantParams,
provider.callbackUrl,
redirect_uri,
codeVerifier ?? "auth" // TODO: review fallback code verifier
)

View File

@@ -1,6 +1,7 @@
import * as jose from "jose"
import * as o from "oauth4webapi"
import { InvalidCheck } from "../../errors.js"
import { encode, decode } from "../../jwt.js"
import { decode, encode } from "../../jwt.js"
import type {
CookiesOptions,
@@ -18,7 +19,8 @@ export async function signCookie(
type: keyof CookiesOptions,
value: string,
maxAge: number,
options: InternalOptions<"oauth">
options: InternalOptions<"oauth" | "oidc">,
data?: any
): Promise<Cookie> {
const { cookies, logger } = options
@@ -26,13 +28,11 @@ export async function signCookie(
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
const token: any = { value }
if (type === "state" && data) token.data = data
return {
name: cookies[type].name,
value: await encode<CheckPayload>({
...options.jwt,
maxAge,
token: { value },
}),
value: await encode({ ...options.jwt, maxAge, token }),
options: { ...cookies[type].options, expires },
}
}
@@ -92,14 +92,45 @@ export const pkce = {
}
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export function decodeState(value: string):
| {
/** If defined, a redirect proxy is being used to support multiple OAuth apps with a single callback URL */
origin?: string
/** Random value for CSRF protection */
random: string
}
| undefined {
try {
const decoder = new TextDecoder()
return JSON.parse(decoder.decode(jose.base64url.decode(value)))
} catch {}
}
export const state = {
async create(options: InternalOptions<"oauth">) {
if (!options.provider.checks.includes("state")) return
// TODO: support customizing the state
const value = o.generateRandomState()
async create(options: InternalOptions<"oauth">, data?: object) {
const { provider } = options
if (!provider.checks.includes("state")) {
if (data) {
throw new InvalidCheck(
"State data was provided but the provider is not configured to use state."
)
}
return
}
const encodedState = jose.base64url.encode(
JSON.stringify({ ...data, random: o.generateRandomState() })
)
const maxAge = STATE_MAX_AGE
const cookie = await signCookie("state", value, maxAge, options)
return { cookie, value }
const cookie = await signCookie(
"state",
encodedState,
maxAge,
options,
data
)
return { cookie, value: encodedState }
},
/**
* Returns state if the provider is configured to use state,
@@ -111,7 +142,8 @@ export const state = {
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">
options: InternalOptions<"oauth">,
paramRandom?: string
): Promise<string | undefined> {
const { provider } = options
if (!provider.checks.includes("state")) return
@@ -121,10 +153,23 @@ export const state = {
if (!state) throw new InvalidCheck("State cookie was missing.")
// IDEA: Let the user do something with the returned state
const value = await decode<CheckPayload>({ ...options.jwt, token: state })
const encodedState = await decode<CheckPayload>({
...options.jwt,
token: state,
})
if (!value?.value)
throw new InvalidCheck("State value could not be parsed.")
if (!encodedState?.value)
throw new InvalidCheck("State (cookie) value could not be parsed.")
const decodedState = decodeState(encodedState.value)
if (!decodedState)
throw new InvalidCheck("State (encoded) value could not be parsed.")
if (decodedState.random !== paramRandom)
throw new InvalidCheck(
`Random state values did not match. Expected: ${decodedState.random}. Got: ${paramRandom}`
)
// Clear the state cookie after use
resCookies.push({
@@ -133,13 +178,13 @@ export const state = {
options: { ...options.cookies.state.options, maxAge: 0 },
})
return value.value
return encodedState.value
},
}
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const nonce = {
async create(options: InternalOptions<"oauth">) {
async create(options: InternalOptions<"oidc">) {
if (!options.provider.checks.includes("nonce")) return
const value = o.generateRandomNonce()
const maxAge = NONCE_MAX_AGE
@@ -156,7 +201,7 @@ export const nonce = {
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">
options: InternalOptions<"oidc">
): Promise<string | undefined> {
const { provider } = options

View File

@@ -0,0 +1,34 @@
import { InvalidCheck } from "../../errors.js"
import { decodeState } from "./checks.js"
import type { OAuthConfigInternal } from "../../providers/oauth.js"
import type { InternalOptions, RequestInternal } from "../../types.js"
/**
* When the authorization flow contains a state, we check if it's a redirect proxy
* and if so, we return the random state and the original redirect URL.
*/
export function handleState(
query: RequestInternal["query"],
provider: OAuthConfigInternal<any>,
isOnRedirectProxy: InternalOptions["isOnRedirectProxy"]
) {
let randomState: string | undefined
let proxyRedirect: string | undefined
if (provider.redirectProxyUrl && !query?.state) {
throw new InvalidCheck(
"Missing state in query, but required for redirect proxy"
)
}
const state = decodeState(query?.state)
randomState = state?.random
if (isOnRedirectProxy) {
if (!state?.origin) return { randomState }
proxyRedirect = `${state.origin}?${new URLSearchParams(query)}`
}
return { randomState, proxyRedirect }
}

View File

@@ -1,6 +1,5 @@
import { merge } from "./utils/merge.js"
import type { InternalProvider } from "../types.js"
import type {
OAuthConfig,
OAuthConfigInternal,
@@ -8,6 +7,7 @@ import type {
OAuthUserConfig,
Provider,
} from "../providers/index.js"
import type { AuthConfig, InternalProvider } from "../types.js"
/**
* Adds `signinUrl` and `callbackUrl` to each provider
@@ -17,11 +17,12 @@ export default function parseProviders(params: {
providers: Provider[]
url: URL
providerId?: string
options: AuthConfig
}): {
providers: InternalProvider[]
provider?: InternalProvider
} {
const { url, providerId } = params
const { url, providerId, options } = params
const providers = params.providers.map((p) => {
const provider = typeof p === "function" ? p() : p
@@ -34,6 +35,7 @@ export default function parseProviders(params: {
})
if (provider.type === "oauth" || provider.type === "oidc") {
merged.redirectProxyUrl ??= options.redirectProxyUrl
return normalizeOAuth(merged)
}
@@ -62,11 +64,17 @@ function normalizeOAuth(
const userinfo = normalizeEndpoint(c.userinfo, c.issuer)
const checks = c.checks ?? ["pkce"]
if (c.redirectProxyUrl) {
if (!checks.includes("state")) checks.push("state")
c.redirectProxyUrl = `${c.redirectProxyUrl}/callback/${c.id}`
}
return {
...c,
authorization,
token,
checks: c.checks ?? ["pkce"],
checks,
userinfo,
profile: c.profile ?? defaultProfile,
}

View File

@@ -1,15 +1,16 @@
import { handleLogin } from "../callback-handler.js"
import { CallbackRouteError, Verification } from "../../errors.js"
import { handleLogin } from "../callback-handler.js"
import { handleOAuth } from "../oauth/callback.js"
import { handleState } from "../oauth/handle-state.js"
import { createHash } from "../web.js"
import { handleAuthorized } from "./shared.js"
import type { AdapterSession } from "../../adapters.js"
import type {
Account,
InternalOptions,
RequestInternal,
ResponseInternal,
InternalOptions,
Account,
} from "../../types.js"
import type { Cookie, SessionStore } from "../cookie.js"
@@ -43,10 +44,22 @@ export async function callback(params: {
try {
if (provider.type === "oauth" || provider.type === "oidc") {
const { proxyRedirect, randomState } = handleState(
query,
provider,
options.isOnRedirectProxy
)
if (proxyRedirect) {
logger.debug("proxy redirect", { proxyRedirect, randomState })
return { redirect: proxyRedirect }
}
const authorizationResult = await handleOAuth(
query,
params.cookies,
options
options,
randomState
)
if (authorizationResult.cookies.length) {

View File

@@ -18,7 +18,7 @@ import type {
export async function signin(
query: RequestInternal["query"],
body: RequestInternal["body"],
options: InternalOptions<"oauth" | "email">
options: InternalOptions<"oauth" | "oidc" | "email">
): Promise<ResponseInternal> {
const { url, logger, provider } = options
try {

View File

@@ -37,7 +37,7 @@ export async function toInternalRequest(
const action = actions.find((a) => pathname.includes(a))
if (!action) {
throw new UnknownAction("Cannot detect action.")
throw new UnknownAction(`Cannot detect action in pathname (${pathname}).`)
}
if (req.method !== "GET" && req.method !== "POST") {

View File

@@ -1,8 +1,14 @@
import type { Client } from "oauth4webapi"
import type { Awaitable, Profile, TokenSet, User } from "../types.js"
import type { CommonProviderOptions } from "../providers/index.js"
import type {
AuthConfig,
Awaitable,
Profile,
TokenSet,
User,
} from "../types.js"
// TODO:
// TODO: fix types
type AuthorizationParameters = any
type CallbackParamsType = any
type IssuerMetadata = any
@@ -95,7 +101,7 @@ export interface OAuthProviderButtonStyles {
textDark: string
}
/** TODO: */
/** TODO: Document */
export interface OAuth2Config<Profile>
extends CommonProviderOptions,
PartialIssuer {
@@ -143,11 +149,14 @@ export interface OAuth2Config<Profile>
* The CSRF protection performed on the callback endpoint.
* @default ["pkce"]
*
* @note When `redirectProxyUrl` or {@link AuthConfig.redirectProxyUrl} is set,
* `"state"` will be added to checks automatically.
*
* [RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients (PKCE)](https://www.rfc-editor.org/rfc/rfc7636.html#section-4) |
* [RFC 6749 - The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.1) |
* [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) |
*/
checks?: Array<"pkce" | "state" | "none" | "nonce">
checks?: Array<"pkce" | "state" | "none">
clientId?: string
clientSecret?: string
/**
@@ -157,9 +166,20 @@ export interface OAuth2Config<Profile>
client?: Partial<Client>
style?: OAuthProviderButtonStyles
/**
* [Documentation](https://authjs.dev/reference/providers/oauth#allowdangerousemailaccountlinking-option)
* Normally, when you sign in with an OAuth provider and another account
* with the same email address already exists,
* the accounts are not linked automatically.
*
* Automatic account linking on sign in is not secure
* between arbitrary providers and is disabled by default.
* Learn more in our [Security FAQ](https://authjs.dev/reference/faq#security).
*
* However, it may be desirable to allow automatic account linking if you trust that the provider involved has securely verified the email address
* associated with the account. Set `allowDangerousEmailAccountLinking: true`
* to enable automatic account linking.
*/
allowDangerousEmailAccountLinking?: boolean
redirectProxyUrl?: AuthConfig["redirectProxyUrl"]
/**
* The options provided by the user.
* We will perform a deep-merge of these values
@@ -170,10 +190,11 @@ export interface OAuth2Config<Profile>
options?: OAuthUserConfig<Profile>
}
/** TODO: */
/** TODO: Document */
export interface OIDCConfig<Profile>
extends Omit<OAuth2Config<Profile>, "type"> {
extends Omit<OAuth2Config<Profile>, "type" | "checks"> {
type: "oidc"
checks?: OAuth2Config<Profile>["checks"] & Array<"nonce">
}
export type OAuthConfig<Profile> = OIDCConfig<Profile> | OAuth2Config<Profile>
@@ -186,7 +207,7 @@ export type OAuthEndpointType = "authorization" | "token" | "userinfo"
*/
export type OAuthConfigInternal<Profile> = Omit<
OAuthConfig<Profile>,
OAuthEndpointType
OAuthEndpointType | "redirectProxyUrl"
> & {
authorization?: { url: URL }
token?: {
@@ -196,8 +217,24 @@ export type OAuthConfigInternal<Profile> = Omit<
conform?: TokenEndpointHandler["conform"]
}
userinfo?: { url: URL; request?: UserinfoEndpointHandler["request"] }
/**
* Reconstructed from {@link OAuth2Config.redirectProxyUrl},
* adding the callback action and provider id onto the URL.
*
* If defined, it is favoured over {@link OAuthConfigInternal.callbackUrl} in the authorization request.
*
* When {@link InternalOptions.isOnRedirectProxy} is set, the actual value is saved in the decoded `state.origin` parameter.
*
* @example `"https://auth.example.com/api/auth/callback/:provider"`
*
*/
redirectProxyUrl?: OAuth2Config<Profile>["redirectProxyUrl"]
} & Pick<Required<OAuthConfig<Profile>>, "clientId" | "checks" | "profile">
export type OIDCConfigInternal<Profile> = OAuthConfigInternal<Profile> & {
checks: OIDCConfig<Profile>["checks"]
}
export type OAuthUserConfig<Profile> = Omit<
Partial<OAuthConfig<Profile>>,
"options" | "type"

View File

@@ -4,7 +4,6 @@ export default function OneLogin(options) {
id: "onelogin",
name: "OneLogin",
type: "oidc",
// TODO: Verify if issuer has "oidc/2" and remove if it does
wellKnown: `${options.issuer}/oidc/2/.well-known/openid-configuration`,
options,
}

View File

@@ -22,7 +22,6 @@ export default function Trakt<P extends TraktUser>(
id: "trakt",
name: "Trakt",
type: "oauth",
// when default, trakt returns auth error. TODO: Does it?
authorization: "https://trakt.tv/oauth/authorize?scope=",
token: "https://api.trakt.tv/oauth/token",
userinfo: {

View File

@@ -169,7 +169,6 @@ export default function Wikimedia<P extends WikimediaProfile>(
type: "oauth",
token: "https://meta.wikimedia.org/w/rest.php/oauth2/access_token",
userinfo: "https://meta.wikimedia.org/w/rest.php/oauth2/resource/profile",
// TODO: is empty scope necessary?
authorization:
"https://meta.wikimedia.org/w/rest.php/oauth2/authorize?scope=",
profile(profile) {

View File

@@ -61,21 +61,22 @@ import type {
OpenIDTokenEndpointResponse,
} from "oauth4webapi"
import type { Adapter, AdapterUser } from "./adapters.js"
import { AuthConfig } from "./index.js"
import type { JWT, JWTOptions } from "./jwt.js"
import type { Cookie } from "./lib/cookie.js"
import type { LoggerInstance } from "./lib/utils/logger.js"
import type {
CredentialInput,
CredentialsConfig,
EmailConfig,
OAuthConfigInternal,
OIDCConfigInternal,
ProviderType,
} from "./providers/index.js"
import type { JWT, JWTOptions } from "./jwt.js"
import type { Cookie } from "./lib/cookie.js"
import type { LoggerInstance } from "./lib/utils/logger.js"
import { AuthConfig } from "./index.js"
export type { AuthConfig } from "./index.js"
export type Awaitable<T> = T | PromiseLike<T>
export type { LoggerInstance }
export type Awaitable<T> = T | PromiseLike<T>
/**
* Change the theme of the built-in pages.
@@ -372,12 +373,15 @@ export interface User {
/** @internal */
export type InternalProvider<T = ProviderType> = (T extends "oauth"
? OAuthConfigInternal<any>
: T extends "oidc"
? OIDCConfigInternal<any>
: T extends "email"
? EmailConfig
: T extends "credentials"
? CredentialsConfig
: never) & {
signinUrl: string
/** @example `"https://example.com/api/auth/callback/id"` */
callbackUrl: string
}
@@ -415,7 +419,6 @@ export interface ResponseInternal<
cookies?: Cookie[]
}
// TODO: rename to AuthConfigInternal
/** @internal */
export interface InternalOptions<TProviderType = ProviderType> {
providers: InternalProvider[]
@@ -436,4 +439,9 @@ export interface InternalOptions<TProviderType = ProviderType> {
callbacks: CallbacksOptions
cookies: CookiesOptions
callbackUrl: string
/**
* If true, the OAuth callback is being proxied by the server to the original URL.
* See also {@link OAuthConfigInternal.redirectProxyUrl}.
*/
isOnRedirectProxy: boolean
}

View File

@@ -204,8 +204,8 @@ We're happy to announce we've recently created an [OpenCollective](https://openc
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://clerk.dev" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/49538330?s=200&v=4" alt="Prisma Logo" />
<a href="https://clerk.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/49538330?s=200&v=4" alt="Clerk Logo" />
</a><br />
<div>Clerk</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>

169
pnpm-lock.yaml generated
View File

@@ -67,7 +67,7 @@ importers:
dotenv: ^16.0.3
fake-smtp-server: ^0.8.0
faunadb: ^4
next: 13.1.1
next: 13.3.0
next-auth: workspace:*
nodemailer: ^6
pg: ^8.7.3
@@ -85,7 +85,7 @@ importers:
'@prisma/client': 3.15.2_prisma@3.15.2
'@supabase/supabase-js': 2.0.5
faunadb: 4.6.0
next: 13.1.1_biqbaboplfbrettd7655fr4n2y
next: 13.3.0_biqbaboplfbrettd7655fr4n2y
next-auth: link:../../../packages/next-auth
nodemailer: 6.8.0
react: 18.2.0
@@ -8332,40 +8332,9 @@ packages:
is-promise: 4.0.0
dev: true
/@next/env/13.1.1:
resolution: {integrity: sha512-vFMyXtPjSAiOXOywMojxfKIqE3VWN5RCAx+tT3AS3pcKjMLFTCJFUWsKv8hC+87Z1F4W3r68qTwDFZIFmd5Xkw==}
dev: false
/@next/env/13.3.0:
resolution: {integrity: sha512-AjppRV4uG3No7L1plinoTQETH+j2F10TEnrMfzbTUYwze5sBUPveeeBAPZPm8OkJZ1epq9OyYKhZrvbD6/9HCQ==}
/@next/swc-android-arm-eabi/13.1.1:
resolution: {integrity: sha512-qnFCx1kT3JTWhWve4VkeWuZiyjG0b5T6J2iWuin74lORCupdrNukxkq9Pm+Z7PsatxuwVJMhjUoYz7H4cWzx2A==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
requiresBuild: true
dev: false
optional: true
/@next/swc-android-arm64/13.1.1:
resolution: {integrity: sha512-eCiZhTzjySubNqUnNkQCjU3Fh+ep3C6b5DCM5FKzsTH/3Gr/4Y7EiaPZKILbvnXmhWtKPIdcY6Zjx51t4VeTfA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: false
optional: true
/@next/swc-darwin-arm64/13.1.1:
resolution: {integrity: sha512-9zRJSSIwER5tu9ADDkPw5rIZ+Np44HTXpYMr0rkM656IvssowPxmhK0rTreC1gpUCYwFsRbxarUJnJsTWiutPg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@next/swc-darwin-arm64/13.3.0:
resolution: {integrity: sha512-DmIQCNq6JtccLPPBzf0dgh2vzMWt5wjxbP71pCi5EWpWYE3MsP6FcRXi4MlAmFNDQOfcFXR2r7kBeG1LpZUh1w==}
engines: {node: '>= 10'}
@@ -8374,15 +8343,6 @@ packages:
requiresBuild: true
optional: true
/@next/swc-darwin-x64/13.1.1:
resolution: {integrity: sha512-qWr9qEn5nrnlhB0rtjSdR00RRZEtxg4EGvicIipqZWEyayPxhUu6NwKiG8wZiYZCLfJ5KWr66PGSNeDMGlNaiA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@next/swc-darwin-x64/13.3.0:
resolution: {integrity: sha512-oQoqFa88OGgwnYlnAGHVct618FRI/749se0N3S8t9Bzdv5CRbscnO0RcX901+YnNK4Q6yeiizfgO3b7kogtsZg==}
engines: {node: '>= 10'}
@@ -8391,33 +8351,6 @@ packages:
requiresBuild: true
optional: true
/@next/swc-freebsd-x64/13.1.1:
resolution: {integrity: sha512-UwP4w/NcQ7V/VJEj3tGVszgb4pyUCt3lzJfUhjDMUmQbzG9LDvgiZgAGMYH6L21MoyAATJQPDGiAMWAPKsmumA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm-gnueabihf/13.1.1:
resolution: {integrity: sha512-CnsxmKHco9sosBs1XcvCXP845Db+Wx1G0qouV5+Gr+HT/ZlDYEWKoHVDgnJXLVEQzq4FmHddBNGbXvgqM1Gfkg==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm64-gnu/13.1.1:
resolution: {integrity: sha512-JfDq1eri5Dif+VDpTkONRd083780nsMCOKoFG87wA0sa4xL8LGcXIBAkUGIC1uVy9SMsr2scA9CySLD/i+Oqiw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm64-gnu/13.3.0:
resolution: {integrity: sha512-Wzz2p/WqAJUqTVoLo6H18WMeAXo3i+9DkPDae4oQG8LMloJ3if4NEZTnOnTUlro6cq+S/W4pTGa97nWTrOjbGw==}
engines: {node: '>= 10'}
@@ -8426,15 +8359,6 @@ packages:
requiresBuild: true
optional: true
/@next/swc-linux-arm64-musl/13.1.1:
resolution: {integrity: sha512-GA67ZbDq2AW0CY07zzGt07M5b5Yaq5qUpFIoW3UFfjOPgb0Sqf3DAW7GtFMK1sF4ROHsRDMGQ9rnT0VM2dVfKA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm64-musl/13.3.0:
resolution: {integrity: sha512-xPVrIQOQo9WXJYgmoTlMnAD/HlR/1e1ZIWGbwIzEirXBVBqMARUulBEIKdC19zuvoJ477qZJgBDCKtKEykCpyQ==}
engines: {node: '>= 10'}
@@ -8443,15 +8367,6 @@ packages:
requiresBuild: true
optional: true
/@next/swc-linux-x64-gnu/13.1.1:
resolution: {integrity: sha512-nnjuBrbzvqaOJaV+XgT8/+lmXrSCOt1YYZn/irbDb2fR2QprL6Q7WJNgwsZNxiLSfLdv+2RJGGegBx9sLBEzGA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-x64-gnu/13.3.0:
resolution: {integrity: sha512-jOFlpGuPD7W2tuXVJP4wt9a3cpNxWAPcloq5EfMJRiXsBBOjLVFZA7boXYxEBzSVgUiVVr1V9T0HFM7pULJ1qA==}
engines: {node: '>= 10'}
@@ -8460,15 +8375,6 @@ packages:
requiresBuild: true
optional: true
/@next/swc-linux-x64-musl/13.1.1:
resolution: {integrity: sha512-CM9xnAQNIZ8zf/igbIT/i3xWbQZYaF397H+JroF5VMOCUleElaMdQLL5riJml8wUfPoN3dtfn2s4peSr3azz/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-x64-musl/13.3.0:
resolution: {integrity: sha512-2OwKlzaBgmuet9XYHc3KwsEilzb04F540rlRXkAcjMHL7eCxB7uZIGtsVvKOnQLvC/elrUegwSw1+5f7WmfyOw==}
engines: {node: '>= 10'}
@@ -8477,15 +8383,6 @@ packages:
requiresBuild: true
optional: true
/@next/swc-win32-arm64-msvc/13.1.1:
resolution: {integrity: sha512-pzUHOGrbgfGgPlOMx9xk3QdPJoRPU+om84hqVoe6u+E0RdwOG0Ho/2UxCgDqmvpUrMab1Deltlt6RqcXFpnigQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-arm64-msvc/13.3.0:
resolution: {integrity: sha512-OeHiA6YEvndxT46g+rzFK/MQTfftKxJmzslERMu9LDdC6Kez0bdrgEYed5eXFK2Z1viKZJCGRlhd06rBusyztA==}
engines: {node: '>= 10'}
@@ -8494,15 +8391,6 @@ packages:
requiresBuild: true
optional: true
/@next/swc-win32-ia32-msvc/13.1.1:
resolution: {integrity: sha512-WeX8kVS46aobM9a7Xr/kEPcrTyiwJqQv/tbw6nhJ4fH9xNZ+cEcyPoQkwPo570dCOLz3Zo9S2q0E6lJ/EAUOBg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-ia32-msvc/13.3.0:
resolution: {integrity: sha512-4aB7K9mcVK1lYEzpOpqWrXHEZympU3oK65fnNcY1Qc4HLJFLJj8AViuqQd4jjjPNuV4sl8jAwTz3gN5VNGWB7w==}
engines: {node: '>= 10'}
@@ -8511,15 +8399,6 @@ packages:
requiresBuild: true
optional: true
/@next/swc-win32-x64-msvc/13.1.1:
resolution: {integrity: sha512-mVF0/3/5QAc5EGVnb8ll31nNvf3BWpPY4pBb84tk+BfQglWLqc5AC9q1Ht/YMWiEgs8ALNKEQ3GQnbY0bJF2Gg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-x64-msvc/13.3.0:
resolution: {integrity: sha512-Reer6rkLLcoOvB0dd66+Y7WrWVFH7sEEkF/4bJCIfsSKnTStTYaHtwIJAwbqnt9I392Tqvku0KkoqZOryWV9LQ==}
engines: {node: '>= 10'}
@@ -24732,50 +24611,6 @@ packages:
/next-tick/1.1.0:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
/next/13.1.1_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-R5eBAaIa3X7LJeYvv1bMdGnAVF4fVToEjim7MkflceFPuANY3YyvFxXee/A+acrSYwYPvOvf7f6v/BM/48ea5w==}
engines: {node: '>=14.6.0'}
hasBin: true
peerDependencies:
fibers: '>= 3.1.0'
node-sass: ^6.0.0 || ^7.0.0
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
peerDependenciesMeta:
fibers:
optional: true
node-sass:
optional: true
sass:
optional: true
dependencies:
'@next/env': 13.1.1
'@swc/helpers': 0.4.14
caniuse-lite: 1.0.30001431
postcss: 8.4.14
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
styled-jsx: 5.1.1_react@18.2.0
optionalDependencies:
'@next/swc-android-arm-eabi': 13.1.1
'@next/swc-android-arm64': 13.1.1
'@next/swc-darwin-arm64': 13.1.1
'@next/swc-darwin-x64': 13.1.1
'@next/swc-freebsd-x64': 13.1.1
'@next/swc-linux-arm-gnueabihf': 13.1.1
'@next/swc-linux-arm64-gnu': 13.1.1
'@next/swc-linux-arm64-musl': 13.1.1
'@next/swc-linux-x64-gnu': 13.1.1
'@next/swc-linux-x64-musl': 13.1.1
'@next/swc-win32-arm64-msvc': 13.1.1
'@next/swc-win32-ia32-msvc': 13.1.1
'@next/swc-win32-x64-msvc': 13.1.1
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
dev: false
/next/13.3.0_4cc5zw5azim2bix77d63le72su:
resolution: {integrity: sha512-OVTw8MpIPa12+DCUkPqRGPS3thlJPcwae2ZL4xti3iBff27goH024xy4q2lhlsdoYiKOi8Kz6uJoLW/GXwgfOA==}
engines: {node: '>=14.6.0'}