Authentication

The B12 SIS API uses JWT (JSON Web Tokens) for authentication with access and refresh token rotation.

Overview

  • Access Token: Short-lived token for API access (default: 24 hours)

  • Refresh Token: Long-lived token for obtaining new access tokens (default: 7 days)

  • Token Rotation: Each refresh generates a new token pair

Endpoints

Login

Authenticate with email and password.

Endpoint: POST /api/auth/login

Request:

{
  "email": "user@school.edu",
  "password": "your-password"
}

Response:

{
  "token": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 86400
  },
  "user": {
    "id": "user-uuid",
    "email": "user@school.edu",
    "name": "User Name",
    "avatar": "https://...",
    "teams": [
      {
        "id": "team-uuid",
        "name": "Main Campus"
      }
    ],
    "roles": [
      {
        "id": "role-uuid",
        "name": "Admin"
      }
    ]
  }
}

Error Responses:

=== “Invalid Credentials” json     {       "error": "Invalid email or password"     }     Status: 401 Unauthorized

=== “Account Disabled” json     {       "error": "Account is disabled"     }     Status: 401 Unauthorized


Refresh Token

Get a new access token using a valid refresh token.

Endpoint: POST /api/auth/refresh

Request:

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 86400
}

!!! warning “Token Rotation” Each refresh invalidates the old refresh token and issues a new pair. Store the new refresh token for future use.


Get Current User

Retrieve the authenticated user’s profile and permissions.

Endpoint: GET /api/auth/me

Headers:

Authorization: Bearer <access_token>

Response:

{
  "success": true,
  "message": "user",
  "data": {
    "user": {
      "id": "user-uuid",
      "email": "user@school.edu",
      "name": "User Name",
      "avatar": "https://...",
      "teams": [...],
      "roles": [...]
    },
    "permissions": {
      "Student": ["list", "read", "create", "update", "delete"],
      "Teacher": ["list", "read"],
      "Class": ["list", "read", "create", "update"]
    }
  }
}

Change Password

Change the authenticated user’s password.

Endpoint: POST /api/auth/change-password

Headers:

Authorization: Bearer <access_token>

Request:

{
  "current_password": "old-password",
  "new_password": "new-password",
  "confirm_password": "new-password"
}

Response:

{
  "success": true,
  "message": "Password changed successfully"
}

Validation Rules:

  • New password must be at least 8 characters

  • New password must contain uppercase, lowercase, and numbers

  • New password must be different from current password


Logout

Invalidate the user’s refresh token.

Endpoint: POST /api/auth/logout

Headers:

Authorization: Bearer <access_token>

Response:

{
  "message": "Successfully logged out"
}

Reset Password (Admin)

Reset a user’s password to the default. Requires admin role.

Endpoint: POST /api/admin/reset-password/:userid

Headers:

Authorization: Bearer <admin_access_token>

Response:

{
  "success": true,
  "message": "Password reset successfully"
}

Using Tokens

Authorization Header

Include the access token in all protected requests:

curl -H "Authorization: Bearer eyJhbGciOi..." \
     https://api.yourschool.edu/api/students/list

Token Structure

The JWT contains these claims:

{
  "sub": "user-uuid",
  "email": "user@school.edu",
  "exp": 1704067200,
  "iat": 1703980800,
  "type": "access"
}

Claim

Description

sub

User ID

email

User email

exp

Expiration timestamp

iat

Issued at timestamp

type

Token type (access or refresh)

Security Features

Token Blacklisting

  • Logout invalidates the refresh token

  • Changing password invalidates all tokens

  • Admin can revoke all user tokens

Rate Limiting

Login attempts are rate limited:

  • 5 failed attempts: 1-minute lockout

  • 10 failed attempts: 5-minute lockout

  • 20 failed attempts: Account temporary lock

Security Audit

All authentication events are logged:

  • Login attempts (success/failure)

  • Token refresh

  • Logout

  • Password changes

Code Examples

JavaScript/TypeScript

class AuthService {
  private accessToken: string | null = null;
  private refreshToken: string | null = null;

  async login(email: string, password: string): Promise<void> {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    });

    if (!response.ok) {
      throw new Error('Login failed');
    }

    const data = await response.json();
    this.accessToken = data.token.access_token;
    this.refreshToken = data.token.refresh_token;
  }

  async refreshTokens(): Promise<void> {
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refresh_token: this.refreshToken })
    });

    if (!response.ok) {
      // Refresh failed, need to re-login
      this.accessToken = null;
      this.refreshToken = null;
      throw new Error('Session expired');
    }

    const data = await response.json();
    this.accessToken = data.access_token;
    this.refreshToken = data.refresh_token;
  }

  getAuthHeaders(): Record<string, string> {
    return {
      'Authorization': `Bearer ${this.accessToken}`
    };
  }
}

Axios Interceptor

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api.yourschool.edu/api'
});

// Request interceptor - add token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor - handle token refresh
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      const refreshToken = localStorage.getItem('refresh_token');
      if (refreshToken) {
        try {
          const response = await axios.post('/api/auth/refresh', {
            refresh_token: refreshToken
          });

          localStorage.setItem('access_token', response.data.access_token);
          localStorage.setItem('refresh_token', response.data.refresh_token);

          // Retry original request
          error.config.headers.Authorization = `Bearer ${response.data.access_token}`;
          return api.request(error.config);
        } catch {
          // Refresh failed, redirect to login
          window.location.href = '/login';
        }
      }
    }
    return Promise.reject(error);
  }
);

SSO / OIDC Login

The system also supports Single Sign-On via OIDC providers. See OIDC Setup Guide for configuration.

OIDC Flow

  1. Redirect to /api/oidc/login?provider=<provider-id>

  2. User authenticates with identity provider

  3. Callback to /api/oidc/callback with authorization code

  4. System exchanges code for tokens and creates/updates user

  5. Returns JWT tokens for API access