UNPKG

@poppinss/oauth-client

Version:

A framework agnostic package to implement "Login with" flow using OAuth compliant authorization servers.

208 lines (207 loc) 6.56 kB
import { a as E_OAUTH_STATE_MISMATCH, i as E_OAUTH_MISSING_TOKEN, n as HttpClient, r as debug_default, t as UrlBuilder } from "../../../url_builder-DGjb8L12.js"; import { t as random } from "../../../helpers-mzKBsjzS.js"; import { RuntimeException } from "@poppinss/exception"; import { parse } from "node:querystring"; import { createHash } from "node:crypto"; //#region src/clients/oauth2/main.ts /** * Generic implementation of OAuth2. */ var Oauth2Client = class { constructor(options) { this.options = options; } /** * Define the authorize url. Can be overridden by config */ authorizeUrl = ""; /** * Define the access token url. Can be overridden by config */ accessTokenUrl = ""; /** * Returns the PKCE code verifier for building the authorization redirect. * Child classes can override this method to generate and persist a verifier. */ getPkceCodeVerifierForRedirect() { return null; } /** * Returns the PKCE code verifier for the access token exchange. * Child classes can override this method to load a previously persisted verifier. */ getPkceCodeVerifierForAccessToken() { return null; } /** * Returns the PKCE code challenge. Child classes can override this method * to customize the challenge derivation or persistence strategy. */ getPkceCodeChallenge(codeVerifier) { return this.makeCodeChallenge(codeVerifier, this.getPkceCodeChallengeMethod()); } /** * Returns the PKCE code challenge method. */ getPkceCodeChallengeMethod() { return "S256"; } /** * Processing the API client response. The child class can overwrite it * for more control */ processClientResponse(client, response) { /** * Return json as it is when parsed response as json */ if (client.getResponseType() === "json") return response; return parse(response); } /** * Configure the redirect request. Invoked before * the user callback. * * The client defaults can be removed using the `clearParam` method */ configureRedirectRequest(_) {} /** * Configure the access token request. Invoked before * the user callback. * * The client defaults can be removed using the `clearParam` or * `clearOauth1Param` methods */ configureAccessTokenRequest(_) {} /** * Returns the instance of the HTTP client for internal use */ httpClient(url) { return new HttpClient(url); } /** * Returns the instance of the URL builder */ urlBuilder(url) { return new UrlBuilder(url); } /** * Generates a random PKCE code verifier. */ makeCodeVerifier() { return random(64); } /** * Generates a PKCE code challenge from the given verifier. */ makeCodeChallenge(codeVerifier, method = "S256") { if (method === "plain") return codeVerifier; return createHash("sha256").update(codeVerifier).digest("base64url"); } /** * Returns the redirect url for redirecting the user. Pre-defines * the following params * * - redirect_uri * - client_id */ getRedirectUrl(callback) { const authorizeUrl = this.options.authorizeUrl || this.authorizeUrl; if (!authorizeUrl) throw new RuntimeException("Missing \"config.authorizeUrl\". The property is required to make redirect url"); const urlBuilder = this.urlBuilder(authorizeUrl); /** * Default params. One can call `clearParam` to remove them */ urlBuilder.param("redirect_uri", this.options.callbackUrl); urlBuilder.param("client_id", this.options.clientId); const codeVerifier = this.getPkceCodeVerifierForRedirect(); if (codeVerifier) { urlBuilder.param("code_challenge", this.getPkceCodeChallenge(codeVerifier)); urlBuilder.param("code_challenge_method", this.getPkceCodeChallengeMethod()); } this.configureRedirectRequest(urlBuilder); /** * Invoke callback when defined. This is the place where one can configure * the request query params */ if (typeof callback === "function") callback(urlBuilder); const url = urlBuilder.makeUrl(); debug_default("oauth2 redirect url: \"%s\"", url); return url; } /** * Generates a random token to be stored as a state and to be sent along * for later verification */ getState() { return random(32); } /** * Verifies the redirect input with the state input */ verifyState(state, inputValue) { if (!state || state !== inputValue) throw new E_OAUTH_STATE_MISMATCH(); } /** * Get the access token from the authorization code. One must define * the authorization code using the callback input. * * ```ts * client.getAccessToken((request) => { * request.field('code', authorizationCode) * }) * ``` * * Pre-defines the following form fields * * - grant_type = 'authorization_code' * - redirect_uri * - client_id * - client_secret */ async getAccessToken(callback) { const accessTokenUrl = this.options.accessTokenUrl || this.accessTokenUrl; if (!accessTokenUrl) throw new RuntimeException("Missing \"config.accessTokenUrl\". The property is required to get access token"); const httpClient = this.httpClient(accessTokenUrl); /** * Default field. One can call `clearField` to remove them */ httpClient.field("grant_type", "authorization_code"); httpClient.field("redirect_uri", this.options.callbackUrl); httpClient.field("client_id", this.options.clientId); httpClient.field("client_secret", this.options.clientSecret); const codeVerifier = this.getPkceCodeVerifierForAccessToken(); if (codeVerifier) httpClient.field("code_verifier", codeVerifier); /** * Expecting JSON response. One can call `parseAs('text')` for urlencoded * response */ httpClient.parseAs("json"); this.configureAccessTokenRequest(httpClient); /** * Invoke the user callback after setting defaults. This allows the callback * to clear/overwrite them */ if (typeof callback === "function") callback(httpClient); /** * Make request and parse response */ const response = await httpClient.post(); const accessTokenResponse = this.processClientResponse(httpClient, response); debug_default("oauth2 access token response %O", accessTokenResponse); const { access_token: accessToken, token_type: tokenType, expires_in: expiresIn, refresh_token: refreshToken, ...parsed } = accessTokenResponse; /** * We expect the response to have "access_token" */ if (!accessToken) throw new E_OAUTH_MISSING_TOKEN(E_OAUTH_MISSING_TOKEN.oauth2Message, { cause: parsed }); return { token: accessToken, type: tokenType, expiresIn, ...expiresIn ? { expiresAt: new Date((/* @__PURE__ */ new Date()).getTime() + 1e3 * expiresIn) } : {}, refreshToken, ...parsed }; } }; //#endregion export { Oauth2Client };