> For AI agents: the complete documentation index is available at https://dotpress.dev/llms.txt, the full documentation bundle is available at https://dotpress.dev/llms-full.txt.

# Data provider

`@dotpress/auth` stores nothing itself. A data provider, an object of a few functions, reads your users and stores the refresh tokens in your database, with the library of your choice.

## The methods

| Method                                      | Required | Called                                                                                                                                                     |
| ------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `findUserIdentifiers(username)`             | ✓        | At login: `{ userId, passwordHash, role? }`, or `undefined` for an unknown username. `passwordHash` is `null` for an account without password (SSO).       |
| `findUserById(userId)`                      | ✓        | On every authenticated request and every refresh: the user, which becomes `ctx.user`                                                                       |
| `storeRefreshToken(payload)`                | ✓        | At login and at each refresh: `{ token, tokenHash, userId, jwtId, familyId, expiresAt }`. Store the hash, never the token.                                 |
| `findRefreshToken(tokenHash)`               | ✓        | At each refresh and logout: `{ userId, jwtId, familyId, expiresAt, isUsed, usedAt, revokedAt }`, or `undefined`. `isUsed` can be derived from `usedAt`.    |
| `markRefreshTokenAsUsed(tokenHash, usedAt)` | ✓        | At each refresh                                                                                                                                            |
| `revokeTokenFamily(familyId)`               | ✓        | At logout, and when a stolen refresh token is detected: revokes one session                                                                                |
| `revokeTokens(userId)`                      | ✓        | At logout of every device: revokes every session of the user                                                                                               |
| `isSessionRevoked(sessionId)`               |          | On every authenticated request: rejects the access tokens of a revoked session at once, see [Revocation](https://dotpress.dev/auth/sessions.md#revocation) |
| `updatePasswordHash(userId, passwordHash)`  |          | After a successful login, when the cost of the hash changed, see [Passwords](https://dotpress.dev/auth/passwords.md#raising-the-cost)                      |

:::warning findUserById decides who is logged in
`findUserById()` must return `undefined` for a deleted, disabled or banned account. It is called on every request: the user is logged out at once, and their sessions are revoked at the next refresh.
:::

Return only what the handlers need from `findUserById()`: it becomes `ctx.user`, and is returned by `GET /auth/me`. Never the password hash.

## The refresh tokens table

A refresh token is stored by its SHA-256 hash, never in clear. The tokens of a session share a `familyId`, the session ID:

```sql
CREATE TABLE refresh_tokens (
  token_hash TEXT PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  jwt_id TEXT NOT NULL,
  family_id TEXT NOT NULL, -- the session
  expires_at TIMESTAMP NOT NULL,
  used_at TIMESTAMP,
  revoked_at TIMESTAMP
);
CREATE INDEX refresh_tokens_family_id ON refresh_tokens (family_id);
CREATE INDEX refresh_tokens_user_id ON refresh_tokens (user_id);
```

Expired tokens can be deleted by a scheduled job: `DELETE FROM refresh_tokens WHERE expires_at < now()`.

## An SQL data provider

The data provider of the [Tasks API example](https://dotpress.dev/examples/tasks-api.md), on SQLite. The queries are the same on PostgreSQL or MySQL, with their placeholders:

```ts file="<root>/../examples/tasks-api/src/features/auth/data-provider.ts"
import type { DataProvider, RefreshTokenData } from '@dotpress/auth'
import { get, run, type Database } from '../../db/database.ts'
import type { UserRepository } from '../users/repository.ts'

interface RefreshTokenRow {
  user_id: string
  jwt_id: string
  family_id: string
  expires_at: string
  used_at: string | null
  revoked_at: string | null
}

/** What @dotpress/auth reads and writes: users and refresh tokens. */
export const createDataProvider = (
  db: Database,
  users: UserRepository
): DataProvider => ({
  storeRefreshToken: async ({
    tokenHash,
    userId,
    jwtId,
    familyId,
    expiresAt,
  }) => {
    run(
      db,
      'INSERT INTO refresh_tokens (token_hash, user_id, jwt_id, family_id, expires_at) VALUES (?, ?, ?, ?, ?)',
      tokenHash,
      userId,
      jwtId,
      familyId,
      expiresAt.toISOString()
    )
  },

  findRefreshToken: async (
    tokenHash
  ): Promise<RefreshTokenData | undefined> => {
    const row = get<RefreshTokenRow>(
      db,
      'SELECT * FROM refresh_tokens WHERE token_hash = ?',
      tokenHash
    )
    return (
      row && {
        userId: row.user_id,
        jwtId: row.jwt_id,
        familyId: row.family_id,
        expiresAt: new Date(row.expires_at),
        isUsed: row.used_at !== null,
        usedAt: row.used_at === null ? null : new Date(row.used_at),
        revokedAt: row.revoked_at === null ? null : new Date(row.revoked_at),
      }
    )
  },

  markRefreshTokenAsUsed: async (tokenHash, usedAt) => {
    run(
      db,
      'UPDATE refresh_tokens SET used_at = ? WHERE token_hash = ?',
      usedAt.toISOString(),
      tokenHash
    )
  },

  revokeTokens: async (userId) => {
    run(
      db,
      'UPDATE refresh_tokens SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL',
      new Date().toISOString(),
      userId
    )
  },

  revokeTokenFamily: async (familyId) => {
    run(
      db,
      'UPDATE refresh_tokens SET revoked_at = ? WHERE family_id = ? AND revoked_at IS NULL',
      new Date().toISOString(),
      familyId
    )
  },

  // A logout stops the access tokens of the session at once.
  isSessionRevoked: async (sessionId) =>
    get(
      db,
      'SELECT 1 FROM refresh_tokens WHERE family_id = ? AND revoked_at IS NOT NULL LIMIT 1',
      sessionId
    ) !== undefined,

  // On every authenticated request: becomes ctx.user.
  findUserById: (userId) => users.findById(userId),

  // At login: the username is the email address.
  findUserIdentifiers: (username) => users.findCredentials(username),

  updatePasswordHash: (userId, passwordHash) =>
    users.setPasswordHash(userId, passwordHash),
})

```

`findUserById()` and `findUserIdentifiers()` come from the users repository of the app: the user table stays yours, with your columns.

## The type of the user

The type of `ctx.user` is the `AuthUser` interface, which you complete once, with module augmentation:

```ts
declare module '@dotpress/auth' {
  interface AuthUser {
    id: string
    email: string
    roles: string[]
  }
}
```

`findUserById()` must return this type. See [Protecting routes](https://dotpress.dev/auth/protecting-routes.md#the-type-of-the-user).

## In tests

A data provider over a `Map` is enough for the tests. The [Auth example](https://dotpress.dev/examples/auth.md) has one, in `src/store.ts`, used both by the app in development and by the tests.
