@adonisjs/ally
Version:
Social authentication provider for AdonisJS
292 lines (289 loc) • 8.3 kB
JavaScript
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
};