@auth/core
Advanced tools
Comparing version 0.2.5 to 0.3.0
@@ -17,3 +17,41 @@ /** | ||
} | ||
/** @todo */ | ||
/** | ||
* There was an error while trying to finish up authenticating the user. | ||
* Depending on the type of provider, this could be for multiple reasons. | ||
* | ||
* :::tip | ||
* Check out `[auth][details]` in the error message to know which provider failed. | ||
* @example | ||
* ```sh | ||
* [auth][details]: { "provider": "github" } | ||
* ``` | ||
* ::: | ||
* | ||
* For an **OAuth provider**, possible causes are: | ||
* - The user denied access to the application | ||
* - There was an error parsing the OAuth Profile: | ||
* Check out the provider's `profile` or `userinfo.request` method to make sure | ||
* it correctly fetches the user's profile. | ||
* - The `signIn` or `jwt` callback methods threw an uncaught error: | ||
* Check the callback method implementations. | ||
* | ||
* For an **Email provider**, possible causes are: | ||
* - The provided email/token combination was invalid/missing: | ||
* Check if the provider's `sendVerificationRequest` method correctly sends the email. | ||
* - The provided email/token combination has expired: | ||
* Ask the user to log in again. | ||
* - There was an error with the database: | ||
* Check the database logs. | ||
* | ||
* For a **Credentials provider**, possible causes are: | ||
* - The `authorize` method threw an uncaught error: | ||
* Check the provider's `authorize` method. | ||
* - The `signIn` or `jwt` callback methods threw an uncaught error: | ||
* Check the callback method implementations. | ||
* | ||
* :::tip | ||
* Check out `[auth][cause]` in the error message for more details. | ||
* It will show the original stack trace. | ||
* ::: | ||
*/ | ||
export declare class CallbackRouteError extends AuthError { | ||
@@ -84,2 +122,9 @@ } | ||
} | ||
/** | ||
* The user's email/token combination was invalid. | ||
* This could be because the email/token combination was not found in the database, | ||
* or because it token has expired. Ask the user to log in again. | ||
*/ | ||
export declare class Verification extends AuthError { | ||
} | ||
//# sourceMappingURL=errors.d.ts.map |
/** @internal */ | ||
export class AuthError extends Error { | ||
constructor(message, metadata) { | ||
constructor(message, cause) { | ||
if (message instanceof Error) { | ||
super(message.message); | ||
this.stack = message.stack; | ||
super(undefined, { | ||
cause: { err: message, ...message.cause, ...cause }, | ||
}); | ||
} | ||
else | ||
super(message); | ||
this.name = this.constructor.name; | ||
this.metadata = metadata; | ||
else if (typeof message === "string") { | ||
if (cause instanceof Error) { | ||
cause = { err: cause, ...cause.cause }; | ||
} | ||
super(message, cause); | ||
} | ||
else { | ||
super(undefined, message); | ||
} | ||
Error.captureStackTrace?.(this, this.constructor); | ||
this.name = | ||
message instanceof AuthError ? message.name : this.constructor.name; | ||
} | ||
@@ -31,3 +39,41 @@ } | ||
} | ||
/** @todo */ | ||
/** | ||
* There was an error while trying to finish up authenticating the user. | ||
* Depending on the type of provider, this could be for multiple reasons. | ||
* | ||
* :::tip | ||
* Check out `[auth][details]` in the error message to know which provider failed. | ||
* @example | ||
* ```sh | ||
* [auth][details]: { "provider": "github" } | ||
* ``` | ||
* ::: | ||
* | ||
* For an **OAuth provider**, possible causes are: | ||
* - The user denied access to the application | ||
* - There was an error parsing the OAuth Profile: | ||
* Check out the provider's `profile` or `userinfo.request` method to make sure | ||
* it correctly fetches the user's profile. | ||
* - The `signIn` or `jwt` callback methods threw an uncaught error: | ||
* Check the callback method implementations. | ||
* | ||
* For an **Email provider**, possible causes are: | ||
* - The provided email/token combination was invalid/missing: | ||
* Check if the provider's `sendVerificationRequest` method correctly sends the email. | ||
* - The provided email/token combination has expired: | ||
* Ask the user to log in again. | ||
* - There was an error with the database: | ||
* Check the database logs. | ||
* | ||
* For a **Credentials provider**, possible causes are: | ||
* - The `authorize` method threw an uncaught error: | ||
* Check the provider's `authorize` method. | ||
* - The `signIn` or `jwt` callback methods threw an uncaught error: | ||
* Check the callback method implementations. | ||
* | ||
* :::tip | ||
* Check out `[auth][cause]` in the error message for more details. | ||
* It will show the original stack trace. | ||
* ::: | ||
*/ | ||
export class CallbackRouteError extends AuthError { | ||
@@ -98,1 +144,8 @@ } | ||
} | ||
/** | ||
* The user's email/token combination was invalid. | ||
* This could be because the email/token combination was not found in the database, | ||
* or because it token has expired. Ask the user to log in again. | ||
*/ | ||
export class Verification extends AuthError { | ||
} |
@@ -49,3 +49,3 @@ /** | ||
* const request = new Request("https://example.com") | ||
* const resposne = await AuthHandler(request, { | ||
* const response = await AuthHandler(request, { | ||
* providers: [...], | ||
@@ -69,3 +69,3 @@ * secret: "...", | ||
* const request = new Request("https://example.com") | ||
* const resposne = await AuthHandler(request, authConfig) | ||
* const response = await AuthHandler(request, authConfig) | ||
* ``` | ||
@@ -209,3 +209,14 @@ * | ||
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 declare const skipCSRFCheck: unique symbol; | ||
//# sourceMappingURL=index.d.ts.map |
12
index.js
@@ -50,3 +50,3 @@ /** | ||
* const request = new Request("https://example.com") | ||
* const resposne = await AuthHandler(request, { | ||
* const response = await AuthHandler(request, { | ||
* providers: [...], | ||
@@ -110,1 +110,11 @@ * secret: "...", | ||
} | ||
/** | ||
* :::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"); |
@@ -20,3 +20,3 @@ import { defaultCookies } from "./cookie.js"; | ||
if (!warned && options.debug) | ||
warnings.push("debug_enabled"); | ||
warnings.push("debug-enabled"); | ||
if (!options.trustHost) { | ||
@@ -23,0 +23,0 @@ return new UntrustedHost(`Host must be trusted. URL was: ${request.url}`); |
@@ -101,3 +101,3 @@ import { AccountNotLinked } from "../errors.js"; | ||
// and need to return an error. | ||
throw new AccountNotLinked("The account is already associated with another user"); | ||
throw new AccountNotLinked("The account is already associated with another user", { provider: account.provider }); | ||
} | ||
@@ -159,3 +159,3 @@ // If there is no active session, but the account being signed in with is already | ||
// to sign in via email to verify their identity and then link the accounts. | ||
throw new AccountNotLinked("Another account already exists with the same e-mail address"); | ||
throw new AccountNotLinked("Another account already exists with the same e-mail address", { provider: account.provider }); | ||
} | ||
@@ -162,0 +162,0 @@ } |
@@ -1,3 +0,3 @@ | ||
import type { RequestInternal, ResponseInternal, AuthConfig } from "../types.js"; | ||
import type { AuthConfig, RequestInternal, ResponseInternal } from "../types.js"; | ||
export declare function AuthInternal<Body extends string | Record<string, any> | any[]>(request: RequestInternal, authOptions: AuthConfig): Promise<ResponseInternal<Body>>; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -0,3 +1,4 @@ | ||
import { UnknownAction } from "../errors.js"; | ||
import { skipCSRFCheck } from "../index.js"; | ||
import { SessionStore } from "./cookie.js"; | ||
import { UnknownAction } from "../errors.js"; | ||
import { init } from "./init.js"; | ||
@@ -8,2 +9,3 @@ import renderPage from "./pages/index.js"; | ||
const { action, providerId, error, method } = request; | ||
const csrfDisabled = authOptions.skipCSRFCheck === skipCSRFCheck; | ||
const { options, cookies } = await init({ | ||
@@ -18,2 +20,3 @@ authOptions, | ||
isPost: method === "POST", | ||
csrfDisabled, | ||
}); | ||
@@ -34,3 +37,12 @@ const sessionStore = new SessionStore(options.cookies.sessionToken, request, options.logger); | ||
} | ||
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 { | ||
@@ -41,2 +53,3 @@ headers: { "Content-Type": "application/json" }, | ||
}; | ||
} | ||
case "signin": | ||
@@ -106,4 +119,3 @@ if (pages.signIn) { | ||
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, options); | ||
@@ -116,4 +128,3 @@ if (signin.cookies) | ||
case "signout": | ||
// Verified CSRF Token required for signout | ||
if (options.csrfTokenVerified) { | ||
if (csrfDisabled || options.csrfTokenVerified) { | ||
const signout = await routes.signout(sessionStore, options); | ||
@@ -129,2 +140,3 @@ if (signout.cookies) | ||
if (options.provider.type === "credentials" && | ||
!csrfDisabled && | ||
!options.csrfTokenVerified) { | ||
@@ -131,0 +143,0 @@ return { redirect: `${options.url}/signin?csrf=true`, cookies }; |
@@ -13,2 +13,3 @@ import * as cookie from "./cookie.js"; | ||
/** Is the incoming request a POST request? */ | ||
csrfDisabled: boolean; | ||
isPost: boolean; | ||
@@ -18,3 +19,3 @@ cookies: RequestInternal["cookies"]; | ||
/** Initialize all internal options and cookies. */ | ||
export declare function init({ authOptions, providerId, action, url: reqUrl, cookies: reqCookies, callbackUrl: reqCallbackUrl, csrfToken: reqCsrfToken, isPost, }: InitParams): Promise<{ | ||
export declare function init({ authOptions, providerId, action, url: reqUrl, cookies: reqCookies, callbackUrl: reqCallbackUrl, csrfToken: reqCsrfToken, csrfDisabled, isPost, }: InitParams): Promise<{ | ||
options: InternalOptions; | ||
@@ -21,0 +22,0 @@ cookies: cookie.Cookie[]; |
@@ -11,3 +11,3 @@ import * as jwt from "../jwt.js"; | ||
/** Initialize all internal options and cookies. */ | ||
export async function init({ authOptions, providerId, action, url: reqUrl, cookies: reqCookies, callbackUrl: reqCallbackUrl, csrfToken: reqCsrfToken, isPost, }) { | ||
export async function init({ authOptions, providerId, action, url: reqUrl, cookies: reqCookies, callbackUrl: reqCallbackUrl, csrfToken: reqCsrfToken, csrfDisabled, isPost, }) { | ||
// TODO: move this to web.ts | ||
@@ -54,3 +54,3 @@ const parsed = parseUrl(reqUrl.origin + | ||
updateAge: 24 * 60 * 60, | ||
generateSessionToken: crypto.randomUUID, | ||
generateSessionToken: () => crypto.randomUUID(), | ||
...authOptions.session, | ||
@@ -78,16 +78,18 @@ }, | ||
const cookies = []; | ||
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, | ||
}); | ||
} | ||
} | ||
@@ -94,0 +96,0 @@ const { callbackUrl, callbackUrlCookie } = await createCallbackUrl({ |
@@ -1,3 +0,2 @@ | ||
import type { CookiesOptions, InternalOptions, RequestInternal, ResponseInternal } from "../../types.js"; | ||
import type { Cookie } from "../cookie.js"; | ||
import type { InternalOptions, RequestInternal, ResponseInternal } from "../../types.js"; | ||
/** | ||
@@ -9,4 +8,2 @@ * Generates an authorization/request token URL. | ||
export declare function getAuthorizationUrl(query: RequestInternal["query"], options: InternalOptions<"oauth">): Promise<ResponseInternal>; | ||
/** Returns a signed cookie. */ | ||
export declare function signCookie(type: keyof CookiesOptions, value: string, maxAge: number, options: InternalOptions<"oauth">): Promise<Cookie>; | ||
//# sourceMappingURL=authorization-url.d.ts.map |
@@ -0,1 +1,2 @@ | ||
import * as checks from "./checks.js"; | ||
import * as o from "oauth4webapi"; | ||
@@ -36,6 +37,6 @@ /** | ||
const cookies = []; | ||
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); | ||
} | ||
@@ -49,12 +50,12 @@ if (provider.checks?.includes("pkce")) { | ||
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); | ||
} | ||
@@ -69,34 +70,1 @@ // TODO: This does not work in normalizeOAuth because authorization endpoint can come from discovery | ||
} | ||
/** Returns a signed cookie. */ | ||
export async function signCookie(type, value, maxAge, options) { | ||
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) { | ||
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) { | ||
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) { | ||
const raw = o.generateRandomNonce(); | ||
const maxAge = NONCE_MAX_AGE; | ||
return await signCookie("nonce", raw, maxAge, options); | ||
} |
@@ -0,6 +1,4 @@ | ||
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"; | ||
/** | ||
@@ -46,3 +44,3 @@ * Handles the following OAuth steps. | ||
const resCookies = []; | ||
const state = await useState(cookies, resCookies, options); | ||
const state = await checks.state.use(cookies, resCookies, options); | ||
const parameters = o.validateAuthResponse(as, client, new URLSearchParams(query), provider.checks.includes("state") ? state : o.skipStateCheck); | ||
@@ -57,12 +55,17 @@ /** https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1 */ | ||
} | ||
const codeVerifier = await usePKCECodeVerifier(cookies?.[options.cookies.pkceCodeVerifier.name], options); | ||
const codeVerifier = await checks.pkce.use(cookies?.[options.cookies.pkceCodeVerifier.name], options); | ||
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(as, client, parameters, provider.callbackUrl, codeVerifier?.codeVerifier ?? "auth" // TODO: review fallback code verifier | ||
let codeGrantResponse = await o.authorizationCodeGrantRequest(as, client, parameters, provider.callbackUrl, codeVerifier?.codeVerifier ?? "auth" // TODO: review fallback code verifier | ||
); | ||
if (provider.token?.conform) { | ||
codeGrantResponse = | ||
(await provider.token.conform(codeGrantResponse.clone())) ?? | ||
codeGrantResponse; | ||
} | ||
let challenges; | ||
@@ -69,0 +72,0 @@ if ((challenges = o.parseWwwAuthenticateChallenges(codeGrantResponse))) { |
@@ -38,4 +38,4 @@ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime"; | ||
`, | ||
} })), theme?.logo && _jsx("img", { src: theme.logo, alt: "Logo", className: "logo" }), _jsxs("div", { className: "card", children: [_jsx("h1", { children: heading }), _jsx("div", { className: "message", children: message }), signin] })] })), | ||
} })), _jsxs("div", { className: "card", children: [theme?.logo && _jsx("img", { src: theme?.logo, alt: "Logo", className: "logo" }), _jsx("h1", { children: heading }), _jsx("div", { className: "message", children: message }), signin] })] })), | ||
}; | ||
} |
@@ -20,10 +20,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime"; | ||
} | ||
if (typeof document !== "undefined" && theme.buttonText) { | ||
document.documentElement.style.setProperty("--button-text-color", theme.buttonText); | ||
} | ||
const error = errorType && | ||
(signinErrors[errorType.toLowerCase()] ?? | ||
signinErrors.default); | ||
// TODO: move logos | ||
const logos = "https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/next-auth/provider-logos"; | ||
const logos = "https://authjs.dev/img/providers"; | ||
return (_jsxs("div", { className: "signin", children: [theme.brandColor && (_jsx("style", { dangerouslySetInnerHTML: { | ||
__html: `:root {--brand-color: ${theme.brandColor}}`, | ||
} })), theme.logo && _jsx("img", { src: theme.logo, alt: "Logo", className: "logo" }), _jsxs("div", { className: "card", children: [error && (_jsx("div", { className: "error", children: _jsx("p", { children: error }) })), providers.map((provider, i) => (_jsxs("div", { className: "provider", children: [provider.type === "oauth" || provider.type === "oidc" ? (_jsxs("form", { action: provider.signinUrl, method: "POST", children: [_jsx("input", { type: "hidden", name: "csrfToken", value: csrfToken }), callbackUrl && (_jsx("input", { type: "hidden", name: "callbackUrl", value: callbackUrl })), _jsxs("button", { type: "submit", className: "button", style: { | ||
} })), theme.buttonText && (_jsx("style", { dangerouslySetInnerHTML: { | ||
__html: ` | ||
:root { | ||
--button-text-color: ${theme.buttonText} | ||
} | ||
`, | ||
} })), _jsxs("div", { className: "card", children: [error && (_jsx("div", { className: "error", children: _jsx("p", { children: error }) })), providers.map((provider, i) => (_jsxs("div", { className: "provider", children: [provider.type === "oauth" || provider.type === "oidc" ? (_jsxs("form", { action: provider.signinUrl, method: "POST", children: [_jsx("input", { type: "hidden", name: "csrfToken", value: csrfToken }), callbackUrl && (_jsx("input", { type: "hidden", name: "callbackUrl", value: callbackUrl })), _jsxs("button", { type: "submit", className: "button", style: { | ||
"--provider-bg": provider.style?.bg ?? "", | ||
@@ -33,3 +41,4 @@ "--provider-dark-bg": provider.style?.bgDark ?? "", | ||
"--provider-dark-color": provider.style?.textDark ?? "", | ||
}, children: [provider.style?.logo && (_jsx("img", { id: "provider-logo", src: `${provider.style.logo.startsWith("/") ? logos : ""}${provider.style.logo}` })), provider.style?.logoDark && (_jsx("img", { id: "provider-logo-dark", src: `${provider.style.logo.startsWith("/") ? logos : ""}${provider.style.logoDark}` })), _jsxs("span", { children: ["Sign in with ", provider.name] })] })] })) : null, (provider.type === "email" || provider.type === "credentials") && | ||
gap: 8, | ||
}, children: [provider.style?.logo && (_jsx("img", { loading: "lazy", height: 24, width: 24, id: "provider-logo", src: `${provider.style.logo.startsWith("/") ? logos : ""}${provider.style.logo}` })), provider.style?.logoDark && (_jsx("img", { loading: "lazy", height: 24, width: 24, id: "provider-logo-dark", src: `${provider.style.logo.startsWith("/") ? logos : ""}${provider.style.logoDark}` })), _jsxs("span", { children: ["Sign in with ", provider.name] })] })] })) : null, (provider.type === "email" || provider.type === "credentials") && | ||
i > 0 && | ||
@@ -39,4 +48,4 @@ providers[i - 1].type !== "email" && | ||
return (_jsxs("div", { children: [_jsx("label", { className: "section-header", htmlFor: `input-${credential}-for-${provider.id}-provider`, children: provider.credentials[credential].label ?? credential }), _jsx("input", { name: credential, id: `input-${credential}-for-${provider.id}-provider`, type: provider.credentials[credential].type ?? "text", placeholder: provider.credentials[credential].placeholder ?? "", ...provider.credentials[credential] })] }, `input-group-${provider.id}`)); | ||
}), _jsxs("button", { type: "submit", children: ["Sign in with ", provider.name] })] })), (provider.type === "email" || provider.type === "credentials") && | ||
}), _jsxs("button", { id: "submitButton", type: "submit", children: ["Sign in with ", provider.name] })] })), (provider.type === "email" || provider.type === "credentials") && | ||
i + 1 < providers.length && _jsx("hr", {})] }, provider.id)))] })] })); | ||
} |
@@ -10,3 +10,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime"; | ||
`, | ||
} })), theme.logo && _jsx("img", { src: theme.logo, alt: "Logo", className: "logo" }), _jsxs("div", { className: "card", children: [_jsx("h1", { children: "Signout" }), _jsx("p", { children: "Are you sure you want to sign out?" }), _jsxs("form", { action: `${url}/signout`, method: "POST", children: [_jsx("input", { type: "hidden", name: "csrfToken", value: csrfToken }), _jsx("button", { type: "submit", children: "Sign out" })] })] })] })); | ||
} })), theme.buttonText && (_jsx("style", { dangerouslySetInnerHTML: { | ||
__html: ` | ||
:root { | ||
--button-text-color: ${theme.buttonText} | ||
} | ||
`, | ||
} })), _jsxs("div", { className: "card", children: [theme.logo && _jsx("img", { src: theme.logo, alt: "Logo", className: "logo" }), _jsx("h1", { children: "Signout" }), _jsx("p", { children: "Are you sure you want to sign out?" }), _jsxs("form", { action: `${url}/signout`, method: "POST", children: [_jsx("input", { type: "hidden", name: "csrfToken", value: csrfToken }), _jsx("button", { id: "submitButton", type: "submit", children: "Sign out" })] })] })] })); | ||
} |
@@ -1,3 +0,3 @@ | ||
declare const _default: ":root {\n --border-width: 1px;\n --border-radius: 0.5rem;\n --color-error: #c94b4b;\n --color-info: #157efb;\n --color-info-text: #fff;\n}\n\n.__next-auth-theme-auto,\n.__next-auth-theme-light {\n --color-background: #fff;\n --color-text: #000;\n --color-primary: #444;\n --color-control-border: #bbb;\n --color-button-active-background: #f9f9f9;\n --color-button-active-border: #aaa;\n --color-seperator: #ccc;\n}\n\n.__next-auth-theme-dark {\n --color-background: #000;\n --color-text: #fff;\n --color-primary: #ccc;\n --color-control-border: #555;\n --color-button-active-background: #060606;\n --color-button-active-border: #666;\n --color-seperator: #444;\n}\n\n@media (prefers-color-scheme: dark) {\n .__next-auth-theme-auto {\n --color-background: #000;\n --color-text: #fff;\n --color-primary: #ccc;\n --color-control-border: #555;\n --color-button-active-background: #060606;\n --color-button-active-border: #666;\n --color-seperator: #444;\n }\n}\n\nbody {\n background-color: var(--color-background);\n margin: 0;\n padding: 0;\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,\n \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif,\n \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n}\n\nh1 {\n font-weight: 400;\n margin-bottom: 1.5rem;\n padding: 0 1rem;\n color: var(--color-text);\n}\n\np {\n color: var(--color-text);\n}\n\nform {\n margin: 0;\n padding: 0;\n}\n\nlabel {\n font-weight: 500;\n text-align: left;\n margin-bottom: 0.25rem;\n display: block;\n color: var(--color-text);\n}\n\ninput[type] {\n box-sizing: border-box;\n display: block;\n width: 100%;\n padding: 0.5rem 1rem;\n border: var(--border-width) solid var(--color-control-border);\n background: var(--color-background);\n font-size: 1rem;\n border-radius: var(--border-radius);\n box-shadow: inset 0 0.1rem 0.2rem rgba(0, 0, 0, 0.2);\n color: var(--color-text);\n}\n\ninput[type]:focus {\n box-shadow: none;\n }\n\np {\n margin: 0 0 1.5rem 0;\n padding: 0 1rem;\n font-size: 1.1rem;\n line-height: 2rem;\n}\n\na.button {\n text-decoration: none;\n line-height: 1rem;\n}\n\na.button:link,\n a.button:visited {\n background-color: var(--color-background);\n color: var(--color-primary);\n }\n\nbutton,\na.button {\n margin: 0 0 0.75rem 0;\n padding: 0.75rem 1rem;\n color: var(--provider-color, var(--color-primary));\n background-color: var(--provider-bg, var(--color-background));\n font-size: 1.1rem;\n min-height: 62px;\n border-color: rgba(0, 0, 0, 0.1);\n border-radius: var(--border-radius);\n transition: all 0.1s ease-in-out;\n box-shadow: #000 0px 0px 0px 0px, #000 0px 0px 0px 0px,\n rgba(0, 0, 0, 0.2) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px;\n font-weight: 500;\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\nbutton:has(img), a.button:has(img) {\n justify-content: unset;\n }\n\nbutton:has(img) span, a.button:has(img) span {\n flex-grow: 1;\n }\n\nbutton:hover, a.button:hover {\n cursor: pointer;\n }\n\nbutton:active, a.button:active {\n box-shadow: 0 0.15rem 0.3rem rgba(0, 0, 0, 0.15),\n inset 0 0.1rem 0.2rem var(--color-background),\n inset 0 -0.1rem 0.1rem rgba(0, 0, 0, 0.1);\n cursor: pointer;\n }\n\nbutton #provider-logo, a.button #provider-logo {\n display: block;\n }\n\nbutton #provider-logo-dark, a.button #provider-logo-dark {\n display: none;\n }\n\n@media (prefers-color-scheme: dark) {\n button,\n a.button {\n color: var(--provider-dark-color, var(--color-primary));\n background-color: var(--provider-dark-bg, var(--color-background));\n border: 1px solid #0d0d0d;\n box-shadow: #000 0px 0px 0px 0px, #ccc 0px 0px 0px 0px,\n rgba(255, 255, 255, 0.01) 0px 5px 5px -3px,\n rgba(255, 255, 255, 0.05) 0px 4px 6px -4px;\n }\n #provider-logo {\n display: none !important;\n }\n #provider-logo-dark {\n display: block !important;\n }\n}\n\na.site {\n color: var(--color-primary);\n text-decoration: none;\n font-size: 1rem;\n line-height: 2rem;\n}\n\na.site:hover {\n text-decoration: underline;\n }\n\n.page {\n position: absolute;\n width: 100%;\n height: 100%;\n display: grid;\n place-items: center;\n margin: 0;\n padding: 0;\n}\n\n.page > div {\n text-align: center;\n padding: 0.5rem;\n }\n\n.error a.button {\n display: inline-block;\n padding-left: 2rem;\n padding-right: 2rem;\n margin-top: 0.5rem;\n }\n\n.error .message {\n margin-bottom: 1.5rem;\n }\n\n.signin input[type=\"text\"] {\n margin-left: auto;\n margin-right: auto;\n display: block;\n }\n\n.signin hr {\n display: block;\n border: 0;\n border-top: 1px solid var(--color-seperator);\n margin: 1.5em auto 0 auto;\n overflow: visible;\n }\n\n.signin hr::before {\n content: \"or\";\n background: var(--color-background);\n color: #888;\n padding: 0 0.4rem;\n position: relative;\n top: -0.6rem;\n }\n\n.signin .error {\n background: #f5f5f5;\n font-weight: 500;\n border-radius: 0.3rem;\n background: var(--color-info);\n }\n\n.signin .error p {\n text-align: left;\n padding: 0.5rem 1rem;\n font-size: 0.9rem;\n line-height: 1.2rem;\n color: var(--color-info-text);\n }\n\n.signin > div,\n .signin form {\n display: block;\n }\n\n.signin > div input[type], .signin form input[type] {\n margin-bottom: 0.5rem;\n }\n\n.signin > div button, .signin form button {\n width: 100%;\n }\n\n.signin > div,\n .signin form {\n\n max-width: 300px;\n}\n.signout .message {\n margin-bottom: 1.5rem;\n }\n\n.logo {\n display: inline-block;\n margin-top: 100px;\n max-width: 300px;\n max-height: 150px;\n}\n\n.card {\n max-width: -moz-max-content;\n max-width: max-content;\n border: 1px solid var(--color-control-border);\n border-radius: 5px;\n padding: 20px 50px;\n margin: 50px auto;\n}\n\n.card .header {\n color: var(--color-primary);\n }\n\n.section-header {\n color: var(--brand-color, var(--color-text));\n}\n"; | ||
declare const _default: ":root {\n --border-width: 1px;\n --border-radius: 0.5rem;\n --color-error: #c94b4b;\n --color-info: #157efb;\n --color-info-text: #fff;\n}\n\n.__next-auth-theme-auto,\n.__next-auth-theme-light {\n --color-background: #ececec;\n --color-background-card: #fff;\n --color-text: #000;\n --color-primary: #444;\n --color-control-border: #bbb;\n --color-button-active-background: #f9f9f9;\n --color-button-active-border: #aaa;\n --color-seperator: #ccc;\n}\n\n.__next-auth-theme-dark {\n --color-background: #161b22;\n --color-background-card: #0d1117;\n --color-text: #fff;\n --color-primary: #ccc;\n --color-control-border: #555;\n --color-button-active-background: #060606;\n --color-button-active-border: #666;\n --color-seperator: #444;\n}\n\n@media (prefers-color-scheme: dark) {\n .__next-auth-theme-auto {\n --color-background: #161b22;\n --color-background-card: #0d1117;\n --color-text: #fff;\n --color-primary: #ccc;\n --color-control-border: #555;\n --color-button-active-background: #060606;\n --color-button-active-border: #666;\n --color-seperator: #444;\n }\n}\n\nbody {\n background-color: var(--color-background);\n margin: 0;\n padding: 0;\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,\n \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif,\n \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n}\n\nh1 {\n font-weight: 400;\n margin-bottom: 1.5rem;\n padding: 0 1rem;\n color: var(--color-text);\n}\n\np {\n color: var(--color-text);\n}\n\nform {\n margin: 0;\n padding: 0;\n}\n\nlabel {\n font-weight: 500;\n text-align: left;\n margin-bottom: 0.25rem;\n display: block;\n color: var(--color-text);\n}\n\ninput[type] {\n box-sizing: border-box;\n display: block;\n width: 100%;\n padding: 0.5rem 1rem;\n border: var(--border-width) solid var(--color-control-border);\n background: var(--color-background-card);\n font-size: 1rem;\n border-radius: var(--border-radius);\n color: var(--color-text);\n}\n\ninput[type]:focus {\n box-shadow: none;\n }\n\np {\n margin: 0 0 1.5rem 0;\n padding: 0 1rem;\n font-size: 1.1rem;\n line-height: 2rem;\n}\n\na.button {\n text-decoration: none;\n line-height: 1rem;\n}\n\na.button:link,\n a.button:visited {\n background-color: var(--color-background);\n color: var(--color-primary);\n }\n\nbutton span {\n flex-grow: 1;\n}\n\nbutton,\na.button {\n margin: 0 0 0.75rem 0;\n padding: 0.75rem 1rem;\n color: var(--provider-color, var(--color-primary));\n background-color: var(--provider-bg, var(--color-background-card));\n font-size: 1.1rem;\n min-height: 62px;\n border-color: rgba(0, 0, 0, 0.1);\n border-radius: var(--border-radius);\n transition: all 0.1s ease-in-out;\n font-weight: 500;\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n@media (max-width: 450px) {\n\nbutton,\na.button {\n font-size: 0.9rem\n}\n }\n\nbutton:hover, a.button:hover {\n cursor: pointer;\n }\n\nbutton:active, a.button:active {\n cursor: pointer;\n }\n\nbutton #provider-logo, a.button #provider-logo {\n width: 25px;\n display: block;\n }\n\nbutton #provider-logo-dark, a.button #provider-logo-dark {\n display: none;\n }\n\n#submitButton {\n color: var(--button-text-color, var(--color-info-text));\n background-color: var(--brand-color, var(--color-info));\n width: 100%;\n}\n\n@media (prefers-color-scheme: dark) {\n button,\n a.button {\n color: var(--provider-dark-color, var(--color-primary));\n background-color: var(--provider-dark-bg, var(--color-background));\n }\n #provider-logo {\n display: none !important;\n }\n #provider-logo-dark {\n width: 25px;\n display: block !important;\n }\n}\n\na.site {\n color: var(--color-primary);\n text-decoration: none;\n font-size: 1rem;\n line-height: 2rem;\n}\n\na.site:hover {\n text-decoration: underline;\n }\n\n.page {\n position: absolute;\n width: 100%;\n height: 100%;\n display: grid;\n place-items: center;\n margin: 0;\n padding: 0;\n}\n\n.page > div {\n text-align: center;\n }\n\n.error a.button {\n display: inline-block;\n padding-left: 2rem;\n padding-right: 2rem;\n margin-top: 0.5rem;\n }\n\n.error .message {\n margin-bottom: 1.5rem;\n }\n\n.signin input[type=\"text\"] {\n margin-left: auto;\n margin-right: auto;\n display: block;\n }\n\n.signin hr {\n display: block;\n border: 0;\n border-top: 1px solid var(--color-seperator);\n margin: 2rem auto 1rem auto;\n overflow: visible;\n }\n\n.signin hr::before {\n content: \"or\";\n background: var(--color-background-card);\n color: #888;\n padding: 0 0.4rem;\n position: relative;\n top: -0.7rem;\n }\n\n.signin .error {\n background: #f5f5f5;\n font-weight: 500;\n border-radius: 0.3rem;\n background: var(--color-error);\n }\n\n.signin .error p {\n text-align: left;\n padding: 0.5rem 1rem;\n font-size: 0.9rem;\n line-height: 1.2rem;\n color: var(--color-info-text);\n }\n\n.signin > div,\n .signin form {\n display: block;\n }\n\n.signin > div input[type], .signin form input[type] {\n margin-bottom: 0.5rem;\n }\n\n.signin > div button, .signin form button {\n width: 100%;\n }\n\n.signin > div,\n .signin form {\n\n max-width: 300px;\n}\n\n.logo {\n display: inline-block;\n max-width: 150px;\n margin-top: 20px;\n margin-bottom: 25px;\n max-height: 70px;\n}\n\n@media screen and (min-width: 450px) {\n\n.card {\n width: 350px\n}\n }\n\n@media screen and (max-width: 450px) {\n\n.card {\n width: 200px\n}\n }\n\n.card {\n margin: 20px 0 20px 0;\n background-color: var(--color-background-card);\n border-radius: 30px;\n padding: 20px 50px;\n}\n\n.card .header {\n color: var(--color-primary);\n }\n\n.section-header {\n color: var(--color-text);\n}\n"; | ||
export default _default; | ||
//# sourceMappingURL=styles.d.ts.map |
@@ -11,3 +11,4 @@ export default `:root { | ||
.__next-auth-theme-light { | ||
--color-background: #fff; | ||
--color-background: #ececec; | ||
--color-background-card: #fff; | ||
--color-text: #000; | ||
@@ -22,3 +23,4 @@ --color-primary: #444; | ||
.__next-auth-theme-dark { | ||
--color-background: #000; | ||
--color-background: #161b22; | ||
--color-background-card: #0d1117; | ||
--color-text: #fff; | ||
@@ -34,3 +36,4 @@ --color-primary: #ccc; | ||
.__next-auth-theme-auto { | ||
--color-background: #000; | ||
--color-background: #161b22; | ||
--color-background-card: #0d1117; | ||
--color-text: #fff; | ||
@@ -84,6 +87,5 @@ --color-primary: #ccc; | ||
border: var(--border-width) solid var(--color-control-border); | ||
background: var(--color-background); | ||
background: var(--color-background-card); | ||
font-size: 1rem; | ||
border-radius: var(--border-radius); | ||
box-shadow: inset 0 0.1rem 0.2rem rgba(0, 0, 0, 0.2); | ||
color: var(--color-text); | ||
@@ -114,2 +116,6 @@ } | ||
button span { | ||
flex-grow: 1; | ||
} | ||
button, | ||
@@ -120,3 +126,3 @@ a.button { | ||
color: var(--provider-color, var(--color-primary)); | ||
background-color: var(--provider-bg, var(--color-background)); | ||
background-color: var(--provider-bg, var(--color-background-card)); | ||
font-size: 1.1rem; | ||
@@ -127,4 +133,2 @@ min-height: 62px; | ||
transition: all 0.1s ease-in-out; | ||
box-shadow: #000 0px 0px 0px 0px, #000 0px 0px 0px 0px, | ||
rgba(0, 0, 0, 0.2) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px; | ||
font-weight: 500; | ||
@@ -137,10 +141,10 @@ position: relative; | ||
button:has(img), a.button:has(img) { | ||
justify-content: unset; | ||
@media (max-width: 450px) { | ||
button, | ||
a.button { | ||
font-size: 0.9rem | ||
} | ||
} | ||
button:has(img) span, a.button:has(img) span { | ||
flex-grow: 1; | ||
} | ||
button:hover, a.button:hover { | ||
@@ -151,5 +155,2 @@ cursor: pointer; | ||
button:active, a.button:active { | ||
box-shadow: 0 0.15rem 0.3rem rgba(0, 0, 0, 0.15), | ||
inset 0 0.1rem 0.2rem var(--color-background), | ||
inset 0 -0.1rem 0.1rem rgba(0, 0, 0, 0.1); | ||
cursor: pointer; | ||
@@ -159,2 +160,3 @@ } | ||
button #provider-logo, a.button #provider-logo { | ||
width: 25px; | ||
display: block; | ||
@@ -167,2 +169,8 @@ } | ||
#submitButton { | ||
color: var(--button-text-color, var(--color-info-text)); | ||
background-color: var(--brand-color, var(--color-info)); | ||
width: 100%; | ||
} | ||
@media (prefers-color-scheme: dark) { | ||
@@ -173,6 +181,2 @@ button, | ||
background-color: var(--provider-dark-bg, var(--color-background)); | ||
border: 1px solid #0d0d0d; | ||
box-shadow: #000 0px 0px 0px 0px, #ccc 0px 0px 0px 0px, | ||
rgba(255, 255, 255, 0.01) 0px 5px 5px -3px, | ||
rgba(255, 255, 255, 0.05) 0px 4px 6px -4px; | ||
} | ||
@@ -183,2 +187,3 @@ #provider-logo { | ||
#provider-logo-dark { | ||
width: 25px; | ||
display: block !important; | ||
@@ -211,3 +216,2 @@ } | ||
text-align: center; | ||
padding: 0.5rem; | ||
} | ||
@@ -236,3 +240,3 @@ | ||
border-top: 1px solid var(--color-seperator); | ||
margin: 1.5em auto 0 auto; | ||
margin: 2rem auto 1rem auto; | ||
overflow: visible; | ||
@@ -243,7 +247,7 @@ } | ||
content: "or"; | ||
background: var(--color-background); | ||
background: var(--color-background-card); | ||
color: #888; | ||
padding: 0 0.4rem; | ||
position: relative; | ||
top: -0.6rem; | ||
top: -0.7rem; | ||
} | ||
@@ -255,3 +259,3 @@ | ||
border-radius: 0.3rem; | ||
background: var(--color-info); | ||
background: var(--color-error); | ||
} | ||
@@ -285,20 +289,30 @@ | ||
} | ||
.signout .message { | ||
margin-bottom: 1.5rem; | ||
} | ||
.logo { | ||
display: inline-block; | ||
margin-top: 100px; | ||
max-width: 300px; | ||
max-height: 150px; | ||
max-width: 150px; | ||
margin-top: 20px; | ||
margin-bottom: 25px; | ||
max-height: 70px; | ||
} | ||
@media screen and (min-width: 450px) { | ||
.card { | ||
max-width: -moz-max-content; | ||
max-width: max-content; | ||
border: 1px solid var(--color-control-border); | ||
border-radius: 5px; | ||
width: 350px | ||
} | ||
} | ||
@media screen and (max-width: 450px) { | ||
.card { | ||
width: 200px | ||
} | ||
} | ||
.card { | ||
margin: 20px 0 20px 0; | ||
background-color: var(--color-background-card); | ||
border-radius: 30px; | ||
padding: 20px 50px; | ||
margin: 50px auto; | ||
} | ||
@@ -311,5 +325,5 @@ | ||
.section-header { | ||
color: var(--brand-color, var(--color-text)); | ||
color: var(--color-text); | ||
} | ||
`; | ||
// Generated by `pnpm css` |
@@ -10,3 +10,3 @@ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime"; | ||
`, | ||
} })), theme.logo && _jsx("img", { src: theme.logo, alt: "Logo", className: "logo" }), _jsxs("div", { className: "card", children: [_jsx("h1", { children: "Check your email" }), _jsx("p", { children: "A sign in link has been sent to your email address." }), _jsx("p", { children: _jsx("a", { className: "site", href: url.origin, children: url.host }) })] })] })); | ||
} })), _jsxs("div", { className: "card", children: [theme.logo && _jsx("img", { src: theme.logo, alt: "Logo", className: "logo" }), _jsx("h1", { children: "Check your email" }), _jsx("p", { children: "A sign in link has been sent to your email address." }), _jsx("p", { children: _jsx("a", { className: "site", href: url.origin, children: url.host }) })] })] })); | ||
} |
@@ -67,5 +67,8 @@ import { merge } from "./utils/merge.js"; | ||
const url = new URL(e?.url ?? "https://authjs.dev"); | ||
for (const k in e?.params) | ||
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 }; | ||
} | ||
return { url, request: e?.request, conform: e?.conform }; | ||
} |
import { handleLogin } from "../callback-handler.js"; | ||
import { CallbackRouteError } from "../../errors.js"; | ||
import { CallbackRouteError, Verification } from "../../errors.js"; | ||
import { handleOAuth } from "../oauth/callback.js"; | ||
@@ -61,11 +61,17 @@ import { createHash } from "../web.js"; | ||
}); | ||
// Encode token | ||
const newToken = await jwt.encode({ ...jwt, token }); | ||
// Set cookie expiry date | ||
const cookieExpires = new Date(); | ||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000); | ||
const sessionCookies = sessionStore.chunk(newToken, { | ||
expires: cookieExpires, | ||
}); | ||
cookies.push(...sessionCookies); | ||
// 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); | ||
const sessionCookies = sessionStore.chunk(newToken, { | ||
expires: cookieExpires, | ||
}); | ||
cookies.push(...sessionCookies); | ||
} | ||
} | ||
@@ -99,5 +105,6 @@ else { | ||
const identifier = query?.email; | ||
// If these are missing, the sign-in URL was manually opened without these params or the `sendVerificationRequest` method did not send the link correctly in the email. | ||
if (!token || !identifier) { | ||
return { redirect: `${url}/error?error=configuration`, cookies }; | ||
const e = new TypeError("Missing token or email. The sign-in URL was manually opened without token/identifier or the link was not sent correctly in the email.", { cause: { hasToken: !!token, hasEmail: !!identifier } }); | ||
e.name = "Configuration"; | ||
throw e; | ||
} | ||
@@ -110,6 +117,7 @@ const secret = provider.secret ?? options.secret; | ||
}); | ||
const invalidInvite = !invite || invite.expires.valueOf() < Date.now(); | ||
if (invalidInvite) { | ||
return { redirect: `${url}/error?error=Verification`, cookies }; | ||
} | ||
const hasInvite = !!invite; | ||
const expired = invite ? invite.expires.valueOf() < Date.now() : undefined; | ||
const invalidInvite = !hasInvite || expired; | ||
if (invalidInvite) | ||
throw new Verification({ hasInvite, expired }); | ||
// @ts-expect-error -- Verified in `assertConfig`. | ||
@@ -142,11 +150,17 @@ const user = await getAdapterUserFromEmail(identifier, adapter); | ||
}); | ||
// Encode token | ||
const newToken = await jwt.encode({ ...jwt, token }); | ||
// Set cookie expiry date | ||
const cookieExpires = new Date(); | ||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000); | ||
const sessionCookies = sessionStore.chunk(newToken, { | ||
expires: cookieExpires, | ||
}); | ||
cookies.push(...sessionCookies); | ||
// 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); | ||
const sessionCookies = sessionStore.chunk(newToken, { | ||
expires: cookieExpires, | ||
}); | ||
cookies.push(...sessionCookies); | ||
} | ||
} | ||
@@ -179,25 +193,14 @@ else { | ||
const credentials = body; | ||
let user; | ||
try { | ||
// TODO: Forward the original request as is, instead of reconstructing it | ||
// prettier-ignore | ||
Object.entries(query ?? {}).forEach(([k, v]) => url.searchParams.set(k, v)); | ||
user = await provider.authorize(credentials, | ||
// prettier-ignore | ||
new Request(url, { headers, method, body: JSON.stringify(body) })); | ||
if (!user) { | ||
return { | ||
status: 401, | ||
redirect: `${url}/error?${new URLSearchParams({ | ||
error: "CredentialsSignin", | ||
provider: provider.id, | ||
})}`, | ||
cookies, | ||
}; | ||
} | ||
} | ||
catch (e) { | ||
// TODO: Forward the original request as is, instead of reconstructing it | ||
Object.entries(query ?? {}).forEach(([k, v]) => url.searchParams.set(k, v)); | ||
const user = await provider.authorize(credentials, | ||
// prettier-ignore | ||
new Request(url, { headers, method, body: JSON.stringify(body) })); | ||
if (!user) { | ||
return { | ||
status: 401, | ||
redirect: `${url}/error?error=${encodeURIComponent(e.message)}`, | ||
redirect: `${url}/error?${new URLSearchParams({ | ||
error: "CredentialsSignin", | ||
provider: provider.id, | ||
})}`, | ||
cookies, | ||
@@ -228,11 +231,17 @@ }; | ||
}); | ||
// Encode token | ||
const newToken = await jwt.encode({ ...jwt, token }); | ||
// Set cookie expiry date | ||
const cookieExpires = new Date(); | ||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000); | ||
const sessionCookies = sessionStore.chunk(newToken, { | ||
expires: cookieExpires, | ||
}); | ||
cookies.push(...sessionCookies); | ||
// 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); | ||
const sessionCookies = sessionStore.chunk(newToken, { | ||
expires: cookieExpires, | ||
}); | ||
cookies.push(...sessionCookies); | ||
} | ||
// @ts-expect-error | ||
@@ -239,0 +248,0 @@ await events.signIn?.({ user, account }); |
@@ -30,18 +30,23 @@ import { JWTSessionError, SessionTokenError } from "../../errors.js"; | ||
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; | ||
// Refresh JWT expiry by re-signing it, with an updated expiry date | ||
const newToken = await jwt.encode({ | ||
...jwt, | ||
token, | ||
maxAge: options.session.maxAge, | ||
}); | ||
// Set cookie, to also update expiry date on cookie | ||
const sessionCookies = sessionStore.chunk(newToken, { | ||
expires: newExpires, | ||
}); | ||
response.cookies?.push(...sessionCookies); | ||
await events.session?.({ session: newSession, token }); | ||
if (token !== null) { | ||
// @ts-expect-error | ||
const newSession = await callbacks.session({ session, token }); | ||
// Return session payload as response | ||
response.body = newSession; | ||
// Refresh JWT expiry by re-signing it, with an updated expiry date | ||
const newToken = await jwt.encode({ | ||
...jwt, | ||
token, | ||
maxAge: options.session.maxAge, | ||
}); | ||
// Set cookie, to also update expiry date on cookie | ||
const sessionCookies = sessionStore.chunk(newToken, { | ||
expires: newExpires, | ||
}); | ||
response.cookies?.push(...sessionCookies); | ||
await events.session?.({ session: newSession, token }); | ||
} | ||
else { | ||
response.cookies?.push(...sessionStore.clean()); | ||
} | ||
} | ||
@@ -48,0 +53,0 @@ catch (e) { |
import { AuthError } from "../../errors.js"; | ||
export type WarningCode = "debug_enabled"; | ||
export type WarningCode = "debug-enabled" | "csrf-disabled"; | ||
/** | ||
@@ -4,0 +4,0 @@ * Override any of the methods, and the rest will use the default logger. |
@@ -8,8 +8,14 @@ const red = "\x1b[31m"; | ||
const url = `https://errors.authjs.dev#${error.name.toLowerCase()}`; | ||
console.error(error.stack); | ||
console.error(`${red}[auth][error][${error.name}]${reset}: Read more at ${url}`); | ||
error.metadata && console.error(JSON.stringify(error.metadata, null, 2)); | ||
console.error(`${red}[auth][error][${error.name}]${reset}:${error.message ? ` ${error.message}.` : ""} Read more at ${url}`); | ||
if (error.cause) { | ||
const { err, ...data } = error.cause; | ||
console.error(`${red}[auth][cause]${reset}:`, err.stack); | ||
console.error(`${red}[auth][details]${reset}:`, JSON.stringify(data, null, 2)); | ||
} | ||
else if (error.stack) { | ||
console.error(error.stack.replace(/.*/, "").substring(1)); | ||
} | ||
}, | ||
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}`); | ||
@@ -16,0 +22,0 @@ }, |
{ | ||
"name": "@auth/core", | ||
"version": "0.2.5", | ||
"version": "0.3.0", | ||
"description": "Authentication for the Web.", | ||
@@ -44,4 +44,4 @@ "keywords": [ | ||
"./errors": { | ||
"import": "./errors.js", | ||
"types": "./errors.d.ts" | ||
"types": "./errors.d.ts", | ||
"import": "./errors.js" | ||
}, | ||
@@ -48,0 +48,0 @@ "./jwt": { |
@@ -0,1 +1,12 @@ | ||
/** | ||
* <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"; | ||
@@ -2,0 +13,0 @@ /** |
@@ -0,1 +1,12 @@ | ||
/** | ||
* <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 | ||
*/ | ||
export default function Apple(options) { | ||
@@ -2,0 +13,0 @@ return { |
@@ -1,9 +0,120 @@ | ||
import type { OAuthConfig, OAuthUserConfig } from "./index.js"; | ||
export interface Auth0Profile extends Record<string, any> { | ||
/** | ||
* <div style={{backgroundColor: "#EB5424", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}> | ||
* <span>Built-in <b>Auth0</b> integration.</span> | ||
* <a href="https://auth0.com"> | ||
* <img style={{display: "block"}} src="https://authjs.dev/img/providers/auth0-dark.svg" height="48" width="48"/> | ||
* </a> | ||
* </div> | ||
* | ||
* --- | ||
* @module providers/auth0 | ||
*/ | ||
import type { 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; | ||
/** 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; | ||
email: 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>; | ||
//# sourceMappingURL=auth0.d.ts.map |
@@ -1,2 +0,58 @@ | ||
export default function Auth0(options) { | ||
/** | ||
* <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 | ||
*/ | ||
/** | ||
* 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) { | ||
return { | ||
@@ -14,4 +70,4 @@ id: "auth0", | ||
}, | ||
options, | ||
options: config, | ||
}; | ||
} |
@@ -13,20 +13,18 @@ export default function AzureAD(options) { | ||
// 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}`, | ||
}; | ||
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 { } | ||
} | ||
else { | ||
return { | ||
id: profile.sub, | ||
name: profile.name, | ||
email: profile.email, | ||
image: null, | ||
}; | ||
} | ||
return { | ||
id: profile.sub, | ||
name: profile.name, | ||
email: profile.email, | ||
image: image ?? null, | ||
}; | ||
}, | ||
@@ -33,0 +31,0 @@ style: { |
@@ -0,3 +1,14 @@ | ||
/** | ||
* <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; | ||
@@ -9,3 +20,3 @@ primary: boolean; | ||
/** @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; | ||
@@ -62,10 +73,8 @@ id: number; | ||
* | ||
* @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: "" })], | ||
@@ -105,3 +114,3 @@ * }) | ||
*/ | ||
export default function GitHub<Profile extends GithubProfile>(options: OAuthUserConfig<Profile>): OAuthConfig<Profile>; | ||
export default function GitHub(config: OAuthUserConfig<GitHubProfile>): OAuthConfig<GitHubProfile>; | ||
//# sourceMappingURL=github.d.ts.map |
/** | ||
* <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 | ||
*/ | ||
/** | ||
* Add GitHub login to your page and make requests to [GitHub APIs](https://docs.github.com/en/rest). | ||
@@ -6,10 +17,8 @@ * | ||
* | ||
* @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: "" })], | ||
@@ -49,3 +58,3 @@ * }) | ||
*/ | ||
export default function GitHub(options) { | ||
export default function GitHub(config) { | ||
return { | ||
@@ -92,8 +101,8 @@ id: "github", | ||
bg: "#fff", | ||
bgDark: "#000", | ||
bgDark: "#24292f", | ||
text: "#000", | ||
textDark: "#fff", | ||
}, | ||
options, | ||
options: config, | ||
}; | ||
} |
@@ -56,3 +56,3 @@ import type { Client } from "oauth4webapi"; | ||
}, Profile>; | ||
export type ProfileCallback<P> = (profile: P, tokens: TokenSet) => Awaitable<User>; | ||
export type ProfileCallback<Profile> = (profile: Profile, tokens: TokenSet) => Awaitable<User>; | ||
export interface OAuthProviderButtonStyles { | ||
@@ -67,3 +67,3 @@ logo: string; | ||
/** TODO: */ | ||
export interface OAuth2Config<P> extends CommonProviderOptions, PartialIssuer { | ||
export interface OAuth2Config<Profile> extends CommonProviderOptions, PartialIssuer { | ||
/** | ||
@@ -108,3 +108,3 @@ * Identifies the provider when you want to sign in to | ||
*/ | ||
profile?: ProfileCallback<P>; | ||
profile?: ProfileCallback<Profile>; | ||
/** | ||
@@ -129,25 +129,9 @@ * The CSRF protection performed on the callback endpoint. | ||
/** 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` | ||
* to always contain a valid `URL`, with the params | ||
*/ | ||
export type OAuthConfigInternal<P> = Omit<OAuthConfig<P>, OAuthEndpointType> & { | ||
authorization?: { | ||
url: URL; | ||
}; | ||
token?: { | ||
url: URL; | ||
request?: TokenEndpointHandler["request"]; | ||
}; | ||
userinfo?: { | ||
url: URL; | ||
request?: UserinfoEndpointHandler["request"]; | ||
}; | ||
} & Pick<Required<OAuthConfig<P>>, "clientId" | "checks" | "profile">; | ||
export type OAuthUserConfig<P> = Omit<Partial<OAuthConfig<P>>, "options" | "type"> & Required<Pick<OAuthConfig<P>, "clientId" | "clientSecret">>; | ||
export type OAuthUserConfig<Profile> = Omit<Partial<OAuthConfig<Profile>>, "options" | "type"> & Required<Pick<OAuthConfig<Profile>, "clientId" | "clientSecret">>; | ||
export type OIDCUserConfig<Profile> = Omit<Partial<OIDCConfig<Profile>>, "options" | "type"> & Required<Pick<OIDCConfig<Profile>, "clientId" | "clientSecret">>; | ||
//# sourceMappingURL=oauth.d.ts.map |
@@ -1,2 +0,2 @@ | ||
import type { OAuthConfig, OAuthUserConfig } from "./index.js"; | ||
import type { OIDCConfig, OIDCUserConfig } from "./index.js"; | ||
export interface TwitchProfile extends Record<string, any> { | ||
@@ -8,3 +8,3 @@ sub: string; | ||
} | ||
export default function Twitch<P extends TwitchProfile>(options: OAuthUserConfig<P>): OAuthConfig<P>; | ||
export default function Twitch(config: OIDCUserConfig<TwitchProfile>): OIDCConfig<TwitchProfile>; | ||
//# sourceMappingURL=twitch.d.ts.map |
@@ -1,2 +0,2 @@ | ||
export default function Twitch(options) { | ||
export default function Twitch(config) { | ||
return { | ||
@@ -7,2 +7,3 @@ issuer: "https://id.twitch.tv/oauth2", | ||
type: "oidc", | ||
client: { token_endpoint_auth_method: "client_secret_post" }, | ||
authorization: { | ||
@@ -12,10 +13,31 @@ params: { | ||
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: { | ||
@@ -29,4 +51,4 @@ logo: "/twitch.svg", | ||
}, | ||
options, | ||
options: config, | ||
}; | ||
} |
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: { | ||
@@ -123,7 +56,28 @@ hashtags: Array<{ | ||
}; | ||
/** | ||
* 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; | ||
@@ -139,5 +93,3 @@ created_at?: string; | ||
} | ||
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>; | ||
//# sourceMappingURL=twitter.d.ts.map |
@@ -1,2 +0,2 @@ | ||
export default function Twitter(options) { | ||
export default function Twitter(config) { | ||
return { | ||
@@ -7,18 +7,5 @@ id: "twitter", | ||
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 }) { | ||
@@ -28,4 +15,3 @@ return { | ||
name: data.name, | ||
// NOTE: E-mail is currently unsupported by OAuth 2 Twitter. | ||
email: null, | ||
email: data.email ?? null, | ||
image: data.profile_image_url, | ||
@@ -42,4 +28,4 @@ }; | ||
}, | ||
options, | ||
options: config, | ||
}; | ||
} |
@@ -0,12 +1,21 @@ | ||
interface ErrorCause extends Record<string, unknown> {} | ||
/** @internal */ | ||
export class AuthError extends Error { | ||
metadata?: Record<string, unknown> | ||
constructor(message: Error | string, metadata?: Record<string, unknown>) { | ||
constructor(message: string | Error | ErrorCause, cause?: ErrorCause) { | ||
if (message instanceof Error) { | ||
super(message.message) | ||
this.stack = message.stack | ||
} else super(message) | ||
this.name = this.constructor.name | ||
this.metadata = metadata | ||
super(undefined, { | ||
cause: { err: message, ...(message.cause as any), ...cause }, | ||
}) | ||
} else if (typeof message === "string") { | ||
if (cause instanceof Error) { | ||
cause = { err: cause, ...(cause.cause as any) } | ||
} | ||
super(message, cause) | ||
} else { | ||
super(undefined, message) | ||
} | ||
Error.captureStackTrace?.(this, this.constructor) | ||
this.name = | ||
message instanceof AuthError ? message.name : this.constructor.name | ||
} | ||
@@ -31,3 +40,41 @@ } | ||
/** @todo */ | ||
/** | ||
* There was an error while trying to finish up authenticating the user. | ||
* Depending on the type of provider, this could be for multiple reasons. | ||
* | ||
* :::tip | ||
* Check out `[auth][details]` in the error message to know which provider failed. | ||
* @example | ||
* ```sh | ||
* [auth][details]: { "provider": "github" } | ||
* ``` | ||
* ::: | ||
* | ||
* For an **OAuth provider**, possible causes are: | ||
* - The user denied access to the application | ||
* - There was an error parsing the OAuth Profile: | ||
* Check out the provider's `profile` or `userinfo.request` method to make sure | ||
* it correctly fetches the user's profile. | ||
* - The `signIn` or `jwt` callback methods threw an uncaught error: | ||
* Check the callback method implementations. | ||
* | ||
* For an **Email provider**, possible causes are: | ||
* - The provided email/token combination was invalid/missing: | ||
* Check if the provider's `sendVerificationRequest` method correctly sends the email. | ||
* - The provided email/token combination has expired: | ||
* Ask the user to log in again. | ||
* - There was an error with the database: | ||
* Check the database logs. | ||
* | ||
* For a **Credentials provider**, possible causes are: | ||
* - The `authorize` method threw an uncaught error: | ||
* Check the provider's `authorize` method. | ||
* - The `signIn` or `jwt` callback methods threw an uncaught error: | ||
* Check the callback method implementations. | ||
* | ||
* :::tip | ||
* Check out `[auth][cause]` in the error message for more details. | ||
* It will show the original stack trace. | ||
* ::: | ||
*/ | ||
export class CallbackRouteError extends AuthError {} | ||
@@ -97,1 +144,8 @@ | ||
export class UntrustedHost extends AuthError {} | ||
/** | ||
* The user's email/token combination was invalid. | ||
* This could be because the email/token combination was not found in the database, | ||
* or because it token has expired. Ask the user to log in again. | ||
*/ | ||
export class Verification extends AuthError {} |
@@ -64,3 +64,3 @@ /** | ||
* const request = new Request("https://example.com") | ||
* const resposne = await AuthHandler(request, { | ||
* const response = await AuthHandler(request, { | ||
* providers: [...], | ||
@@ -161,3 +161,3 @@ * secret: "...", | ||
* const request = new Request("https://example.com") | ||
* const resposne = await AuthHandler(request, authConfig) | ||
* const response = await AuthHandler(request, authConfig) | ||
* ``` | ||
@@ -301,2 +301,14 @@ * | ||
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") |
@@ -48,3 +48,3 @@ import { defaultCookies } from "./cookie.js" | ||
if (!warned && options.debug) warnings.push("debug_enabled") | ||
if (!warned && options.debug) warnings.push("debug-enabled") | ||
@@ -51,0 +51,0 @@ if (!options.trustHost) { |
@@ -136,3 +136,4 @@ import { AccountNotLinked } from "../errors.js" | ||
throw new AccountNotLinked( | ||
"The account is already associated with another user" | ||
"The account is already associated with another user", | ||
{ provider: account.provider } | ||
) | ||
@@ -197,3 +198,4 @@ } | ||
throw new AccountNotLinked( | ||
"Another account already exists with the same e-mail address" | ||
"Another account already exists with the same e-mail address", | ||
{ provider: account.provider } | ||
) | ||
@@ -200,0 +202,0 @@ } |
@@ -0,3 +1,4 @@ | ||
import { UnknownAction } from "../errors.js" | ||
import { skipCSRFCheck } from "../index.js" | ||
import { SessionStore } from "./cookie.js" | ||
import { UnknownAction } from "../errors.js" | ||
import { init } from "./init.js" | ||
@@ -8,6 +9,6 @@ import renderPage from "./pages/index.js" | ||
import type { | ||
AuthConfig, | ||
ErrorPageParam, | ||
RequestInternal, | ||
ResponseInternal, | ||
AuthConfig, | ||
ErrorPageParam, | ||
} from "../types.js" | ||
@@ -23,2 +24,4 @@ | ||
const csrfDisabled = authOptions.skipCSRFCheck === skipCSRFCheck | ||
const { options, cookies } = await init({ | ||
@@ -33,2 +36,3 @@ authOptions, | ||
isPost: method === "POST", | ||
csrfDisabled, | ||
}) | ||
@@ -54,3 +58,12 @@ | ||
} | ||
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 { | ||
@@ -61,2 +74,3 @@ headers: { "Content-Type": "application/json" }, | ||
} | ||
} | ||
case "signin": | ||
@@ -133,4 +147,3 @@ if (pages.signIn) { | ||
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( | ||
@@ -147,4 +160,3 @@ request.query, | ||
case "signout": | ||
// Verified CSRF Token required for signout | ||
if (options.csrfTokenVerified) { | ||
if (csrfDisabled || options.csrfTokenVerified) { | ||
const signout = await routes.signout(sessionStore, options) | ||
@@ -160,2 +172,3 @@ if (signout.cookies) cookies.push(...signout.cookies) | ||
options.provider.type === "credentials" && | ||
!csrfDisabled && | ||
!options.csrfTokenVerified | ||
@@ -162,0 +175,0 @@ ) { |
@@ -28,2 +28,3 @@ import * as jwt from "../jwt.js" | ||
/** Is the incoming request a POST request? */ | ||
csrfDisabled: boolean | ||
isPost: boolean | ||
@@ -42,2 +43,3 @@ cookies: RequestInternal["cookies"] | ||
csrfToken: reqCsrfToken, | ||
csrfDisabled, | ||
isPost, | ||
@@ -96,3 +98,3 @@ }: InitParams): Promise<{ | ||
updateAge: 24 * 60 * 60, | ||
generateSessionToken: crypto.randomUUID, | ||
generateSessionToken: () => crypto.randomUUID(), | ||
...authOptions.session, | ||
@@ -123,22 +125,24 @@ }, | ||
const { | ||
csrfToken, | ||
cookie: csrfCookie, | ||
csrfTokenVerified, | ||
} = await createCSRFToken({ | ||
options, | ||
cookieValue: reqCookies?.[options.cookies.csrfToken.name], | ||
isPost, | ||
bodyValue: reqCsrfToken, | ||
}) | ||
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 | ||
options.csrfToken = csrfToken | ||
options.csrfTokenVerified = csrfTokenVerified | ||
if (csrfCookie) { | ||
cookies.push({ | ||
name: options.cookies.csrfToken.name, | ||
value: csrfCookie, | ||
options: options.cookies.csrfToken.options, | ||
}) | ||
if (csrfCookie) { | ||
cookies.push({ | ||
name: options.cookies.csrfToken.name, | ||
value: csrfCookie, | ||
options: options.cookies.csrfToken.options, | ||
}) | ||
} | ||
} | ||
@@ -145,0 +149,0 @@ |
@@ -0,5 +1,5 @@ | ||
import * as checks from "./checks.js" | ||
import * as o from "oauth4webapi" | ||
import type { | ||
CookiesOptions, | ||
InternalOptions, | ||
@@ -61,6 +61,6 @@ RequestInternal, | ||
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) | ||
} | ||
@@ -74,13 +74,13 @@ | ||
} 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) | ||
} | ||
@@ -97,50 +97,1 @@ | ||
} | ||
/** 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) | ||
} |
@@ -0,6 +1,4 @@ | ||
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" | ||
@@ -76,3 +74,3 @@ import type { | ||
const state = await useState(cookies, resCookies, options) | ||
const state = await checks.state.use(cookies, resCookies, options) | ||
@@ -95,3 +93,3 @@ const parameters = o.validateAuthResponse( | ||
const codeVerifier = await usePKCECodeVerifier( | ||
const codeVerifier = await checks.pkce.use( | ||
cookies?.[options.cookies.pkceCodeVerifier.name], | ||
@@ -104,3 +102,6 @@ options | ||
// 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") { | ||
@@ -110,3 +111,3 @@ resCookies.push(nonce.cookie) | ||
const codeGrantResponse = await o.authorizationCodeGrantRequest( | ||
let codeGrantResponse = await o.authorizationCodeGrantRequest( | ||
as, | ||
@@ -119,2 +120,8 @@ client, | ||
if (provider.token?.conform) { | ||
codeGrantResponse = | ||
(await provider.token.conform(codeGrantResponse.clone())) ?? | ||
codeGrantResponse | ||
} | ||
let challenges: o.WWWAuthenticateChallenge[] | undefined | ||
@@ -121,0 +128,0 @@ if ((challenges = o.parseWwwAuthenticateChallenges(codeGrantResponse))) { |
@@ -11,3 +11,4 @@ export default `:root { | ||
.__next-auth-theme-light { | ||
--color-background: #fff; | ||
--color-background: #ececec; | ||
--color-background-card: #fff; | ||
--color-text: #000; | ||
@@ -22,3 +23,4 @@ --color-primary: #444; | ||
.__next-auth-theme-dark { | ||
--color-background: #000; | ||
--color-background: #161b22; | ||
--color-background-card: #0d1117; | ||
--color-text: #fff; | ||
@@ -34,3 +36,4 @@ --color-primary: #ccc; | ||
.__next-auth-theme-auto { | ||
--color-background: #000; | ||
--color-background: #161b22; | ||
--color-background-card: #0d1117; | ||
--color-text: #fff; | ||
@@ -84,6 +87,5 @@ --color-primary: #ccc; | ||
border: var(--border-width) solid var(--color-control-border); | ||
background: var(--color-background); | ||
background: var(--color-background-card); | ||
font-size: 1rem; | ||
border-radius: var(--border-radius); | ||
box-shadow: inset 0 0.1rem 0.2rem rgba(0, 0, 0, 0.2); | ||
color: var(--color-text); | ||
@@ -114,2 +116,6 @@ } | ||
button span { | ||
flex-grow: 1; | ||
} | ||
button, | ||
@@ -120,3 +126,3 @@ a.button { | ||
color: var(--provider-color, var(--color-primary)); | ||
background-color: var(--provider-bg, var(--color-background)); | ||
background-color: var(--provider-bg, var(--color-background-card)); | ||
font-size: 1.1rem; | ||
@@ -127,4 +133,2 @@ min-height: 62px; | ||
transition: all 0.1s ease-in-out; | ||
box-shadow: #000 0px 0px 0px 0px, #000 0px 0px 0px 0px, | ||
rgba(0, 0, 0, 0.2) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px; | ||
font-weight: 500; | ||
@@ -137,10 +141,10 @@ position: relative; | ||
button:has(img), a.button:has(img) { | ||
justify-content: unset; | ||
@media (max-width: 450px) { | ||
button, | ||
a.button { | ||
font-size: 0.9rem | ||
} | ||
} | ||
button:has(img) span, a.button:has(img) span { | ||
flex-grow: 1; | ||
} | ||
button:hover, a.button:hover { | ||
@@ -151,5 +155,2 @@ cursor: pointer; | ||
button:active, a.button:active { | ||
box-shadow: 0 0.15rem 0.3rem rgba(0, 0, 0, 0.15), | ||
inset 0 0.1rem 0.2rem var(--color-background), | ||
inset 0 -0.1rem 0.1rem rgba(0, 0, 0, 0.1); | ||
cursor: pointer; | ||
@@ -159,2 +160,3 @@ } | ||
button #provider-logo, a.button #provider-logo { | ||
width: 25px; | ||
display: block; | ||
@@ -167,2 +169,8 @@ } | ||
#submitButton { | ||
color: var(--button-text-color, var(--color-info-text)); | ||
background-color: var(--brand-color, var(--color-info)); | ||
width: 100%; | ||
} | ||
@media (prefers-color-scheme: dark) { | ||
@@ -173,6 +181,2 @@ button, | ||
background-color: var(--provider-dark-bg, var(--color-background)); | ||
border: 1px solid #0d0d0d; | ||
box-shadow: #000 0px 0px 0px 0px, #ccc 0px 0px 0px 0px, | ||
rgba(255, 255, 255, 0.01) 0px 5px 5px -3px, | ||
rgba(255, 255, 255, 0.05) 0px 4px 6px -4px; | ||
} | ||
@@ -183,2 +187,3 @@ #provider-logo { | ||
#provider-logo-dark { | ||
width: 25px; | ||
display: block !important; | ||
@@ -211,3 +216,2 @@ } | ||
text-align: center; | ||
padding: 0.5rem; | ||
} | ||
@@ -236,3 +240,3 @@ | ||
border-top: 1px solid var(--color-seperator); | ||
margin: 1.5em auto 0 auto; | ||
margin: 2rem auto 1rem auto; | ||
overflow: visible; | ||
@@ -243,7 +247,7 @@ } | ||
content: "or"; | ||
background: var(--color-background); | ||
background: var(--color-background-card); | ||
color: #888; | ||
padding: 0 0.4rem; | ||
position: relative; | ||
top: -0.6rem; | ||
top: -0.7rem; | ||
} | ||
@@ -255,3 +259,3 @@ | ||
border-radius: 0.3rem; | ||
background: var(--color-info); | ||
background: var(--color-error); | ||
} | ||
@@ -285,20 +289,30 @@ | ||
} | ||
.signout .message { | ||
margin-bottom: 1.5rem; | ||
} | ||
.logo { | ||
display: inline-block; | ||
margin-top: 100px; | ||
max-width: 300px; | ||
max-height: 150px; | ||
max-width: 150px; | ||
margin-top: 20px; | ||
margin-bottom: 25px; | ||
max-height: 70px; | ||
} | ||
@media screen and (min-width: 450px) { | ||
.card { | ||
max-width: -moz-max-content; | ||
max-width: max-content; | ||
border: 1px solid var(--color-control-border); | ||
border-radius: 5px; | ||
width: 350px | ||
} | ||
} | ||
@media screen and (max-width: 450px) { | ||
.card { | ||
width: 200px | ||
} | ||
} | ||
.card { | ||
margin: 20px 0 20px 0; | ||
background-color: var(--color-background-card); | ||
border-radius: 30px; | ||
padding: 20px 50px; | ||
margin: 50px auto; | ||
} | ||
@@ -311,5 +325,5 @@ | ||
.section-header { | ||
color: var(--brand-color, var(--color-text)); | ||
color: var(--color-text); | ||
} | ||
` | ||
// Generated by `pnpm css` |
@@ -99,4 +99,7 @@ import { merge } from "./utils/merge.js" | ||
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 } | ||
} |
import { handleLogin } from "../callback-handler.js" | ||
import { CallbackRouteError } from "../../errors.js" | ||
import { CallbackRouteError, Verification } from "../../errors.js" | ||
import { handleOAuth } from "../oauth/callback.js" | ||
@@ -11,3 +11,2 @@ import { createHash } from "../web.js" | ||
ResponseInternal, | ||
User, | ||
InternalOptions, | ||
@@ -116,13 +115,18 @@ Account, | ||
// 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 { | ||
@@ -160,5 +164,9 @@ // Save Session Token in cookie | ||
// If these are missing, the sign-in URL was manually opened without these params or the `sendVerificationRequest` method did not send the link correctly in the email. | ||
if (!token || !identifier) { | ||
return { redirect: `${url}/error?error=configuration`, cookies } | ||
const e = new TypeError( | ||
"Missing token or email. The sign-in URL was manually opened without token/identifier or the link was not sent correctly in the email.", | ||
{ cause: { hasToken: !!token, hasEmail: !!identifier } } | ||
) | ||
e.name = "Configuration" | ||
throw e | ||
} | ||
@@ -173,6 +181,6 @@ | ||
const invalidInvite = !invite || invite.expires.valueOf() < Date.now() | ||
if (invalidInvite) { | ||
return { redirect: `${url}/error?error=Verification`, cookies } | ||
} | ||
const hasInvite = !!invite | ||
const expired = invite ? invite.expires.valueOf() < Date.now() : undefined | ||
const invalidInvite = !hasInvite || expired | ||
if (invalidInvite) throw new Verification({ hasInvite, expired }) | ||
@@ -218,13 +226,18 @@ // @ts-expect-error -- Verified in `assertConfig`. | ||
// 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 { | ||
@@ -261,29 +274,18 @@ // Save Session Token in cookie | ||
let user: User | null | ||
try { | ||
// TODO: Forward the original request as is, instead of reconstructing it | ||
// TODO: Forward the original request as is, instead of reconstructing it | ||
Object.entries(query ?? {}).forEach(([k, v]) => | ||
url.searchParams.set(k, v) | ||
) | ||
const user = await provider.authorize( | ||
credentials, | ||
// prettier-ignore | ||
Object.entries(query ?? {}).forEach(([k, v]) => url.searchParams.set(k, v)) | ||
user = await provider.authorize( | ||
credentials, | ||
// prettier-ignore | ||
new Request(url, { headers, method, body: JSON.stringify(body) }) | ||
) | ||
if (!user) { | ||
return { | ||
status: 401, | ||
redirect: `${url}/error?${new URLSearchParams({ | ||
error: "CredentialsSignin", | ||
provider: provider.id, | ||
})}`, | ||
cookies, | ||
} | ||
} | ||
} catch (e) { | ||
new Request(url, { headers, method, body: JSON.stringify(body) }) | ||
) | ||
if (!user) { | ||
return { | ||
status: 401, | ||
redirect: `${url}/error?error=${encodeURIComponent( | ||
(e as Error).message | ||
)}`, | ||
redirect: `${url}/error?${new URLSearchParams({ | ||
error: "CredentialsSignin", | ||
provider: provider.id, | ||
})}`, | ||
cookies, | ||
@@ -322,14 +324,19 @@ } | ||
// 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) | ||
} | ||
@@ -336,0 +343,0 @@ // @ts-expect-error |
@@ -51,23 +51,28 @@ import { JWTSessionError, SessionTokenError } from "../../errors.js" | ||
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) { | ||
@@ -74,0 +79,0 @@ logger.error(new JWTSessionError(e as Error)) |
import { AuthError } from "../../errors.js" | ||
export type WarningCode = "debug_enabled" | ||
export type WarningCode = "debug-enabled" | "csrf-disabled" | ||
@@ -24,10 +24,20 @@ /** | ||
const url = `https://errors.authjs.dev#${error.name.toLowerCase()}` | ||
console.error(error.stack) | ||
console.error( | ||
`${red}[auth][error][${error.name}]${reset}: Read more at ${url}` | ||
`${red}[auth][error][${error.name}]${reset}:${ | ||
error.message ? ` ${error.message}.` : "" | ||
} Read more at ${url}` | ||
) | ||
error.metadata && console.error(JSON.stringify(error.metadata, null, 2)) | ||
if (error.cause) { | ||
const { err, ...data } = error.cause as any | ||
console.error(`${red}[auth][cause]${reset}:`, (err as Error).stack) | ||
console.error( | ||
`${red}[auth][details]${reset}:`, | ||
JSON.stringify(data, null, 2) | ||
) | ||
} else if (error.stack) { | ||
console.error(error.stack.replace(/.*/, "").substring(1)) | ||
} | ||
}, | ||
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}`) | ||
@@ -34,0 +44,0 @@ }, |
@@ -0,1 +1,13 @@ | ||
/** | ||
* <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" | ||
@@ -2,0 +14,0 @@ |
@@ -1,13 +0,126 @@ | ||
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 | ||
/** 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 | ||
email: 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 { | ||
@@ -25,4 +138,4 @@ id: "auth0", | ||
}, | ||
options, | ||
options: config, | ||
} | ||
} |
@@ -37,19 +37,18 @@ import type { OAuthConfig, OAuthUserConfig } from "./index.js" | ||
// 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, | ||
} | ||
}, | ||
@@ -56,0 +55,0 @@ style: { |
@@ -0,4 +1,16 @@ | ||
/** | ||
* <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 | ||
@@ -11,3 +23,3 @@ primary: boolean | ||
/** @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 | ||
@@ -65,10 +77,8 @@ id: number | ||
* | ||
* @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: "" })], | ||
@@ -108,5 +118,5 @@ * }) | ||
*/ | ||
export default function GitHub<Profile extends GithubProfile>( | ||
options: OAuthUserConfig<Profile> | ||
): OAuthConfig<Profile> { | ||
export default function GitHub( | ||
config: OAuthUserConfig<GitHubProfile> | ||
): OAuthConfig<GitHubProfile> { | ||
return { | ||
@@ -136,3 +146,3 @@ id: "github", | ||
if (res.ok) { | ||
const emails: GithubEmail[] = await res.json() | ||
const emails: GitHubEmail[] = await res.json() | ||
profile.email = (emails.find((e) => e.primary) ?? emails[0]).email | ||
@@ -157,8 +167,8 @@ } | ||
bg: "#fff", | ||
bgDark: "#000", | ||
bgDark: "#24292f", | ||
text: "#000", | ||
textDark: "#fff", | ||
}, | ||
options, | ||
options: config, | ||
} | ||
} |
@@ -45,2 +45,4 @@ import type { Client } from "oauth4webapi" | ||
request?: EndpointRequest<C, R, P> | ||
/** @internal */ | ||
conform?: (response: Response) => Awaitable<Response | undefined> | ||
} | ||
@@ -83,4 +85,4 @@ | ||
export type ProfileCallback<P> = ( | ||
profile: P, | ||
export type ProfileCallback<Profile> = ( | ||
profile: Profile, | ||
tokens: TokenSet | ||
@@ -99,3 +101,5 @@ ) => Awaitable<User> | ||
/** TODO: */ | ||
export interface OAuth2Config<P> extends CommonProviderOptions, PartialIssuer { | ||
export interface OAuth2Config<Profile> | ||
extends CommonProviderOptions, | ||
PartialIssuer { | ||
/** | ||
@@ -140,3 +144,3 @@ * Identifies the provider when you want to sign in to | ||
*/ | ||
profile?: ProfileCallback<P> | ||
profile?: ProfileCallback<Profile> | ||
/** | ||
@@ -166,11 +170,12 @@ * The CSRF protection performed on the callback endpoint. | ||
*/ | ||
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> | ||
@@ -180,15 +185,29 @@ 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,2 +0,2 @@ | ||
import type { OAuthConfig, OAuthUserConfig } from "./index.js" | ||
import type { OIDCConfig, OIDCUserConfig } from "./index.js" | ||
@@ -10,5 +10,5 @@ export interface TwitchProfile extends Record<string, any> { | ||
export default function Twitch<P extends TwitchProfile>( | ||
options: OAuthUserConfig<P> | ||
): OAuthConfig<P> { | ||
export default function Twitch( | ||
config: OIDCUserConfig<TwitchProfile> | ||
): OIDCConfig<TwitchProfile> { | ||
return { | ||
@@ -19,2 +19,3 @@ issuer: "https://id.twitch.tv/oauth2", | ||
type: "oidc", | ||
client: { token_endpoint_auth_method: "client_secret_post" }, | ||
authorization: { | ||
@@ -24,10 +25,35 @@ params: { | ||
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: { | ||
@@ -41,4 +67,4 @@ logo: "/twitch.svg", | ||
}, | ||
options, | ||
options: config, | ||
} | ||
} |
@@ -1,118 +0,49 @@ | ||
// 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: { | ||
@@ -126,7 +57,28 @@ hashtags: Array<{ | ||
} | ||
/** | ||
* 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 | ||
@@ -143,5 +95,5 @@ created_at?: string | ||
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 { | ||
@@ -152,23 +104,7 @@ id: "twitter", | ||
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 }) { | ||
@@ -178,4 +114,3 @@ return { | ||
name: data.name, | ||
// NOTE: E-mail is currently unsupported by OAuth 2 Twitter. | ||
email: null, | ||
email: data.email ?? null, | ||
image: data.profile_image_url, | ||
@@ -192,4 +127,4 @@ } | ||
}, | ||
options, | ||
options: config, | ||
} | ||
} |
@@ -206,2 +206,7 @@ /** | ||
* 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. | ||
* | ||
@@ -219,3 +224,3 @@ * By default the JWT is encrypted. | ||
isNewUser?: boolean | ||
}) => Awaitable<JWT> | ||
}) => Awaitable<JWT|null> | ||
} | ||
@@ -222,0 +227,0 @@ |
@@ -188,2 +188,7 @@ /** | ||
* | ||
* 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. | ||
@@ -200,3 +205,3 @@ * | ||
isNewUser?: boolean; | ||
}) => Awaitable<JWT>; | ||
}) => Awaitable<JWT | null>; | ||
} | ||
@@ -203,0 +208,0 @@ /** [Documentation](https://authjs.dev/reference/configuration/auth-config#cookies) */ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
695282
17889