@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
JavaScript
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 };