# Checking the person: `sign_in:`

Signing in has two steps. First Jadpo checks the person is who they say,
for example that they can read email sent to their address. Then it gives
them a session: a cookie for a browser, or an API token for your own
client. `sign_in:` in `authentication { }` lists the ways Jadpo checks a
person for you, so your app writes no sign-in code:

```jadpo
// authentication.jadpo
authentication {
    browser_session: {
        cookie: "notes_session"
        origin: config.browser_origin
    }
    api_token: {}
    sign_in: {
        magic_link: {
            address: Member.email
            link: "/sign-in/confirm"
        }
        providers: [google, github]
    }
    on_first_sign_in: accounts.create_member
}
```

`sign_in:` holds up to three ways in:

- `magic_link:`, an email with a link and a code ([Magic links](/docs/magic-links.md));
- `passkey:`, a key held on the person's device ([Passkeys](/docs/passkeys.md));
- `providers:`, sign-in with another service such as Google or GitHub.

Each way ends by starting a session in the same way.

`on_first_sign_in:` names an action in your app, here `create_member` in
the `accounts` domain. It decides what happens the first time someone
signs in whom your app has no user for, whichever way they came in, an
external token included. See
[A person the app has not seen](/docs/a-person-the-app-has-not-seen.md).

You never write a magic-link action. `sessions.start` is only for a person
your app checks some other way, such as one accepting an invitation it
sent ([Starting and ending a session](/docs/starting-and-ending-a-session.md)).

## Providers

Each sign-in provider is a connection in `app.jadpo`'s `connections:`
([External services](/docs/external-services.md)), and `providers: [...]` names those
connections, as `email:` names its connection. Google, Microsoft and
other OpenID Connect providers are each `driver: Driver.oidc`, and the
provider's discovery document gives the rest. GitHub and Apple aren't
plain OpenID Connect, so they have the built-in drivers `Driver.github`
and `Driver.apple`.

```jadpo
// app.jadpo
connections: {
    google: {
        driver: Driver.oidc
        issuer: config.google_issuer
        client_id: config.google_client_id
        credential: config.google_client_secret
    }
}
```

- `issuer:` reads a `Url` setting. Jadpo compares it exactly with the
  issuer the provider's discovery document names, and with every ID
  token's. Google's is `https://accounts.google.com`. Microsoft's is
  `https://login.microsoftonline.com/<tenant-id>/v2.0` for one
  organisation's accounts, or the consumer tenant's,
  `9188040d-6c67-4c5b-b112-36a304b66dad`, for personal accounts. Its
  `common` and `organizations` addresses publish an issuer with a
  placeholder in it, which no exact comparison matches, so they are
  refused as `authentication_misconfigured`.
- `client_id:` reads a `Text` setting: the client ID the provider gave
  your app. Every ID token must name it as its audience.
- `credential:` reads a secret setting: the client secret
  ([Secrets](/docs/secrets.md)).
- `organisation:`, optional, reads a `Text` setting: the company whose
  accounts alone it signs in ([Company accounts](/docs/checking-the-person-sign-in.md#company-accounts)).
- Register `<origin>/auth/<name>/callback` with the provider as the
  redirect address, `<origin>` being the browser session's `origin:`.

`SignInMethod` has a value for each provider, its connection's name, so
`on_first_sign_in:`'s action can tell `SignInMethod.google` from
`SignInMethod.microsoft`.

A provider signs a person in to a browser session, so `providers:` needs
`browser_session:` with its `origin:`. It also needs `on_first_sign_in:`.
A provider account finds its user only through the link a first sign-in
made ([A person the app has not seen](/docs/a-person-the-app-has-not-seen.md)),
never by the principal's `subject:` field or an email address, so without
the action no one could sign in.

### Company accounts

A company's staff sign in with their work accounts. `organisation:` keeps
a provider to that company's accounts, so an account outside it, a
personal Gmail address among them, can't sign in or be linked:

```jadpo
// app.jadpo
connections: {
    google: {
        driver: Driver.oidc
        issuer: config.google_issuer
        client_id: config.google_client_id
        credential: config.google_client_secret
        organisation: config.google_domain
    }
}
```

The setting is checked on every sign-in against the claim the provider's
ID token names the account's company by:

- Google's `hd`, the Google Workspace domain (`desk.example`), for the
  issuer `https://accounts.google.com`. Google's own page then offers
  only that domain's accounts.
- Microsoft's `tid`, the tenant ID, for a tenant's issuer,
  `https://login.microsoftonline.com/<tenant-id>/v2.0`. The issuer
  already names one tenant; `tid` is checked as well.

An account whose token names another company, or none, is refused with
`sign_in_refused`. A provider whose tokens name no company can't keep to
one, so `organisation:` on a connection whose issuer setting's `default:`
or `test:` is another provider's is `connection.key_invalid`; one whose
issuer is only known when the app starts is
`authentication_misconfigured` at sign-in.

### The routes

Jadpo generates two routes for each provider, in the first API that
accepts browser sessions:

- `POST /auth/<name>/start` needs no one signed in, but its `Origin` must
  be the browser session's origin. It takes `{ "return_to": "/books" }`,
  a path on your site, or an empty body for `/`. It answers
  `{ "address": ... }`, the provider's sign-in address, which your page
  sends the browser to. It also sets the `<cookie>_provider` cookie
  (`HttpOnly`, `Secure`, `SameSite=Lax`, ten minutes), which binds the
  sign-in to this browser.
- `GET /auth/<name>/callback` is where the provider sends the browser
  back. Jadpo checks the sign-in and starts a browser session. Then it
  answers `303` to `return_to`.

Every refusal also answers `303` to `return_to`, with `?error=<code>`
after it, so your page can say what happened:

| Code | When |
|---|---|
| `sign_in_provider_failed` | The sign-in expired, was used already, came back to another browser, or the provider refused it or sent an ID token that doesn't check |
| `sign_in_refused` | `on_first_sign_in:` refused the person, or their account is outside the connection's `organisation:` |
| `user_disabled` | The person's user isn't `active:` |
| `authentication_unavailable` | The provider or the database can't be reached |
| `authentication_misconfigured` | The provider's settings or its discovery document are invalid |

Your page then reads the session with its own requests. The session's
cookie is `SameSite=Strict`, so a browser doesn't send it on the
navigation that came back from the provider.

### What Jadpo checks

- The authorization code flow with PKCE (`S256`), a `state` that works
  once, for ten minutes, in the browser that started it, and a `nonce`
  the ID token must carry. Jadpo keeps the `state` only as a digest, and
  works out the PKCE verifier and the nonce from the binding cookie, so
  the database holds neither.
- The code is exchanged at the provider's token endpoint with the client
  secret. The ID token's signature is checked against the provider's
  keys, fetched as an external token's are, under the same limits
  ([Where Jadpo fetches the provider's keys](/docs/ways-to-sign-in.md#where-jadpo-fetches-the-providers-keys)).
  So are its exact issuer, its audience (`client_id:`, with `azp` naming
  it when there are several), its expiry and its nonce.
- Only the token's `sub`, `email`, `email_verified` and `name` are read,
  into `FirstSignIn`. None is ever stored as the user's identity, and no
  claim becomes a grant. `address_verified` is `true` only when the
  provider says `email_verified: true`. Microsoft doesn't say it, so an
  action can't link a Microsoft account by address.

### In tests

A route test has `test_providers`, which signs in through the provider
routes for real, with the test run's local issuer as the provider. Only
tests can call it:

| Operation | Does |
|---|---|
| `test_providers.sign_in(method: SignInMethod, account: TestAccount) -> Response` | Starts the sign-in through `POST /auth/<name>/start` from the app's own page. Then the local issuer signs `account` in and sends the browser back to `GET /auth/<name>/callback` with the binding cookie. It gives the callback's `Response`, whose `code` is the `error` its redirect names, if any |

`TestAccount` holds what a provider says about the person: `subject:
Text`, `email: Email?`, `email_verified: Bool` and `name: Text?`, and two
fields a test may leave out: `organisation: Text?`, the company its token
names ([Company accounts](/docs/checking-the-person-sign-in.md#company-accounts)), and `strength:
AuthenticationStrength?`, `multi_factor` for a token whose `amr` names a
second factor. Only a test, fixture or seed builds one. The session it
starts records the provider, as a real sign-in's does.

Here a lending desk's staff sign in with Google. A member of staff is
taken on under their address, and `on_first_sign_in:` links the Google
account whose address Google has verified to them. The route test signs
Ann in through Google twice, the second time through the link, and shows
an unverified address and a Microsoft account refused:

```jadpo
use desk/entities { Librarian }

application {
    display_name: "Lending desk"
    connections: {
        google: {
            driver: Driver.oidc
            issuer: config.google_issuer
            client_id: config.google_client_id
            credential: config.google_client_secret
        }
        microsoft: {
            driver: Driver.oidc
            issuer: config.microsoft_issuer
            client_id: config.microsoft_client_id
            credential: config.microsoft_client_secret
        }
    }
}

principal {
    user: {
        subject: Librarian.email
        active: Librarian.active == true
    }
}
```

```jadpo
use desk

authentication {
    browser_session: {
        cookie: "desk_session"
        origin: config.site
    }
    sign_in: {
        providers: [google, microsoft]
    }
    on_first_sign_in: desk.link_staff
}
```

```jadpo
use desk/entities { Librarian }
use desk/failures { NotOnTheStaff }
use desk/queries { staff_member }

/// Links a Google account to the member of staff with its address, once
/// Google has verified it.
action link_staff(first: FirstSignIn) fails NotOnTheStaff -> Librarian.id {
    if (first.method != SignInMethod.google or not first.address_verified) {
        reject NotOnTheStaff {}
    }
    match first.address {
        none -> {
            reject NotOnTheStaff {}
        }
        some(address) -> {
            var librarian: Librarian = attempt staff_member(address)
            return librarian.id
        }
    }
}
```

```jadpo
use desk/entities { Librarian }
use desk/types { NewLibrarian }
use desk/actions { take_on }

test "staff sign in with Google under the address they were taken on with" {
    var ann: Librarian = attempt take_on(
        NewLibrarian {
            email: "ann@desk.example"
            name: "Ann"
        }
    )
    var first: Response = test_providers.sign_in(
        SignInMethod.google,
        TestAccount {
            subject: "110248495921238986420"
            email: "ann@desk.example"
            email_verified: true
            name: "Ann Lee"
        }
    )
    assert first.status == 303
    assert first.code == none
    var second_sign_in: Response = test_providers.sign_in(
        SignInMethod.google,
        TestAccount {
            subject: "110248495921238986420"
            email: none
            email_verified: false
            name: none
        }
    )
    assert second_sign_in.code == none
    var own_card: Response = request {
        method: HttpMethod.get
        path: "/staff/me"
        as: Principal.user(ann)
    }
    assert own_card.status == 200
}

test "an address Google hasn't verified signs no one in" {
    attempt take_on(
        NewLibrarian {
            email: "bo@desk.example"
            name: "Bo"
        }
    )
    var refused: Response = test_providers.sign_in(
        SignInMethod.google,
        TestAccount {
            subject: "2001"
            email: "bo@desk.example"
            email_verified: false
            name: none
        }
    )
    assert refused.status == 303
    assert refused.code == "sign_in_refused"
}

test "a Microsoft account isn't linked by its address" {
    attempt take_on(
        NewLibrarian {
            email: "cy@desk.example"
            name: "Cy"
        }
    )
    var refused: Response = test_providers.sign_in(
        SignInMethod.microsoft,
        TestAccount {
            subject: "AAAAAAAAAAAAAAAAAAAAAPv0"
            email: "cy@desk.example"
            email_verified: true
            name: "Cy"
        }
    )
    assert refused.code == "sign_in_refused"
}
```

### In `jadpo dev`

`jadpo dev` never reaches the provider. Its local issuer
([External tokens in tests and `jadpo dev`](/docs/ways-to-sign-in.md#external-tokens-in-tests-and-jadpo-dev))
stands in for every provider, so the provider's settings aren't needed:
any of `issuer:`, `client_id:` and `credential:` that nothing gives gets a
stand-in. `POST /auth/<name>/start` answers with the address of the
local issuer's account-chooser page, which `jadpo dev` serves on its own
port on your machine; the ready screen gives it on its `sign-in` line.

The page lists the provider's test accounts. Each is one of the
company's, when the connection has `organisation:`, and signs in with a
second factor, so the staff roles that need them can be tried. Choosing
one signs in as it, as the provider's own page would: the browser goes back to
`GET /auth/<name>/callback` with a code, and the route checks it as it
checks the provider's. **Add a test account** takes what a provider says
about a person, `TestAccount`'s fields: a subject (made up in Google's
shape until you change it), an email, whether the provider verified it
and a name; adding one signs in as it. **Cancel sign-in** answers as a
refusal at the provider does, so the callback's redirect names
`sign_in_provider_failed`. The accounts are kept in
`build/dev/accounts.json`, by provider, for the next session.

The page works from the keyboard and a screen reader alone: each account
is a button, each field has its label and its problem beside it, and the
first account, or the first field with a problem, has the focus. It
follows the system's light or dark setting, and runs no script.

The page takes only the sign-in the app's start route made: the
provider must be one of the app's, and the client ID and the return
address the app's own, or it says why and sends the browser nowhere. It
answers only requests addressed to this machine by name, and a choice
only from its own page, so another site can't sign you in. A deployable
never serves it nor accepts its codes.

Not built yet: `Driver.github` and `Driver.apple`.

## The old name

`sign_in_providers:` is refused, and the error renames it to `sign_in:`.
