Buckets:
| /** | |
| * Use the OpenAuth client kick off your OAuth flows, exchange tokens, refresh tokens, | |
| * and verify tokens. | |
| * | |
| * First, create a client. | |
| * | |
| * ```ts title="client.ts" | |
| * import { createClient } from "@openauthjs/openauth/client" | |
| * | |
| * const client = createClient({ | |
| * clientID: "my-client", | |
| * issuer: "https://auth.myserver.com" | |
| * }) | |
| * ``` | |
| * | |
| * Kick off the OAuth flow by calling `authorize`. | |
| * | |
| * ```ts | |
| * const redirect_uri = "https://myserver.com/callback" | |
| * | |
| * const { url } = await client.authorize( | |
| * redirect_uri, | |
| * "code" | |
| * ) | |
| * ``` | |
| * | |
| * When the user completes the flow, `exchange` the code for tokens. | |
| * | |
| * ```ts | |
| * const tokens = await client.exchange(query.get("code"), redirect_uri) | |
| * ``` | |
| * | |
| * And `verify` the tokens. | |
| * | |
| * ```ts | |
| * const verified = await client.verify(subjects, tokens.access) | |
| * ``` | |
| * | |
| * @packageDocumentation | |
| */ | |
| import { | |
| createLocalJWKSet, | |
| errors, | |
| JSONWebKeySet, | |
| jwtVerify, | |
| decodeJwt, | |
| } from "jose" | |
| import { SubjectSchema } from "./subject.js" | |
| import type { v1 } from "@standard-schema/spec" | |
| import { | |
| InvalidAccessTokenError, | |
| InvalidAuthorizationCodeError, | |
| InvalidRefreshTokenError, | |
| InvalidSubjectError, | |
| } from "./error.js" | |
| import { generatePKCE } from "./pkce.js" | |
| /** | |
| * The well-known information for an OAuth 2.0 authorization server. | |
| * @internal | |
| */ | |
| export interface WellKnown { | |
| /** | |
| * The URI to the JWKS endpoint. | |
| */ | |
| jwks_uri: string | |
| /** | |
| * The URI to the token endpoint. | |
| */ | |
| token_endpoint: string | |
| /** | |
| * The URI to the authorization endpoint. | |
| */ | |
| authorization_endpoint: string | |
| } | |
| /** | |
| * The tokens returned by the auth server. | |
| */ | |
| export interface Tokens { | |
| /** | |
| * The access token. | |
| */ | |
| access: string | |
| /** | |
| * The refresh token. | |
| */ | |
| refresh: string | |
| /** | |
| * The number of seconds until the access token expires. | |
| */ | |
| expiresIn: number | |
| } | |
| interface ResponseLike { | |
| json(): Promise<unknown> | |
| ok: Response["ok"] | |
| } | |
| type FetchLike = (...args: any[]) => Promise<ResponseLike> | |
| /** | |
| * The challenge that you can use to verify the code. | |
| */ | |
| export type Challenge = { | |
| /** | |
| * The state that was sent to the redirect URI. | |
| */ | |
| state: string | |
| /** | |
| * The verifier that was sent to the redirect URI. | |
| */ | |
| verifier?: string | |
| } | |
| /** | |
| * Configure the client. | |
| */ | |
| export interface ClientInput { | |
| /** | |
| * The client ID. This is just a string to identify your app. | |
| * | |
| * If you have a web app and a mobile app, you want to use different client IDs both. | |
| * | |
| * @example | |
| * ```ts | |
| * { | |
| * clientID: "my-client" | |
| * } | |
| * ``` | |
| */ | |
| clientID: string | |
| /** | |
| * The URL of your OpenAuth server. | |
| * | |
| * @example | |
| * ```ts | |
| * { | |
| * issuer: "https://auth.myserver.com" | |
| * } | |
| * ``` | |
| */ | |
| issuer?: string | |
| /** | |
| * Optionally, override the internally used fetch function. | |
| * | |
| * This is useful if you are using a polyfilled fetch function in your application and you | |
| * want the client to use it too. | |
| */ | |
| fetch?: FetchLike | |
| } | |
| export interface AuthorizeOptions { | |
| /** | |
| * Enable the PKCE flow. This is for SPA apps. | |
| * | |
| * ```ts | |
| * { | |
| * pkce: true | |
| * } | |
| * ``` | |
| * | |
| * @default false | |
| */ | |
| pkce?: boolean | |
| /** | |
| * The provider you want to use for the OAuth flow. | |
| * | |
| * ```ts | |
| * { | |
| * provider: "google" | |
| * } | |
| * ``` | |
| * | |
| * If no provider is specified, the user is directed to a page where they can select from the | |
| * list of configured providers. | |
| * | |
| * If there's only one provider configured, the user will be redirected to that. | |
| */ | |
| provider?: string | |
| } | |
| export interface AuthorizeResult { | |
| /** | |
| * The challenge that you can use to verify the code. This is for the PKCE flow for SPA apps. | |
| * | |
| * This is an object that you _stringify_ and store it in session storage. | |
| * | |
| * ```ts | |
| * sessionStorage.setItem("challenge", JSON.stringify(challenge)) | |
| * ``` | |
| */ | |
| challenge: Challenge | |
| /** | |
| * The URL to redirect the user to. This starts the OAuth flow. | |
| * | |
| * For example, for SPA apps. | |
| * | |
| * ```ts | |
| * location.href = url | |
| * ``` | |
| */ | |
| url: string | |
| } | |
| /** | |
| * Returned when the exchange is successful. | |
| */ | |
| export interface ExchangeSuccess { | |
| /** | |
| * This is always `false` when the exchange is successful. | |
| */ | |
| err: false | |
| /** | |
| * The access and refresh tokens. | |
| */ | |
| tokens: Tokens | |
| } | |
| /** | |
| * Returned when the exchange fails. | |
| */ | |
| export interface ExchangeError { | |
| /** | |
| * The type of error that occurred. You can handle this by checking the type. | |
| * | |
| * @example | |
| * ```ts | |
| * import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error" | |
| * | |
| * console.log(err instanceof InvalidAuthorizationCodeError) | |
| *``` | |
| */ | |
| err: InvalidAuthorizationCodeError | |
| } | |
| export interface RefreshOptions { | |
| /** | |
| * Optionally, pass in the access token. | |
| */ | |
| access?: string | |
| } | |
| /** | |
| * Returned when the refresh is successful. | |
| */ | |
| export interface RefreshSuccess { | |
| /** | |
| * This is always `false` when the refresh is successful. | |
| */ | |
| err: false | |
| /** | |
| * Returns the refreshed tokens only if they've been refreshed. | |
| * | |
| * If they are still valid, this will be `undefined`. | |
| */ | |
| tokens?: Tokens | |
| } | |
| /** | |
| * Returned when the refresh fails. | |
| */ | |
| export interface RefreshError { | |
| /** | |
| * The type of error that occurred. You can handle this by checking the type. | |
| * | |
| * @example | |
| * ```ts | |
| * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error" | |
| * | |
| * console.log(err instanceof InvalidRefreshTokenError) | |
| *``` | |
| */ | |
| err: InvalidRefreshTokenError | InvalidAccessTokenError | |
| } | |
| export interface VerifyOptions { | |
| /** | |
| * Optionally, pass in the refresh token. | |
| * | |
| * If passed in, this will automatically refresh the access token if it has expired. | |
| */ | |
| refresh?: string | |
| /** | |
| * @internal | |
| */ | |
| issuer?: string | |
| /** | |
| * @internal | |
| */ | |
| audience?: string | |
| /** | |
| * Optionally, override the internally used fetch function. | |
| * | |
| * This is useful if you are using a polyfilled fetch function in your application and you | |
| * want the client to use it too. | |
| */ | |
| fetch?: FetchLike | |
| } | |
| export interface VerifyResult<T extends SubjectSchema> { | |
| /** | |
| * This is always `undefined` when the verify is successful. | |
| */ | |
| err?: undefined | |
| /** | |
| * Returns the refreshed tokens only if they’ve been refreshed. | |
| * | |
| * If they are still valid, this will be undefined. | |
| */ | |
| tokens?: Tokens | |
| /** | |
| * @internal | |
| */ | |
| aud: string | |
| /** | |
| * The decoded subjects from the access token. | |
| * | |
| * Has the same shape as the subjects you defined when creating the issuer. | |
| */ | |
| subject: { | |
| [type in keyof T]: { type: type; properties: v1.InferOutput<T[type]> } | |
| }[keyof T] | |
| } | |
| /** | |
| * Returned when the verify call fails. | |
| */ | |
| export interface VerifyError { | |
| /** | |
| * The type of error that occurred. You can handle this by checking the type. | |
| * | |
| * @example | |
| * ```ts | |
| * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error" | |
| * | |
| * console.log(err instanceof InvalidRefreshTokenError) | |
| *``` | |
| */ | |
| err: InvalidRefreshTokenError | InvalidAccessTokenError | |
| } | |
| /** | |
| * An instance of the OpenAuth client contains the following methods. | |
| */ | |
| export interface Client { | |
| /** | |
| * Start the autorization flow. For example, in SSR sites. | |
| * | |
| * ```ts | |
| * const { url } = await client.authorize(<redirect_uri>, "code") | |
| * ``` | |
| * | |
| * This takes a redirect URI and the type of flow you want to use. The redirect URI is the | |
| * location where the user will be redirected to after the flow is complete. | |
| * | |
| * Supports both the _code_ and _token_ flows. We recommend using the _code_ flow as it's more | |
| * secure. | |
| * | |
| * :::tip | |
| * This returns a URL to redirect the user to. This starts the OAuth flow. | |
| * ::: | |
| * | |
| * This returns a URL to the auth server. You can redirect the user to the URL to start the | |
| * OAuth flow. | |
| * | |
| * For SPA apps, we recommend using the PKCE flow. | |
| * | |
| * ```ts {4} | |
| * const { challenge, url } = await client.authorize( | |
| * <redirect_uri>, | |
| * "code", | |
| * { pkce: true } | |
| * ) | |
| * ``` | |
| * | |
| * This returns a redirect URL and a challenge that you need to use later to verify the code. | |
| */ | |
| authorize( | |
| redirectURI: string, | |
| response: "code" | "token", | |
| opts?: AuthorizeOptions, | |
| ): Promise<AuthorizeResult> | |
| /** | |
| * Exchange the code for access and refresh tokens. | |
| * | |
| * ```ts | |
| * const exchanged = await client.exchange(<code>, <redirect_uri>) | |
| * ``` | |
| * | |
| * You call this after the user has been redirected back to your app after the OAuth flow. | |
| * | |
| * :::tip | |
| * For SSR sites, the code is returned in the query parameter. | |
| * ::: | |
| * | |
| * So the code comes from the query parameter in the redirect URI. The redirect URI here is | |
| * the one that you passed in to the `authorize` call when starting the flow. | |
| * | |
| * :::tip | |
| * For SPA sites, the code is returned through the URL hash. | |
| * ::: | |
| * | |
| * If you used the PKCE flow for an SPA app, the code is returned as a part of the redirect URL | |
| * hash. | |
| * | |
| * ```ts {4} | |
| * const exchanged = await client.exchange( | |
| * <code>, | |
| * <redirect_uri>, | |
| * <challenge.verifier> | |
| * ) | |
| * ``` | |
| * | |
| * You also need to pass in the previously stored challenge verifier. | |
| * | |
| * This method returns the access and refresh tokens. Or if it fails, it returns an error that | |
| * you can handle depending on the error. | |
| * | |
| * ```ts | |
| * import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error" | |
| * | |
| * if (exchanged.err) { | |
| * if (exchanged.err instanceof InvalidAuthorizationCodeError) { | |
| * // handle invalid code error | |
| * } | |
| * else { | |
| * // handle other errors | |
| * } | |
| * } | |
| * | |
| * const { access, refresh } = exchanged.tokens | |
| * ``` | |
| */ | |
| exchange( | |
| code: string, | |
| redirectURI: string, | |
| verifier?: string, | |
| ): Promise<ExchangeSuccess | ExchangeError> | |
| /** | |
| * Refreshes the tokens if they have expired. This is used in an SPA app to maintain the | |
| * session, without logging the user out. | |
| * | |
| * ```ts | |
| * const next = await client.refresh(<refresh_token>) | |
| * ``` | |
| * | |
| * Can optionally take the access token as well. If passed in, this will skip the refresh | |
| * if the access token is still valid. | |
| * | |
| * ```ts | |
| * const next = await client.refresh(<refresh_token>, { access: <access_token> }) | |
| * ``` | |
| * | |
| * This returns the refreshed tokens only if they've been refreshed. | |
| * | |
| * ```ts | |
| * if (!next.err) { | |
| * // tokens are still valid | |
| * } | |
| * if (next.tokens) { | |
| * const { access, refresh } = next.tokens | |
| * } | |
| * ``` | |
| * | |
| * Or if it fails, it returns an error that you can handle depending on the error. | |
| * | |
| * ```ts | |
| * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error" | |
| * | |
| * if (next.err) { | |
| * if (next.err instanceof InvalidRefreshTokenError) { | |
| * // handle invalid refresh token error | |
| * } | |
| * else { | |
| * // handle other errors | |
| * } | |
| * } | |
| * ``` | |
| */ | |
| refresh( | |
| refresh: string, | |
| opts?: RefreshOptions, | |
| ): Promise<RefreshSuccess | RefreshError> | |
| /** | |
| * Verify the token in the incoming request. | |
| * | |
| * This is typically used for SSR sites where the token is stored in an HTTP only cookie. And | |
| * is passed to the server on every request. | |
| * | |
| * ```ts | |
| * const verified = await client.verify(<subjects>, <token>) | |
| * ``` | |
| * | |
| * This takes the subjects that you had previously defined when creating the issuer. | |
| * | |
| * :::tip | |
| * If the refresh token is passed in, it'll automatically refresh the access token. | |
| * ::: | |
| * | |
| * This can optionally take the refresh token as well. If passed in, it'll automatically | |
| * refresh the access token if it has expired. | |
| * | |
| * ```ts | |
| * const verified = await client.verify(<subjects>, <token>, { refresh: <refresh_token> }) | |
| * ``` | |
| * | |
| * This returns the decoded subjects from the access token. And the tokens if they've been | |
| * refreshed. | |
| * | |
| * ```ts | |
| * // based on the subjects you defined earlier | |
| * console.log(verified.subject.properties.userID) | |
| * | |
| * if (verified.tokens) { | |
| * const { access, refresh } = verified.tokens | |
| * } | |
| * ``` | |
| * | |
| * Or if it fails, it returns an error that you can handle depending on the error. | |
| * | |
| * ```ts | |
| * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error" | |
| * | |
| * if (verified.err) { | |
| * if (verified.err instanceof InvalidRefreshTokenError) { | |
| * // handle invalid refresh token error | |
| * } | |
| * else { | |
| * // handle other errors | |
| * } | |
| * } | |
| * ``` | |
| */ | |
| verify<T extends SubjectSchema>( | |
| subjects: T, | |
| token: string, | |
| options?: VerifyOptions, | |
| ): Promise<VerifyResult<T> | VerifyError> | |
| } | |
| /** | |
| * Create an OpenAuth client. | |
| * | |
| * @param input - Configure the client. | |
| */ | |
| export function createClient(input: ClientInput): Client { | |
| const jwksCache = new Map<string, ReturnType<typeof createLocalJWKSet>>() | |
| const issuerCache = new Map<string, WellKnown>() | |
| const issuer = input.issuer || process.env.OPENAUTH_ISSUER | |
| if (!issuer) throw new Error("No issuer") | |
| const f = input.fetch ?? fetch | |
| async function getIssuer() { | |
| const cached = issuerCache.get(issuer!) | |
| if (cached) return cached | |
| const wellKnown = (await (f || fetch)( | |
| `${issuer}/.well-known/oauth-authorization-server`, | |
| ).then((r) => r.json())) as WellKnown | |
| issuerCache.set(issuer!, wellKnown) | |
| return wellKnown | |
| } | |
| async function getJWKS() { | |
| const wk = await getIssuer() | |
| const cached = jwksCache.get(issuer!) | |
| if (cached) return cached | |
| const keyset = (await (f || fetch)(wk.jwks_uri).then((r) => | |
| r.json(), | |
| )) as JSONWebKeySet | |
| const result = createLocalJWKSet(keyset) | |
| jwksCache.set(issuer!, result) | |
| return result | |
| } | |
| const result = { | |
| async authorize( | |
| redirectURI: string, | |
| response: "code" | "token", | |
| opts?: AuthorizeOptions, | |
| ) { | |
| const result = new URL(issuer + "/authorize") | |
| const challenge: Challenge = { | |
| state: crypto.randomUUID(), | |
| } | |
| result.searchParams.set("client_id", input.clientID) | |
| result.searchParams.set("redirect_uri", redirectURI) | |
| result.searchParams.set("response_type", response) | |
| result.searchParams.set("state", challenge.state) | |
| if (opts?.provider) result.searchParams.set("provider", opts.provider) | |
| if (opts?.pkce && response === "code") { | |
| const pkce = await generatePKCE() | |
| result.searchParams.set("code_challenge_method", "S256") | |
| result.searchParams.set("code_challenge", pkce.challenge) | |
| challenge.verifier = pkce.verifier | |
| } | |
| return { | |
| challenge, | |
| url: result.toString(), | |
| } | |
| }, | |
| /** | |
| * @deprecated use `authorize` instead, it will do pkce by default unless disabled with `opts.pkce = false` | |
| */ | |
| async pkce( | |
| redirectURI: string, | |
| opts?: { | |
| provider?: string | |
| }, | |
| ) { | |
| const result = new URL(issuer + "/authorize") | |
| if (opts?.provider) result.searchParams.set("provider", opts.provider) | |
| result.searchParams.set("client_id", input.clientID) | |
| result.searchParams.set("redirect_uri", redirectURI) | |
| result.searchParams.set("response_type", "code") | |
| const pkce = await generatePKCE() | |
| result.searchParams.set("code_challenge_method", "S256") | |
| result.searchParams.set("code_challenge", pkce.challenge) | |
| return [pkce.verifier, result.toString()] | |
| }, | |
| async exchange( | |
| code: string, | |
| redirectURI: string, | |
| verifier?: string, | |
| ): Promise<ExchangeSuccess | ExchangeError> { | |
| const tokens = await f(issuer + "/token", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/x-www-form-urlencoded", | |
| }, | |
| body: new URLSearchParams({ | |
| code, | |
| redirect_uri: redirectURI, | |
| grant_type: "authorization_code", | |
| client_id: input.clientID, | |
| code_verifier: verifier || "", | |
| }).toString(), | |
| }) | |
| const json = (await tokens.json()) as any | |
| if (!tokens.ok) { | |
| return { | |
| err: new InvalidAuthorizationCodeError(), | |
| } | |
| } | |
| return { | |
| err: false, | |
| tokens: { | |
| access: json.access_token as string, | |
| refresh: json.refresh_token as string, | |
| expiresIn: json.expires_in as number, | |
| }, | |
| } | |
| }, | |
| async refresh( | |
| refresh: string, | |
| opts?: RefreshOptions, | |
| ): Promise<RefreshSuccess | RefreshError> { | |
| if (opts && opts.access) { | |
| const decoded = decodeJwt(opts.access) | |
| if (!decoded) { | |
| return { | |
| err: new InvalidAccessTokenError(), | |
| } | |
| } | |
| // allow 30s window for expiration | |
| if ((decoded.exp || 0) > Date.now() / 1000 + 30) { | |
| return { | |
| err: false, | |
| } | |
| } | |
| } | |
| const tokens = await f(issuer + "/token", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/x-www-form-urlencoded", | |
| }, | |
| body: new URLSearchParams({ | |
| grant_type: "refresh_token", | |
| refresh_token: refresh, | |
| }).toString(), | |
| }) | |
| const json = (await tokens.json()) as any | |
| if (!tokens.ok) { | |
| return { | |
| err: new InvalidRefreshTokenError(), | |
| } | |
| } | |
| return { | |
| err: false, | |
| tokens: { | |
| access: json.access_token as string, | |
| refresh: json.refresh_token as string, | |
| expiresIn: json.expires_in as number, | |
| }, | |
| } | |
| }, | |
| async verify<T extends SubjectSchema>( | |
| subjects: T, | |
| token: string, | |
| options?: VerifyOptions, | |
| ): Promise<VerifyResult<T> | VerifyError> { | |
| const jwks = await getJWKS() | |
| try { | |
| const result = await jwtVerify<{ | |
| mode: "access" | |
| type: keyof T | |
| properties: v1.InferInput<T[keyof T]> | |
| }>(token, jwks, { | |
| issuer, | |
| }) | |
| const validated = await subjects[result.payload.type][ | |
| "~standard" | |
| ].validate(result.payload.properties) | |
| if (!validated.issues && result.payload.mode === "access") | |
| return { | |
| aud: result.payload.aud as string, | |
| subject: { | |
| type: result.payload.type, | |
| properties: validated.value, | |
| } as any, | |
| } | |
| return { | |
| err: new InvalidSubjectError(), | |
| } | |
| } catch (e) { | |
| if (e instanceof errors.JWTExpired && options?.refresh) { | |
| const refreshed = await this.refresh(options.refresh) | |
| if (refreshed.err) return refreshed | |
| const verified = await result.verify( | |
| subjects, | |
| refreshed.tokens!.access, | |
| { | |
| refresh: refreshed.tokens!.refresh, | |
| issuer, | |
| fetch: options?.fetch, | |
| }, | |
| ) | |
| if (verified.err) return verified | |
| verified.tokens = refreshed.tokens | |
| return verified | |
| } | |
| return { | |
| err: new InvalidAccessTokenError(), | |
| } | |
| } | |
| }, | |
| } | |
| return result | |
| } | |
Xet Storage Details
- Size:
- 19.2 kB
- Xet hash:
- 5d4a3f3270304e028fd7ad4b5cd015840afa2566336c26106badda84fa554a26
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.