UNPKG

@scayle/storefront-core

Version:

Collection of essential utilities to work with the Storefront API

304 lines (303 loc) 9.57 kB
import { decodeJwt } from "jose"; import { FetchError } from "../utils/fetch.mjs"; import { encodeBase64 } from "../utils/hash.mjs"; class MissingCredentialsError extends Error { constructor() { super("[OAuth API] No credentials configured"); this.name = "MissingCredentialsError"; } } export class OAuthRequestError extends Error { constructor(message, options) { super(message, options); this.name = "OAuthRequestError"; } } function stringifyOAuthError(error) { if (error.message) { return error.hint ? `${error.error}: ${error.message} (hint: ${error.hint})` : `${error.error}: ${error.message}`; } else { return error.error; } } async function oauthResponseHandler(response) { const data = await response.json(); if (!response.ok) { const fetchError = new FetchError(response, data); if (data) { const error = new OAuthRequestError(stringifyOAuthError(data), { cause: fetchError }); throw error; } throw fetchError; } return data; } function emptyOAuthResponseHandler(response) { if (!response.ok) { throw new FetchError(response); } } export function getOAuthClient(context) { if (!context.oauth) { throw new Error("OAuth configuration is missing"); } const { clientId, clientSecret, apiHost } = context.oauth; const accessHeader = context.internalAccessHeader; const requestHeaders = context.headers; const host = requestHeaders.get("host"); const userAgent = requestHeaders.get("user-agent"); const contentType = requestHeaders.get("content-type"); const ip = context.originalIp; const additionalHeaders = {}; if (ip) { additionalHeaders["x-original-client-ip"] = ip; } if (userAgent) { additionalHeaders["x-original-user-agent"] = userAgent; } if (host) { additionalHeaders["x-original-host"] = host; } if (contentType) { additionalHeaders["x-original-content-type"] = contentType; } if (accessHeader) { additionalHeaders["x-internal-access"] = accessHeader; } return new OAuthClient( { clientId, clientSecret, apiHost, additionalHeaders }, context.log ); } export class OAuthClient { /** * Headers for API requests. */ headers; /** * Base URL for the API. */ baseURL; /** * Logger instance. */ logger; /** * OAuth client ID. */ clientId; /** * Creates a new instance of the OAuthClient. * * @param options OAuth client options. * @param logger Optional logger instance. * * @throws {MissingCredentialsError} If client ID or client secret are missing. */ constructor(options, logger) { const { clientId, clientSecret, apiHost, additionalHeaders } = options; if (!clientId || !clientSecret) { throw new MissingCredentialsError(); } this.clientId = clientId; this.baseURL = `${apiHost}/v1`; this.logger = logger ? logger.space("auth-client") : void 0; const basicAuthHash = encodeBase64(`${clientId}:${clientSecret}`); this.headers = { Authorization: `Basic ${basicAuthHash}`, Accept: "application/json", "Content-Type": "application/json", ...additionalHeaders }; } /** * Register a new User and receive an access token. * * @param payload The registration data. * * @returns The OAuth response containing access and refresh tokens. * * @see https://scayle.dev/en/api-guides/authentication-api/resources/oauth-client/create-new-user */ async register(payload) { this.logger?.debug("Registering user"); return await fetch(`${this.baseURL}/auth/register`, { method: "POST", headers: this.headers, body: JSON.stringify(payload) }).then(oauthResponseHandler); } /** * Login a User and receive an access token. * * @param payload The login credentials, including the `shopId`. * * @returns The OAuth response containing access and refresh tokens. * * @see https://scayle.dev/en/api-guides/authentication-api/resources/oauth-client/log-in-users */ async login(payload) { this.logger?.debug("Logging in"); return await fetch(`${this.baseURL}/auth/login`, { method: "POST", headers: this.headers, body: JSON.stringify(payload) }).then(oauthResponseHandler); } /** * Login a User as a guest and receive an access token. * * @param payload The guest login data. * * @returns The OAuth response containing access and refresh tokens. * * @see https://scayle.dev/en/api-guides/authentication-api/resources/oauth-client/log-in-users-as-guest */ async guestLogin(payload) { this.logger?.debug("Logging in as guest"); return await fetch(`${this.baseURL}/auth/login/guest`, { method: "POST", headers: this.headers, body: JSON.stringify(payload) }).then(oauthResponseHandler); } /** * Send a reset password email to a User. * * @param payload The data for sending the reset password email. * * @returns Nothing (resolves when the request completes successfully). * * @see https://scayle.dev/en/api-guides/authentication-api/resources/oauth-client/send-password-reset-email */ async sendPasswordResetEmail(payload) { this.logger?.debug("Sending password reset email"); await fetch(`${this.baseURL}/auth/password/send-reset-email`, { method: "POST", headers: this.headers, body: JSON.stringify(payload) }).then(emptyOAuthResponseHandler); } /** * Update password by using hash. * All older tokens of the User are also invalidated. * * @param payload The data for updating the password by hash. * * @returns The new OAuth response with updated tokens. * * @see https://scayle.dev/en/api-guides/authentication-api/resources/oauth-client/update-password-by-hash */ async updatePasswordByHash(payload) { this.logger?.debug("Updating password by hash"); return await fetch(`${this.baseURL}/auth/password/update-by-hash`, { method: "PUT", headers: this.headers, body: JSON.stringify(payload) }).then(oauthResponseHandler); } /** * Update password via plain string. * * @param payload The data for updating the password (current and new password). * @param accessToken The current access token. * * @returns Nothing (resolves when the request completes successfully). */ async updatePassword(payload, accessToken) { this.logger?.debug("Updating password"); return await fetch(`${this.baseURL}/auth/password`, { method: "PUT", headers: { ...this.headers, Authorization: `Bearer ${accessToken}` }, body: JSON.stringify(payload) }).then(emptyOAuthResponseHandler); } /** * Generate a new access token via a refresh token. * * @param payload The refresh token request data. * @returns The new OAuth response with updated access token. */ async refreshToken(payload) { this.logger?.debug("Refreshing access token"); return await fetch(`${this.baseURL}/oauth/token`, { method: "POST", headers: this.headers, body: JSON.stringify(payload) }).then(oauthResponseHandler); } /** * Validate a token. * * @param accessToken The access token to validate. * * @returns Nothing (resolves if the token is valid, rejects otherwise). * * @see https://scayle.dev/en/api-guides/authentication-api/resources/bearer-auth/validate-present-token */ async validateToken(accessToken) { this.logger?.debug("Validating access token"); await fetch(`${this.baseURL}/oauth/token/validate`, { headers: { ...this.headers, Authorization: `Bearer ${accessToken}` } }).then(emptyOAuthResponseHandler); } /** * Revoke an Access Token and all related Refresh Tokens. * * Uses a valid Bearer Access Token in the Authorization header and will * revoke the token with the given ID (_which could be a different token_). * If a external identity provider was used for the target token, * the corresponding IDP-AccessToken and IDP-RefreshToken will be revoked as well. * In case the identity provider does not support revoking tokens over api calls * (_because a frontend redirect is required_) this step will be skipped and the IDP-Tokens * will remain valid until they expire or are revoked from IDP side. * * @param shopId The ID of the shop. * @param accessToken The access token to use for authorization. * @returns Nothing (resolves when the token is successfully revoked). * * @see https://scayle.dev/en/api-guides/authentication-api/resources/bearer-auth/delete-access-token-with-id */ async revokeToken(shopId, accessToken) { this.logger?.debug("Revoking access token"); const decodedAccessToken = decodeJwt(accessToken); await fetch(`${this.baseURL}/oauth/tokens/${decodedAccessToken.jti}`, { method: "DELETE", headers: { ...this.headers, "X-Shop-Id": `${shopId}`, Authorization: `Bearer ${accessToken}` } }).then(emptyOAuthResponseHandler); } /** * Generate a new token based on authorization code. * * @param code The authorization code. * * @returns The new OAuth response. */ async generateToken(code) { return await fetch(`${this.baseURL}/oauth/token`, { method: "POST", headers: this.headers, body: JSON.stringify({ grant_type: "authorization_code", code }) }).then(oauthResponseHandler); } }