Skip to main content
Glama
auth0.service.ts7.33 kB
import Axios from "axios"; import { nanoid } from "nanoid"; import { ManagementClient } from "auth0"; import JWT from "jsonwebtoken"; import { ApiError } from "../lib/api-error"; import { tryCatch } from "../lib/try-catch"; import { getQueryString } from "../lib/querystring"; import { getCache, setCache } from "../lib/cache"; // const auth0Client = new AuthenticationClient({ // /* eslint-disable @typescript-eslint/no-non-null-assertion */ // domain: process.env.AUTH0_DOMAIN!, // clientId: process.env.AUTH0_CLIENT_ID!, // }); const LOGIN_CALLBACK_URL = `${process.env.AUTH_API_URL}/auth/login-callback`; const LOGOUT_CALLBACK_URL = `${process.env.AUTH_API_URL}/auth/logout-callback`; const auth0Api = Axios.create({ baseURL: `https://${process.env.AUTH0_DOMAIN}`, }); export async function getAuth0UserCredential(username: string, password: string) { const authResult = await auth0Api.request({ method: "post", url: "/oauth/token", headers: { "content-type": "application/json" }, data: JSON.stringify({ grant_type: "password", username, password, client_id: process.env.AUTH0_CLIENT_ID, client_secret: process.env.AUTH0_CLIENT_SECRET, audience: `https://${process.env.AUTH0_DOMAIN}/api/v2/`, }), }); const token_raw = authResult.data?.access_token; if (!token_raw) { throw Error("Bad User"); } // Only allow login with this method if the account has the "Test User" role const token = JWT.decode(token_raw); if (!token || typeof token !== "object") { throw Error("Bad Token Format"); } const user_roles = token?.["https://systeminit.com/roles"] ?? []; if (typeof user_roles !== "object" || !user_roles.includes("Test User")) { throw Error("Non 'Test User' account"); } return token_raw; } export function getAuth0LoginUrl(signup = false) { // lots of ways to generate this, but... nanoid is pretty good and already url-safe const randomState = nanoid(16); const loginParams = getQueryString({ response_type: "code", // or 'token' client_id: process.env.AUTH0_CLIENT_ID, redirect_uri: LOGIN_CALLBACK_URL, state: randomState, scope: "openid profile email", ...signup && { screen_hint: "signup" }, // `connection=CONNECTION` // not quite sure // prompt=none // for silent authentication - https://auth0.com/docs/authenticate/login/configure-silent-authentication // audience -- can be used to specify which "api" we are connecting to? // invitation -- used for auth0's native orgs / invite system // ADDITIONAL_PARAMETERS - anything else will be sent to the provider // access_type=offline // for google }); return { randomState, url: `https://${process.env.AUTH0_DOMAIN}/authorize?${loginParams}`, }; } export function getAuth0LogoutUrl() { const logoutParams = getQueryString({ client_id: process.env.AUTH0_CLIENT_ID, returnTo: LOGOUT_CALLBACK_URL, }); return `https://${process.env.AUTH0_DOMAIN}/v2/logout?${logoutParams}`; } export async function completeAuth0TokenExchange(code: string) { const tokenAndBasicProfile = await tryCatch(async () => { const tokenReq = await auth0Api.post( "/oauth/token", getQueryString({ grant_type: "authorization_code", client_id: process.env.AUTH0_CLIENT_ID, client_secret: process.env.AUTH0_CLIENT_SECRET, code, redirect_uri: LOGIN_CALLBACK_URL, }), { headers: { "content-type": "application/x-www-form-urlencoded" }, }, ); return { accessToken: tokenReq.data.access_token as string, idTokenData: JWT.decode(tokenReq.data.id_token) as any, }; }, (err) => { // if err is an http error from auth0, it will usually look something like: // err.response.data.error -- ex: 'invalid_grant' // err.response.data.error_description -- ex: 'Invalid authorization code' // if the error doesn't look like a normal auth0 response just throw it if (!err?.response.data.error_description) throw err; throw new ApiError( "Conflict", "AUTH0_EXCHANGE_FAILURE", err.response.data.error_description, ); }); // access token is an "opaque token" so does not contain any info and cannot be decoded // but id_token data is decoded into idTokenData and includes some basic info const { accessToken, idTokenData } = tokenAndBasicProfile!; const profile = await fetchAuth0Profile(idTokenData.sub); return { profile, token: accessToken }; } const AUTH0_MANAGEMENT_TOKEN_REDIS_KEY = "auth0-management-api-key"; type SavedAuth0TokenData = { clientId: string, token: string, }; async function getManagementApiToken() { // first check if we have a valid token in redis (and make sure the client id has not changed) const savedTokenInfo = await getCache<SavedAuth0TokenData>(AUTH0_MANAGEMENT_TOKEN_REDIS_KEY); if (savedTokenInfo?.clientId === process.env.AUTH0_M2M_CLIENT_ID) { return savedTokenInfo?.token; } // otherwise we'll generate a new token, save it in redis for future requests, and return it // TODO: with actual volume, we'd want to acquire some kind of mutex so only one running instance is refreshing the token at a time // but since nothing bad happens if we refresh the token multiple times and we have very low volume, we can ignore safely ignore for now const result = await auth0Api.request({ method: "post", url: "/oauth/token", headers: { "content-type": "application/json" }, data: JSON.stringify({ client_id: process.env.AUTH0_M2M_CLIENT_ID, client_secret: process.env.AUTH0_M2M_CLIENT_SECRET, audience: `https://${process.env.AUTH0_DOMAIN}/api/v2/`, grant_type: "client_credentials", }), }); const token = result.data.access_token; await setCache(AUTH0_MANAGEMENT_TOKEN_REDIS_KEY, { clientId: process.env.AUTH0_M2M_CLIENT_ID, token, }, { expiresIn: result.data.expires_in - (5 * 60), }); return token; } export async function setManagementApiTokenForTesting() { if (process.env.NODE_ENV !== "test") { throw new Error("This should only be used in test mode..."); } await setCache(AUTH0_MANAGEMENT_TOKEN_REDIS_KEY, { clientId: process.env.AUTH0_M2M_CLIENT_ID!, token: "mocktoken", }); } async function getManagementClient() { const m2mToken = await getManagementApiToken(); /* eslint-disable @typescript-eslint/no-non-null-assertion */ return new ManagementClient({ domain: process.env.AUTH0_DOMAIN!, // clientId: process.env.AUTH0_M2M_CLIENT_ID!, token: m2mToken, }); } export async function fetchAuth0Profile(auth0Id: string) { const auth0ManagementClient = await getManagementClient(); const profile = await tryCatch(async () => { return await auth0ManagementClient.users.get({ id: auth0Id }); }, (err) => { if (!err?.response.data.error_description) throw err; throw new ApiError( "Conflict", "Auth0ProfileError", err.response.data.error_description, ); }); if (!profile) throw new Error("no profile"); // just for TS return profile; } export async function resendAuth0EmailVerification(auth0Id: string) { const auth0ManagementClient = await getManagementClient(); await auth0ManagementClient.jobs.verifyEmail({ user_id: auth0Id }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server