UNPKG

@poppinss/oauth-client

Version:

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

295 lines (294 loc) 10.1 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 { escape, parse } from "node:querystring"; import { createHmac } from "node:crypto"; import { URL } from "node:url"; //#region src/clients/oauth1/signature.ts /** * Creates the signature for the OAuth1 request by following the spec * https://tools.ietf.org/html/rfc5849#section-3.5.1 * https://oauth.net/core/1.0a/#auth_step1 */ var Oauth1Signature = class { #options; constructor(options) { this.#options = options; } /** * Generate signature and the oauth header */ generate() { /** * The oauth request params. They are used to generate * the signature */ const params = { ...this.#options.params, ...this.#options.oauthToken ? { oauth_token: this.#options.oauthToken } : {}, oauth_consumer_key: this.#options.consumerKey, oauth_nonce: this.#options.nonce, oauth_signature_method: "HMAC-SHA1", oauth_timestamp: this.#options.unixTimestamp, oauth_version: "1.0" }; /** * Creates the params string as per https://tools.ietf.org/html/rfc5849#section-3.5.1 */ const orderedParamsString = Object.entries(params).map((entry) => entry.map((key) => escape(String(key))).join("=")).sort().join("&"); /** * Constructing a URL and converting it to a string will remove the standard * port if defined. * * For example: * * Input: https://example.com:443/wp-json/wp/v2/posts * Output: https://example.com/wp-json/wp/v2/posts * * Input: http://example.com:80/wp-json/wp/v2/posts * Output: http://example.com/wp-json/wp/v2/posts * */ const url = new URL(this.#options.url).toString(); /** * Creates the base string as per https://oauth1.wp-api.org/docs/basics/Signing.html#base-string */ const baseString = [ this.#options.method.toUpperCase(), escape(url), escape(orderedParamsString) ].join("&"); /** * Generate signing secret * https://oauth1.wp-api.org/docs/basics/Signing.html#signature-key */ let signatureKey = `${escape(this.#options.consumerSecret)}&`; if (this.#options.oauthTokenSecret) signatureKey = `${signatureKey}${escape(this.#options.oauthTokenSecret)}`; const signature = createHmac("SHA1", signatureKey).update(baseString, "utf8").digest("base64"); /** * A collection of `oauth_` params. They should be sent using the Authorization * header */ const oauthParams = Object.keys(params).reduce((result, key) => { if (key.startsWith("oauth_")) result[key] = params[key]; return result; }, { oauth_signature: signature }); return { params, oauthParams, oauthHeader: Object.entries(oauthParams).map((entry) => { return `${escape(entry[0])}="${escape(String(entry[1]))}"`; }).sort().join(","), signature }; } }; //#endregion //#region src/clients/oauth1/main.ts /** * Generic implementation of Oauth1 three leged authorization flow. */ var Oauth1Client = class { constructor(options) { this.options = options; } /** * Define the request token url. Can be overridden by config */ requestTokenUrl = ""; /** * Define the authorize url. Can be overridden by config */ authorizeUrl = ""; /** * Define the access token url. Can be overridden by config */ accessTokenUrl = ""; /** * Get the signature for the request */ getSignature(baseUrl, method, params, requestToken) { return new Oauth1Signature({ url: baseUrl, method: method.toUpperCase(), params, consumerKey: this.options.clientId, consumerSecret: this.options.clientSecret, nonce: random(32), unixTimestamp: Math.floor((/* @__PURE__ */ new Date()).getTime() / 1e3), oauthToken: requestToken && requestToken.token, oauthTokenSecret: requestToken && requestToken.secret }).generate(); } /** * Make a signed request to the authorization server. The request follows * the Oauth1 spec and generates the Authorization header using the * [[Oauth1Signature]] class. */ async makeSignedRequest(url, method, requestToken, callback) { const httpClient = this.httpClient(url); /** * Invoke callback to allow configuring request */ if (typeof callback === "function") callback(httpClient); /** * Generate oauth header */ const { oauthHeader } = this.getSignature(url, method, { ...httpClient.getParams(), ...httpClient.getOauth1Params(), ...httpClient.getRequestType() === "urlencoded" ? httpClient.getFields() : {} }, requestToken); /** * Set the oauth header */ debug_default("oauth1 signature: %s", oauthHeader); httpClient.header("Authorization", `OAuth ${oauthHeader}`); /** * Make HTTP request */ const response = await httpClient[method](); return this.processClientResponse(url, httpClient, 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(_) {} /** * Configure the request token request. Invoked before * the user callback. * * The client defaults can be removed using the `clearParam` or * `clearOauth1Param` methods */ configureRequestTokenRequest(_) {} /** * 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); } /** * 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); } /** * Verify state and the input value and raise exception if different or missing */ verifyState(state, inputValue) { if (!state || state !== inputValue) throw new E_OAUTH_STATE_MISMATCH(); } /** * Returns the oauth token and secret for the upcoming requests */ async getRequestToken(callback) { const requestTokenUrl = this.options.requestTokenUrl || this.requestTokenUrl; if (!requestTokenUrl) throw new RuntimeException("Missing \"config.requestTokenUrl\". The property is required to get request token"); const requestTokenResponse = await this.makeSignedRequest(requestTokenUrl, "post", void 0, (request) => { request.oauth1Param("oauth_callback", this.options.callbackUrl); this.configureRequestTokenRequest(request); if (typeof callback === "function") callback(request); }); debug_default("oauth1 request token response %O", requestTokenResponse); const { oauth_token: oauthToken, oauth_token_secret: oauthTokenSecret, ...parsed } = requestTokenResponse; /** * We expect the response to have "oauth_token" and "oauth_token_secret" */ if (!oauthToken || !oauthTokenSecret) throw new E_OAUTH_MISSING_TOKEN(E_OAUTH_MISSING_TOKEN.oauth1Message, { cause: parsed }); return { token: oauthToken, secret: oauthTokenSecret, ...parsed }; } /** * Returns the redirect url for redirecting the user. We don't pre-define * any params here. However, one must define the "oauth_token" param * by passing a callback. * * ```ts * client.getRedirectUrl((request) => { * request.param('oauth_token', value) * }) * ``` */ 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); 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("oauth1 redirect url: \"%s\"", url); return url; } /** * Get the access token from the oauth_verifier code. One must define * the "oauth_verifier" code using the callback input. * * ```ts * client.getAccessToken({ token, secret }, (request) => { * request.oauth1Param('oauth_verifier', verifierValue) * }) * ``` */ async getAccessToken(requestToken, callback) { const accessTokenUrl = this.options.accessTokenUrl || this.accessTokenUrl; /** * Even though the spec allows to generate access token without the "oauthTokenSecret". * We enforce both the "oauthToken" and "oauthTokenSecret" to exist. This ensures * better security */ if (!requestToken.token) throw new RuntimeException("Missing \"requestToken.token\". The property is required to generate access token"); if (!requestToken.secret) throw new RuntimeException("Missing \"requestToken.secret\". The property is required to generate access token"); if (!accessTokenUrl) throw new RuntimeException("Missing \"config.accessTokenUrl\". The property is required to generate access token"); /** * Make signed request. */ const accessTokenResponse = await this.makeSignedRequest(accessTokenUrl, "post", requestToken, (request) => { this.configureAccessTokenRequest(request); if (typeof callback === "function") callback(request); }); debug_default("oauth1 access token response %O", accessTokenResponse); const { oauth_token: accessOauthToken, oauth_token_secret: accessOauthTokenSecret, ...parsed } = accessTokenResponse; /** * We expect the response to have "oauth_token" and "oauth_token_secret" */ if (!accessOauthToken || !accessOauthTokenSecret) throw new E_OAUTH_MISSING_TOKEN(E_OAUTH_MISSING_TOKEN.oauth1Message, { cause: parsed }); return { token: accessOauthToken, secret: accessOauthTokenSecret, ...parsed }; } }; //#endregion export { Oauth1Client };