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