diff --git a/packages/core/auth-js/src/GoTrueClient.ts b/packages/core/auth-js/src/GoTrueClient.ts index 341d35ce0..0b2a77a17 100644 --- a/packages/core/auth-js/src/GoTrueClient.ts +++ b/packages/core/auth-js/src/GoTrueClient.ts @@ -105,6 +105,9 @@ import type { MFAVerifyWebauthnParamFields, MFAVerifyWebauthnParams, OAuthResponse, + AuthOAuthServerApi, + AuthOAuthAuthorizationDetailsResponse, + AuthOAuthConsentResponse, Prettify, Provider, ResendParams, @@ -196,6 +199,12 @@ export default class GoTrueClient { * Namespace for the MFA methods. */ mfa: GoTrueMFAApi + /** + * Namespace for the OAuth 2.1 authorization server methods. + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + * Used to implement the authorization code flow on the consent page. + */ + oauth: AuthOAuthServerApi /** * The storage key used to identify the values saved in localStorage */ @@ -322,6 +331,12 @@ export default class GoTrueClient { webauthn: new WebAuthnApi(this), } + this.oauth = { + getAuthorizationDetails: this._getAuthorizationDetails.bind(this), + approveAuthorization: this._approveAuthorization.bind(this), + denyAuthorization: this._denyAuthorization.bind(this), + } + if (this.persistSession) { if (settings.storage) { this.storage = settings.storage @@ -3344,6 +3359,165 @@ export default class GoTrueClient { }) } + /** + * Retrieves details about an OAuth authorization request. + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + */ + private async _getAuthorizationDetails( + authorizationId: string, + options?: { skipBrowserRedirect?: boolean } + ): Promise { + try { + return await this._useSession(async (result) => { + const { + data: { session }, + error: sessionError, + } = result + + if (sessionError) { + return { data: null, error: sessionError } + } + + if (!session) { + return { data: null, error: new AuthSessionMissingError() } + } + + return await _request( + this.fetch, + 'GET', + `${this.url}/oauth/authorizations/${authorizationId}`, + { + headers: this.headers, + jwt: session.access_token, + xform: (data: any) => { + // If the API returns redirect_uri, it means consent was already given + if (data.redirect_uri) { + // Automatically redirect in browser unless skipBrowserRedirect is true + if (isBrowser() && !options?.skipBrowserRedirect) { + window.location.assign(data.redirect_uri) + } + } + + return { data, error: null } + }, + } + ) + }) + } catch (error) { + if (isAuthError(error)) { + return { data: null, error } + } + + throw error + } + } + + /** + * Approves an OAuth authorization request. + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + */ + private async _approveAuthorization( + authorizationId: string, + options?: { skipBrowserRedirect?: boolean } + ): Promise { + try { + return await this._useSession(async (result) => { + const { + data: { session }, + error: sessionError, + } = result + + if (sessionError) { + return { data: null, error: sessionError } + } + + if (!session) { + return { data: null, error: new AuthSessionMissingError() } + } + + const response = await _request( + this.fetch, + 'POST', + `${this.url}/oauth/authorizations/${authorizationId}/consent`, + { + headers: this.headers, + jwt: session.access_token, + body: { action: 'approve' }, + xform: (data: any) => ({ data, error: null }), + } + ) + + if (response.data && response.data.redirect_url) { + // Automatically redirect in browser unless skipBrowserRedirect is true + if (isBrowser() && !options?.skipBrowserRedirect) { + window.location.assign(response.data.redirect_url) + } + } + + return response + }) + } catch (error) { + if (isAuthError(error)) { + return { data: null, error } + } + + throw error + } + } + + /** + * Denies an OAuth authorization request. + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + */ + private async _denyAuthorization( + authorizationId: string, + options?: { skipBrowserRedirect?: boolean } + ): Promise { + try { + return await this._useSession(async (result) => { + const { + data: { session }, + error: sessionError, + } = result + + if (sessionError) { + return { data: null, error: sessionError } + } + + if (!session) { + return { data: null, error: new AuthSessionMissingError() } + } + + const response = await _request( + this.fetch, + 'POST', + `${this.url}/oauth/authorizations/${authorizationId}/consent`, + { + headers: this.headers, + jwt: session.access_token, + body: { action: 'deny' }, + xform: (data: any) => ({ data, error: null }), + } + ) + + if (response.data && response.data.redirect_url) { + // Automatically redirect in browser unless skipBrowserRedirect is true + if (isBrowser() && !options?.skipBrowserRedirect) { + window.location.assign(response.data.redirect_url) + } + } + + return response + }) + } catch (error) { + if (isAuthError(error)) { + return { data: null, error } + } + + throw error + } + } + private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise { // try fetching from the supplied jwks let jwk = jwks.keys.find((key) => key.kid === kid) diff --git a/packages/core/auth-js/src/lib/types.ts b/packages/core/auth-js/src/lib/types.ts index 734d8ef0a..116130b84 100644 --- a/packages/core/auth-js/src/lib/types.ts +++ b/packages/core/auth-js/src/lib/types.ts @@ -1590,3 +1590,103 @@ export interface GoTrueAdminOAuthApi { */ regenerateClientSecret(clientId: string): Promise } + +/** + * OAuth client details in an authorization request. + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + */ +export type OAuthAuthorizationClient = { + /** Unique identifier for the OAuth client (UUID) */ + client_id: string + /** Human-readable name of the OAuth client */ + client_name: string + /** URI of the OAuth client's website */ + client_uri: string + /** URI of the OAuth client's logo */ + logo_uri: string +} + +/** + * OAuth authorization details for the consent flow. + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + */ +export type OAuthAuthorizationDetails = { + /** The authorization ID */ + authorization_id: string + /** Redirect URI - present if user already consented (can be used to trigger immediate redirect) */ + redirect_uri?: string + /** OAuth client requesting authorization */ + client: OAuthAuthorizationClient + /** User object associated with the authorization */ + user: { + /** User ID (UUID) */ + id: string + /** User email */ + email: string + } + /** Space-separated list of requested scopes */ + scope: string +} + +/** + * Response type for getting OAuth authorization details. + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + */ +export type AuthOAuthAuthorizationDetailsResponse = RequestResult + +/** + * Response type for OAuth consent decision (approve/deny). + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + */ +export type AuthOAuthConsentResponse = RequestResult<{ + /** URL to redirect the user back to the OAuth client */ + redirect_url: string +}> + +/** + * Contains all OAuth 2.1 authorization server user-facing methods. + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + * + * These methods are used to implement the consent page. + */ +export interface AuthOAuthServerApi { + /** + * Retrieves details about an OAuth authorization request. + * Used to display consent information to the user. + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + * + * @param authorizationId - The authorization ID from the authorization request + * @param options - Optional parameters including skipBrowserRedirect + * @returns Authorization details including client info and requested scopes + */ + getAuthorizationDetails( + authorizationId: string, + options?: { skipBrowserRedirect?: boolean } + ): Promise + + /** + * Approves an OAuth authorization request. + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + * + * @param authorizationId - The authorization ID to approve + * @param options - Optional parameters including skipBrowserRedirect + * @returns Redirect URL to send the user back to the OAuth client + */ + approveAuthorization( + authorizationId: string, + options?: { skipBrowserRedirect?: boolean } + ): Promise + + /** + * Denies an OAuth authorization request. + * Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + * + * @param authorizationId - The authorization ID to deny + * @param options - Optional parameters including skipBrowserRedirect + * @returns Redirect URL to send the user back to the OAuth client + */ + denyAuthorization( + authorizationId: string, + options?: { skipBrowserRedirect?: boolean } + ): Promise +}