Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions packages/core/auth-js/src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ import type {
MFAVerifyWebauthnParamFields,
MFAVerifyWebauthnParams,
OAuthResponse,
AuthOAuthServerApi,
AuthOAuthAuthorizationDetailsResponse,
AuthOAuthConsentResponse,
Prettify,
Provider,
ResendParams,
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<AuthOAuthAuthorizationDetailsResponse> {
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<AuthOAuthConsentResponse> {
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<AuthOAuthConsentResponse> {
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<JWK | null> {
// try fetching from the supplied jwks
let jwk = jwks.keys.find((key) => key.kid === kid)
Expand Down
100 changes: 100 additions & 0 deletions packages/core/auth-js/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1590,3 +1590,103 @@ export interface GoTrueAdminOAuthApi {
*/
regenerateClientSecret(clientId: string): Promise<OAuthClientResponse>
}

/**
* 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<OAuthAuthorizationDetails>

/**
* 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<AuthOAuthAuthorizationDetailsResponse>

/**
* 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<AuthOAuthConsentResponse>

/**
* 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<AuthOAuthConsentResponse>
}