OAuth¶
OAuth 2.0 social login. Enable it with oauth={...} on CRUDAuth. An OpenID
Connect provider needs only OAuthCredentials(issuer=...), which configures
GenericOIDCProvider; anything else is added by implementing AbstractOAuthProvider and
registering it with OAuthProviderFactory.
crudauth.oauth.OAuthCredentials
¶
Bases: BaseModel
Client credentials for a provider, supplied via oauth={...}.
Leave client_secret empty for a public client (PKCE only). Set issuer
for any OpenID Connect provider that isn't built in (Keycloak, Zitadel,
Authentik, Auth0, Okta, Entra ID, ...): its endpoints are then read from
{issuer}/.well-known/openid-configuration when the app starts, instead of
the name being looked up in
OAuthProviderFactory.
Example
crudauth.oauth.OAuthUserInfo
¶
Bases: BaseModel
Normalized profile returned by AbstractOAuthProvider.process_user_info.
crudauth.oauth.GenericOIDCProvider
¶
GenericOIDCProvider(
client_id: str,
client_secret: str,
redirect_uri: str,
*,
issuer: str,
provider_name: str = "oidc",
scopes: list[str] | None = None,
authorize_endpoint: str = "",
token_endpoint: str = "",
userinfo_endpoint: str = "",
transport: Any | None = None,
)
Bases: AbstractOAuthProvider
An OIDC provider whose endpoints come from its issuer's discovery document.
Configure it through oauth= by giving the credentials an issuer:
Example
The key you use is the provider name, so it's also the column the account is
linked on (keycloak -> keycloak_id) and the path segment in
/oauth/keycloak/authorize. process_user_info reads the standard OIDC
claims, which every conformant provider returns from /userinfo.
Note
The provider is unusable until initialize() has resolved the
endpoints. CRUDAuth.initialize() does that, which is the same
lifespan call Redis-backed storage already needs.
Build an unresolved provider; initialize() fills in the endpoints.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
issuer
|
str
|
The provider's issuer identifier, e.g.
|
required |
provider_name
|
str
|
The linked-account name, so |
'oidc'
|
scopes
|
list[str] | None
|
Override the default |
None
|
authorize_endpoint
|
str
|
Skip discovery for this endpoint by passing it (along with the other two) explicitly. |
''
|
token_endpoint
|
str
|
See |
''
|
userinfo_endpoint
|
str
|
See |
''
|
transport
|
Any | None
|
An |
None
|
from_discovery
async
classmethod
¶
from_discovery(
issuer: str,
client_id: str,
client_secret: str,
redirect_uri: str,
*,
provider_name: str = "oidc",
scopes: list[str] | None = None,
transport: Any | None = None,
) -> GenericOIDCProvider
Build a provider and resolve its endpoints in one await.
For code that owns its providers directly (a hand-written OAuth route, a
script). Through oauth= you don't need this: CRUDAuth constructs
the provider and initialize() resolves it.
initialize
async
¶
Fetch the discovery document and resolve the endpoints (idempotent).
Raises:
| Type | Description |
|---|---|
ValueError
|
The document is missing a required endpoint, or declares an issuer other than the configured one. |
HTTPStatusError
|
The discovery endpoint returned an error status. |
process_user_info
async
¶
process_user_info(
user_info: dict[str, Any],
) -> OAuthUserInfo
Normalize the standard OIDC userinfo claims into OAuthUserInfo.
sub is the account's stable identifier, so a response without one is
an error rather than a user linked to the string "None".
email_verified is passed through as the provider stated it: a
verified provider email is what lets the callback link the profile to an
existing local account, so a provider that doesn't claim verification
doesn't get that.
crudauth.oauth.AbstractOAuthProvider
¶
AbstractOAuthProvider(
client_id: str,
client_secret: str,
redirect_uri: str,
*,
scopes: list[str],
authorize_endpoint: str,
token_endpoint: str,
userinfo_endpoint: str,
provider_name: str,
transport: Any | None = None,
)
Bases: ABC
Port for an OAuth provider - implements the Authorization-Code-with-PKCE flow.
Subclass it, pass the three endpoints + scopes + provider_name to
super().__init__, implement process_user_info, and register it
with OAuthProviderFactory. Set email_verified honestly -
auto-linking to an existing account requires a verified provider email.
Set requires_client_secret = True on a provider that never accepts a
public client, so a missing secret fails at startup instead of at the first
login.
Note
A custom provider named "gitlab" requires a gitlab_id column on
your user model (that's where its account id is stored and matched). Add
it to your model (or map it via column_map=); CRUDAuth
raises at startup if a configured provider has no {provider}_id
column. Only google_id/github_id ship on
AuthUserMixin.
Example
class GitLabOAuthProvider(AbstractOAuthProvider):
def __init__(self, client_id, client_secret, redirect_uri, scopes=None):
super().__init__(
client_id, client_secret, redirect_uri,
scopes=scopes or ["read_user"],
authorize_endpoint="https://gitlab.com/oauth/authorize",
token_endpoint="https://gitlab.com/oauth/token",
userinfo_endpoint="https://gitlab.com/api/v4/user",
provider_name="gitlab",
)
async def process_user_info(self, info):
return OAuthUserInfo(
provider="gitlab", provider_user_id=str(info["id"]),
email=info.get("email"), email_verified=True, raw_data=info,
)
OAuthProviderFactory.register_provider("gitlab", GitLabOAuthProvider)
# ...and add `gitlab_id` to your user model.
initialize
async
¶
Resolve whatever the provider needs before it can serve a login.
A no-op for a provider with static endpoints. CRUDAuth.initialize awaits this for every configured provider, so a provider that discovers its endpoints can do it there.
generate_pkce_codes
staticmethod
¶
Return a PKCE pair: {"code_verifier": ..., "code_challenge": ...} (S256).
get_authorization_url
¶
get_authorization_url(
state: str | None = None,
pkce: bool = True,
extra_params: dict[str, str] | None = None,
) -> dict[str, str]
Build the provider authorization URL and the values to stash server-side.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
str | None
|
CSRF state to embed; generated if omitted. |
None
|
pkce
|
bool
|
Include a PKCE challenge (recommended). |
True
|
extra_params
|
dict[str, str] | None
|
Provider-specific query params to merge in (e.g. Google's
|
None
|
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
|
dict[str, str]
|
|
dict[str, str]
|
persisted to verify the callback. |
exchange_code
async
¶
exchange_code(
code: str,
code_verifier: str | None = None,
headers: dict[str, str] | None = None,
) -> dict[str, Any]
Exchange an authorization code for tokens at the token endpoint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
code
|
str
|
The authorization code from the callback. |
required |
code_verifier
|
str | None
|
The stored PKCE verifier (required if PKCE was used). |
None
|
headers
|
dict[str, str] | None
|
Extra request headers (some providers need |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The provider's raw token response ( |
Raises:
| Type | Description |
|---|---|
HTTPStatusError
|
If the token endpoint returns an error status. |
Note
client_secret is sent only when set, so a public client (PKCE
only) sends no client authentication. It rides in the form body
(client_secret_post) unless token_auth_method says the
provider wants HTTP Basic (client_secret_basic).
get_user_info
async
¶
Fetch the raw user profile from the userinfo endpoint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
A valid provider access token. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The provider's raw profile JSON (normalize it in |
dict[str, Any]
|
Raises:
| Type | Description |
|---|---|
HTTPStatusError
|
If the userinfo endpoint returns an error status. |
process_user_info
abstractmethod
async
¶
process_user_info(
user_info: dict[str, Any],
) -> OAuthUserInfo
Normalize the provider's raw user payload into OAuthUserInfo.
crudauth.oauth.OAuthProviderFactory
¶
Process-wide registry of provider name -> provider class.
Built-in providers ("google", "github") register themselves when
[crudauth.oauth][] is imported; register your own with
register_provider.
register_provider
classmethod
¶
register_provider(
provider_name: str,
provider_class: type[AbstractOAuthProvider],
) -> None
Register provider_class under provider_name (overwrites any existing).
get_provider_class
classmethod
¶
get_provider_class(
provider_name: str,
) -> type[AbstractOAuthProvider] | None
Return the registered class for provider_name, or None.
create_provider
classmethod
¶
create_provider(
provider_name: str,
client_id: str,
client_secret: str,
redirect_uri: str,
scopes: list[str] | None = None,
) -> AbstractOAuthProvider
Instantiate a registered provider.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider_name
|
str
|
A registered provider name. |
required |
client_id
|
str
|
OAuth client id. |
required |
client_secret
|
str
|
OAuth client secret. |
required |
redirect_uri
|
str
|
The callback URI registered with the provider. |
required |
scopes
|
list[str] | None
|
Override the provider's default scopes. |
None
|
Returns:
| Type | Description |
|---|---|
AbstractOAuthProvider
|
A configured AbstractOAuthProvider. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
crudauth.oauth.OAuthAccountService
¶
OAuthAccountService(
repo: UserRepository,
new_user_fields: NewUserFields | None = None,
new_user_defaults: dict[str, Any] | None = None,
*,
session_manager: "SessionManager | None" = None,
)
Resolve an OAuth identity to a user, creating or linking as needed.
The linking rules live here (lookup order: provider id → verified email →
create), so a hand-written callback reuses them. Reachable as auth.oauth
(None when OAuth isn't configured). session_manager is used to sign
out the sessions of an unverified account that a provider claims.
get_or_create_user
async
¶
get_or_create_user(
info: OAuthUserInfo, db: AsyncSession
) -> tuple[Any, bool]
Resolve an OAuth identity to a user; lookup order: provider id → email → create.
Returns:
| Type | Description |
|---|---|
Any
|
|
bool
|
inserted (provider-id and email-link hits return the existing user). |
Raises:
| Type | Description |
|---|---|
OAuthAccountException
|
When the provider gives no email, an
unverified email, an email longer than the |
Note
Only a verified provider email links or creates an account. Linking
to an account whose own email was never verified claims it: its
password becomes unusable, any two-factor enrollment is removed, its
token_version is bumped, its sessions are signed out, and its
email is marked verified.