# Authenticated flows

Sign in with a password and a one-time code, save the session, and let the next job start already signed in.

This guide builds a program that signs in to a site protected by a password and a one-time code:
the credentials come from secret inputs, the job parks to ask you for the code, and the signed-in
state is saved as a session. The next job with that session skips the login entirely, so you are
asked for a code once, not on every run.

## When to use it

Use this pattern when **you** (your backend, your script, an operator) can supply the one-time
code: you read it from an authenticator, a shared mailbox, or an SMS gateway, and answer through
the API or the SDK.

When the code arrives on **someone else's** phone, such as your customer's, do not relay it
through your own systems. Declare the input as hosted instead and send that person a link to a
page where they type it themselves: see [Hosted resolve pages](https://docs.kaab.ist/guides/hosted-resolve). The
program is the same apart from one key.

If a site needs no second factor, you still want the session part of this guide: sign in once,
save the session, and stop sending the password on every job.

## The building blocks

### Secret inputs

Declare the password as a `secret` input. Its value is given per job and never appears in the
job's `meta`, in logs, in snapshots, in screenshots or in webhooks; a field the value was filled
into is redacted.

```json
"inputs": {
  "email": { "type": "string", "default": "dev@kaab.ist" },
  "password": { "type": "secret" }
}
```

A secret can be filled into a field, and nothing else: lint refuses a program that reads a secret
in an `extract` or renders it into a URL ([E013](https://docs.kaab.ist/reference/dsl/lint/e013)).

### TypeScript

```ts
// `password` is declared `{ "type": "secret" }` in the program: it is encrypted at rest, never
// returned, and redacted from logs, step records and snapshots.
const job = await kaab.jobs.create({
  program: name,
  inputs: {
    base_url: `${site}`,
    password: process.env.LOGIN_PASSWORD ?? "secret",
  },
});
```

### Python

```python
# password is declared {"type": "secret"} in the program: encrypted at rest, never returned,
# and redacted from logs, step records and snapshots.
password = os.environ.get("LOGIN_PASSWORD", "secret")
job = kaab.jobs.create(program=name, inputs={"base_url": f"{site}", "password": password})
```

### Go

```go
// password is declared {"type": "secret"} in the program: encrypted at rest, never returned,
// and redacted from logs, step records and snapshots.
password := os.Getenv("LOGIN_PASSWORD")
if password == "" {
	password = "secret"
}
body, err := kaab.Body[client.CreateJobJSONBody](map[string]any{
	"program": name,
	"inputs":  map[string]any{"base_url": site + "", "password": password},
})
if err != nil {
	log.Fatal(err)
}
job, err := c.Jobs.Create(ctx, body)
if err != nil {
	log.Fatal(err)
}
```

### Parking for the code

An `await_input` step stops the job and asks for a named value:

```json
{ "await_input": { "name": "otp", "prompt": "The code from your authenticator", "type": "string",
  "pattern": "^[0-9]{6}$", "ttl": "5m", "expires": "24h", "collect": "api" } }
```

The job's status becomes `awaiting_input`, the `job.awaiting_input` event is sent, and the
envelope carries an `input_request` with the name, the prompt, the pattern, a screenshot of the
page and two deadlines:

- `ttl` (default 5 minutes, at most 15) is how long the browser stays open on the page. Answer
  inside it and the run carries on in that same page.
- `expires` (at most 7 days) is how long the job waits for the value at all. Answer after `ttl`
  but before `expires` and the job restores its last checkpoint and continues; past `expires` the
  answer is refused with `410 input_expired`.

The value you send lands in `vars.otp`, which the next step fills in with `{{vars.otp}}`.

### Answering

Post the value to `POST /v1/jobs/{id}/input`, keyed by the input's name:

```json
{ "otp": "482913" }
```

The job answers `200` and is `running` again, or `queued` when its browser was already released.
`409 job_not_waiting` means the job is not parked on an input any more.

With an SDK, `jobs.wait` does this for you: give it an `onInput` handler, and whenever the job parks
on an input, the handler's return value is posted and the wait continues to the end.

### TypeScript

```ts
await kaab.programs.push({ ...definition, name }, { publish: true });

// `password` is declared `{ "type": "secret" }` in the program: it is encrypted at rest, never
// returned, and redacted from logs, step records and snapshots.
const job = await kaab.jobs.create({
  program: name,
  inputs: {
    base_url: `${site}`,
    password: process.env.LOGIN_PASSWORD ?? "secret",
  },
});

// The job parks at the code; `onInput` answers it and the wait goes on.
const done = await kaab.jobs.wait(job.id, {
  timeoutMs: 300_000,
  onInput: async (request) => {
    console.log(`${job.id} asks for ${request.name}`);
    return askForCode(request.prompt ?? request.name);
  },
});
console.log(`${done.id}: ${done.status}; session saved: ${done.meta.session?.saved === true}`);
```

### Python

```python
# password is declared {"type": "secret"} in the program: encrypted at rest, never returned,
# and redacted from logs, step records and snapshots.
password = os.environ.get("LOGIN_PASSWORD", "secret")
job = kaab.jobs.create(program=name, inputs={"base_url": f"{site}", "password": password})
# The job parks at the code; on_input answers it and the wait goes on.
done = kaab.jobs.wait(job.id, timeout=300, on_input=ask_for_code)
```

### Go

```go
name := fmt.Sprintf("login-2fa-example-%x", time.Now().UnixMilli())
publish := true
if _, err := c.Programs.Push(ctx, definition, kaab.PushOptions{Name: name, Publish: &publish}); err != nil {
	log.Fatal(err)
}
defer func() { _ = c.Programs.Delete(ctx, name) }()

// password is declared {"type": "secret"} in the program: encrypted at rest, never returned,
// and redacted from logs, step records and snapshots.
password := os.Getenv("LOGIN_PASSWORD")
if password == "" {
	password = "secret"
}
body, err := kaab.Body[client.CreateJobJSONBody](map[string]any{
	"program": name,
	"inputs":  map[string]any{"base_url": site + "", "password": password},
})
if err != nil {
	log.Fatal(err)
}
job, err := c.Jobs.Create(ctx, body)
if err != nil {
	log.Fatal(err)
}
// The job parks at the code; OnInput answers it and the wait goes on.
done, err := c.Jobs.Wait(ctx, job.ID, kaab.WaitOptions{Timeout: 5 * time.Minute, OnInput: askForCode})
if err != nil {
	log.Fatal(err)
}
if done.Status != "succeeded" {
	log.Fatalf("job %s is %s", done.ID, done.Status)
}
fmt.Printf("%s: signed in\n", done.ID)
```

### Saving the session

A `checkpoint` with `save_session` writes the browser's signed-in state to a named
[session](https://docs.kaab.ist/concepts/sessions):

```json
{ "checkpoint": { "name": "logged_in", "save_session": "portal-login" } }
```

Bind the same session in the program's options and let the program start after that checkpoint
whenever the session is still valid:

```json
"options": { "max_tier": "browser", "session": "portal-login", "resume_from_session": true }
```

Before each job, Kaabist checks the session against the program's `logged_in` and `login_page`
landmarks. If the session is valid, the job starts at the step after the checkpoint, and the login
steps, the `await_input` included, never run. If it has expired, the program runs from the top and
signs in again; `policies.on_session_expired` (default `refresh`) decides what happens when a
session expires in the middle of a run.

## Worked example

Take a customer portal at `https://portal.example.com` that asks for an e-mail address and a
password, then a six-digit code, then shows a report of 40 rows. The program below signs in, asks for the code only when the code form is
actually shown, saves the session, and reads the report:

```json
{
  "kaab": "1",
  "name": "portal-report",
  "inputs": {
    "base_url": { "type": "string", "default": "https://portal.example.com" },
    "email": { "type": "string", "default": "dev@kaab.ist" },
    "password": { "type": "secret" },
    "report_id": { "type": "string", "default": "1", "pattern": "^[0-9]+$" }
  },
  "options": { "max_tier": "browser", "session": "portal-login", "resume_from_session": true },
  "landmarks": {
    "login_page": { "visible": "form[action$='/login']" },
    "logged_in": { "url": "**/dashboard" }
  },
  "steps": [
    { "id": "open", "goto": "{{inputs.base_url}}/login", "expect": { "landmark": "login_page" } },
    { "id": "email", "fill": "input[name=email]", "value": "{{inputs.email}}" },
    { "id": "password", "fill": "input[name=password]", "value": "{{inputs.password}}" },
    {
      "id": "submit", "click": "form button[type=submit]", "side_effect": true,
      "expect": { "any": [{ "visible": "input[name=code]" }, { "landmark": "logged_in" }] }
    },
    {
      "id": "second-factor",
      "when": { "visible": "input[name=code]" },
      "do": [
        { "id": "ask-otp", "await_input": { "name": "otp", "prompt": "The code from your authenticator", "type": "string",
          "pattern": "^[0-9]{6}$", "ttl": "5m", "expires": "24h", "collect": "api" } },
        { "id": "fill-otp", "fill": "input[name=code]", "value": "{{vars.otp}}" },
        { "id": "verify", "click": "form button[type=submit]", "side_effect": true,
          "expect": { "landmark": "logged_in" } }
      ]
    },
    { "id": "save", "checkpoint": { "name": "logged_in", "save_session": "portal-login" } },
    {
      "id": "report", "goto": "{{inputs.base_url}}/reports/{{inputs.report_id}}",
      "expect": { "count": { "target": "#report tbody tr", "min": 40 } }
    },
    {
      "id": "rows",
      "extract": {
        "rows": {
          "selector": "#report tbody tr", "many": true, "min_items": 40,
          "fields": {
            "date": { "selector": "td:nth-child(1)", "type": "date", "date_format": "dd.MM.yyyy" },
            "description": "td:nth-child(2)",
            "amount": { "selector": "td:nth-child(3)", "type": "money", "locale": "tr", "currency": "TRY" }
          }
        }
      }
    }
  ],
  "policies": { "on_session_expired": "refresh", "screenshot": "on_failure" }
}
```

Run it with the account's password:

1. Create the job with `inputs: { "password": "<the account password>" }`. It runs the login and parks
   `awaiting_input` with `input_request.name` set to `otp`.
2. Post the code to `POST /v1/jobs/{id}/input`, or return it from the `onInput` handler as in the
   snippet above, for example `{ "otp": "482913" }`.
3. The job verifies the code, saves the session `portal-login` and reads the report. The envelope
   shows `status: "succeeded"`, 40 rows in `data.rows` and `meta.session.saved: true`.
4. Create a second job with the same inputs. Its session is valid, so it starts after the
   checkpoint: `meta.steps` has no `await_input` step, and nobody is asked for a code.

## Pitfalls

- **Clicks that sign in are side effects.** Mark the submit buttons `side_effect: true`, as above,
  so that a retry never presses them twice; see [side-effect safety](https://docs.kaab.ist/guides/side-effect-safety).
- **A second factor that only sometimes appears.** Wrap the `await_input` in a `when`, as the worked
  example does. An unconditional `await_input` asks for a code even when the site trusted the
  device and skipped it.
- **One session, one job at a time.** A session is exclusive by default: a second job that binds it
  waits until the first one ends. Run parallel work under different sessions.
- **The browser window is short.** `ttl` is at most 15 minutes. A late answer still works, until
  `expires`, but costs a few seconds while the job restores its checkpoint.
- **Login programs are rate limited and gated.** A program that fills a password field or a secret
  input runs at most 60 times an hour per organisation (`429 login_flow_rate_limited`), and needs a
  verified payment method on your organisation. Reusing a session keeps you well under the limit.
- **Never put a password in `default`.** A default is stored with the program version; a secret is
  given per job.

## Reference

- [`await_input`](https://docs.kaab.ist/reference/dsl/actions/await-input), [`checkpoint`](https://docs.kaab.ist/reference/dsl/actions/checkpoint),
  [`when`](https://docs.kaab.ist/reference/dsl/actions/when)
- [Inputs](https://docs.kaab.ist/reference/dsl/inputs) and [options](https://docs.kaab.ist/reference/dsl/options) (`session`,
  `resume_from_session`), [policies](https://docs.kaab.ist/reference/dsl/policies) (`on_session_expired`)
- [Submit a job's input](https://docs.kaab.ist/reference/api/jobs/submit-job-input), [get a job](https://docs.kaab.ist/reference/api/jobs/get-job)
- [create a session](https://docs.kaab.ist/reference/api/sessions/create-session) and the [session schema](https://docs.kaab.ist/reference/api/schemas/session)
- [`kaab jobs input`](https://docs.kaab.ist/reference/cli/kaab-jobs-input)
- Concepts: [sessions](https://docs.kaab.ist/concepts/sessions), [human in the loop](https://docs.kaab.ist/concepts/human-in-the-loop)
