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.
View as MarkdownApplies 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 |
| 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
- Go to Project Settings → Authentication Profiles and click New Profile.
- Enter a Profile Name and click Next.
- Under Authentication Method, select Sign in with steps.
- 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
- Enter the Username.
- 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, …):
- Give the step a name, choose the HTTP method, and enter the URL.
- 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.
- 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
verifyOTPTokenordata.token). - 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 calledotp. - 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.
- Called: the code's name. The body uses
- 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
- Select Renew the token with a refresh request.
- Choose the method and enter the refresh URL.
- 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
- Continue to Save Profile, or click Sign in in the Sign-in panel (Studio saves the profile first).
- 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.
- Type the code you received and click Submit.
- 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.
- 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
- OAuth 2.0 Sign-In: Browser and Device Code
- Project Settings: Environments and Auth
- Test Run
- 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. |
Previous
OAuth 2.0 Sign-In: Browser and Device Code
Product documentation
Next
Postman Collection Import
Product documentation
Related articles
- Create Your First Project in Shift-Left API · Product documentation
- Project Operations: Import API Definitions · Product documentation
- Managing Projects and Endpoints · Product documentation
- Project Settings: Environments and Auth · Product documentation
- Authentication Profiles: Choose and Configure a Method · Product documentation
- OAuth 2.0 Sign-In: Browser and Device Code · Product documentation
Next steps
- Getting started · Install + connect your spec
- Configuration fundamentals · Stabilize runs
- Initial configuration · Users, licensing, projects
- Release notes · Updates and fixes
Still stuck?
Tell us what you’re trying to accomplish and we’ll point you to the right setup—installation, auth, or CI/CD wiring.