Skip to content

Authentication

Implementation status

Authentication is a planned component. The master_key field in the configuration is the foundation; the JWT issuance and validation logic is not yet implemented. This page documents the intended design.


Purpose

Authentication answers the question: who is making this request? Every non-public endpoint must establish an identity before allowing the request to proceed.


Responsibilities

  • Issue signed tokens on successful login
  • Validate tokens on every protected request
  • Inject the authenticated identity into the request context
  • Refresh tokens before they expire
  • Invalidate tokens on logout

Intended Design: JWT with master_key

EERP uses stateless JWT authentication. The master_key from eerp-config.json is the HMAC signing key. No external identity provider is required for basic deployments.

sequenceDiagram
    participant Client
    participant AuthHandler
    participant DB as PostgreSQL
    participant JWT as JWT Library

    Client->>AuthHandler: POST /api/auth/login {email, password}
    AuthHandler->>DB: SELECT user WHERE email=$1
    DB-->>AuthHandler: User row
    AuthHandler->>AuthHandler: bcrypt.CompareHashAndPassword
    AuthHandler->>JWT: Sign({sub: user_id, tenant: tenant_id, roles: [...], exp: now+1h})
    JWT-->>AuthHandler: access_token (signed with master_key)
    AuthHandler->>JWT: Sign({sub: user_id, exp: now+7d})
    JWT-->>AuthHandler: refresh_token
    AuthHandler-->>Client: {access_token, refresh_token}

Token Payload

{
    "sub": "01J...",
    "tenant": "01J...",
    "roles": ["admin", "crm:write"],
    "iat": 1705312200,
    "exp": 1705315800
}
Claim Description
sub User ID (UUID)
tenant Tenant/organisation ID
roles Effective roles for permission checks
iat Issued at
exp Expiry (access: 1h, refresh: 7d)

Validation Middleware

On every protected request:

flowchart TD
    A["Extract Authorization header"] --> B{Bearer token present?}
    B -- No --> C["401 Unauthorized"]
    B -- Yes --> D["jwt.Parse(token, masterKey)"]
    D --> E{Valid signature?}
    E -- No --> F["401 Unauthorized"]
    E --> G{Expired?}
    G -- Yes --> H["401 Unauthorized\n(hint: use refresh endpoint)"]
    G -- No --> I["Inject Identity into context"]
    I --> J["Next middleware / handler"]

Identity in Context

Downstream handlers and services retrieve the identity from context:

type Identity struct {
    UserID   uuid.UUID
    TenantID uuid.UUID
    Roles    []string
}

func IdentityFromContext(ctx context.Context) (Identity, bool) {
    id, ok := ctx.Value(identityKey{}).(Identity)
    return id, ok
}

Services use IdentityFromContext when they need to filter by tenant or check roles:

func (s *Service) ListContacts(ctx context.Context) ([]Contact, error) {
    identity, _ := auth.IdentityFromContext(ctx)
    return s.contacts.Query().
        Where(orm.Cond("tenant_id = $1", identity.TenantID)).
        All(ctx, s.db)
}

Multi-Tenancy

Every entity that belongs to a tenant has a tenant_id column. The authentication middleware injects the tenant ID from the token; services filter by it. The ORM provides no automatic tenant filter — services are responsible for applying the WHERE tenant_id = $1 condition.


Token Rotation

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: POST /api/auth/refresh {refresh_token}
    Server->>Server: Validate refresh_token (signature + expiry)
    Server->>Server: Issue new access_token (1h) + new refresh_token (7d)
    Server-->>Client: {access_token, refresh_token}
    note over Server: Old refresh_token is now invalid (rotation)

Refresh tokens are single-use. Presenting a used refresh token invalidates the entire session (theft detection).


Interactions

graph LR
    Config["eerp-config.json\n(master_key)"] -->|signs with| JWT["JWT Library"]
    AuthMiddleware -->|validates with| JWT
    AuthMiddleware -->|injects| Context["request context\n(Identity)"]
    LoginHandler -->|issues| JWT
    Services -->|reads from| Context
    Permissions -->|reads roles from| Context

Extension Points

Extension How
External IdP (OAuth2/OIDC) Replace LoginHandler with OIDC callback; map claims to Identity
Session-based auth Replace JWT with server-side session store; keep the Identity context contract
API keys Issue long-lived tokens with restricted roles; validate via same middleware
MFA Add a second factor check between password validation and token issuance