UNPKG

@adonisjs/ally

Version:

Social authentication provider for AdonisJS

292 lines (289 loc) 8.3 kB
import { RedirectRequest } from "./chunk-NK6X76EQ.js"; import { E_OAUTH_MISSING_CODE, E_OAUTH_STATE_MISMATCH } from "./chunk-TSIMPJ6I.js"; // src/abstract_drivers/oauth2.ts import { Exception } from "@adonisjs/core/exceptions"; import { Oauth2Client } from "@poppinss/oauth-client/oauth2"; var Oauth2Driver = class extends Oauth2Client { /** * Create a new OAuth2 driver instance. * * @param ctx - The current HTTP context * @param config - OAuth2 driver configuration */ constructor(ctx, config) { super(config); this.ctx = ctx; this.config = config; } ctx; config; /** * Whether the authorization process is stateless. When true, * state verification via cookies is disabled. */ isStateless = false; /** * The cookie name for storing the PKCE code verifier. Define this property * in child classes that require PKCE. */ codeVerifierCookieName; /** * OAuth protocol version identifier */ version = "oauth2"; /** * Cached state value read from the cookie */ stateCookieValue; /** * Cached PKCE code verifier value read from the cookie via * loadState */ codeVerifierCookieValue; /** * Creates a URL builder instance for constructing authorization URLs * with scope support. * * @param url - The base authorization URL * @returns A redirect request builder for the given URL. */ urlBuilder(url) { return new RedirectRequest(url, this.scopeParamName, this.scopesSeparator); } /** * Find if the driver uses PKCE for the OAuth2 authorization code flow. * * @returns `true` when the driver has PKCE enabled. */ #usesPkce() { return !!this.codeVerifierCookieName; } /** * Loads the state value from the encrypted cookie and immediately clears * the cookie. When PKCE is enabled, it also loads the PKCE code verifier. * This must be called by child classes in their constructor. * * @example * ```ts * constructor(ctx: HttpContext, config: DriverConfig) { * super(ctx, config) * this.loadState() * } * ``` */ loadState() { if (this.isStateless) { return; } this.stateCookieValue = this.ctx.request.encryptedCookie(this.stateCookieName); this.ctx.response.clearCookie(this.stateCookieName); if (this.#usesPkce()) { this.codeVerifierCookieValue = this.ctx.request.encryptedCookie(this.codeVerifierCookieName); this.ctx.response.clearCookie(this.codeVerifierCookieName); } } /** * Returns the PKCE code verifier for building the authorization redirect. * This method is expected to create and persist the verifier for later use. * * @returns The generated PKCE code verifier or `null` when PKCE is disabled. */ getPkceCodeVerifierForRedirect() { if (!this.#usesPkce()) { return null; } const codeVerifier = this.makeCodeVerifier(); this.ctx.response.encryptedCookie(this.codeVerifierCookieName, codeVerifier, { sameSite: false, httpOnly: true }); this.codeVerifierCookieValue = codeVerifier; return codeVerifier; } /** * Returns the PKCE code verifier for the access token exchange. * This method only reads the verifier that was persisted during redirect. * * @returns The persisted PKCE code verifier or `null` when PKCE is disabled. */ getPkceCodeVerifierForAccessToken() { if (!this.#usesPkce()) { return null; } if (!this.codeVerifierCookieValue) { throw new E_OAUTH_MISSING_CODE(["code_verifier"]); } return this.codeVerifierCookieValue; } /** * Enable stateless authentication by disabling CSRF state verification. * Only use this in scenarios where state verification is not required. * * @returns The current driver instance. * * @example * ```ts * await ally.use('github').stateless().redirect() * ``` */ stateless() { this.isStateless = true; return this; } /** * Get the authorization redirect URL without performing the redirect. * Useful when you need to manually handle the redirect or use the URL * in a different context. * * @param callback - Optional callback to customize the redirect request * @returns A promise resolving to the authorization URL. * * @example * ```ts * const url = await ally.use('github').redirectUrl((request) => { * request.scopes(['user:email']) * }) * ``` */ async redirectUrl(callback) { const url = this.getRedirectUrl(callback); return url; } /** * Redirect the user to the OAuth provider's authorization page. * The state parameter is automatically set for CSRF protection. * * @param callback - Optional callback to customize the redirect request * @returns A promise that resolves after the redirect response is prepared. * * @example * ```ts * await ally.use('github').redirect((request) => { * request.scopes(['user:email', 'read:org']) * request.param('allow_signup', 'false') * }) * ``` */ async redirect(callback) { const url = await this.redirectUrl((request) => { if (!this.isStateless) { const state = this.getState(); this.ctx.response.encryptedCookie(this.stateCookieName, state, { sameSite: false, httpOnly: true }); request.param(this.stateParamName, state); } if (typeof callback === "function") { callback(request); } }); if ("inertia" in this.ctx && this.ctx.inertia.requestInfo().isInertiaRequest) { this.ctx.inertia.location(url); } else { this.ctx.response.redirect(url); } } /** * Check if the state parameter from the callback matches the state * stored in the cookie. Returns false in stateless mode. * * @returns `true` when the state validation fails. */ stateMisMatch() { if (this.isStateless) { return false; } if (this.stateCookieValue !== this.ctx.request.input(this.stateParamName)) { return true; } if (this.#usesPkce() && !this.codeVerifierCookieValue) { return true; } return false; } /** * Check if an error was returned by the OAuth provider. * * @returns `true` when an error exists on the callback request. */ hasError() { return !!this.getError(); } /** * Get the error code or message returned by the OAuth provider. * Returns 'unknown_error' if no code is present and no error was specified. * * @returns The provider error value when present. */ getError() { const error = this.ctx.request.input(this.errorParamName); if (error) { return error; } if (!this.hasCode()) { return "unknown_error"; } return null; } /** * Get the authorization code from the callback request. * * @returns The authorization code when present. */ getCode() { return this.ctx.request.input(this.codeParamName, null); } /** * Check if the authorization code is present in the callback request. * * @returns `true` when the callback request contains an authorization code. */ hasCode() { return !!this.getCode(); } /** * Exchange the authorization code for an access token. This method * validates the state and checks for errors before making the request. * * @param callback - Optional callback to customize the token request * @returns A promise resolving to the access token payload. * * @example * ```ts * const token = await ally.use('github').accessToken() * ``` */ async accessToken(callback) { if (this.hasError()) { throw new E_OAUTH_MISSING_CODE([this.codeParamName]); } if (this.stateMisMatch()) { throw new E_OAUTH_STATE_MISMATCH(); } return this.getAccessToken((request) => { request.field(this.codeParamName, this.getCode()); if (typeof callback === "function") { callback(request); } }); } /** * Not applicable with OAuth2. Use `userFromToken` instead. * * @returns This method never returns. */ async userFromTokenAndSecret() { throw new Exception( '"userFromTokenAndSecret" is not applicable with Oauth2. Use "userFromToken" instead' ); } }; export { Oauth2Driver };