IV Users and Access

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/"
}
PropertyMeaning
codeThe authorization code the provider returned.
issThe issuer, as in trust.
forThe 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

KeyMeaningDefault
trustTrusted providers, see below.[]
assertCreate an identity for a valid token of a person nobody has seen.true
principalauthority, iss and sub of the account that is given system.none

Each entry of trust:

KeyMeaning
issRequired. The issuer URI.
audRequired. The accepted audience, or a list of them.
secretClient secret; lets the gateway exchange codes.
signatureiss, 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:

CodeReason
TRUSTThe issuer is not in trust.
TOKENThe token is malformed, expired, badly signed, or for another audience.
ISS, SUBThe token has no iss or sub.
NOT_FOUNDValid token, unknown person, and assert is false.
CODE_NOT_ENABLEDA code was sent for a provider without secret or signature.
CODE_SCHEMAThe Code credentials are not the JSON described above.
CONFIG, RESPONSE, NO_TOKENThe 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.

Search
Search the documentation