# Sign In With Steps: Login, One-Time Code, Token and Refresh

> Some APIs sign you in with a sequence of calls they designed themselves. A typical example: post a username and password, receive a one-time code by SMS or email, post the code back, and receive a bearer token plus a refresh token.

Source: https://totalshiftleft.ai/help-center/product-documentation/sign-in-with-steps

> **Applies to:** All editions · Web app and Desktop app · Access to the project's settings

## Overview

Some APIs sign you in with a sequence of calls they designed themselves. A typical example: post a username and password, receive a one-time code by SMS or email, post the code back, and receive a bearer token plus a refresh token. Later, a refresh call of the API's own design renews the token. No single-request method, and not standard OAuth 2.0, can do that.

The **Sign in with steps** method in Shift-Left Studio can. You describe each request once. You type the one-time code **once per sign-in**, not on every run. After that, the server keeps the sign-in alive with your refresh request, so scheduled runs, packs, CI runs and the local runner (Agent) keep working until the API's refresh token itself runs out.

## Key concepts

| Term | Meaning |
|---|---|
| Step | One request in the sign-in, in order. A sign-in can have up to 5 steps. |
| Keep from the response | A value a step saves for later steps, such as a verification token. |
| Code step | The one step that waits for a person to type a one-time code. |
| Template | A placeholder in a URL or body, filled in when the step runs. |
| Grant | The result of a sign-in: the token, the refresh token, and their expiry times. The server holds it. |
| Machine flow | A sign-in with no code step (or with a fixed test code). Runs sign in by themselves. |

### Templates you can use

| Template | Filled with | Where it can be used |
|---|---|---|
| `{{credentials.username}}`, `{{credentials.password}}` | The profile's **Username** and **Password** | Any step |
| `{{step.<name>}}` | A value an earlier step kept | Later steps only |
| `{{input.<name>}}` | The code a person types | The code step only |
| `{{grant.accessToken}}`, `{{grant.refreshToken}}` | The current tokens | The refresh request only |
| `{{MY_VARIABLE}}` | A project variable | Anywhere |

## When to use it

| Your API signs in with… | Use |
|---|---|
| Login → one-time code → token (+ refresh) | **Sign in with steps** |
| Login → token, no code | **Sign in with steps** (no code step); runs sign in by themselves |
| Standard OAuth 2.0 (Entra ID, Okta, Auth0, Keycloak, Google) | [OAuth 2.0 Sign-In](/help-center/product-documentation/oauth-2-interactive-sign-in) |
| One request that exchanges an API key for a token | **API Key + Bearer Token** |
| A login that sets a session cookie | **Cookie Login Flow** |

## Before you begin

- Know each sign-in request: its URL, method, body, and which response fields hold the values you need.
- Use a dedicated test account. Every run acts as this user.
- Have access to the account's one-time codes (phone or mailbox) when you sign in.

## Step 1 — Create the profile and pick a starting point

1. Go to **Project Settings** → **Authentication Profiles** and click **New Profile**.
2. Enter a **Profile Name** and click **Next**.
3. Under **Authentication Method**, select **Sign in with steps**.
4. In **Start from**, choose a starting point:
   - **Log in → one-time code → token (renews itself)**: two steps, the token locations and a refresh request. Only the URLs are left for you.
   - **Log in → token (no code)**: one login step.
   - **Start from a blank step**.

## Step 2 — Enter the credentials

1. Enter the **Username**.
2. Enter the **Password**. It is stored encrypted.

> **Important:** Never type the password into a step body. Write `{{credentials.password}}` instead. The profile will not save if a body contains the password itself, because step bodies are not encrypted.

## Step 3 — Build each step

For each step card (**Step 1**, **Step 2**, …):

1. Give the step a name, choose the HTTP method, and enter the URL.
2. For anything other than GET, choose the **Body type** (**JSON** or **Form (x-www-form-urlencoded)**) and write the **Body** using templates. The hint under the box lists the templates available to that step.
3. Under **Keep from the response (for later steps)**, click **Keep a value** for each value a later step needs. Enter the name to save it as, choose **Response body**, **Response header** or **Cookie**, and enter the path (for example `verifyOTPToken` or `data.token`).
4. On the step that sends the one-time code, select **Needs a code from a person (sent by SMS, email or an app)**. Then set:
   - **Called**: the code's name. The body uses `{{input.otp}}` if it is called `otp`.
   - **Shown as**: the label on the code box, for example `One-time code`.
   - **Code expires at (saved value)**: a value an earlier step kept that holds the code's expiry. The code box then shows a countdown.
5. Use the arrow icons to reorder steps, the bin icon to remove one, and **Add a step** to add another.

Only one step can wait for a code. Problems are shown in red under the step, for example "Step 2 uses {{step.verifyOTPToken}}, but no earlier step saves "verifyOTPToken"."

## Step 4 — Say where the token is

Under **The token, from the last step's response**:

| Field | What to enter |
|---|---|
| **Token is at** | JSON path to the token, for example `bearerToken`. Required. |
| **It expires at** | Path to its expiry: an ISO date with a time zone, an epoch time, or seconds. Optional. |
| **Refresh token is at** | Path to the refresh token. |
| **Refresh token expires at** | Path to the refresh token's expiry. |
| **Send it in the header** | Default `Authorization`. |
| **With the prefix** | Usually `Bearer`. Leave empty to send the token alone. |

How Studio decides when the token expires: a date with a time zone (`Z` or `+03:00`) is read exactly, including seven-digit fractional seconds. A date without a time zone cannot be read reliably, so Studio uses a configured lifetime instead (default one hour). When the response states nothing, Studio reads a JWT's own expiry, and otherwise uses that default.

## Step 5 — Add the refresh request

1. Select **Renew the token with a refresh request**.
2. Choose the method and enter the refresh URL.
3. Write the **Body**, for example `{"accessToken": "{{grant.accessToken}}", "refreshToken": "{{grant.refreshToken}}"}`.

The access token is sent even when it has expired, because many refresh endpoints require it. The refresh request cannot use `{{step.…}}` or `{{input.…}}` values; it runs long after sign-in. Without a refresh request, a person must sign in again whenever the token runs out.

## Step 6 — Save and sign in

1. Continue to **Save Profile**, or click **Sign in** in the **Sign-in** panel (Studio saves the profile first).
2. Studio runs the steps up to the code step, and a code box appears with a countdown, for example **The code expires in 4:32.**
3. Type the code you received and click **Submit**.
4. The panel shows, for example, **Signed in as jane. Token valid until 23 Sep 2026, 11:55. It renews itself until 7 Oct 2026, 10:55.**
5. **Activate** the profile for its environment if it is not already active.

About the code box:

- The code is sent once and never stored. It is masked in the audit log.
- A mistyped code can be retyped up to three times (your API may allow fewer). After that, the sign-in starts again.
- An expired code is refused before it is sent to your API, because many APIs lock an account after a failed or reused code. The box says **The code has expired. Start the sign-in again for a new one.**
- Changing a step's URL or body, or the username, while a code is waiting cancels that sign-in.

## Worked example

This API signs in with three calls:

```
POST /API/Auth/Login           {"username", "password"}
  → {"username", "verifyOTPToken", "otpExpiresOn": "2026-09-23T10:55:43.4709579+03:00"}

POST /API/Auth/VerifyLoginOTP  {"username", "otp", "verifyOTPToken"}
  → {"bearerToken", "bearerTokenExpiresOn", "refreshToken", "refreshTokenExpiresOn", "message"}

POST /API/Auth/RefreshToken    {"accessToken", "refreshToken"}
```

| Setting | Value |
|---|---|
| Step 1 URL | `https://<host>/API/Auth/Login` |
| Step 1 **Body** | `{"username": "{{credentials.username}}", "password": "{{credentials.password}}"}` |
| Step 1 keeps | `verifyOTPToken` ← `verifyOTPToken`; `otpExpiresOn` ← `otpExpiresOn` |
| Step 2 URL | `https://<host>/API/Auth/VerifyLoginOTP` |
| Step 2 **Body** | `{"username": "{{credentials.username}}", "otp": "{{input.otp}}", "verifyOTPToken": "{{step.verifyOTPToken}}"}` |
| Step 2 | **Needs a code from a person**, **Called** `otp`, **Code expires at** `otpExpiresOn` |
| **Token is at** / **It expires at** | `bearerToken` / `bearerTokenExpiresOn` |
| **Refresh token is at** / **Refresh token expires at** | `refreshToken` / `refreshTokenExpiresOn` |
| Refresh | `POST https://<host>/API/Auth/RefreshToken`, body `{"accessToken": "{{grant.accessToken}}", "refreshToken": "{{grant.refreshToken}}"}` |

Every test request then carries `Authorization: Bearer <bearerToken>`. The **Log in → one-time code → token (renews itself)** starting point fills in all of this except the three URLs, and **Load Sample Configuration** fills in a complete example.

## A test server with a fixed code

If your staging server accepts the same code every time, enter it under **Fixed code for a test server (optional)**. It is stored encrypted. No step then needs a person: the first run signs in by itself, and if a renewal is refused, the run signs in again. Use this only on test servers, because it turns off the protection the code provides.

## What happens on a run

| Situation | What the run does |
|---|---|
| Token valid | Sends it. |
| Token expires within 60 seconds and a refresh request exists | Renews once, even when many tests ask at the same moment, stores the new tokens, and sends the new token. |
| The API refuses the renewal | Marks the sign-in **expired**. Tests fail with "sign in again", and run analysis names one cause: **The sign-in for "<profile name>" has expired**, not a list of test failures. |
| The renewal fails for a temporary reason (network, 5xx) | Uses the current token while it is still valid; otherwise fails with a retryable error. |
| No refresh request and the token has expired | A person signs in again. |
| Machine flow (no code step, or a fixed code) | Signs in by itself whenever it needs to. |

The local runner (Agent) never runs the steps and never receives the password, the refresh token, the steps or a fixed code. It receives a live access token and asks the server for a new one when it runs low. Performance runs read authentication again while they run, so a long soak test keeps a valid token.

When the refresh token is within two days of running out, the panel warns: "The renewal token runs out on … Sign in again before then so scheduled runs keep working."

## Editing a signed-in profile

Changing a label, the password or the refresh request keeps the sign-in. Changing what decides which sign-in it is (a step's method, URL, body or headers, the username, or where the token is and how it is sent) drops it, and the dialog tells you to sign in again. A duplicated profile never shares the original's sign-in. **Sign out** removes the stored tokens.

## Configure it with the Project Assistant

You can ask the Project Assistant to set up a **Sign in with steps** profile from a description of your API's sign-in calls. Because bodies are templates, it can set up the whole flow: URLs, bodies, saved values, token paths and the refresh request. It never accepts the password or a fixed code in chat, because a secret typed into a conversation stays in the transcript; it opens the **Authentication** screen with the **Password** field marked instead. It can also start the sign-in, but you type the one-time code on the **Authentication** screen, not in the chat. The assistant's card completes by itself when you submit the code.

## Troubleshooting

| Symptom | Why it happens | What to do |
|---|---|---|
| The profile will not save and mentions the password | A step body contains the literal password. | Replace it with `{{credentials.password}}`. |
| "Say where the token is in the last step's response." | **Token is at** is empty. | Enter the JSON path to the token. |
| "Step N uses {{step.x}}, but no earlier step saves "x"." | A template refers to a value no earlier step keeps. | Add **Keep a value** on an earlier step, or fix the name. |
| "The code was not accepted." | The code was wrong, reused, or expired at the API. | Retype it (up to three tries), or start the sign-in again. |
| The countdown never appears | **Code expires at (saved value)** is not set. | Keep the expiry field from the login step and select it. |
| Token expires sooner than expected, or "(the API did not say — assumed)" appears | The response gives no expiry, or a date without a time zone. | Point **It expires at** at a field with a time zone, or at seconds remaining. |
| Runs fail with "The sign-in for … has expired" | The API refused the refresh, or the refresh token ran out. | Click **Sign in again**. |
| An error names a step and a status | That step's request failed. Errors show the status and the API's own message, never the response body. | Check that step's URL, method and body. |

## Best practices

- Start from the **Log in → one-time code → token (renews itself)** starting point and change only what differs.
- Always add a refresh request if the API has one; otherwise every token expiry needs a person.
- Keep the code's expiry so the countdown tells you how long you have.
- Use a fixed code only on test servers, never on an environment with real users.
- Sign in again before the refresh token runs out if you rely on scheduled runs.

## Limitations

- Up to 5 steps, and only one step can wait for a code.
- The token is sent in a header only (default `Authorization: Bearer …`). There is no query-parameter delivery.
- Step bodies are JSON or form-encoded.
- Values kept by earlier steps are held on the server, encrypted, for at most 15 minutes during a sign-in, and never sent to the browser.
- The sign-in requests go from the server to the URLs you configure, so anyone who can edit the project's authentication chooses where they go.

## FAQ

**How often do I type the code?**
Once per sign-in. With a refresh request, runs renew the token until the API's refresh token expires.

**Can scheduled and CI runs use this profile?**
Yes. The server holds the sign-in, so no one needs to be present.

**Is the password ever sent to the Agent?**
No. The Agent only receives a live access token.

**What if my API needs no code?**
Choose **Log in → token (no code)**. Runs then sign in by themselves.

## Related articles

- [Authentication Profiles: Choose and Configure a Method](/help-center/product-documentation/authentication-profiles-and-methods)
- [OAuth 2.0 Sign-In: Browser and Device Code](/help-center/product-documentation/oauth-2-interactive-sign-in)
- [Project Settings: Environments and Auth](/help-center/product-documentation/project-settings)
- [Test Run](/help-center/product-documentation/test-run)
- [Project Assistant Overview](/help-center/product-documentation/project-assistant-overview)

## For administrators (self-hosted installations)

| Setting | Default | What switching it does |
|---|---|---|
| `SIGN_IN_FLOW_AUTH_ENABLED` | on | `false` removes the method. Existing profiles then refuse to authenticate with a message that says so; they never send requests without authentication. |

