# Run actions at sign-in

Run your own rules while a person signs in, to add claims to their token, deny the sign-in, or require multi-factor authentication.

An action is a rule that runs inside sign-in. Use an action to put your own facts in a token, or to decide who gets in: an action can deny a sign-in, or require multi-factor authentication (MFA).

This guide explains the kinds of action, then shows how to add claims to a token from your own server.

## Before you begin

You need:

- Access to **Settings > Actions** in the AuthFI console.
- For a webhook action, an `https` endpoint on your server that answers within the action's timeout. The default is 2 seconds.

## Add an action

In the console, open **Settings > Actions** and choose **New action**.

| Field   | Enter                                                          |
| ------- | -------------------------------------------------------------- |
| Name    | A name for the action                                          |
| Trigger | When the action runs. See [Triggers](#triggers).               |
| Kind    | What the action does. See [Kinds of action](#kinds-of-action). |
| Order   | A number. Actions on one trigger run lowest first.             |

The dialog then shows the fields for the kind you chose. Fill them in, and choose **Create (live now)**.

A new action starts enabled. The action appears in the list with its order, a switch that turns it on or off, and **Edit** and **Delete**. An action that can deny a sign-in carries the badge `can block`.

You set the trigger and the kind when you create an action. To change the rest, choose **Edit**. To remove an action, choose **Delete** and confirm with **Delete rule**.

### Triggers

An action runs at one of two points in sign-in.

| Trigger                       | Runs                                    | Can                                          |
| ----------------------------- | --------------------------------------- | -------------------------------------------- |
| At sign-in                    | When a person submits their credentials | Deny the sign-in, or require MFA             |
| Before a credential is issued | Each time a token is about to be issued | Add claims, deny the sign-in, or require MFA |

Actions on one trigger run in the order you set. The first action that denies ends the sign-in.

### Kinds of action

| Kind               | Does                                                                        | Fields                                                       |
| ------------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------ |
| **Claim rule**     | Adds a claim to the token, from the person's details or a fixed value       | Claim, Value                                                 |
| **Condition rule** | Denies, requires MFA, or sets a claim, when a condition on the person holds | CEL expression, Effect                                       |
| **Risk rule**      | Denies, or requires MFA, when the sign-in looks risky                       | Deny at score ≥, Step-up at score ≥, Required ACR (optional) |
| **Webhook**        | Asks your server for claims to add                                          | Endpoint URL, Timeout (ms), Credential header, Credential    |

For a **Condition rule**, **Effect** is one of **Require step-up (MFA)**, **Deny (block the flow)** and **Set a claim**. Depending on the effect, you also enter **Claim** and **Value**, or **Required ACR (optional)**.

For a **Risk rule**, enter at least one of the two scores.

For a **Claim rule**, an empty **Value** copies the subject.

A webhook action adds claims. To deny a sign-in, use a condition rule or a risk rule.

## Add claims from your own server

A webhook action calls your server while the token is being issued, and adds the claims you return.

<Steps>

### Add a webhook action

In **Settings > Actions**, choose **New action**. Set **Trigger** to **Before a credential is issued** and **Kind** to **Webhook**, then fill in the webhook fields.

| Field                         | Enter                                                                                |
| ----------------------------- | ------------------------------------------------------------------------------------ |
| Endpoint URL                  | The `https` URL of your endpoint                                                     |
| Timeout (ms)                  | How long AuthFI waits, in milliseconds. `2000` by default. Between `100` and `5000`. |
| Credential header, Credential | A header and a credential for your endpoint                                          |

The dialog shows the alert **Every call proves it is AuthFI**, with a snippet that verifies the call. Choose **Create (live now)**.

### Verify the call

AuthFI sends your endpoint a JSON body that names the person and what they hold.

```json
{
  "subject": "user:ada@acme.com",
  "context": {
    "subject": "user:ada@acme.com",
    "roles": ["developer"],
    "permissions": ["read:reports"]
  }
}
```

The call carries an `Authorization: Bearer` header with a token signed by your project. Verify it the way you verify any token from your project, against these values:

| Check     | Expect                                                                 |
| --------- | ---------------------------------------------------------------------- |
| Signature | Your project's keys, at `https://acme.authfi.io/.well-known/jwks.json` |
| `alg`     | `RS256`                                                                |
| `iss`     | Your project's issuer, `https://acme.authfi.io`                        |
| `sub`     | `authfi:actions`                                                       |
| `aud`     | The exact URL of your endpoint                                         |
| `exp`     | Within 60 seconds                                                      |

<Tabs labels={["Node.js", "Python"]}>

```js
import { createRemoteJWKSet, jwtVerify } from "jose";

const keys = createRemoteJWKSet(new URL("https://acme.authfi.io/.well-known/jwks.json"));

// `authorization` is the value of the Authorization header.
export async function fromAuthFI(authorization) {
  const token = (authorization ?? "").replace(/^Bearer /, "");
  const { payload } = await jwtVerify(token, keys, {
    issuer: "https://acme.authfi.io",
    subject: "authfi:actions",
    audience: process.env.ENDPOINT_URL,
    algorithms: ["RS256"],
  });
  return payload;
}
```

```python
import os, jwt
from jwt import PyJWKClient

keys = PyJWKClient("https://acme.authfi.io/.well-known/jwks.json")

# `authorization` is the value of the Authorization header.
def from_authfi(authorization: str) -> dict:
    token = authorization.removeprefix("Bearer ")
    key = keys.get_signing_key_from_jwt(token)
    claims = jwt.decode(
        token,
        key.key,
        algorithms=["RS256"],
        issuer="https://acme.authfi.io",
        audience=os.environ["ENDPOINT_URL"],
    )
    if claims.get("sub") != "authfi:actions":
        raise jwt.InvalidTokenError("not a call from AuthFI actions")
    return claims
```

</Tabs>

`ENDPOINT_URL` is the exact URL you entered in **Endpoint URL**. Both functions raise an error when a check fails, and both also check that `exp` has not passed. Replace `https://acme.authfi.io` with your project's issuer.

### Return the claims

Answer with a JSON object that holds the claims to add.

```json
{ "claims": { "plan": "enterprise", "cost_center": "R-204" } }
```

Each claim is added to the token under `custom`:

```json
{ "sub": "ada@acme.com", "custom": { "plan": "enterprise", "cost_center": "R-204" } }
```

</Steps>

## Check that it works

Sign in to an application in the project and read the token it receives. The claims your server returned appear under `custom`.

## Timeouts and failures

| Setting      | Value                                            |
| ------------ | ------------------------------------------------ |
| Timeout (ms) | 2 seconds by default. Between 0.1 and 5 seconds. |
| On failure   | Sign-in continues without the claims             |

Your server is on the path of every sign-in. Answer from memory or a fast cache.

Treat the claims as helpful rather than essential. A token issued while your server was slow does not carry them, so your application must work when `custom` is absent.

## Guidelines for claims

- **Keep claims small.** They travel in every token.
- **Do not put secrets in claims.** Whoever holds a token can read it.
- **Name claims for what they mean in your product**, not for the system they came from.

## Troubleshooting

| What you see                           | Cause                                                    | Fix                                |
| -------------------------------------- | -------------------------------------------------------- | ---------------------------------- |
| A token has no claims from your server | Your server failed, or did not answer within the timeout | Answer from memory or a fast cache |

## Next steps

<Cards>
  <Card href="/extend/webhooks" title="Receive webhooks">
    Have your project call your server after something happens.
  </Card>
  <Card href="/reference/tokens" title="Tokens and claims">
    The claims a token carries before you add your own.
  </Card>
  <Card href="/applications/sessions" title="Sessions, sign-out and MFA">
    Require MFA or a fresh sign-in from your application.
  </Card>
  <Card href="/security/keys" title="Keys and secrets">
    The keys that sign the call to your server, and how they rotate.
  </Card>
</Cards>
