# Routes: signed in by default

Every route needs a signed-in caller unless it says otherwise. You don't
write anything for that. A request without a valid session or token is
refused with a 401 before your code runs.

A route changes this with an `authentication:` line, which takes one of two
values.

## Open to anyone: `RouteAuthentication.none`

Use it for routes people call before they have an account, such as a health
check or a sign-up form.

```jadpo
GET /health/live {
    authentication: RouteAuthentication.none
    output: Health
    run: live()
}
```

The route doesn't look for a session or token, so it never knows who is
calling. The compiler stops anything the route runs from reading the caller
(`type.auth_public_principal`).

Only the sign-in check is skipped. Input is still validated and limits still
count. [Policy](/docs/policy.md) still decides whether the route's action may
run for someone who isn't signed in. A route can be open to anyone and still
be refused by policy.

## Check the caller again: `RouteAuthentication.fresh`

Use it where acting for someone who was signed out a moment ago would do
harm, such as disabling an account, changing permissions, billing or exports.

```jadpo
DELETE /users/{user_id} {
    authentication: RouteAuthentication.fresh
    run: disable_user(user_id, current_principal.user)
}
```

`fresh` only changes anything if your app sets a `revocation_delay`. By
default, Jadpo looks up the caller's record on every request, so a session
you revoke stops working at once. With a delay, for example
`revocation_delay: 5m`, sessions are trusted for up to five minutes without
that lookup, so a disabled user may keep working until then (see
[The application and revocation](/docs/the-application-and-revocation.md)). A
`fresh` route always does the lookup, whatever the delay.

## Any other value

Any other `authentication:` value is a compile error. The error offers both
fixes rather than guessing, because one keeps the route protected and the
other opens it to anyone.

## Confirm one request with a passkey: `confirm: Confirmation.passkey`

Use it where a stolen session or token must not be enough, such as
releasing a package, moving money or deleting an account. The signed-in
person approves this one request with one of their own passkeys.

```jadpo
use payouts/entities { Payout }
use payouts/actions { send_payout }

routes Payouts {
    prefix: /payouts

    POST /{payout_id: Payout.id}/send {
        confirm: Confirmation.passkey
        status: SuccessStatus.no_content
        run: send_payout(payout_id)
    }
}
```

A client confirms a request in three steps, around the browser's own
prompt. It first sends the same request to the route's generated
`<path>/confirmation`, with the same method, placeholders, query or body
and headers (a `GET`'s goes as a `POST`, since asking keeps a challenge),
for the options to give `navigator.credentials.get()`
(`PublicKeyCredential.parseRequestOptionsFromJSON` reads what `publicKey`
holds):

```text
POST /payouts/0192d3c4-.../send/confirmation

200 OK
{ "publicKey": { "challenge": "Vh3q...", "rpId": "payouts.example", "allowCredentials": [{ "type": "public-key", "id": "AQID..." }], "userVerification": "required", "timeout": 120000 } }
```

The browser has the person present a passkey, and the request itself
carries the device's answer in `X-Jadpo-Confirmation`, the JSON WebAuthn's
`toJSON()` writes, in base64 (`btoa(JSON.stringify(credential))`):

```text
POST /payouts/0192d3c4-.../send
X-Jadpo-Confirmation: eyJpZCI6IkFRSUQi...

204 No Content
```

- **The challenge is bound to the request.** The runtime reads the
  options request as the route itself would, so input the route would
  refuse is refused here, before anyone is asked for a passkey. It binds
  the challenge to the signed-in person and a digest of the route and
  that input, the one an `Idempotency-Key` is checked against. So the
  same input written differently, with its members in another order, is
  the same request.
- **The options list only the person's passkeys.** A person with none is
  refused with 403 `confirmation_required` rather than given options any
  passkey could answer, so a frontend sends them to add one.
- **It is checked before anything runs**: the passkey must be one the
  signed-in person registered, the answer must show the person was
  verified (a fingerprint, a face or a PIN), and the challenge must be
  unused, under two minutes old and match this request's input. Anything
  else is refused with 403 `confirmation_required`, and nothing runs. The
  answer is checked as a passkey's sign-in is
  ([Signing in](/docs/passkeys.md#signing-in)): the origin, the relying party, the
  signature and the counter. On success the passkey's counter, backup
  state and last use are written.
- **Each challenge works once**, so an answer can't be replayed, and one
  made for one request can't approve another. It is used up when the
  answer arrives, whether or not the answer checks. A new options request
  for the same person, route and input replaces the earlier one.
- **The two minutes are fixed.** `passkey:`'s `expires_after:` doesn't
  change them.
- **A retry confirms again.** An `Idempotency-Key` is checked after the
  confirmation, so a client repeating a request with its key presents a
  passkey again and gets the first response. A confirmation can't be
  skipped by repeating a key.
- **It needs a person signed in.** `confirm:` on a route with
  `authentication: RouteAuthentication.none`, or one that passes on
  `current_principal.service`, is `route.confirm_invalid`. Any credential
  of a signed-in person can carry an answer, since the answer is what
  proves it's them; a route that also needs their record read now adds
  `authentication: RouteAuthentication.fresh`.
- **It needs `passkey:`** in `sign_in:` (`semantic.auth_setting_required`),
  and a route that needs it says so in its API's record (`confirm passkey`)
  and OpenAPI (`x-jadpo-confirm`, with the header and the 403). The
  generated route is listed beside it in both and in `jadpo routes`, marked
  generated. A route your app writes at that path is `route.path_overlap`,
  as is a confirmed `GET` beside a confirmed `POST`, which would share one.
- **Taking it off needs a person.** A route that stops asking for a
  confirmation needs an acceptance, `PolicyChange.confirmation_removed`
  ([Records](/docs/records-and-manifest.md)), since a stolen session is then
  enough again.
- **In route tests**, `test_passkeys.confirm(passkey)` answers the next
  confirmation with that test passkey
  ([Passkeys in tests](/docs/in-development-and-tests.md#passkeys-in-tests)).

A route test confirms the next request with a test passkey. Without it,
or with another person's, nothing runs:

```jadpo
use payouts/entities { Payout }
use payouts/fixtures { drafted_by, treasurer_called }

test "a payout goes out once its treasurer confirms it with a passkey" {
    var ada: Principal.user = treasurer_called("ada@example.com")
    var phone: TestPasskey = test_passkeys.register(ada, "Phone")
    var payout: Payout = drafted_by(ada)
    test_passkeys.confirm(phone)
    var response: Response = request {
        method: HttpMethod.post
        path: "/payouts/{payout_id}/send"
        params: {
            payout_id: payout.id
        }
        as: ada
    }
    assert response.status == 204
}

test "a stolen session alone sends nothing" {
    var ada: Principal.user = treasurer_called("ada@example.com")
    var phone: TestPasskey = test_passkeys.register(ada, "Phone")
    var payout: Payout = drafted_by(ada)
    var response: Response = request {
        method: HttpMethod.post
        path: "/payouts/{payout_id}/send"
        params: {
            payout_id: payout.id
        }
        as: ada
    }
    assert response.status == 403
    assert response.code == "confirmation_required"
    var signed_in: Response = test_passkeys.sign_in(phone)
    assert signed_in.status == 204
}

test "another person's passkey confirms nothing" {
    var ada: Principal.user = treasurer_called("ada@example.com")
    var mallory: Principal.user = treasurer_called("mallory@example.com")
    var ada_phone: TestPasskey = test_passkeys.register(ada, "Phone")
    var mallory_phone: TestPasskey = test_passkeys.register(mallory, "Phone")
    var payout: Payout = drafted_by(ada)
    test_passkeys.confirm(mallory_phone)
    var response: Response = request {
        method: HttpMethod.post
        path: "/payouts/{payout_id}/send"
        params: {
            payout_id: payout.id
        }
        as: ada
    }
    assert response.status == 403
    assert response.code == "confirmation_required"
    var signed_in: Response = test_passkeys.sign_in(ada_phone)
    assert signed_in.status == 204
}
```

The API record says the route asks for a passkey, and lists the route Jadpo
answers beside it:

```text
POST /payouts/{payout_id: Uuid}/send
    confirm passkey
    status 204
    fails payout_not_found

POST /payouts/{payout_id: Uuid}/send/confirmation
    generated confirmation
    caller user
    output PasskeyRequestOptions
    status 200
```

## Apps with more than one API

An app can serve several APIs, for example `main` for its own clients and
`partners` for other companies' servers, each under its own path prefix
([APIs](/docs/http.md)). Each API lists the ways a caller may prove who they
are, and every route in it accepts those:

```jadpo
// app.jadpo
apis: {
    main: {
        prefix: /
        authentication: [AuthenticationMethod.browser_session, AuthenticationMethod.api_token]
    }
    partners: {
        prefix: /partners
        authentication: [AuthenticationMethod.service_key]
    }
}
```

Here a browser session or an API token works on any route under `/`, and
only a service key works under `/partners`. A route can narrow its API's
list but never add to it. An API with no `authentication:` line accepts every
method the application declares, and one that says `authentication: []`
accepts none: every route in it is open to anyone, and says so
([Serving the document](/docs/openapi.md#serving-the-document)).

Webhooks from a provider such as Stripe aren't routes you write, so there's
nothing here to set. Jadpo generates them, checks each one's signature and
passes it to your app as an event ([External services](/docs/external-services.md)).
