Skip to content

API (Bearer) auth

Use Bearer token authentication for API clients, mobile apps, or SPAs that manage tokens in JavaScript memory.

Login

curl -X POST http://localhost:8000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "my-password"}'

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer"
}

Store both tokens in your client. The access token is short-lived (30 min default), the refresh token is long-lived (7 days default).

Accessing protected routes

Send the access token in the Authorization header:

curl http://localhost:8000/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

No CSRF token is needed for Bearer requests.

Refreshing tokens

When the access token expires, use the refresh token to get a new pair:

curl -X POST http://localhost:8000/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "eyJhbGciOiJIUzI1NiIs..."}'

Refresh tokens are single-use

Each refresh token can only be used once. After refreshing, discard the old tokens and use the new pair. Replaying an old refresh token will revoke the entire token family.

Logout

curl -X POST http://localhost:8000/auth/logout \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "eyJhbGciOiJIUzI1NiIs..."}'

This revokes all tokens in the family. The access token will continue to work until it expires (it's stateless), but the refresh token is immediately invalidated.

JavaScript example

class AuthClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.accessToken = null;
    this.refreshToken = null;
  }

  async login(email, password) {
    const res = await fetch(`${this.baseUrl}/auth/login`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, password }),
    });
    const data = await res.json();
    this.accessToken = data.access_token;
    this.refreshToken = data.refresh_token;
  }

  async fetchWithAuth(url, options = {}) {
    const res = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${this.accessToken}`,
      },
    });

    if (res.status === 401) {
      await this.refresh();
      return fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${this.accessToken}`,
        },
      });
    }

    return res;
  }

  async refresh() {
    const res = await fetch(`${this.baseUrl}/auth/refresh`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ refresh_token: this.refreshToken }),
    });
    const data = await res.json();
    this.accessToken = data.access_token;
    this.refreshToken = data.refresh_token;
  }
}