# Starting and ending a session

Magic links, passkeys and providers start the session themselves
([Checking the person](checking-the-person-sign-in.md)). Jadpo has no
password sign-in.

When your app checks a person some other way, such as someone accepting
an invitation it sent, the action that checks them starts the session
with `sessions.start`. `sessions.end` signs someone out. Jadpo sets and
clears the cookie and its CSRF token on the route's response:

```jadpo
action accept_invitation(token: InvitationToken) fails InvitationExpired -> Welcome {
    var invitation: Invitation = attempt Invitation.redeem(token)
    sessions.start(invitation.member_id)
    ...
}

action sign_out() -> Unit {
    sessions.end()
}
```

`sessions.start(invitation.member_id)` starts a session for that member,
and the response of the route that ran the action sets its cookie.
`sessions.end()` ends the session the request came with.

- `sessions.start` keeps the session in the action's own transaction, in
  the `jadpo_sessions` system table of the store the principal's user
  entity lives in ([System tables](system-tables.md)). So an action
  that fails starts no session. The route's response sets the cookies once
  the action commits.
- `sessions.end` deletes the session the request came with, and the
  response clears the cookies.
- A session or token past its end, which a lookup ignores, and a service
  key past its expiry, are deleted on the tick, Jadpo's regular background
  sweep, in the store each is kept in
  ([Jobs and events](jobs-and-events.md)).
- Only an action calls them (`type.auth_sessions_context`). A test reaches
  them through the route, or calls the action, which keeps the session
  without a response to set it on.
- Only verifiers are kept: a session's token and its CSRF token are 32
  random bytes each, stored as their digests, so a copy of the database
  signs no one in.

## API tokens

Your own mobile and command-line clients sign in with an API token. An
action issues one once it has checked the person, and returns it in its
output. That is the one time anyone sees it:

```jadpo
action accept_invitation_in_app(token: InvitationToken) fails InvitationExpired -> AppSignedIn {
    var invitation: Invitation = attempt Invitation.redeem(token)
    return AppSignedIn {
        token: sessions.issue_token(invitation.member_id)
    }
}
```

`sessions.issue_token` gives an `ApiToken`, which the client keeps and
sends as `Authorization: Bearer`. With magic links, the generated
`POST /auth/magic-link/redeem` trades a code for a token instead
([Magic links](magic-links.md)).

- `sessions.issue_token(user_id)` takes the user entity's `id` and gives
  an `ApiToken`, text an `Output` can send. The token is a session without
  a cookie: kept as a verifier in `jadpo_sessions` in the action's
  transaction, lasting 30 days from when it is issued, with no CSRF token,
  since a browser never sends it by itself. The response that carries it
  is never kept for an idempotent repeat, and an `ApiToken` can't be
  stored in an entity.
- `sessions.end()` ends the token a request came with, as it ends a
  browser session.
- `sessions.tokens(user_id)` lists the user's tokens that haven't ended,
  oldest first, as `IssuedToken`s, which code reads `id`, `started_at`
  and `ends_at` from; `sessions.revoke_token(token_id)` ends one by its
  `id`, and an `id` no token has ends nothing. Together they let an
  application end a user's other devices.
- Like `sessions.start`, only an action calls them, except that a `GET`
  route can run `sessions.tokens` itself (below). Each needs `api_token:`
  in `authentication { }`.

### Listing from a `GET` route

An application's "signed-in devices" screen is one route. A `GET` runs
`sessions.tokens(user_id)`, and `Service.keys(service_id)` for a service's
keys, directly, as generated queries that read what Jadpo keeps:

```jadpo
routes Devices {
    prefix: /devices

    GET / {
        output: List<IssuedToken>
        run: sessions.tokens(current_principal.user.id)
    }
}
```

- `run:` passes the holder's `id`, from a placeholder or from
  `current_principal`, and nothing else; the route's `output:` is
  `List<IssuedToken>` (`List<IssuedKey>` for keys), exactly what the
  listing returns. Any other method is `route.run_kind`, and the writes
  (`sessions.issue_token`, `.revoke_token`, `issue_key`, `revoke_key`) are
  `route.run_invalid`: an action calls those.
- `IssuedToken` and `IssuedKey` are built-in values an `Output` can hold
  (`tokens: List<IssuedToken>`) and a route can send. On the wire each is
  an object of its `id`, `started_at` and `ends_at`, never the credential;
  the API record and OpenAPI give it a section of its own.
- The list goes through [Policy](policy.md) like any query's. It holds
  the tokens or keys of a holder whose record the caller may `read`, and is
  empty for any other.
- A route that lists a holder nobody may read is
  `policy.effect_ungranted`. One anyone may call needs `Access.public` on
  the holder's `read:`.

## Signing in, end to end

A whole app that signs members up, keeps their notes and refuses a
suspended member. To show sessions on their own, signing up here starts a
session for a handle without checking the person. A real app checks the
person with [`sign_in:`](checking-the-person-sign-in.md). The principal and
its one way in:

```jadpo
use journal/entities { Member }
use journal/types { MemberStatus }

application {}

principal {
    user: {
        subject: Member.handle
        active: Member.status == MemberStatus.active
    }
}
```

```jadpo
authentication {
    browser_session: {
        cookie: "notes_session"
    }
}
```

Signing up checks the handle, starts a session and answers with no content;
its response sets the cookies. Writing a note takes the signed-in member as
a parameter:

```jadpo
use journal/entities { Member }
use journal/types { Handle }
use journal/failures { HandleTaken }
use journal/actions { register }

/// What signing up takes.
type SignUp = Input {
    handle: Handle
}

/// Registers a member and signs them in.
action sign_up(input: SignUp) fails HandleTaken -> Unit {
    var member: Member = attempt register(input.handle)
    sessions.start(member.id)
}
```

```jadpo
/// Ends the session the request came with.
action sign_out() -> Unit {
    sessions.end()
}
```

```jadpo
use journal/entities { Note }
use journal/types { NoteText, NoteView }

/// What writing a note takes.
type WriteNote = Input {
    text: NoteText
}

/// Writes a note as the signed-in member.
action write_note(input: WriteNote, author: Principal.user) -> NoteView {
    var note: Note = Note.create {
        set: {
            author_id: author.id
            text: input.text
        }
    }
    return NoteView {
        text: note.text
    }
}
```

Signing up is the one route anyone may call; every other needs the session:

```jadpo
use journal/actions { SignUp, sign_out, sign_up }

/// Signing up and out.
routes Account {
    prefix: /account

    /// Anyone may sign up; the response sets the session's cookies.
    POST /sign-up {
        authentication: RouteAuthentication.none
        input: SignUp
        status: SuccessStatus.no_content
        run: sign_up(input)
    }

    /// Ends the session and clears its cookies.
    POST /sign-out {
        status: SuccessStatus.no_content
        run: sign_out()
    }
}
```

```jadpo
use journal/types { NoteView }
use journal/actions { WriteNote, write_note }

/// The signed-in member's notes.
routes Notes {
    prefix: /notes

    POST / {
        input: WriteNote
        output: NoteView
        status: SuccessStatus.created
        run: write_note(input, current_principal.user)
    }
}
```

A route test signs in with `as:` through the real browser session (a session
kept for the member, its cookie, its CSRF token and an `Origin`), so a
suspended member is refused exactly as a browser would be:

```jadpo
use journal/entities { Member }
use journal/actions { WriteNote, register, suspend }

test "a note needs someone signed in" {
    var response: Response = request {
        method: HttpMethod.post
        path: "/notes"
        input: WriteNote {
            text: "Buy milk"
        }
    }
    assert response.status == 401
    assert response.code == "authentication_required"
}

test "a signed-in member writes a note" {
    var member: Member = attempt register("ann")
    var response: Response = request {
        method: HttpMethod.post
        path: "/notes"
        as: Principal.user(member)
        input: WriteNote {
            text: "Buy milk"
        }
    }
    assert response.status == 201
}

test "a suspended member is refused" {
    var member: Member = attempt register("bob")
    var suspended: Member = attempt suspend(member.id)
    var response: Response = request {
        method: HttpMethod.post
        path: "/notes"
        as: Principal.user(suspended)
        input: WriteNote {
            text: "Buy milk"
        }
    }
    assert response.status == 403
    assert response.code == "user_disabled"
}
```

## Tokens and keys, end to end

The same notes, written from the members' app with an API token, and a
partner system that reads itself with a service key. Both kinds of caller
are declared, and both bearer methods:

```jadpo
use journal/entities { Member, Partner }
use journal/types { MemberStatus, PartnerStatus }

application {}

principal {
    user: {
        subject: Member.handle
        active: Member.status == MemberStatus.active
    }
    service: {
        subject: Partner.name
        active: Partner.status == PartnerStatus.active
    }
}
```

```jadpo
authentication {
    api_token: {}
    service_key: {}
}
```

```jadpo
use journal/entities { Member }
use journal/types { PartnerName, PartnerStatus }

/// Another system that reads the journal with its keys, managed by the
/// member who added it.
entity Partner {
    id: Uuid
    owner_id: Member.id {
        on_delete: OnDelete.restricted
    }
    name: PartnerName {
        unique: true
    }
    status: PartnerStatus

    policy: {
        create: [owner_id]
        read: [id, owner_id]
        update: [owner_id]
    }
}
```

Signing up issues the app's token and answers with it, once; ending
every device lists the member's tokens and revokes each:

```jadpo
use journal/entities { Member }
use journal/types { Handle }
use journal/failures { HandleTaken }
use journal/actions { register }

/// What signing up takes.
type SignUp = Input {
    handle: Handle
}

/// The app's token, which it sends back as `Authorization: Bearer`.
type SignedUp = Output {
    token: ApiToken
}

/// Registers a member and gives their app a token, shown once.
action sign_up(input: SignUp) fails HandleTaken -> SignedUp {
    var member: Member = attempt register(input.handle)
    return SignedUp {
        token: sessions.issue_token(member.id)
    }
}
```

```jadpo
/// Ends every token the member's apps hold, this one's too.
action sign_out_everywhere(member: Principal.user) -> Unit {
    for token in sessions.tokens(member.id) {
        sessions.revoke_token(token.id)
    }
}
```

Adding a partner issues its first key, which expires when the input
says; the partner reads itself with it. The member who adds a partner owns
it, so its grants let them issue and revoke its keys
([Grants](grants.md)):

```jadpo
use journal/entities { Partner }
use journal/types { PartnerName }
use journal/failures { PartnerNameTaken }
use journal/actions { create_partner }

/// What adding a partner takes.
type AddPartner = Input {
    name: PartnerName
    key_expires_at: Instant
}

/// The partner's key, which it sends as `Authorization: Bearer`.
type PartnerAdded = Output {
    key: ServiceKey
}

/// Adds a partner and gives its first key, shown once.
action add_partner(
    input: AddPartner,
    owner: Principal.user
) fails PartnerNameTaken -> PartnerAdded {
    var partner: Partner = attempt create_partner(input.name, owner)
    return PartnerAdded {
        key: Partner.issue_key(partner.id, input.key_expires_at)
    }
}
```

```jadpo
use journal/entities { Partner, PartnerNotFound }
use journal/types { PartnerView }

/// The partner a key belongs to.
query this_partner(partner: Principal.service) fails PartnerNotFound -> PartnerView {
    from: Partner
    where: Partner.id == partner.id
    missing: PartnerNotFound
}
```

```jadpo
use journal/actions { SignUp, SignedUp, sign_out, sign_out_everywhere, sign_up }

/// Signing up and out, from the app.
routes Account {
    prefix: /account

    /// Anyone may sign up; the response holds the app's token.
    POST /sign-up {
        authentication: RouteAuthentication.none
        input: SignUp
        output: SignedUp
        status: SuccessStatus.created
        run: sign_up(input)
    }

    /// Ends the token the request came with.
    POST /sign-out {
        status: SuccessStatus.no_content
        run: sign_out()
    }

    /// Ends every token the member's apps hold.
    POST /sign-out-everywhere {
        status: SuccessStatus.no_content
        run: sign_out_everywhere(current_principal.user)
    }
}
```

A member's own devices are one `GET` route, and so are another member's,
for whoever may read their record. The list is limited the way a query's
records are ([Policy](policy.md)), so it is empty for a member the
caller may not read. `Member` grants a read of itself (`read: [id]`):

```jadpo
use journal/entities { Member }

/// The devices that hold a token for a member.
routes Devices {
    prefix: /devices

    /// The signed-in member's own, oldest first.
    GET / {
        output: List<IssuedToken>
        run: sessions.tokens(current_principal.user.id)
    }

    /// Another member's: a list holds only the tokens of members the
    /// caller may read, so it is empty for anyone else's.
    GET /{member_id: Member.id} {
        output: List<IssuedToken>
        run: sessions.tokens(member_id)
    }
}
```

```jadpo
use journal/entities { Partner }
use journal/types { PartnerView }
use journal/actions { AddPartner, PartnerAdded, add_partner }
use journal/queries { this_partner }

/// Partners and their keys.
routes Partners {
    prefix: /partners

    /// A signed-in member adds a partner; the response holds its key.
    POST / {
        input: AddPartner
        output: PartnerAdded
        status: SuccessStatus.created
        run: add_partner(input, current_principal.user)
    }

    /// A partner reads itself, signed in with its key.
    GET /me {
        output: PartnerView
        run: this_partner(current_principal.service)
    }

    /// The keys a partner holds that haven't expired, for the member who
    /// owns it.
    GET /{partner_id: Partner.id}/keys {
        output: List<IssuedKey>
        run: Partner.keys(partner_id)
    }
}
```

A route test signs in with `as:` through the method the route's API
accepts: a member with an API token here, since nothing declares browser
sessions, and a partner with a key from `Partner.issue_key`, each kept for
real and sent as `Authorization: Bearer`:

```jadpo
use journal/entities { Member, Partner }
use journal/types { PartnerView }
use journal/actions { WriteNote, create_partner, register, suspend_partner }

test "a member writes a note with the app's token" {
    var member: Member = attempt register("ann")
    var response: Response = request {
        method: HttpMethod.post
        path: "/notes"
        as: Principal.user(member)
        input: WriteNote {
            text: "Buy milk"
        }
    }
    assert response.status == 201
}

test "a partner reads itself with its key" {
    var ann: Member = attempt register("ann")
    var partner: Partner = attempt create_partner("Acme", Principal.user(ann))
    var response: Response = request {
        method: HttpMethod.get
        path: "/partners/me"
        as: Principal.service(partner)
    }
    assert response.status == 200
    assert response.output == PartnerView {
        name: "Acme"
    }
}

test "a partner can't write a member's note" {
    var ann: Member = attempt register("ann")
    var partner: Partner = attempt create_partner("Acme", Principal.user(ann))
    var response: Response = request {
        method: HttpMethod.post
        path: "/notes"
        as: Principal.service(partner)
        input: WriteNote {
            text: "Buy milk"
        }
    }
    assert response.status == 403
    assert response.code == "not_permitted"
}

test "a suspended partner is refused" {
    var ann: Member = attempt register("ann")
    var partner: Partner = attempt create_partner("Acme", Principal.user(ann))
    var suspended: Partner = attempt suspend_partner(partner.id)
    var response: Response = request {
        method: HttpMethod.get
        path: "/partners/me"
        as: Principal.service(suspended)
    }
    assert response.status == 403
    assert response.code == "service_disabled"
}
```

A route test lists as each member and partner would: a signed-in caller's
request keeps a real token or key, so there is one to list, and policy
decides whose:

```jadpo
use journal/entities { Member, Partner }
use journal/actions { create_partner, register }

test "a member lists the devices that hold a token" {
    var ann: Member = attempt register("ann")
    var response: Response = request {
        method: HttpMethod.get
        path: "/devices"
        as: Principal.user(ann)
    }
    assert response.status == 200
    assert response.output != []
}

test "a member's tokens are listed to no one else" {
    var ann: Member = attempt register("ann")
    var bob: Member = attempt register("bob")
    var by_bob: Response = request {
        method: HttpMethod.get
        path: "/devices"
        as: Principal.user(bob)
    }
    assert by_bob.output != []
    var by_ann: Response = request {
        method: HttpMethod.get
        path: "/devices/{member_id}"
        params: {
            member_id: ann.id
        }
        as: Principal.user(ann)
    }
    assert by_ann.output != []
    var ann_on_bob: Response = request {
        method: HttpMethod.get
        path: "/devices/{member_id}"
        params: {
            member_id: bob.id
        }
        as: Principal.user(ann)
    }
    assert ann_on_bob.status == 200
    assert ann_on_bob.output == []
}

test "a partner's keys are listed to the partner and its owner" {
    var ann: Member = attempt register("ann")
    var bob: Member = attempt register("bob")
    var partner: Partner = attempt create_partner("Acme", Principal.user(ann))
    var by_bob: Response = request {
        method: HttpMethod.get
        path: "/devices"
        as: Principal.user(bob)
    }
    assert by_bob.output != []
    var by_partner: Response = request {
        method: HttpMethod.get
        path: "/partners/{partner_id}/keys"
        params: {
            partner_id: partner.id
        }
        as: Principal.service(partner)
    }
    assert by_partner.output != []
    var by_owner: Response = request {
        method: HttpMethod.get
        path: "/partners/{partner_id}/keys"
        params: {
            partner_id: partner.id
        }
        as: Principal.user(ann)
    }
    assert by_owner.output != []
    var by_member: Response = request {
        method: HttpMethod.get
        path: "/partners/{partner_id}/keys"
        params: {
            partner_id: partner.id
        }
        as: Principal.user(bob)
    }
    assert by_member.output == []
}
```
