Adding a federation provider
This guide adds a sign-in provider, such as GitLab, next to GitHub and Google. The shared federation code already runs the flow: state, PKCE, browser binding, signup, linking and unlinking. A provider supplies only its protocol facts and the way it proves who the user is.
The examples add a hypothetical GitLab provider. Replace gitlab, GitLab and
GITLAB with your provider’s names.
What you’ll have at the end
- People can sign up and sign in with the provider, and link or unlink it in their settings.
- Administrators unlink it on the dashboard, and operators with
auth-service account <user> identities unlink gitlab, with no change of yours: both read the provider list. - The provider stays off in every environment until its client ID and secret are set.
Before you start
The shared code exchange, FederationApplication::exchange in
apps/auth-service/src/federation/providers/mod.rs, requires the provider to
support:
- the authorization code flow with PKCE
S256; - a client secret sent in the token request’s form body.
If the provider needs something else, change the shared exchange for every provider rather than adding a special case.
Then decide how the provider proves who the user is:
- OpenID Connect, as Google does: take the identity only from a signed ID
token. Verify its signature against the provider’s published keys, then its
issuer, exclusive audience, nonce and expiry. Cache the keys:
GoogleProtocolholds the Rust SDK’ssourceorigin_auth::KeyCache, which keeps them for the response’smax-age, refetches once when a token names an unknown key, and fetches at most once every 30 seconds. - OAuth only, as GitHub does: fetch the profile with the access token.
Either way, the subject is the provider’s stable account ID, never an email. Keep an email only if the provider says it is verified. Call it authoritative only if the provider hosts the mailbox, as Google does for Gmail addresses and its Workspace domains: another provider verified the address once, and it may have changed owners since, as a lapsed domain does. Signup and linking import an authoritative address as verified, and any other as unverified, with a verification email.
1. Name the provider
Add a variant to Provider in crates/common/src/lib.rs, and its display name
to Provider::name below it:
#![allow(unused)]
fn main() {
/// GitLab `OpenID` Connect identity.
GitLab = "gitlab",
}
The wire name is stored in the database and appears in URLs, such as the
callback’s /login/gitlab/callback, so never change it once released. Every
match on Provider now fails to compile: the errors list the places the next
steps fill in. The sign-in and sign-in methods pages list enabled providers by
Provider::name, so they need no change.
2. Allow it in the database
Three tables restrict provider_name with a CHECK constraint:
external_login_identities, federation_transactions and pending_federation.
Add a migration that widens all three, following
adding a migration. Widening a constraint is an expand step: the
previous release never writes the new value.
3. Declare its variables
In apps/auth-service/src/config/federation.rs, add two variables to the
sourceorigin_common::variables! block, then map them in ProviderConfig::variables:
#![allow(unused)]
fn main() {
sourceorigin_common::variables! {
// ...
/// The client ID of auth's GitLab OAuth app, as `AUTH_GITHUB_CLIENT_ID` is
/// GitHub's.
const GITLAB_CLIENT_ID = "AUTH_GITLAB_CLIENT_ID";
/// Where to read the GitLab OAuth app's secret, as for
/// `AUTH_GITHUB_CLIENT_SECRET`.
const GITLAB_CLIENT_SECRET = "AUTH_GITLAB_CLIENT_SECRET";
}
}
#![allow(unused)]
fn main() {
Provider::GitLab => (GITLAB_CLIENT_ID, GITLAB_CLIENT_SECRET),
}
ProviderConfig::from_environment already reads every provider’s pair: an empty
or unset client ID leaves the provider off, and a client ID requires the secret.
The doc comments are the variables’ entries in the configuration reference, so
write them for operators. See
adding a configuration variable.
4. Write the provider module
Create apps/auth-service/src/federation/providers/gitlab.rs. Model it on
google.rs for OpenID Connect, or on github.rs for OAuth. It holds everything
specific to the provider:
SCOPES: the scopes requested.impl ProviderEndpoints:pub(super) fn gitlab() -> Self, with the issuer stored beside each subject, the authorization and token URLs, the scopes and the identity strategy. A provider whose facts need a request, such as Google’s key cache, takes the client and may fail:fn google(client: &reqwest::Client) -> Result<Self, FederationConfigError>.GitLabProtocol: the back-channel URLs the identity step calls, such as the key set or the profile endpoint. They areStrings, so tests can point them at a local fixture.GitLabError: one variant per provider-specific check, each with a message that says what failed.impl FederationApplication:pub(super) async fn gitlab_identity(...), which turns the token response into aVerifiedIdentity.- Read every response through
Self::json. It enforces the size limit, and keeps the provider’s OAuth error when a request fails. - Build the result with
VerifiedIdentity::new(Provider::GitLab, subject, (email, authoritative), (name, fallback_name), picture). It refuses an empty or overlong subject, drops an invalid email or a picture off the provider’s host, and bounds the display name.authoritativeistrueonly for an address the provider hosts. A social signup importspictureas the account’s first picture.
- Read every response through
- Response types: private
Deserializestructs for the provider’s JSON.
5. Connect it in providers/mod.rs
In apps/auth-service/src/federation/providers/mod.rs:
- Declare
mod gitlab;andpub(super) use gitlab::GitLabProtocol;. - Add
GitLab(#[from] gitlab::GitLabError)toProviderError, and list it inProviderError::sign_in_failurewith the other evidence checks, which recordSignInFailure::InvalidCredential. - Add
Provider::GitLab => Ok(Self::gitlab())toProviderEndpoints::new. - Add a
GitLab(GitLabProtocol)variant toIdentityStrategy, then its arms:- in
IdentityStrategy::extend_authorization: an OpenID Connect provider sends the flow’snonce, asGoogleProtocol::extend_authorizationdoes; - in
FederationApplication::exchange: callgitlab_identity.
- in
- Add the
Provider::GitLabarm toProviderPicture::parse: the host its pictures are on, and how to ask for the stored size, if it can. Nothing else is ever fetched.
Test it
Add tests to apps/auth-service/src/federation/tests/protocols.rs, serving the
provider’s endpoints from a local mock, as the Google tests there do. Cover the
evidence the provider must reject, such as a wrong audience, a wrong nonce, an
expired token or a bad signature, not only the successful sign-in. TestApp
in tests/mod.rs points the GitHub fixture at a local server; its match on
IdentityStrategy needs the new variant’s arm too.
just test -p auth-service federation
just db test -E 'test(federation)'
The first runs the protocol tests; the second the sign-in, signup and linking flows, which need a database. See testing.
Enable it
-
Register an application with the provider in each environment that enables it, with that environment’s callback URL:
https://<auth origin>/login/gitlab/callback -
In
infra/piqueld.toml, addgitlab_client_id = ""andgitlab_secret = "no-provider-secret"to[spec.variables], setAUTH_GITLAB_CLIENT_IDandAUTH_GITLAB_CLIENT_SECRETin theauthservice’s environment, and mount the secret, as GitHub’s and Google’s are. -
Ship the provider before you enable it.
Provideris a closed vocabulary that older releases cannot read (see domain events), so set an environment’s client ID in a later deploy than the one that adds the variant: sign-in providers says how.
Document it
- Name the provider in the help of
account <user> identities unlink, the doc comment ofIdentities::Unlinkinapps/auth-service/src/cli/account.rs. Then runjust docs reference, which adds the two variables to the configuration reference and the help to the command reference. - Add the provider’s callback URL, scopes and registration steps to sign-in providers.
- Name the provider where the others are named: the user guide’s
introduction, and the privacy policy and terms
in
apps/web/assets/policies/, which say what each provider tells SourceOrigin.
The settings, the dashboard and the CLI list providers from Provider::ALL, so
every surface gets the provider without more work; see
every surface gets the feature.