Skip to content

Generic Providers

If your OAuth provider doesn’t have a dedicated built-in class, you can connect it using GenericOAuthProvider or GenericOIDCProvider.

For OAuth 2.0 providers where you know the endpoints:

from authfort import GenericOAuthProvider

gitlab = GenericOAuthProvider(
    "gitlab",
    client_id="...",
    client_secret="...",
    authorize_url="https://gitlab.com/oauth/authorize",
    token_url="https://gitlab.com/oauth/token",
    userinfo_url="https://gitlab.com/api/v4/user",
    scopes=("read_user",),
)
from authfort import GenericOAuthProvider

gitlab = GenericOAuthProvider(
    "gitlab",
    client_id="...",
    client_secret="...",
    authorize_url="https://gitlab.com/oauth/authorize",
    token_url="https://gitlab.com/oauth/token",
    userinfo_url="https://gitlab.com/api/v4/user",
    scopes=("read_user",),
)
ParameterTypeDescription
namestrProvider name (positional). Used in URLs: /auth/oauth/{name}/authorize
client_idstrOAuth client ID
client_secretstrOAuth client secret
authorize_urlstrAuthorization endpoint URL
token_urlstrToken exchange endpoint URL
userinfo_urlstrUser info endpoint URL
scopestuple[str, ...]Required scopes for this provider
extra_scopestuple[str, ...] | NoneAdditional scopes to request
map_user_infocallable | NoneCustom function to map provider response to user info
redirect_uristr | NoneOverride the callback URL (auto-detected by default)

For OpenID Connect providers that support discovery:

from authfort import GenericOIDCProvider

keycloak = GenericOIDCProvider(
    "keycloak",
    client_id="...",
    client_secret="...",
    discovery_url="https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration",
)
from authfort import GenericOIDCProvider

keycloak = GenericOIDCProvider(
    "keycloak",
    client_id="...",
    client_secret="...",
    discovery_url="https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration",
)

Automatically fetches authorization, token, and userinfo endpoints from the discovery document. The discovery response is cached with a configurable TTL (default: 1 hour). Default scopes: openid, email, profile.

ParameterTypeDescription
namestrProvider name (positional). Used in URLs: /auth/oauth/{name}/authorize
client_idstrOAuth client ID
client_secretstrOAuth client secret
discovery_urlstrOIDC discovery endpoint (.well-known/openid-configuration)
scopestuple[str, ...] | NoneOverride default scopes (openid, email, profile)
extra_scopestuple[str, ...] | NoneAdditional scopes to request
map_user_infocallable | NoneCustom function to map provider response to user info
discovery_ttlintCache TTL for discovery document in seconds (default: 3600)
redirect_uristr | NoneOverride the callback URL (auto-detected by default)

By default, providers map standard OIDC claims (sub/id, email, name, picture/avatar_url). For providers with non-standard responses, pass a custom mapper:

from authfort.providers.base import OAuthUserInfo

def map_discord(provider_name, data, access_token):
    return OAuthUserInfo(
        provider=provider_name,
        provider_account_id=str(data["id"]),
        email=data["email"],
        email_verified=data.get("verified", False),
        name=data.get("username"),
        avatar_url=(
            f"https://cdn.discordapp.com/avatars/{data['id']}/{data['avatar']}.png"
            if data.get("avatar")
            else None
        ),
        access_token=access_token,
    )

discord = GenericOAuthProvider(
    "discord",
    client_id="...",
    client_secret="...",
    authorize_url="https://discord.com/api/oauth2/authorize",
    token_url="https://discord.com/api/oauth2/token",
    userinfo_url="https://discord.com/api/users/@me",
    scopes=("identify", "email"),
    map_user_info=map_discord,
)
from authfort.providers.base import OAuthUserInfo

def map_discord(provider_name, data, access_token):
    return OAuthUserInfo(
        provider=provider_name,
        provider_account_id=str(data["id"]),
        email=data["email"],
        email_verified=data.get("verified", False),
        name=data.get("username"),
        avatar_url=(
            f"https://cdn.discordapp.com/avatars/{data['id']}/{data['avatar']}.png"
            if data.get("avatar")
            else None
        ),
        access_token=access_token,
    )

discord = GenericOAuthProvider(
    "discord",
    client_id="...",
    client_secret="...",
    authorize_url="https://discord.com/api/oauth2/authorize",
    token_url="https://discord.com/api/oauth2/token",
    userinfo_url="https://discord.com/api/users/@me",
    scopes=("identify", "email"),
    map_user_info=map_discord,
)

The mapper receives the provider name, the raw JSON response from the userinfo endpoint, and the access token. Return an OAuthUserInfo object.

Add generic providers to the providers list alongside built-in ones:

from authfort import AuthFort, GoogleProvider

auth = AuthFort(
    database_url="...",
    providers=[
        GoogleProvider(client_id="...", client_secret="..."),
        gitlab,
        keycloak,
        discord,
    ],
)
from authfort import AuthFort, GoogleProvider

auth = AuthFort(
    database_url="...",
    providers=[
        GoogleProvider(client_id="...", client_secret="..."),
        gitlab,
        keycloak,
        discord,
    ],
)

The provider name becomes the URL path: /auth/oauth/gitlab/authorize, /auth/oauth/keycloak/authorize, etc.

The client SDK signInWithProvider() accepts any string, so generic providers work out of the box:

auth.signInWithProvider('gitlab');
auth.signInWithProvider('keycloak', { mode: 'popup' });
auth.signInWithProvider('gitlab');
auth.signInWithProvider('keycloak', { mode: 'popup' });