# Manage sessions, sign-out and MFA

Keep a person signed in with refresh tokens, sign them out, and require a fresh sign-in or multi-factor authentication (MFA).

When a person signs in, your project keeps a _session_ for them. Your application never sees the session directly. It sees the tokens issued from it, and each of them names the session in the `sid` claim.

Use this guide to control that session from your application: keep the person signed in, sign them out, and require a fresh sign-in or multi-factor authentication (MFA) before a sensitive action.

## Before you begin

You need:

- An application that signs people in with the authorization code flow. To build one, follow [Add sign-in to an app](/quickstart/sign-in).
- The application's client ID, and its client secret if it has one.
- Your project's issuer, the project's URL. This guide uses `https://acme.authfi.io`.

## Keep a person signed in

An access token lasts 15 minutes by default. To keep a person signed in for longer, exchange the refresh token for new tokens when the access token expires.

AuthFI issues a refresh token to every application that holds a client secret. An application without a client secret receives one when the project allows it, which it does by default. The setting is **Refresh tokens for browser apps**, in **Settings > Sessions** in the console.

<Endpoint method="POST" path="/oauth/token" />

```bash
curl https://acme.authfi.io/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN" \
  -d client_id="$CLIENT_ID"
```

`$REFRESH_TOKEN` is the most recent `refresh_token` your application received. `$CLIENT_ID` is the application's client ID. An application with a client secret also sends it, as HTTP Basic authentication or as `client_secret` in the body.

| Parameter       | Value                                           |
| --------------- | ----------------------------------------------- |
| `grant_type`    | `refresh_token`                                 |
| `refresh_token` | The most recent refresh token                   |
| `scope`         | Optional. A subset of the scopes first granted. |

AuthFI returns new tokens, including a new refresh token:

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6Ii4uLiJ9...",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "...",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9...",
  "scope": "openid profile email"
}
```

Follow these rules when you refresh:

- **Use each refresh token once.** Every use returns a new refresh token. Store the new one and discard the old one.
- **Ask for the same scopes, or fewer.** `scope` on a refresh can be a subset of what was first granted, and never more.
- **Read the sign-in claims as those of the original sign-in.** `sid`, `amr` and `auth_time` in the new tokens are unchanged. Refreshing does not make a sign-in look newer.

<Warning title="A reused refresh token ends the refresh chain">
  If a refresh token that was already used is presented again, AuthFI revokes every refresh token descended from the
  same sign-in and answers `invalid_grant`. Access tokens already issued expire at the end of their lifetime, 15 minutes
  by default. This is what stops a stolen refresh token.
</Warning>

## Sign a person out

To sign a person out, send the browser to the project's end-session endpoint.

<Endpoint method="GET" path="/oauth/logout" />

```text
https://acme.authfi.io/oauth/logout?client_id=CLIENT_ID&post_logout_redirect_uri=https%3A%2F%2Fapp.acme.com%2F&state=abc
```

| Parameter                  | Meaning                                                  |
| -------------------------- | -------------------------------------------------------- |
| `client_id`                | Your application. Needed for the person to be sent back. |
| `post_logout_redirect_uri` | Where to send the person afterwards                      |
| `state`                    | Returned to you unchanged                                |

AuthFI ends the session and removes its cookie. What the person sees next depends on the request:

- When `client_id` is given and `post_logout_redirect_uri` has the same scheme and host as one of the application's registered redirect URIs, the person is sent back to it.
- Otherwise, the person sees a page that says they are signed out.

Signing out ends the session. To also stop the refresh token your application holds, revoke it:

<Endpoint method="POST" path="/oauth/token/revoke" />

```bash
curl https://acme.authfi.io/oauth/token/revoke \
  -d token="$REFRESH_TOKEN" \
  -d client_id="$CLIENT_ID"
```

The response is `200` with an empty body. See [Revoke a token](/reference/oauth#revoke-a-token).

A session also ends without your application, when it reaches its total length or its idle time, or when the person's account is removed. See [Session and token lifetimes](#session-and-token-lifetimes).

### End a session from the console

An administrator can end sessions in two places:

- **One or more sessions.** Open **Security > Sessions**, select the rows, and choose **End session**. Confirm with the **End session** button, which names how many sessions it ends.
- **Every session of one person.** Open **Identity > Users**, open the person's page, and choose **Sign out everywhere**. The person can sign in again.

## Require a fresh sign-in

To make a person sign in again, add one of these parameters to the authorization request.

| Parameter      | Effect                                                                                  |
| -------------- | --------------------------------------------------------------------------------------- |
| `prompt=login` | The person signs in again, even with a session                                          |
| `max_age=600`  | The person signs in again if they last did so more than 600 seconds ago. `0` means now. |
| `login_hint`   | Fills in the account name on the sign-in page                                           |

For example, this request makes the person sign in again and fills in their account name:

```bash
echo "https://acme.authfi.io/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.acme.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256&prompt=login&login_hint=ada%40acme.com"
```

`$STATE` and `$CHALLENGE` are created as in [Add sign-in to an app](/quickstart/sign-in). The browser returns to your redirect URI with a `code`, as for any sign-in.

The `auth_time` claim in the ID token tells you when the person last signed in. When you depend on `max_age`, check `auth_time` yourself.

## Require MFA

To require multi-factor authentication (MFA) or a passkey, add `acr_values` to the authorization request. `acr` stands for _authentication context class reference_: a name for how strongly the person signed in.

```bash
echo "https://acme.authfi.io/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.acme.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256&acr_values=urn%3Aauthfi%3Aacr%3Amfa"
```

| `acr_values`                 | The person must have             |
| ---------------------------- | -------------------------------- |
| `urn:authfi:acr:1fa`         | Signed in                        |
| `urn:authfi:acr:mfa`         | Completed MFA                    |
| `urn:authfi:acr:passkey`     | Signed in with a passkey         |
| `urn:authfi:acr:passkey_mfa` | Used a passkey and completed MFA |

Several values, separated by spaces, mean "any of these, or stronger".

If the level you asked for cannot be reached, the browser returns to your redirect URI with `error=unmet_authentication_requirements`.

<Note title="Check acr yourself">
  Asking for a level is not the same as getting it. Before a sensitive action, read `acr` from the token and deny the
  action if the level is weaker than you need.
</Note>

### Require MFA from the console

`acr_values` requires MFA for one sign-in to one application. An administrator can also require it in the console, where MFA is labelled **Second factor**:

- **For everyone in the project.** Open **Settings > Sign-in**. Under **What they have to prove**, set **Second factor** to **Required (at every sign-in)**. The other choice is **Optional (only under risk or a rule)**. Then choose **Review and apply**, and apply the change.
- **For one person.** Open **Identity > Users**, open the person's page, and choose **Require MFA** in the **More actions** menu. The person enrols a factor at their next sign-in.

## Check that it works

Send a person through an authorization request with `prompt=login` and `acr_values=urn:authfi:acr:mfa`, then exchange the code for tokens. In the new tokens:

- `auth_time` is the time of the sign-in you just completed.
- `acr` is `urn:authfi:acr:mfa` or a stronger level.
- `amr` lists the methods the person used.

Then exchange the refresh token. The response contains a new `refresh_token`, and `auth_time` is unchanged.

## Session and token lifetimes

A session has two limits. Whichever is reached first ends it.

| Limit        | Default  | Ends the session when                          |
| ------------ | -------- | ---------------------------------------------- |
| Total length | 24 hours | The time has passed, however active the person |
| Idle time    | 8 hours  | The session was not used in that time          |

Tokens have their own lifetimes.

| Token         | Default lifetime             |
| ------------- | ---------------------------- |
| Access token  | 15 minutes                   |
| ID token      | The same as the access token |
| Refresh token | 30 days                      |

### Change the lifetimes

1. In the console, open **Settings > Sessions**.
2. Under **How long they stay signed in**, enter a value in seconds.
3. Choose **Review and apply**, and apply the changes.

| Field             | Sets                            |
| ----------------- | ------------------------------- |
| **Access token**  | The lifetime of an access token |
| **Refresh token** | The lifetime of a refresh token |
| **Session**       | The total length of a session   |
| **Idle timeout**  | The idle time of a session      |

A shorter lifetime applies to what is issued next. It does not revoke anything already issued.

## Sign-in strength claims

The tokens record what happened at sign-in.

| Claim | Meaning                                                                   |
| ----- | ------------------------------------------------------------------------- |
| `acr` | The level reached, one of the `acr_values` in [Require MFA](#require-mfa) |
| `amr` | How the person signed in: a list of the methods below                     |

| `amr` value | Method                                               |
| ----------- | ---------------------------------------------------- |
| `pwd`       | A password                                           |
| `otp`       | A one-time code                                      |
| `mfa`       | More than one method                                 |
| `hwk`       | A key held in hardware, such as a passkey            |
| `user`      | The passkey verified the person (a PIN or biometric) |
| `fed`       | Signed in through another identity provider          |

## Sign-in protections

These protections apply on the sign-in page of every project. Your application does nothing to turn them on.

| Protection         | Applies when                                                              | Result                                                         |
| ------------------ | ------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Lockout            | 5 failed attempts in 30 minutes, for one account from one IP address      | Further attempts are rejected                                  |
| Breached passwords | The password is found in a known breach                                   | The password is rejected, and the person is asked to change it |
| Expired passwords  | The password is past its age limit                                        | The person must change it before sign-in completes             |
| Rate limit         | More than 300 requests a minute from one IP address, across all endpoints | `429`                                                          |

A breached password is rejected even when it is the correct one.

To change the lockout numbers, open **Settings > Sign-in** in the console. Under **Failed sign-ins**, **Lock out after** is the number of failed attempts, and **Lockout lasts** is the time in seconds. To change when a password expires, open **Settings > Passwords** and set **Expires after**, in days. In both places, choose **Review and apply**, and apply the changes.

## Troubleshooting

| What you see                                                       | Cause                                                                                          | Fix                                                                                   |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `invalid_grant` on a refresh                                       | A refresh token that was already used was presented again                                      | Store the new refresh token from every response, and send the person to sign in again |
| `invalid_scope` on a refresh                                       | The refresh asked for more than was first granted                                              | Send the scopes first granted, or a subset                                            |
| A signed-out page instead of a return to your application          | `client_id` is missing, or `post_logout_redirect_uri` does not match a registered redirect URI | Send `client_id`, and a URI with the scheme and host of a registered redirect URI     |
| The browser returns with `error=unmet_authentication_requirements` | The `acr_values` you asked for could not be reached                                            | Have the person enrol in MFA, or ask for a lower level                                |
| `429`                                                              | More than 300 requests a minute came from one IP address                                       | Send fewer requests                                                                   |

Every error is listed in the [endpoint reference](/reference/oauth#errors).

## Next steps

<Cards>
  <Card href="/applications/types" title="Application types and redirect URIs">
    Which application types hold a client secret, and how redirect URIs are matched.
  </Card>
  <Card href="/reference/tokens" title="Tokens and claims">
    Every claim in an ID token and an access token.
  </Card>
  <Card href="/reference/oauth" title="OAuth and OpenID Connect endpoints">
    Every parameter of the authorization, token, revocation and end-session endpoints.
  </Card>
  <Card href="/connections/sign-in-with" title="Add sign-in with another identity provider">
    Let people sign in with an account they already have.
  </Card>
</Cards>
