Sign In with Google or Apple
A customer taps “Continue with Google” and is in, without choosing a password. The client gets a token from the provider, sends it to the shop, and the gateway turns it into an identity. It works with any OpenID Connect provider: Google, Apple, a corporate directory, a CI system. You list the providers you trust.
§Trust a provider
# context.toa.yaml
configuration:
identity.federation:
trust:
- iss: https://accounts.google.com
aud: 1045282659797-example.apps.googleusercontent.com iss is the issuer, exactly as it appears in the provider’s tokens. aud is the audience a token
must be issued for: the client id the provider gave your application. A token of another issuer,
or of this issuer for another audience, is refused.
The gateway must be able to reach <iss>/.well-known/openid-configuration. It reads the Context
when the application is restarted:
$ npm run restart §Sign in with the provider’s token
The client obtains an id_token with the provider’s SDK and sends it as Bearer:
GET /identity/ HTTP/1.1
host: api.store.example
authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6...
accept: application/yaml 200 OK
authorization: Token eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIi...
id: 9c1f0a6d5b7e4f3a8d2c1b0a9e8f7d6c
roles: [] The client keeps the Token and drops the id_token, as after any other sign-in.
The same person at the same provider is always the same identity. A person the shop has not seen
before gets a new identity on the spot: there is no sign-up step, and the first request of a new
Google account answers 200 with a fresh id.
A token of a provider that is not trusted, or one that is expired or damaged, authenticates nobody:
401 Unauthorized §Create the account together with the identity
An identity that appears on its own has no account in the shop. Since accounts is an associated entity, that is fine:
the account reads as blank until something is written to it. When signing up must run an
operation (ask for a name, send a welcome email), turn the automatic creation off:
# context.toa.yaml
configuration:
identity.federation:
trust:
- iss: https://accounts.google.com
aud: 1045282659797-example.apps.googleusercontent.com
assert: false Now a valid token of a person nobody has seen is answered 401 wherever an identity is required,
and the identity is created by the sign-up route, which accepts a
provider’s token just as it accepts a username and password:
# components/accounts/manifest.toa.yaml
exposition:
/:
POST:
auth:incept: id
io:input: [name]
io:output: [id, name]
endpoint: create POST /accounts/ HTTP/1.1
host: api.store.example
authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6...
content-type: application/yaml
accept: application/yaml
name: Bill Smith 201 Created
authorization: Token eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIi...
id: 4344518184ad44228baffce7a44fd0b1
name: Bill Smith The account and the identity share the id, and that Google account now signs in to it. A second POST with the same token is answered 403: it has an identity already.
A route with auth:assert accepts Bearer too, when one request should serve new and returning users.
§Use what the provider says about the user
A provider’s token carries an email, a name and more. map:claims copies claims into the operation’s input; keys are input
properties, values are claim names:
# components/accounts/manifest.toa.yaml
exposition:
/:
POST:
auth:incept: id
map:claims:
email: email
verified: email_verified
io:input: [name]
io:output: [id, name]
endpoint: create Declare email and verified in the input of create. A claim the token does not have leaves
the property unset, and so does a request that is not authenticated with a provider’s token. That
includes every later request made with the gateway’s Token: read claims on the request that
presents the id_token.
To admit a request by its claims alone, without an identity in the shop, see auth:claims.
§Several client ids
A web application and an iOS application have client ids of their own. aud may be a list, and a
token is accepted when its audience is one of them:
trust:
- iss: https://accounts.google.com
aud:
- 1045282659797-web.apps.googleusercontent.com
- 1045282659797-ios.apps.googleusercontent.com §Let the gateway exchange a code
Some sign-in flows hand the browser a code instead of a token, to be exchanged by a server that holds a client secret. The gateway does the exchange once the provider’s entry has the secret:
# context.toa.yaml
configuration:
identity.federation:
trust:
- iss: https://accounts.google.com
aud: 1045282659797-example.apps.googleusercontent.com
secret: $GOOGLE_CLIENT_SECRET or, for a provider that wants a signed client assertion instead of a secret, as Apple does, the signing key:
- iss: https://appleid.apple.com
aud: com.example.store.web
signature:
iss: 3L7XKJ5X9Q # the issuer of the assertion: your team id
kid: 8YH4ZB2K7M # the id of the key
key: $APPLE_PRIVATE_KEY # PKCS8 PEM, the contents of the .p8 file secret and signature.key are secrets: $NAME references whose
values are provided separately. Where aud is a list, its first value is the client id of the
exchange.
The client redirects the user to the provider, receives the code at its redirect URI, and sends:
GET /identity/ HTTP/1.1
host: api.store.example
authorization: Code eyJjb2RlIjoiNC8wQWJDLi4uIiwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwiZm9yIjoiaHR0cHM6Ly9zdG9yZS5leGFtcGxlL2NhbGxiYWNrLyJ9 The credentials are Base64 of a JSON object:
{
"code": "4/0AbC...",
"iss": "https://accounts.google.com",
"for": "https://store.example/callback/"
} | Property | Meaning |
|---|---|
code | The authorization code the provider returned. |
iss | The issuer, as in trust. |
for | The redirect URI the code was issued for. |
The reply is the same as for an id_token: the identity and a Token. A code works once, so the
client sends it once and continues with the token.
§Add Google to an existing account
A customer who signed up with a password adds Google as a second way in:
POST /identity/federation/2428c31ecb6e4a51a24ef52f0c4181b9/ HTTP/1.1
host: api.store.example
authorization: Token eyJhbGciOiJkaXIi...
content-type: application/yaml
accept: application/yaml
scheme: bearer
credentials: eyJhbGciOiJSUzI1NiIsImtpZCI6... 201 Created
id: 09200d0bcc2448c5863e3f50ae18af84
iss: https://accounts.google.com
CREATED: 1754899200000 scheme is bearer with an id_token as credentials, or code with the Base64 value described above. From now on that Google account signs in to
this identity. An identity can have several providers, and several accounts of one provider.
Adding what the identity already has returns the existing entry. A provider’s account that belongs to another identity is refused:
422 Unprocessable Entity
code: EXISTS Only the identity itself may add, list and remove its providers.
§List the providers
GET /identity/federation/2428c31ecb6e4a51a24ef52f0c4181b9/ HTTP/1.1
host: api.store.example
authorization: Token eyJhbGciOiJkaXIi...
accept: application/yaml - id: 09200d0bcc2448c5863e3f50ae18af84
iss: https://accounts.google.com
CREATED: 1754899200000 id identifies the entry, not the identity.
§Remove a provider
DELETE /identity/federation/2428c31ecb6e4a51a24ef52f0c4181b9/09200d0bcc2448c5863e3f50ae18af84/ HTTP/1.1
host: api.store.example
authorization: Token eyJhbGciOiJkaXIi... 204 No Content Nothing stops the removal of the last way to sign in.
§Make a provider’s account the first administrator
As with a username, one account of a provider can be
named to receive the system role when its identity is created:
# context.toa.yaml
configuration:
identity.federation:
principal:
authority: local
iss: https://accounts.google.com
sub: '104218230498234917283' sub is the account’s id at the provider, the sub claim of its tokens. local is the authority the generated Context declares
for localhost:8000.
§Test it
A scenario cannot obtain an id_token from Google, so the routes of the shop are tested with
users who signed up with a password or as guests. What a scenario can
check here is that nothing but a trusted token gets in, with the steps toa create wrote:
# features/accounts.feature
Feature: Accounts
Scenario: A token of nobody we trust
When the request is sent:
"""
GET /identity/ HTTP/1.1
authorization: Bearer not-a-token
"""
Then the response is received:
"""
401 Unauthorized
""" $ npm run restart
$ npm run features §Reference
§identity.federation
| Key | Meaning | Default |
|---|---|---|
trust | Trusted providers, see below. | [] |
assert | Create an identity for a valid token of a person nobody has seen. | true |
principal | authority, iss and sub of the account that is given system. | none |
Each entry of trust:
| Key | Meaning |
|---|---|
iss | Required. The issuer URI. |
aud | Required. The accepted audience, or a list of them. |
secret | Client secret; lets the gateway exchange codes. |
signature | iss, kid, key (PKCS8 PEM) of a signed client assertion; lets the gateway exchange codes. |
§Why a token was refused
Every refusal looks the same to the client: the request has no identity, and a route that needs
one answers 401. The reason is written to the gateway’s log as a code:
| Code | Reason |
|---|---|
TRUST | The issuer is not in trust. |
TOKEN | The token is malformed, expired, badly signed, or for another audience. |
ISS, SUB | The token has no iss or sub. |
NOT_FOUND | Valid token, unknown person, and assert is false. |
CODE_NOT_ENABLED | A code was sent for a provider without secret or signature. |
CODE_SCHEMA | The Code credentials are not the JSON described above. |
CONFIG, RESPONSE, NO_TOKEN | The provider’s metadata or its answer to the exchange was unusable. |
POST /identity/federation/:identity/ returns the same codes to the client as 422, since there
the client asked for a provider to be added and can be told why it was not.