integreat-authenticator-oauth2
Version:
OAuth 2.0 authenticator for Integreat
152 lines • 5.19 kB
JavaScript
import debugFn from 'debug';
import formTransformer from 'integreat-adapter-form/transformer.js';
import signJwt from './signJwt.js';
import { isObject } from './utils/is.js';
const VALID_GRANT_TYPES = [
'authorizationCode',
'refreshToken',
'clientCredentials',
'jwtAssertion',
];
const debug = debugFn('integreat:authenticator:oauth2');
const serializeForm = (data) => formTransformer({})({})(data, { rev: true, context: [], value: data });
const base64Auth = (key, secret) => Buffer.from(`${key}:${secret}`).toString('base64');
const isValidOptions = (options) => !!options &&
typeof options.grantType === 'string' &&
VALID_GRANT_TYPES.includes(options.grantType);
const baseFields = ['grantType', 'uri', 'key', 'secret'];
const conditionalFields = {
authorizationCode: ['redirectUri', 'code'],
refreshToken: ['redirectUri', 'refreshToken'],
};
function hasRequiredOptions(options) {
const requiredFields = [
...baseFields,
...(conditionalFields[options.grantType] || []),
];
return requiredFields.filter((field) => !options[field]);
}
async function parseData(response) {
try {
return (await response.json());
}
catch {
return null;
}
}
function resolveTypeAndToken(options, authentication) {
if (options.grantType === 'authorizationCode' &&
!authentication?.refreshToken) {
return {
grant_type: 'authorization_code',
client_id: options.key,
client_secret: options.secret,
redirect_uri: options.redirectUri,
code: options.code,
};
}
else {
return {
grant_type: 'refresh_token',
client_id: options.key,
client_secret: options.secret,
redirect_uri: options.redirectUri,
refresh_token: options.grantType === 'refreshToken'
? options.refreshToken
: authentication?.refreshToken,
};
}
}
function prepareFormData(options, authentication) {
switch (options.grantType) {
case 'authorizationCode':
case 'refreshToken':
return resolveTypeAndToken(options, authentication);
case 'jwtAssertion':
return {
grantType: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: signJwt(options),
};
default:
return {
grant_type: 'client_credentials',
};
}
}
function getHeaders(options) {
const headers = isObject(options.headers)
? Object.fromEntries(Object.entries(options.headers).filter((entry) => typeof entry[0] === 'string' || Array.isArray(entry[0])))
: {};
headers['content-type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
if (options.grantType === 'clientCredentials') {
headers.Authorization = `Basic ${base64Auth(options.key, options.secret)}`;
}
return headers;
}
const generateAuthentication = (data, options) => ({
status: 'granted',
token: data.access_token,
expire: Date.now() + data.expires_in * 1000,
type: options.authHeaderType,
...(data.refresh_token ? { refreshToken: data.refresh_token } : {}),
});
async function requestAuth(options, authentication) {
const uri = options.uri;
const body = serializeForm({
...prepareFormData(options, authentication),
...(options.scope ? { scope: options.scope } : {}),
});
const request = {
method: 'POST',
body,
headers: getHeaders(options),
};
debug(`Sending auth request: ${JSON.stringify(request)}`);
let response;
try {
response = await fetch(uri, request);
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
debug(`Auth attempt failed: ${message}`);
return { status: 'error', error: message };
}
const data = await parseData(response);
debug(`Received auth response: ${JSON.stringify(data)}`);
if (response.ok) {
if (data) {
return generateAuthentication(data, options);
}
else {
return { status: 'error', error: 'Invalid json response' };
}
}
else if (response.status === 400) {
return {
status: 'refused',
error: data?.error_description
? `Refused by service: ${data?.error_description}`
: 'Refused by service',
};
}
else {
return { status: 'error', error: response.statusText };
}
}
export default async function authenticate(options, _action, _dispatch, authentication) {
if (!isValidOptions(options)) {
return {
status: 'error',
error: 'Unknown or missing grant type option',
};
}
const missingFields = hasRequiredOptions(options);
if (missingFields.length > 0) {
return {
status: 'error',
error: `Missing ${missingFields.join(', ')} option${missingFields.length > 1 ? 's' : ''}`,
};
}
return await requestAuth(options, authentication);
}
//# sourceMappingURL=authenticate.js.map