# Sign in with your STX account

> Let members sign in with their STX account, or connect their STX account to an app that has its own login.

Source: https://docs.stxapp.io/sdks/typescript/sign-in-with-stx/

> **Availability.** Available on US demo now. Live access is coming soon in the US and Ontario. See [Environments](https://docs.stxapp.io/environments/).

:::note
OAuth is on in US demo today. On an exchange without it, these calls throw `STXNotAvailableException` with `feature: "oauth"` (or `"connect"` for `createConnect`), and `await stx.capabilities()` tells you up front. See [What you can do](/sdks/typescript/capabilities/).
:::

There are two ways to bring a member's STX account into your app. Both end with tokens your server uses to call STX for that member.

| | Use it when | You call |
|---|---|---|
| **Connect your STX account** | Your app has its own login, and a signed-in user links their STX account. It works with any login system: your own passwords or a login service you already use. STX never sees it. | `OAuthClient` |
| **Sign in with your STX account** | The STX account is the login to your app. | `createConnect()` |

Choosing between them, the scopes and the consent screen are described in [Add STX accounts to your app](/oauth/); this page shows the SDK calls.

It is for confidential, server-side apps. The client secret and the tokens never go to a browser or a mobile app.

## Sign in with your STX account

`createConnect` runs the OpenID Connect sign-in from your server. The member taps one button in your app, signs in to STX (with Google, Apple, X or email), finishes STX onboarding if they are new, and approves your app. Your server gets back:

- **tokens** to call STX for the member. Keep them on your server.
- **identity**: a signed statement of who the member is. Key your own user record on `identity.sub`.

:::note
If your login runs through a login service, you can add the STX account there as one more sign-in option instead of calling `createConnect`. That service then runs the sign-in. You use this SDK only afterwards, to call STX with the member's access token: `new STX({ host, auth: new OAuthBearerAuth(accessToken) })`. See [Using a login service](/oauth/sign-in-with-your-stx-account/login-vendor/) for the setup.
:::

### Set up

```ts
import { createConnect } from "@stxapp/stx-typescript/oauth";

const connect = createConnect({
  issuer: "https://demo.stxapp.io",
  clientId: process.env.STX_CLIENT_ID!,
  clientSecret: process.env.STX_CLIENT_SECRET!,
  redirectUri: "https://your.app/auth/stx/callback",
  scopes: ["profile.read", "balance.read", "orders.read", "orders.write"],
});
```

`openid` is added to the scopes for you. Endpoints and signing keys come from the issuer's `/.well-known/openid-configuration`.

### Start

```ts
app.get("/auth/stx/start", async (req, res) => {
  const signIn = await connect.start({ connection: "google", loginHint: req.query.email });
  req.session.stxSignIn = signIn; // state, codeVerifier, nonce
  res.redirect(signIn.url);
});
```

- `connection`: `google`, `apple` or `x` goes straight to that provider and skips STX's login page. Leave it out to show STX's login page.
- `loginHint`: the member's email, if you know it, so the provider can preselect the account.
- `prompt: "none"`: a silent reconnect for a member who has already approved your app.

### Finish

```ts
app.get("/auth/stx/callback", async (req, res) => {
  const { tokens, identity } = await connect.finish(req.originalUrl, req.session.stxSignIn);
  delete req.session.stxSignIn;
  await users.upsert({ stxMemberId: identity.sub, email: identity.email });
  await tokenStore.save(identity.sub, tokens);
  res.redirect("/");
});
```

`finish` checks the `state`, exchanges the code, and verifies the ID token: its signature against the published keys, the issuer, that it was issued to your app, the `nonce` from this sign-in, and that it has not expired. Any failure throws `STXOAuthException`; see [When the member does not come back connected](#when-the-member-does-not-come-back-connected).

### Stay connected

```ts
const fresh = await connect.refresh(stored.refreshToken); // STX rotates the refresh token: store what comes back
await connect.disconnect(stored); // when the member disconnects
```

`disconnect` revokes the refresh token, which ends the connection at STX: every token on it stops working, and the member's next sign-in asks for consent again.

Call the exchange for the member with the access token, through `OAuthClient.memberClient()` or `new STX({ auth: new OAuthBearerAuth(token) })`.

### Read the member's claims again

```ts
const me = await connect.userInfo(stored.accessToken, { sub: user.stxMemberId });
console.log(me.sub, me.email, me.emailVerified);
```

`userInfo` reads the same claims from the userinfo endpoint with the member's access token. Pass the `sub` you stored and an answer about anyone else is refused. A token that has stopped working throws `STXOAuthException` with `error: "invalid_token"`.

## Connect your STX account

Your app already knows who the user is. `OAuthClient` sends them to STX to approve your app, and stores the tokens under your own user key.

```ts
import { MemoryPendingAuthorizationStore, MemoryTokenStore, OAuthClient } from "@stxapp/stx-typescript/oauth";

// Reads the exchange's discovery document, so its endpoints and scopes are used.
const oauth = await OAuthClient.discover({
  baseUrl: "https://demo.stxapp.io",
  clientId: process.env.STX_CLIENT_ID!,
  clientSecret: process.env.STX_CLIENT_SECRET!,
  redirectUri: "https://your.app/stx/callback",
  scope: ["balance.read", "orders.read", "orders.write"],
});
const pending = new MemoryPendingAuthorizationStore(); // sign-ins in progress; use your session store or database
const store = new MemoryTokenStore(); // members' tokens; use your database

app.get("/stx/connect", async (req, res) => {
  const { url } = await oauth.beginAuthorization(pending, {
    connection: "google", // optional: go straight to Google, Apple ("apple") or X ("x")
    loginHint: req.user.email, // optional: preselect the account
    data: { userId: req.user.id },
  });
  res.redirect(url);
});

app.get("/stx/callback", async (req, res) => {
  await oauth.completeAuthorization(pending, req.originalUrl, { store, memberKey: (data) => String(data?.userId) });
  res.redirect("/");
});
```

`beginAuthorization` creates the PKCE pair and the `state` and remembers them; `completeAuthorization` checks the `state` (single use), exchanges the code and saves the tokens.

`OAuthClient.discover()` throws `STXNotAvailableException` (`feature: "oauth"`) on an exchange without OAuth. Built with `discover()`, the client also knows which scopes the exchange offers: asking for one it does not offer throws `STXNotAvailableException` with `feature: "scope:<name>"` instead of a bare `invalid_scope`. `new OAuthClient({...})` still works and skips the discovery request.

```ts
const stx = oauth.memberClient(store, userId); // an STX client acting for that member
console.log(await stx.balance());

await oauth.unlink(store, userId); // when the user disconnects
```

`memberClient` refreshes the access token before it expires and stores the rotated refresh token for you. See [After the member is connected](#after-the-member-is-connected) for the errors a call can throw once the member's situation changes.

## When the member does not come back connected

`finish` and `completeAuthorization` throw one of these, all `STXOAuthException`. No tokens exist in any of these cases.

| Exception | `error` | What happened | What to do |
|---|---|---|---|
| `STXAccountPendingException` | `account_pending` | The member's STX account is still being verified (approval pending, or verification not finished). See [Members still being verified](/oauth/members-being-verified/). | Show your own "we'll let you know" state, and start again once STX has approved the account. |
| `STXAccessDeniedException` | `access_denied` | The member declined, or their STX account cannot be connected (for example it is suspended or closed). | Show your "not connected" state. |
| `STXTermsNotAcceptedException` | `terms_required` | STX showed the member its terms page during sign-in, and the member went back to your app without agreeing. | Show your "not connected" state. The member can start again when they are ready. |
| `STXOAuthException` | `interaction_required` | A silent reconnect (`prompt: "none"`) needs the member, for example to accept new terms. | Start again without `prompt: "none"`. |
| `STXOAuthException` | `invalid_state` | The callback does not belong to a sign-in you started, or it was used already. | Start again. |
| `STXOAuthException` | `invalid_grant` | The code was used or expired. | Start again. |
| `STXOAuthException` | `invalid_id_token` | (`finish` only) The ID token did not verify. | Do not sign the member in. |

```ts
import { STXAccessDeniedException, STXAccountPendingException } from "@stxapp/stx-typescript/oauth";

try {
  await connect.finish(req.originalUrl, req.session.stxSignIn);
} catch (err) {
  if (err instanceof STXAccountPendingException) return res.redirect("/stx/pending");
  if (err instanceof STXAccessDeniedException) return res.redirect("/stx/not-connected");
  throw err;
}
```

### After the member is connected

A member's account can go back into verification after they connected. STX then issues no new tokens for them until it is done, and the connection itself stays.

| Exception | From | What happened | What to do |
|---|---|---|---|
| `STXAccountPendingException` (`error` is `invalid_grant`, `errorReason` is `account_pending`) | `refresh()`, a `memberClient()` call, the code exchange | The member's STX account is being verified. | Keep the stored tokens: the same refresh token works once the member is verified. `memberClient()` keeps them for you. Show your "being verified" state and try again later. |
| `STXGrantRevokedException` | `refresh()`, a `memberClient()` call | The connection has ended: the member disconnected your app, or the refresh token expired or was used twice. | Delete the tokens (`memberClient()` does) and send the member through the connect step again. |

### When STX has new terms for the member

A member accepts the STX terms on an STX page, signed in to their own account. Your app cannot accept them for a member, and no scope asks for it. During sign-in STX shows that page itself, before the consent screen.

When new terms take effect for a member who is already connected, the connection stays. The tokens, reads, cancels and WebSocket channels keep working. Only placing orders is refused until the member has agreed. STX gives no advance notice, so handle it when it happens.

| What your app sees | From | What to do |
|---|---|---|
| `STXTermsRequiredException`, with `termsUrl` | `placeOrder()` and `placeOrders()` on a `memberClient()` | Send the member's browser to `termsUrlWithReturn(err.termsUrl, page)`. The member signs in to STX if needed, agrees, and lands back on `page`. The same call then works with the same tokens. |
| `termsRequired: true` and `termsUrl` on the refreshed tokens | `refresh()`, and a `memberClient()` refresh (which also calls the `onTermsRequired` session hook) | The refresh succeeded: store the tokens as usual. Ask the member to accept before they next trade. Both fields are gone from the next refresh after the member agrees. |

`page` is where the member comes back to in your app. It must be one of your registered redirect URIs, or a page on the same origin as one. STX sends the member back to it unchanged.

```ts
import { STXTermsRequiredException, termsUrlWithReturn } from "@stxapp/stx-typescript/oauth";

app.post("/orders", async (req, res) => {
  const stx = oauth.memberClient(store, req.user.id);
  try {
    res.json(await stx.placeOrder("0c1e7a52-9b3f-4d8e-a6c4-2f5b8d1e9a70", "buy", "limit", { price: "0.55", quantity: "10" }));
  } catch (err) {
    if (err instanceof STXTermsRequiredException && err.termsUrl) {
      // https://demo.stxapp.io/connect/terms?client_id=stxapp_client_9f2c41d7&return_to=https%3A%2F%2Fyour.app%2Fportfolio
      const termsUrl = termsUrlWithReturn(err.termsUrl, "https://your.app/portfolio");
      return res.status(409).json({ error: "terms_required", termsUrl });
    }
    throw err;
  }
});
```

`acceptTerms()` is for your own account with your own API key. Called with a member's access token it throws `STXForbiddenException` (`terms_not_delegable`).

## Acting as your app

Some calls belong to your app, not to a member: reading the catalog, and [creating markets](/sdks/typescript/markets#creating-markets). They use an app token, minted from your client id and secret with no member involved.

```ts
import { AppScopes, OAuthClient } from "@stxapp/stx-typescript/oauth";

const oauth = new OAuthClient({ baseUrl: "https://demo.stxapp.io", clientId, clientSecret });
const stx = oauth.appClient([AppScopes.EVENTS]); // mints the token, reuses it and renews it before it expires
const events = await stx.events({ status: "scheduled", limit: 10 });
```

`oauth.clientCredentials(scopes)` returns one raw token when you want to manage it yourself. An app token has no refresh token; mint another.

A client that holds a write scope (`markets.write`) has an IP allow-list, and it applies to every write that client makes. A write from another address throws `STXIPNotAllowedException` (a `STXForbiddenException`, `error: "ip_not_allowed"`); reads keep working.

## Mobile apps

The same flow, started from your server. Your app opens `/auth/stx/start` in the system browser sheet (`ASWebAuthenticationSession` on iOS, Custom Tabs on Android), and your server sends the member back to the app with a universal link or app link carrying your own session. The app never holds STX tokens.

### Expo / React Native

```ts
import * as WebBrowser from "expo-web-browser";

// ticket: a one-time code from your server, so your own session token never goes into a URL
const start = `https://api.example.com/auth/stx/start?ticket=${ticket}&connection=google&login_hint=${encodeURIComponent(email)}`;
const result = await WebBrowser.openAuthSessionAsync(start, "myapp://stx-connected");
// result.url is myapp://stx-connected?status=connected (or cancelled)
```

`connection` and `login_hint` are hints: they preselect the member's account, and the member still signs in themselves. Your server's `/auth/stx/start` and `/auth/stx/callback` are the `start()` and `finish()` routes above; on success it redirects to `myapp://stx-connected?status=connected`. Use the system sheet, not an embedded web view: Google blocks sign-in inside embedded web views, and RFC 8252 (OAuth 2.0 for Native Apps) requires the system browser.
