@badgateway/oauth2-client
Version:
OAuth2 client for browsers and Node.js. Tiny footprint, PKCE support
138 lines • 5.36 kB
JavaScript
import { OAuth2Error } from "../error.js";
export class OAuth2AuthorizationCodeClient {
constructor(client) {
this.client = client;
}
/**
* Returns the URi that the user should open in a browser to initiate the
* authorization_code flow.
*/
async getAuthorizeUri(params) {
const [codeChallenge, authorizationEndpoint] = await Promise.all([
params.codeVerifier ? getCodeChallenge(params.codeVerifier) : undefined,
this.client.getEndpoint('authorizationEndpoint')
]);
const query = new URLSearchParams({
client_id: this.client.settings.clientId,
response_type: 'code',
redirect_uri: params.redirectUri,
});
if (codeChallenge) {
query.set('code_challenge_method', codeChallenge[0]);
query.set('code_challenge', codeChallenge[1]);
}
if (params.state) {
query.set('state', params.state);
}
if (params.scope) {
query.set('scope', params.scope.join(' '));
}
if (params.resource)
for (const resource of [].concat(params.resource)) {
query.append('resource', resource);
}
if (params.responseMode && params.responseMode !== 'query') {
query.append('response_mode', params.responseMode);
}
if (params.extraParams)
for (const [k, v] of Object.entries(params.extraParams)) {
if (query.has(k))
throw new Error(`Property in extraParams would overwrite standard property: ${k}`);
query.set(k, v);
}
return authorizationEndpoint + '?' + query.toString();
}
async getTokenFromCodeRedirect(url, params) {
const { code } = this.validateResponse(url, {
state: params.state
});
return this.getToken({
code,
redirectUri: params.redirectUri,
codeVerifier: params.codeVerifier,
});
}
/**
* After the user redirected back from the authorization endpoint, the
* url will contain a 'code' and other information.
*
* This function takes the url and validate the response. If the user
* redirected back with an error, an error will be thrown.
*/
validateResponse(url, params) {
var _a;
url = new URL(url);
let queryParams = url.searchParams;
if (!queryParams.has('code') && !queryParams.has('error') && url.hash.length > 0) {
// Try the fragment
queryParams = new URLSearchParams(url.hash.slice(1));
}
if (queryParams.has('error')) {
throw new OAuth2Error((_a = queryParams.get('error_description')) !== null && _a !== void 0 ? _a : 'OAuth2 error', queryParams.get('error'));
}
if (!queryParams.has('code'))
throw new Error(`The url did not contain a code parameter ${url}`);
if (params.state && params.state !== queryParams.get('state')) {
throw new Error(`The "state" parameter in the url did not match the expected value of ${params.state}`);
}
return {
code: queryParams.get('code'),
scope: queryParams.has('scope') ? queryParams.get('scope').split(' ') : undefined,
};
}
/**
* Receives an OAuth2 token using 'authorization_code' grant
*/
async getToken(params) {
const body = {
grant_type: 'authorization_code',
code: params.code,
redirect_uri: params.redirectUri,
code_verifier: params.codeVerifier,
resource: params.resource,
};
return this.client.tokenResponseToOAuth2Token(this.client.request('tokenEndpoint', body));
}
}
export async function generateCodeVerifier() {
const webCrypto = await getWebCrypto();
const arr = new Uint8Array(32);
webCrypto.getRandomValues(arr);
return base64Url(arr);
}
export async function getCodeChallenge(codeVerifier) {
const webCrypto = await getWebCrypto();
return ['S256', base64Url(await webCrypto.subtle.digest('SHA-256', stringToBuffer(codeVerifier)))];
}
async function getWebCrypto() {
var _a;
// Browsers
if ((typeof window !== 'undefined' && window.crypto)) {
if (!((_a = window.crypto.subtle) === null || _a === void 0 ? void 0 : _a.digest)) {
throw new Error("The context/environment is not secure, and does not support the 'crypto.subtle' module. See: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/subtle for details");
}
return window.crypto;
}
// Web workers possibly
if ((typeof self !== 'undefined' && self.crypto)) {
return self.crypto;
}
// Node
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = await import('crypto');
return crypto.webcrypto;
}
function stringToBuffer(input) {
const buf = new Uint8Array(input.length);
for (let i = 0; i < input.length; i++) {
buf[i] = input.charCodeAt(i) & 0xFF;
}
return buf;
}
function base64Url(buf) {
return (btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, ''));
}
//# sourceMappingURL=authorization-code.js.map