UNPKG

@authress/sdk

Version:

Client SDK for Authress authorization as a service. Provides managed authorization api to secure service resources including user data.

127 lines (108 loc) 6.07 kB
const { SignJWT } = require('jose'); const { createPrivateKey } = require('crypto'); const ArgumentRequiredError = require('./argumentRequiredError'); const InvalidAccessKeyError = require('./invalidAccessKeyError'); const { sanitizeUrl } = require('./util'); const packageInfo = require('../package.json'); function getIssuer(unsanitizedAuthressCustomDomain, decodedAccessKey) { const authressCustomDomain = sanitizeUrl(unsanitizedAuthressCustomDomain).replace(/\/+$/, ''); return `${authressCustomDomain}/v1/clients/${encodeURIComponent(decodedAccessKey.clientId)}`; } class ServiceClientTokenProvider { constructor(accessKey, authressCustomDomain) { const accountId = accessKey.split('.')[2]; this.accountId = accountId; this.authressCustomDomain = authressCustomDomain; this.decodedAccessKey = { clientId: accessKey.split('.')[0], keyId: accessKey.split('.')[1], audience: `${accountId}.accounts.authress.io`, privateKey: accessKey.split('.')[3] }; } async getToken(options = { jwtOverrides: { header: {}, payload: {} } }) { if (this.cachedKeyData && this.cachedKeyData.token && this.cachedKeyData.expires > Date.now() + 3600000) { return this.cachedKeyData.token; } // Do not set the issuer to be ${accountId}.api-region.authress.io it should always be set as the authress custom domain, the custom domain, or the generic api.authress.io one const useAuthressCustomDomain = this.authressCustomDomain && !this.authressCustomDomain.match(/authress\.io/); const now = Math.round(Date.now() / 1000); const jwt = Object.assign({ aud: this.decodedAccessKey.audience, iss: getIssuer(useAuthressCustomDomain && this.authressCustomDomain || `${this.accountId}.api.authress.io`, this.decodedAccessKey), sub: this.decodedAccessKey.clientId, client_id: this.decodedAccessKey.clientId, iat: now, // valid for 24 hours exp: now + 60 * 60 * 24, scope: 'openid' }, options?.jwtOverrides?.payload || {}); if (!this.decodedAccessKey.privateKey) { throw new InvalidAccessKeyError(); } try { const importedKey = createPrivateKey({ key: Buffer.from(this.decodedAccessKey.privateKey, 'base64'), format: 'der', type: 'pkcs8' }); const header = Object.assign({ alg: 'EdDSA', kid: this.decodedAccessKey.keyId, typ: 'at+jwt' }, options?.jwtOverrides?.header || {}); const token = await new SignJWT(jwt).setProtectedHeader(header).sign(importedKey); this.cachedKeyData = { token, expires: jwt.exp * 1000 }; return token; } catch (error) { if (error.code === 'ERR_OSSL_ASN1_NOT_ENOUGH_DATA' || error.code === 'ERR_OSSL_ASN1_HEADER_TOO_LONG' || error.message === 'Failed to read private key') { throw new InvalidAccessKeyError(); } throw error; } } async generateUserLoginUrl(authressCustomDomainLoginUrlInput, authenticationRequestIdInput, clientIdInput, userIdInput) { if (!authressCustomDomainLoginUrlInput) { throw new ArgumentRequiredError('authressCustomDomainLoginUrl', 'The authressCustomDomainLoginUrl is not specified in the incoming login request, this should match the configured Authress custom domain.'); } let authressCustomDomainLoginUrl = authressCustomDomainLoginUrlInput; let authenticationRequestId = authenticationRequestIdInput; let clientId = clientIdInput; let userId = userIdInput; if (typeof authressCustomDomainLoginUrlInput === 'object' && authressCustomDomainLoginUrlInput.authenticationUrl) { userId = authenticationRequestIdInput; const parameters = [...new URL(authressCustomDomainLoginUrlInput.authenticationUrl).searchParams.entries()].reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}); authressCustomDomainLoginUrl = authressCustomDomainLoginUrlInput.authenticationUrl; clientId = parameters.client_id; authenticationRequestId = parameters.state; } if (!authenticationRequestId) { throw new ArgumentRequiredError('state', 'The state is required to generate a authorization code redirect for is required, and should be present in the authenticationUrl.'); } if (!clientId || clientId !== this.decodedAccessKey.clientId) { throw new ArgumentRequiredError('clientId', 'The clientId should be specified in the authenticationUrl. It should match the service client ID.'); } if (!userId) { throw new ArgumentRequiredError('userId', 'The user to generate an authorization code redirect for is required.'); } const customDomainFallback = new URL(authressCustomDomainLoginUrl).origin; const issuer = getIssuer(this.authressCustomDomain || customDomainFallback, this.decodedAccessKey); const now = Math.round(Date.now() / 1000); const clientSdkString = `Authress SDK; Javascript; ${packageInfo.version}; ${this.decodedAccessKey.clientId}`; const codeJwt = { aud: this.decodedAccessKey.audience, iss: issuer, sub: userId, client_id: this.decodedAccessKey.clientId, jti: `${clientSdkString}-${userId}`, iat: now, exp: now + 3600, max_age: 3600, scope: 'openid' }; if (userId.match(/@/)) { codeJwt.email = userId; codeJwt.email_verified = true; } const importedKey = createPrivateKey({ key: Buffer.from(this.decodedAccessKey.privateKey, 'base64'), format: 'der', type: 'pkcs8' }); const encodedCode = await new SignJWT(codeJwt).setProtectedHeader({ alg: 'EdDSA', kid: this.decodedAccessKey.keyId, typ: 'oauth-authz-req+jwt' }).sign(importedKey); const url = new URL(authressCustomDomainLoginUrl); // the UI always redirects to /login for handling redirects, we definitely cannot change this now as customers will have registered this path in third party tools. url.pathname = '/login'; url.searchParams.set('code', encodedCode); url.searchParams.set('iss', issuer); url.searchParams.set('state', authenticationRequestId); return url.toString(); } } module.exports = ServiceClientTokenProvider;