# Sign in with TaskStation

Gate your own app behind TaskStation identity with one route, and act as the signed-in user through the SDK.

Canonical page: https://taskstation.co/docs/sdk/sign-in

"Sign in with TaskStation" makes TaskStation the identity provider for an app you run:
a dashboard, an internal tool, a vertical product built on TaskStation. Your users
sign in with their TaskStation account, your server knows who they are, and every
TaskStation call your app makes runs as that user with that user's role
assignments. The whole flow lives in `@taskstation/sdk`. Your app never stores a
TaskStation token in the browser and never talks to Supabase.

It is standard OAuth 2.1 (authorization code + PKCE) served by the TaskStation API,
so it works the same against `api.taskstation.co` and against a self-hosted
instance.

> **Note**
> Building an App **hosted by TaskStation** (`*.apps.taskstation.co`)? You need none of
> this. The Apps gate already authenticated the viewer — read them with
> `taskstationAppViewerToken()` / `readAppViewer()`. See
> [Apps → Your App already knows who is looking](/docs/sdk/apps).

## 1. Register your app

Go to **Account → Tokens → OAuth apps → Register app**, or call the SDK:

```ts
const app = await taskstation.iam.oauthClients.create(accountId, {
  name: 'Dashboards',
  client_type: 'confidential',            // 'public' for a browser/native app (PKCE only, no secret)
  redirect_uris: ['https://dashboards.example.com/api/taskstation/auth/callback'],
  scopes: ['profile', 'email', 'taskstation'],
});
// app.client_id, app.client_secret (shown once)
```

Registration needs `token.create` on the account. Redirect URIs are compared
byte for byte; `https` is required except on `localhost`.

| Scope | Grants the app |
|---|---|
| `profile` | The user's id, email and account memberships (`GET /v1/accounts/me`). |
| `email` | The email address (an alias for OIDC-shaped clients). |
| `taskstation` | Acting as the user on the whole TaskStation API — projects, sessions, files, IAM probes. Without it the token is identity-only. |

## 2. Mount the handler

```ts
// lib/taskstation-auth.ts
import { createTaskStationAuth } from '@taskstation/sdk/server';

export const auth = createTaskStationAuth({
  backendUrl: 'https://api.taskstation.co/v1',
  clientId: process.env.TASKSTATION_OAUTH_CLIENT_ID!,
  clientSecret: process.env.TASKSTATION_OAUTH_CLIENT_SECRET,   // omit for a public client
  redirectUri: 'https://dashboards.example.com/api/taskstation/auth/callback',
  cookieSecret: process.env.TASKSTATION_AUTH_COOKIE_SECRET!,   // ≥ 32 chars; encrypts the session cookie
});
```

```ts
// app/api/taskstation/auth/[...taskstation]/route.ts  (Next.js App Router)
import { auth } from '@/lib/taskstation-auth';
const handle = (request: Request) => auth.handler(request);
export { handle as GET, handle as POST };
```

The handler serves every route under `basePath` (derived from the redirect
URI — `/api/taskstation/auth` above):

| Path | Does |
|---|---|
| `/signin?return_to=/path` | Starts sign-in (PKCE S256 + state in a 10-minute cookie) and redirects to TaskStation. |
| `/callback` | Exchanges the code, sets the encrypted `HttpOnly` session cookie, redirects to `return_to`. |
| `/refresh?return_to=` | Rotates the token pair and redirects. Used by `requireViewer`. |
| `/signout?return_to=` | Revokes the refresh token at TaskStation and clears the cookie. |
| `/me` | The viewer as JSON, or `401`. Refreshes inline when the access token expired. |
| `/proxy/*` | Forwards to the TaskStation API as the viewer. The browser SDK's `backendUrl`. |

`return_to` is always confined to a same-origin path.

## 3. Gate pages and act as the user

```ts
// middleware.ts — every page needs a viewer
import { auth } from '@/lib/taskstation-auth';

export async function middleware(request: Request) {
  const gate = await auth.requireViewer(request);
  if (gate.response) return gate.response;   // 302 → /refresh or /signin
}
export const config = { matcher: ['/((?!api/taskstation/auth|_next).*)'] };
```

```ts
// a server component / route handler
const viewer = await auth.viewer(request);       // { userId, email, accounts, scopes, token, expiresAt } | null
const taskstation = await auth.taskstation(request);       // request-scoped client acting as the viewer
const projects = await taskstation.projects.list();
const allowed = await taskstation.iam.can(accountId, viewer!.userId, { action: 'project.write', resourceType: 'project', resourceId });
```

`viewer()` is read-only and never consumes the single-use refresh token; use
`requireViewer()` in middleware so a page never renders signed-out for a user
whose refresh token is still good.

## 4. The browser

```tsx
import { createTaskStation } from '@taskstation/sdk';
import { SignInWithTaskStation, useTaskStationViewer } from '@taskstation/sdk/react';

const taskstation = createTaskStation(auth.clientConfig());   // backendUrl = '/api/taskstation/auth/proxy'

function Header() {
  const { status, viewer } = useTaskStationViewer();
  if (status === 'signed-in') return <span>{viewer.email}</span>;
  return <SignInWithTaskStation className="button" />;
}
```

The browser client sends a sentinel bearer; `/proxy` swaps it for the viewer's
real token on the server. `useSession`, `taskstation.project(id).sessions.*` and
every other SDK call work unchanged through it.

## What the user sees

The first time, TaskStation shows a consent screen naming your app and the scopes.
TaskStation remembers the decision per user and app, so later sign-ins redirect
straight back. Revoking an app deletes every token it minted.

## Discovery

`GET https://api.taskstation.co/.well-known/oauth-authorization-server` (also
under `/v1/oauth/.well-known/…`) publishes the endpoints for a generic OAuth
client. The SDK does not need it — it derives every endpoint from `backendUrl`.
