Skip to content

Commit 255d8dd

Browse files
authored
feat(auth): add OAuth 2.1 authorization consent management API calls (#1793)
1 parent b45a31c commit 255d8dd

File tree

2 files changed

+274
-0
lines changed

2 files changed

+274
-0
lines changed

packages/core/auth-js/src/GoTrueClient.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ import type {
105105
MFAVerifyWebauthnParamFields,
106106
MFAVerifyWebauthnParams,
107107
OAuthResponse,
108+
AuthOAuthServerApi,
109+
AuthOAuthAuthorizationDetailsResponse,
110+
AuthOAuthConsentResponse,
108111
Prettify,
109112
Provider,
110113
ResendParams,
@@ -196,6 +199,12 @@ export default class GoTrueClient {
196199
* Namespace for the MFA methods.
197200
*/
198201
mfa: GoTrueMFAApi
202+
/**
203+
* Namespace for the OAuth 2.1 authorization server methods.
204+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
205+
* Used to implement the authorization code flow on the consent page.
206+
*/
207+
oauth: AuthOAuthServerApi
199208
/**
200209
* The storage key used to identify the values saved in localStorage
201210
*/
@@ -322,6 +331,12 @@ export default class GoTrueClient {
322331
webauthn: new WebAuthnApi(this),
323332
}
324333

334+
this.oauth = {
335+
getAuthorizationDetails: this._getAuthorizationDetails.bind(this),
336+
approveAuthorization: this._approveAuthorization.bind(this),
337+
denyAuthorization: this._denyAuthorization.bind(this),
338+
}
339+
325340
if (this.persistSession) {
326341
if (settings.storage) {
327342
this.storage = settings.storage
@@ -3344,6 +3359,165 @@ export default class GoTrueClient {
33443359
})
33453360
}
33463361

3362+
/**
3363+
* Retrieves details about an OAuth authorization request.
3364+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
3365+
*/
3366+
private async _getAuthorizationDetails(
3367+
authorizationId: string,
3368+
options?: { skipBrowserRedirect?: boolean }
3369+
): Promise<AuthOAuthAuthorizationDetailsResponse> {
3370+
try {
3371+
return await this._useSession(async (result) => {
3372+
const {
3373+
data: { session },
3374+
error: sessionError,
3375+
} = result
3376+
3377+
if (sessionError) {
3378+
return { data: null, error: sessionError }
3379+
}
3380+
3381+
if (!session) {
3382+
return { data: null, error: new AuthSessionMissingError() }
3383+
}
3384+
3385+
return await _request(
3386+
this.fetch,
3387+
'GET',
3388+
`${this.url}/oauth/authorizations/${authorizationId}`,
3389+
{
3390+
headers: this.headers,
3391+
jwt: session.access_token,
3392+
xform: (data: any) => {
3393+
// If the API returns redirect_uri, it means consent was already given
3394+
if (data.redirect_uri) {
3395+
// Automatically redirect in browser unless skipBrowserRedirect is true
3396+
if (isBrowser() && !options?.skipBrowserRedirect) {
3397+
window.location.assign(data.redirect_uri)
3398+
}
3399+
}
3400+
3401+
return { data, error: null }
3402+
},
3403+
}
3404+
)
3405+
})
3406+
} catch (error) {
3407+
if (isAuthError(error)) {
3408+
return { data: null, error }
3409+
}
3410+
3411+
throw error
3412+
}
3413+
}
3414+
3415+
/**
3416+
* Approves an OAuth authorization request.
3417+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
3418+
*/
3419+
private async _approveAuthorization(
3420+
authorizationId: string,
3421+
options?: { skipBrowserRedirect?: boolean }
3422+
): Promise<AuthOAuthConsentResponse> {
3423+
try {
3424+
return await this._useSession(async (result) => {
3425+
const {
3426+
data: { session },
3427+
error: sessionError,
3428+
} = result
3429+
3430+
if (sessionError) {
3431+
return { data: null, error: sessionError }
3432+
}
3433+
3434+
if (!session) {
3435+
return { data: null, error: new AuthSessionMissingError() }
3436+
}
3437+
3438+
const response = await _request(
3439+
this.fetch,
3440+
'POST',
3441+
`${this.url}/oauth/authorizations/${authorizationId}/consent`,
3442+
{
3443+
headers: this.headers,
3444+
jwt: session.access_token,
3445+
body: { action: 'approve' },
3446+
xform: (data: any) => ({ data, error: null }),
3447+
}
3448+
)
3449+
3450+
if (response.data && response.data.redirect_url) {
3451+
// Automatically redirect in browser unless skipBrowserRedirect is true
3452+
if (isBrowser() && !options?.skipBrowserRedirect) {
3453+
window.location.assign(response.data.redirect_url)
3454+
}
3455+
}
3456+
3457+
return response
3458+
})
3459+
} catch (error) {
3460+
if (isAuthError(error)) {
3461+
return { data: null, error }
3462+
}
3463+
3464+
throw error
3465+
}
3466+
}
3467+
3468+
/**
3469+
* Denies an OAuth authorization request.
3470+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
3471+
*/
3472+
private async _denyAuthorization(
3473+
authorizationId: string,
3474+
options?: { skipBrowserRedirect?: boolean }
3475+
): Promise<AuthOAuthConsentResponse> {
3476+
try {
3477+
return await this._useSession(async (result) => {
3478+
const {
3479+
data: { session },
3480+
error: sessionError,
3481+
} = result
3482+
3483+
if (sessionError) {
3484+
return { data: null, error: sessionError }
3485+
}
3486+
3487+
if (!session) {
3488+
return { data: null, error: new AuthSessionMissingError() }
3489+
}
3490+
3491+
const response = await _request(
3492+
this.fetch,
3493+
'POST',
3494+
`${this.url}/oauth/authorizations/${authorizationId}/consent`,
3495+
{
3496+
headers: this.headers,
3497+
jwt: session.access_token,
3498+
body: { action: 'deny' },
3499+
xform: (data: any) => ({ data, error: null }),
3500+
}
3501+
)
3502+
3503+
if (response.data && response.data.redirect_url) {
3504+
// Automatically redirect in browser unless skipBrowserRedirect is true
3505+
if (isBrowser() && !options?.skipBrowserRedirect) {
3506+
window.location.assign(response.data.redirect_url)
3507+
}
3508+
}
3509+
3510+
return response
3511+
})
3512+
} catch (error) {
3513+
if (isAuthError(error)) {
3514+
return { data: null, error }
3515+
}
3516+
3517+
throw error
3518+
}
3519+
}
3520+
33473521
private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise<JWK | null> {
33483522
// try fetching from the supplied jwks
33493523
let jwk = jwks.keys.find((key) => key.kid === kid)

packages/core/auth-js/src/lib/types.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,3 +1590,103 @@ export interface GoTrueAdminOAuthApi {
15901590
*/
15911591
regenerateClientSecret(clientId: string): Promise<OAuthClientResponse>
15921592
}
1593+
1594+
/**
1595+
* OAuth client details in an authorization request.
1596+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1597+
*/
1598+
export type OAuthAuthorizationClient = {
1599+
/** Unique identifier for the OAuth client (UUID) */
1600+
client_id: string
1601+
/** Human-readable name of the OAuth client */
1602+
client_name: string
1603+
/** URI of the OAuth client's website */
1604+
client_uri: string
1605+
/** URI of the OAuth client's logo */
1606+
logo_uri: string
1607+
}
1608+
1609+
/**
1610+
* OAuth authorization details for the consent flow.
1611+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1612+
*/
1613+
export type OAuthAuthorizationDetails = {
1614+
/** The authorization ID */
1615+
authorization_id: string
1616+
/** Redirect URI - present if user already consented (can be used to trigger immediate redirect) */
1617+
redirect_uri?: string
1618+
/** OAuth client requesting authorization */
1619+
client: OAuthAuthorizationClient
1620+
/** User object associated with the authorization */
1621+
user: {
1622+
/** User ID (UUID) */
1623+
id: string
1624+
/** User email */
1625+
email: string
1626+
}
1627+
/** Space-separated list of requested scopes */
1628+
scope: string
1629+
}
1630+
1631+
/**
1632+
* Response type for getting OAuth authorization details.
1633+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1634+
*/
1635+
export type AuthOAuthAuthorizationDetailsResponse = RequestResult<OAuthAuthorizationDetails>
1636+
1637+
/**
1638+
* Response type for OAuth consent decision (approve/deny).
1639+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1640+
*/
1641+
export type AuthOAuthConsentResponse = RequestResult<{
1642+
/** URL to redirect the user back to the OAuth client */
1643+
redirect_url: string
1644+
}>
1645+
1646+
/**
1647+
* Contains all OAuth 2.1 authorization server user-facing methods.
1648+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1649+
*
1650+
* These methods are used to implement the consent page.
1651+
*/
1652+
export interface AuthOAuthServerApi {
1653+
/**
1654+
* Retrieves details about an OAuth authorization request.
1655+
* Used to display consent information to the user.
1656+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1657+
*
1658+
* @param authorizationId - The authorization ID from the authorization request
1659+
* @param options - Optional parameters including skipBrowserRedirect
1660+
* @returns Authorization details including client info and requested scopes
1661+
*/
1662+
getAuthorizationDetails(
1663+
authorizationId: string,
1664+
options?: { skipBrowserRedirect?: boolean }
1665+
): Promise<AuthOAuthAuthorizationDetailsResponse>
1666+
1667+
/**
1668+
* Approves an OAuth authorization request.
1669+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1670+
*
1671+
* @param authorizationId - The authorization ID to approve
1672+
* @param options - Optional parameters including skipBrowserRedirect
1673+
* @returns Redirect URL to send the user back to the OAuth client
1674+
*/
1675+
approveAuthorization(
1676+
authorizationId: string,
1677+
options?: { skipBrowserRedirect?: boolean }
1678+
): Promise<AuthOAuthConsentResponse>
1679+
1680+
/**
1681+
* Denies an OAuth authorization request.
1682+
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1683+
*
1684+
* @param authorizationId - The authorization ID to deny
1685+
* @param options - Optional parameters including skipBrowserRedirect
1686+
* @returns Redirect URL to send the user back to the OAuth client
1687+
*/
1688+
denyAuthorization(
1689+
authorizationId: string,
1690+
options?: { skipBrowserRedirect?: boolean }
1691+
): Promise<AuthOAuthConsentResponse>
1692+
}

0 commit comments

Comments
 (0)