Federated Identity Providers
Every upstream identity provider is configuration, not deployment. A provider is one row in
espuser-identity-providers, reached over the public internet, and our deployment holds no authority
over it. A provider in another AWS account, another organisation, or a non-AWS product is described
the same way. Cognito is one such row.
The provider row
Field |
Meaning |
|---|---|
|
Key; the value |
|
|
|
Whether |
|
Label for the login chooser |
|
The provider’s OIDC issuer; endpoints and keys are discovered from it |
|
Where a login begins: the upstream authorize endpoint, or the hosted page for an |
|
The one app client we hold for this provider |
|
That client’s secret |
|
Space-separated, requested on the upstream authorize |
|
How the client authenticates at the token endpoint: |
|
Whether |
|
Our claim name → the provider’s claim name |
|
Optional cap on OUR session’s absolute lifetime for logins this provider authenticated — the operator’s assertion of what the upstream sanctions (not discoverable over OIDC). Combined as |
|
The provider’s sign-out endpoint. Unset ⇒ the upstream hop is skipped entirely and only OUR session ends. See Sign-out below |
|
The query parameter that provider uses for “come back here afterwards”. Defaults to |
|
Optional pins that override discovery |
Nothing else about a provider exists anywhere: no SSM parameters, no Lambda environment variables, no IAM grant naming it, no deploy step. Adding a provider is one write to this table.
Sign-out — two fields here, one URL there
Adding a provider is not finished when logins work. Ending our session leaves the provider’s own untouched, and a provider that still holds a cookie answers the next authorize request without prompting: the person signs out, signs in, and is straight back in without typing anything. Signed out in every technical sense, and not at all in the sense they meant.
Set on this row:
Field |
Cognito example |
|---|---|
|
|
|
|
Most OIDC providers publish the endpoint as end_session_endpoint in their discovery document
and name the return parameter post_logout_redirect_uri, in which case the second field is
left unset. Cognito publishes the endpoint but names the parameter logout_uri and expects
client_id on the URL itself — which is exactly why both fields exist rather than a
provider-type branch in code.
Register with the provider — exactly one URL, forever:
{issuer}/oauth2/logout/done
That is this authorization server’s sign-out return point, and it is the whole list. It does
not grow when a product is added, because the provider is never told about products: it knows
this authorization server and nothing behind it, exactly as it does on the login leg where it
holds a single /oauth2/federation/callback. A product’s own post-sign-out page is registered
here, in that client’s post_logout_redirect_uris, and applied after the provider hands
the browser back.
In Cognito that list is the app client’s Allowed sign-out URLs. A provider that has none registered refuses the hop, and sign-out silently stops ending the upstream session.
Checklist for a new provider:
end_session_urlon the row — or leave unset and accept that only our session ends.end_session_redirect_param, if the provider does not use the OIDC-standard name.{issuer}/oauth2/logout/doneregistered with the provider. Once.
Then verify the way that matters: sign in, sign out, sign in again — the provider must ask for a password. If it does not, one of the three is missing.
One confidential client per provider
A provider is reached through exactly one app client, and it is confidential. The brokered authorization-code flow and the legacy password grant share it.
It is confidential because a public client can be driven by anyone who learns its id — client ids are not secret — which would expose sign-up, password reset and password sign-in on the provider’s pool to the internet. Only our issuer holds the secret. Clients of ours never talk to the provider at all, so they never need a client of their own.
Sharing the client means the provider requires proof of possession on operations that are otherwise unauthenticated. Cognito’s form of that proof is a keyed hash of the username and client id under the client secret, sent with every such call.
The secret is stored as given
client_secret holds the secret in the row. DynamoDB encrypts at rest and the table is reachable only
through the IAM grants on the issuer’s own Lambdas, so the trust boundary is the table’s access policy.
Keys and endpoints come from the issuer
The provider’s authorize, token, userinfo and key-set endpoints are read from its discovery document, cached per issuer for the container lifetime. Its verify keys are then fetched from the key-set URL and cached per URL, so upstream key rotation is picked up on cold start. A row may pin any of these as a URL, which a provider that publishes no discovery document needs and nothing else does.
The password grant
The legacy /v1/user/auth/* surface needs six things from a provider: password sign-in, sign-up,
resend of a sign-up code, sign-up confirmation, password-reset start, and password-reset confirmation.
All six are unauthenticated provider operations — they take the app client id and no AWS credential —
which is what lets the surface work against a provider in any account.
The first enabled provider that sets password_grant and carries a client_id serves the surface.
When none does, it fails closed: the routes exist but authenticate nothing.
Operations that presume ownership of the provider’s pool are not available. Nothing on this path is given a pool identifier, so an operation that would administer the pool has nothing to address. Operator-driven user management against an external provider would require an explicit cross-account role and is not offered.
The bundled pool
Our own CDK creates a Cognito pool and seeds it as one ordinary row, so a fresh deployment has a working login without manual configuration. It is a default, not a dependency: deleting the row leaves a system with no provider configured, and adding a different row is the same operation for Cognito as for anything else.