UNPKG

@poppinss/oauth-client

Version:

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

311 lines (308 loc) 9.43 kB
import { random } from "../../../chunk-KDYPQVIV.js"; import { E_OAUTH_MISSING_TOKEN, E_OAUTH_STATE_MISMATCH, HttpClient, UrlBuilder, debug_default } from "../../../chunk-2CCQZGHU.js"; // src/clients/oauth1/main.ts import { parse } from "node:querystring"; import { RuntimeException } from "@poppinss/exception"; // src/clients/oauth1/signature.ts import { URL } from "node:url"; import { createHmac } from "node:crypto"; import { escape } from "node:querystring"; var Oauth1Signature = class { #options; constructor(options) { this.#options = options; } /** * Generate signature and the oauth header */ generate() { 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" }; const orderedParamsString = Object.entries(params).map((entry) => entry.map((key) => escape(String(key))).join("=")).sort().join("&"); const url = new URL(this.#options.url).toString(); const baseString = [ this.#options.method.toUpperCase(), escape(url), escape(orderedParamsString) ].join("&"); 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"); 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 }; } }; // src/clients/oauth1/main.ts 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); if (typeof callback === "function") { callback(httpClient); } const { oauthHeader } = this.getSignature( url, method, { ...httpClient.getParams(), ...httpClient.getOauth1Params(), /** * Consider URLEncoded request body when creating signature header. * However, the fields from JSON body should not be included * in the signature base string. * https://oauth1.wp-api.org/docs/basics/Signing.html#json-data */ ...httpClient.getRequestType() === "urlencoded" ? httpClient.getFields() : {} }, requestToken ); debug_default("oauth1 signature: %s", oauthHeader); httpClient.header("Authorization", `OAuth ${oauthHeader}`); 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) { 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; 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); 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; 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' ); } 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; if (!accessOauthToken || !accessOauthTokenSecret) { throw new E_OAUTH_MISSING_TOKEN(E_OAUTH_MISSING_TOKEN.oauth1Message, { cause: parsed }); } return { token: accessOauthToken, secret: accessOauthTokenSecret, ...parsed }; } }; export { Oauth1Client }; //# sourceMappingURL=main.js.map