Skip to main content

Overview

MinistryHub implements JWT (JSON Web Token) authentication using HMAC-SHA256 signing. Tokens are generated upon successful login and must be included in API requests to access protected resources.

Token Structure

JWT tokens consist of three base64url-encoded parts separated by dots:
header.payload.signature
{
  "typ": "JWT",
  "alg": "HS256"
}
  • typ: Token type (JWT)
  • alg: Signing algorithm (HMAC-SHA256)

Payload

The JWT payload contains user identification and timing information:
{
  "uid": 123,
  "email": "[email protected]",
  "iat": 1709160000,
  "exp": 1709163600
}
uid
integer
User’s member ID from the database
email
string
User’s email address
iat
integer
Issued At timestamp (Unix timestamp)
exp
integer
Expiration timestamp (Unix timestamp, iat + 3600 seconds)

Signature

The signature is generated by:
  1. Concatenating the base64url-encoded header and payload with a dot
  2. Signing with HMAC-SHA256 using the server’s secret key
  3. Base64url-encoding the resulting hash
$signature = hash_hmac('sha256', $header . "." . $payload, JWT_SECRET, true);

Token Generation

Tokens are generated during:
  1. Standard Login (POST /api/auth/login)
  2. Google OAuth Callback (/api/auth/google/callback)
  3. Invitation Acceptance (POST /api/auth/accept-invite)

Generation Process

$payload = [
    'uid' => $user['member_id'],
    'email' => $user['email'],
    'iat' => time(),
    'exp' => time() + 3600  // 1 hour expiration
];

$token = Jwt::encode($payload);

Token Validation

The AuthMiddleware validates tokens on every protected API request:

Validation Steps

  1. Extract Token: From Authorization: Bearer <token> header or query parameter
  2. Verify Signature: Recalculate signature and compare with token signature
  3. Check Expiration: Ensure current time is before the exp timestamp
  4. Return User ID: Extract uid from payload for request context

Token Extraction Methods

Tokens are accepted from multiple sources (in order of priority):
  1. Authorization header
  2. HTTP_AUTHORIZATION server variable
  3. REDIRECT_HTTP_AUTHORIZATION (for some server configurations)
  4. token query parameter (for SSE/EventSource)
# Standard header-based authentication
curl -H "Authorization: Bearer <token>" https://api.example.com/bootstrap

# Query parameter (for SSE)
curl https://api.example.com/notifications?token=<token>

Token Expiration

  • Lifetime: 3600 seconds (1 hour)
  • Behavior: Expired tokens return 401 Unauthorized
  • Refresh: Re-authenticate via /api/auth/login to obtain a new token

Checking Token Expiration

function isTokenExpired(token) {
  const payload = JSON.parse(atob(token.split('.')[1]));
  return payload.exp < Math.floor(Date.now() / 1000);
}

Security Implementation

Secret Key

The JWT secret is stored in the server configuration:
# config/database.env
JWT_SECRET=your-secure-random-secret-key
Never expose the JWT_SECRET. It should be a cryptographically secure random string and never committed to version control.

Base64URL Encoding

MinistryHub uses URL-safe base64 encoding:
  • Replace + with -
  • Replace / with _
  • Remove padding =
This ensures tokens are safe for use in URLs and HTTP headers.

Error Handling

Invalid Token

{
  "error": "Unauthorized"
}
Returned when:
  • Signature verification fails
  • Token is expired
  • Token format is invalid

Missing Token

{
  "error": "Token missing"
}
Returned when:
  • No Authorization header is present
  • No token query parameter is found
  • Header format is incorrect (not Bearer <token>)

Implementation Examples

JavaScript/Fetch

const token = localStorage.getItem('access_token');

fetch('https://your-domain.com/api/bootstrap', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
})
.then(response => response.json())
.then(data => console.log(data));

PHP/cURL

$ch = curl_init('https://your-domain.com/api/bootstrap');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $token,
    'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

Python/Requests

import requests

headers = {
    'Authorization': f'Bearer {token}',
    'Content-Type': 'application/json'
}

response = requests.get('https://your-domain.com/api/bootstrap', headers=headers)
data = response.json()

Best Practices

Secure Storage

Store tokens in secure, HTTP-only cookies or encrypted local storage

HTTPS Only

Always transmit tokens over HTTPS to prevent interception

Token Refresh

Implement token refresh logic before expiration

Logout Handling

Clear tokens from client storage on logout

Authentication Guide

Learn how to authenticate and obtain JWT tokens

Build docs developers (and LLMs) love