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