Repository navigation
feat(auth): register a custom OAuth/OIDC provider as an admin - #590
Merged
Merged
Conversation
This was referenced Oct 29, 2026
Tr00d
force-pushed
the
feat/auth.admin.create_provider
branch
2 times, most recently
from
October 29, 2026 14:19
2d4275e to
df67450
Compare
Tr00d
force-pushed
the
feat/auth.admin.create_provider
branch
from
October 29, 2026 14:28
df67450 to
627dbc6
Compare
Tr00d
marked this pull request as ready for review
October 29, 2026 14:41
This was referenced Oct 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Admins can register a custom OAuth 2.0 or OpenID Connect provider. This PR also adds the shared custom-provider types that the get, list and update PRs build on.
Capability:
auth.admin.create_provider: Admin Create Provider.Public API
New types, laid out by use case (CONVENTIONS §2):
CustomProviderTypeSupabase.Gotrue.CommonOAuth2,OidcCustomProvider(response record)Supabase.Gotrue.CommonId,Type,Identifier,Name,ClientId,AcceptableClientIds,Scopes,PkceEnabled,AttributeMapping,CustomClaimsAllowlist,AuthorizationParameters,Enabled,EmailOptional,Issuer,DiscoveryUrl,SkipNonceCheck,DiscoveryDocument,AuthorizationUrl,TokenUrl,UserInfoUrl,JwksUri,CreatedAt,UpdatedAt. Collections are read-only and never null.OidcDiscoveryDocumentSupabase.Gotrue.CommonIssuer,AuthorizationEndpoint,TokenEndpoint,UserInfoEndpoint,JwksUri,ScopesSupported,ResponseTypesSupported,GrantTypesSupported,SubjectTypesSupportedNewCustomProviderSupabase.Gotrue.CreateCustomProviderNewCustomProvider.OAuth2(identifier, name, clientId, clientSecret, authorizationUrl, tokenUrl, userInfoUrl)orNewCustomProvider.Oidc(identifier, name, clientId, clientSecret, issuer). The constructor is private.CustomProviderOptionsSupabase.Gotrue.CommonIGotrueApi<TUser, TSession>gains the matchingCreateCustomProviderAsync(string jwt, NewCustomProvider, CustomProviderOptions?, CancellationToken).Behavior
POST /admin/custom-providerswith the service-role keyprovider_type,identifier,name,client_idandclient_secret, plusissuer(OIDC) orauthorization_url,token_urlanduserinfo_url(OAuth2). Options are sent only when set. Otherwise the server's defaults apply: PKCE on, enabled, email required, nonce checked. For OIDC the server addsopenidto the scopes.CustomProvider. Null collections in the response become empty.ArgumentNullExceptionfor a nullprovideror a null factory argument.ArgumentException(the same check as delete) when the identifier lacks the case-sensitivecustom:prefix or contains/,\,?,#or%. The server accepts such identifiers on create, but the SDK could then never get, update or delete the provider.GotrueException.ErrorCodeisconflict(identifier taken),over_custom_provider_quotaorvalidation_failed(bad settings, a non-HTTPS or loopback URL, an OIDC discovery fetch or issuer mismatch), all with 400.Reason = AdminTokenRequiredon 401/403.ErrorCode = feature_disabledon 404. An unrecognizedprovider_typeor anullbody in the response also throwsGotrueException.NewCustomProvider.ToString()leaves out the client secret. By default a record prints every property.Deviations from supabase-js
supported_scopes,supported_response_typesand so on. The auth server sendsscopes_supported,response_types_supported,grant_types_supportedandsubject_types_supported, andOidcDiscoveryDocumentfollows the server.Tests
CreateCustomProviderTestsErrorCode = conflict. A non-admin key →AdminTokenRequired.CreateCustomProviderContractTestsnullbody →GotrueException. Null provider. A cancelled token sends nothing. The call completes when the caller blocks on a context that never runs continuations.CreateCustomProviderApprovalTestsNewCustomProviderTestsToString()Changed-line coverage: 100% (208 lines). Mutation: 20/20 killed on changed lines, 0 survivors (3 compile errors, 3 ignored).
Layout
The
Common/types are inCommon/from this PR, even though only create uses them here. They are the result type that get (#591), list (#592) and update (#593) return, andCustomProviderOptionsis also accepted by update (#593). Putting them inCommon/now avoids moving them in the next PR. The same reasoning applies to the response fixtures and the E2E base inGotrue.Tests/Common/.CustomProviderOptionsBodymaps the optional settings once for create's body and update's body. Each body is its own type, with no mode switch.[JsonPropertyOrder]keeps the approved field order.The shipped delete code (
CustomProviders/CustomProviderIdentifier.cs,Admin/AdminCustomProvider*Tests.cs) is not moved.Notes
Helpers.MakeRequestAsync<T>now awaits withConfigureAwait(false). Before, every typed Gotrue request resumed on the caller's synchronization context. The deadlock contract test above pins this, and it is the only way the new await'sConfigureAwait(false)can be checked.ReadAsStringAsyncin the same helper still lacks it. Adding it could not be tested: the content is already buffered, so the await never yields. I left that line unchanged.RequestApprovalFixturegainsRespondTo(path, body)for operations that read their response and reject{}.custom:prefix. The server (v2.197.0 and master) rejects identifiers without the prefix.supabase/config.tomlchange: custom providers are on by default (GOTRUE_CUSTOM_OAUTH_ENABLED=true).