TGM Expert provides comprehensive authentication and authorization features including JWT-based authentication, SSO integration, two-factor authentication, and granular role-based access control.

Multi-Tenant Context

All authentication in TGM Expert operates within a multi-tenant architecture:

  • Client (Database): Each organization has their own isolated database
  • Sandbox (Schema): Optional development/testing environments within a client's database
  • Company: Organizational unit within a client for user grouping and configuration

Required Headers

Header Required Description
Authorization Yes JWT token or API token
X-Client-ID Yes* Client identifier (or use subdomain)
X-Tenant-ID No Sandbox identifier (omit for production)

*Can be resolved via subdomain: acme-corp.tgm-expert.com

Table of Contents


Overview

Authentication Flow

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Client    │────>│   Auth      │────>│  Protected  │
│             │     │   Server    │     │   Resource  │
└─────────────┘     └─────────────┘     └─────────────┘
       │                   │                   │
       │ 1. Login request  │                   │
       │──────────────────>│                   │
       │                   │                   │
       │ 2. JWT token      │                   │
       │<──────────────────│                   │
       │                   │                   │
       │ 3. Request + JWT  │                   │
       │───────────────────────────────────────>
       │                   │                   │
       │ 4. Response       │                   │
       │<───────────────────────────────────────

Supported Authentication Methods

Method Use Case
Username/Password Standard user login
SSO (SAML/OIDC) Enterprise integration
TOTP (2FA) Enhanced security
API Token Programmatic access

Authentication Methods

Standard Login

Login is client-scoped. Include the client identifier via header or subdomain:

# Via header
curl -X POST https://api.tgm-expert.com/auth/local \
  -H "Content-Type: application/json" \
  -H "X-Client-ID: acme-corp" \
  -d '{
    "identifier": "user@example.com",
    "password": "your_password"
  }'

# Via subdomain
curl -X POST https://acme-corp.tgm-expert.com/auth/local \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "user@example.com",
    "password": "your_password"
  }'

Response includes sandbox information:

{
  "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
  "user": {
    "id": 1,
    "email": "user@example.com",
    "firstName": "John",
    "lastName": "Doe",
    "role": {
      "id": 2,
      "name": "Manager",
      "type": "MANAGER"
    }
  },
  "sandbox": {
    "id": 0,
    "name": "Production",
    "slug": "main",
    "schemaName": "public",
    "isMain": true
  }
}

Registration

curl -X POST https://api.tgm-expert.com/auth/local/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "newuser@example.com",
    "password": "secure_password123",
    "firstName": "Jane",
    "lastName": "Smith"
  }'

Password Reset

The password reset flow allows users to recover access to their account when they've forgotten their password.

Flow Overview

┌─────────────────────────────────────────────────────────────────┐
│                    Password Reset Flow                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. User requests reset    POST /auth/forgot-password           │
│         │                                                        │
│         ▼                                                        │
│  2. System generates UUID token and stores it                   │
│         │                                                        │
│         ▼                                                        │
│  3. Email sent with reset link (token valid for 60 minutes)     │
│         │                                                        │
│         ▼                                                        │
│  4. User clicks link → Frontend reset page                      │
│         │                                                        │
│         ▼                                                        │
│  5. User submits new password  POST /auth/reset-password        │
│         │                                                        │
│         ▼                                                        │
│  6. Token validated, password updated, token cleared            │
│         │                                                        │
│         ▼                                                        │
│  7. Confirmation email sent, user can login                     │
└─────────────────────────────────────────────────────────────────┘

Step 1: Request Password Reset

Endpoint: POST /auth/forgot-password

Request:

curl -X POST https://api.tgm-expert.com/auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com"
  }'

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | email | string | Yes | User's registered email address |

Response (Success - 200 OK):

{
  "success": true,
  "data": "If an account exists with this email, a password reset link has been sent."
}

Security Notes: - Always returns success to prevent email enumeration attacks - No indication whether the email exists in the system - Rate limited to 3 requests per hour per email - Reset token expires after 60 minutes

Email Sent: The user receives an email containing: - Reset link: https://app.tgm-expert.com/reset-password?code={token} - Token expiration time - Request timestamp and IP address - Security warning if they didn't request the reset

Step 2: Complete Password Reset

Endpoint: POST /auth/reset-password

Request:

curl -X POST https://api.tgm-expert.com/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{
    "code": "550e8400-e29b-41d4-a716-446655440000",
    "password": "NewSecureP@ssw0rd",
    "passwordConfirmation": "NewSecureP@ssw0rd"
  }'

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | code | string | Yes | Reset token from email | | password | string | Yes | New password (min 8 characters) | | passwordConfirmation | string | Yes | Must match password |

Response (Success - 200 OK):

{
  "success": true,
  "data": "Password has been reset successfully. You can now login with your new password."
}

Response (Error - 400 Bad Request):

{
  "success": false,
  "message": "Invalid or expired reset code"
}

{
  "success": false,
  "message": "Password confirmation does not match"
}
{
  "success": false,
  "message": "Password must be at least 8 characters"
}

Password Requirements

Requirement Value
Minimum length 8 characters
Confirmation Must match password field

Recommended (Frontend Validation): - At least one uppercase letter - At least one lowercase letter - At least one number - At least one special character

Error Codes

Error HTTP Status Description
Invalid or expired reset code 400 Token doesn't exist or has expired
Password confirmation does not match 400 password ≠ passwordConfirmation
Password must be at least 8 characters 400 Password too short
Email is required 400 Missing email in request

Audit Trail

All password reset actions are logged: - PASSWORD_RESET_REQUESTED - When forgot-password is called - PASSWORD_RESET_COMPLETED - When reset-password succeeds

Logged metadata includes: - User ID and username - Request IP address - Timestamp

Frontend Integration (Angular)

// password-reset.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { environment } from '../environments/environment';

export interface ForgotPasswordRequest {
  email: string;
}

export interface ResetPasswordRequest {
  code: string;
  password: string;
  passwordConfirmation: string;
}

export interface ApiResponse<T> {
  success: boolean;
  data?: T;
  message?: string;
}

@Injectable({
  providedIn: 'root'
})
export class PasswordResetService {
  private baseUrl = environment.apiUrl;

  constructor(private http: HttpClient) {}

  forgotPassword(email: string): Observable<void> {
    return this.http.post<ApiResponse<string>>(`${this.baseUrl}/auth/forgot-password`, { email })
      .pipe(
        map(() => undefined),
        catchError(this.handleError)
      );
  }

  resetPassword(code: string, password: string, passwordConfirmation: string): Observable<void> {
    return this.http.post<ApiResponse<string>>(`${this.baseUrl}/auth/reset-password`, {
      code,
      password,
      passwordConfirmation
    }).pipe(
      map(() => undefined),
      catchError(this.handleError)
    );
  }

  private handleError(error: HttpErrorResponse) {
    let errorMessage = 'An error occurred';
    if (error.error?.message) {
      errorMessage = error.error.message;
    } else if (error.status === 400) {
      errorMessage = 'Invalid request';
    }
    return throwError(() => new Error(errorMessage));
  }
}

// forgot-password.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { PasswordResetService } from '../services/password-reset.service';

@Component({
  selector: 'app-forgot-password',
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input type="email" formControlName="email" placeholder="Enter your email">
      <button type="submit" [disabled]="loading || form.invalid">
        {{ loading ? 'Sending...' : 'Send Reset Link' }}
      </button>
      <div *ngIf="error" class="error">{{ error }}</div>
      <div *ngIf="success" class="success">
        If an account exists with this email, a password reset link has been sent.
      </div>
    </form>
  `
})
export class ForgotPasswordComponent {
  form: FormGroup;
  loading = false;
  error: string | null = null;
  success = false;

  constructor(
    private fb: FormBuilder,
    private passwordResetService: PasswordResetService
  ) {
    this.form = this.fb.group({
      email: ['', [Validators.required, Validators.email]]
    });
  }

  onSubmit(): void {
    if (this.form.invalid) return;

    this.loading = true;
    this.error = null;

    this.passwordResetService.forgotPassword(this.form.value.email).subscribe({
      next: () => {
        this.success = true;
        this.loading = false;
      },
      error: (err) => {
        this.error = err.message;
        this.loading = false;
      }
    });
  }
}

// reset-password.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { PasswordResetService } from '../services/password-reset.service';

@Component({
  selector: 'app-reset-password',
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input type="password" formControlName="password" placeholder="New password">
      <input type="password" formControlName="passwordConfirmation" placeholder="Confirm password">
      <button type="submit" [disabled]="loading || form.invalid">
        {{ loading ? 'Resetting...' : 'Reset Password' }}
      </button>
      <div *ngIf="error" class="error">{{ error }}</div>
    </form>
  `
})
export class ResetPasswordComponent implements OnInit {
  form: FormGroup;
  loading = false;
  error: string | null = null;
  private code: string = '';

  constructor(
    private fb: FormBuilder,
    private route: ActivatedRoute,
    private router: Router,
    private passwordResetService: PasswordResetService
  ) {
    this.form = this.fb.group({
      password: ['', [Validators.required, Validators.minLength(8)]],
      passwordConfirmation: ['', [Validators.required]]
    });
  }

  ngOnInit(): void {
    this.code = this.route.snapshot.queryParams['code'] || '';
  }

  onSubmit(): void {
    if (this.form.invalid) return;

    const { password, passwordConfirmation } = this.form.value;
    this.loading = true;
    this.error = null;

    this.passwordResetService.resetPassword(this.code, password, passwordConfirmation).subscribe({
      next: () => {
        this.router.navigate(['/login'], {
          queryParams: { message: 'Password reset successful. Please login.' }
        });
      },
      error: (err) => {
        this.error = err.message;
        this.loading = false;
      }
    });
  }
}

Configuration

Token expiration can be configured in application.yml:

app:
  security:
    password-reset:
      token-expiry-minutes: 60  # Default: 60 minutes

Email Verification

curl -X GET "https://api.tgm-expert.com/auth/email-confirmation?confirmation=token_from_email"

Change Password (Authenticated Users)

Allows authenticated users to change their password when they know their current password.

Endpoint: POST /api/custom/change-password

Authentication: Required (Bearer Token)

Request:

curl -X POST https://api.tgm-expert.com/api/custom/change-password \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "currentPassword": "OldP@ssw0rd",
    "password": "NewSecureP@ssw0rd",
    "passwordConfirmation": "NewSecureP@ssw0rd"
  }'

Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | currentPassword | string | Yes | User's current password | | password | string | Yes | New password (min 6 characters) | | passwordConfirmation | string | Yes | Must match password |

Response (Success - 200 OK):

{
  "success": true,
  "data": null
}

Response (Error - 400 Bad Request):

{
  "success": false,
  "message": "Current password is incorrect"
}

{
  "success": false,
  "message": "Password confirmation does not match"
}

Security Features: - Requires current password verification - Audit logged as PASSWORD_CHANGE - Confirmation email sent to user - Email includes timestamp, IP address, and device info


JWT Authentication

Token Structure

TGM Expert uses JSON Web Tokens (JWT) with the following claims:

{
  "sub": "1",
  "email": "user@example.com",
  "role": "MANAGER",
  "companyId": 1,
  "iat": 1706745600,
  "exp": 1706832000
}

Note: The JWT is issued within a specific client's database context. The client is determined at login time via X-Client-ID header or subdomain.

Using JWT

Include the JWT in the Authorization header along with the client identifier:

# Production data access
curl -X GET https://api.tgm-expert.com/api/inspections \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "X-Client-ID: acme-corp"

# Sandbox data access
curl -X GET https://api.tgm-expert.com/api/inspections \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "X-Client-ID: acme-corp" \
  -H "X-Tenant-ID: sandbox_dev"

# Using subdomain (no X-Client-ID needed)
curl -X GET https://acme-corp.tgm-expert.com/api/inspections \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Token Expiration

Token Type Expiration
Access Token 24 hours
Refresh Token 7 days

Refresh Token

curl -X POST https://api.tgm-expert.com/auth/refresh-token \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "your_refresh_token"
  }'

Token Validation

The server validates: 1. Token signature 2. Expiration time 3. User existence 4. User account status (not blocked/deleted)


Single Sign-On (SSO)

Supported Protocols

Protocol Description
SAML 2.0 Enterprise SSO standard
OpenID Connect Modern OAuth-based SSO
OAuth 2.0 Social login providers

SAML Configuration

curl -X POST https://api.tgm-expert.com/api/admin/identity-providers \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Corporate SSO",
    "type": "SAML",
    "entityId": "https://idp.company.com",
    "ssoUrl": "https://idp.company.com/sso",
    "certificate": "-----BEGIN CERTIFICATE-----...",
    "companyId": 1,
    "isEnabled": true
  }'

OpenID Connect Configuration

curl -X POST https://api.tgm-expert.com/api/admin/identity-providers \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Azure AD",
    "type": "OIDC",
    "clientId": "your_client_id",
    "clientSecret": "your_client_secret",
    "issuerUrl": "https://login.microsoftonline.com/tenant/v2.0",
    "companyId": 1,
    "isEnabled": true
  }'

SSO Login Flow

  1. User clicks "Login with SSO"
  2. Redirect to IdP
  3. User authenticates with IdP
  4. Redirect back with SAML assertion or OIDC token
  5. TGM Expert validates and creates session
  6. JWT token issued

SSO Endpoints

Method Endpoint Description
GET /sso/providers List available SSO providers
GET /sso/login/{provider} Initiate SSO login
POST /sso/callback/saml SAML callback
GET /sso/callback/oidc OIDC callback

Two-Factor Authentication (2FA)

TOTP Setup

Enable 2FA

curl -X POST https://api.tgm-expert.com/auth/2fa/enable \
  -H "Authorization: Bearer $JWT_TOKEN"

Response:

{
  "secret": "JBSWY3DPEHPK3PXP",
  "qrCodeUrl": "otpauth://totp/TGM%20Expert:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=TGM%20Expert"
}

Verify and Confirm

curl -X POST https://api.tgm-expert.com/auth/2fa/verify \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "123456"
  }'

Login with 2FA

When 2FA is enabled, login returns a partial token:

{
  "requires2FA": true,
  "tempToken": "temporary_token..."
}

Complete with TOTP code:

curl -X POST https://api.tgm-expert.com/auth/2fa/authenticate \
  -H "Content-Type: application/json" \
  -d '{
    "tempToken": "temporary_token...",
    "code": "123456"
  }'

Disable 2FA

curl -X POST https://api.tgm-expert.com/auth/2fa/disable \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "123456"
  }'

Recovery Codes

When enabling 2FA, recovery codes are provided:

{
  "recoveryCodes": [
    "ABC12-DEF34",
    "GHI56-JKL78",
    "MNO90-PQR12"
  ]
}

Use a recovery code if TOTP device is lost:

curl -X POST https://api.tgm-expert.com/auth/2fa/recover \
  -H "Content-Type: application/json" \
  -d '{
    "tempToken": "temporary_token...",
    "recoveryCode": "ABC12-DEF34"
  }'

API Token Authentication

For programmatic access, use API tokens instead of user credentials.

Creating API Tokens

curl -X POST https://api.tgm-expert.com/api/admin/tokens \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CI/CD Pipeline Token",
    "description": "Used for automated deployments",
    "expiresAt": "2027-01-01T00:00:00Z",
    "isFullAccess": false,
    "permissions": [
      {
        "resourceName": "inspections",
        "canCreate": true,
        "canRead": true,
        "canUpdate": true,
        "canDelete": false
      }
    ]
  }'

Using API Tokens

curl -X GET https://api.tgm-expert.com/api/inspections \
  -H "Authorization: Bearer tgm_xxxxxxxxxxxxxxxxxxxx"

See API Tokens Documentation for full details.


Role-Based Access Control

Built-in Roles

Role Type Description
Super Admin SUPER_ADMIN Full system access
Admin ADMIN Company administration
Manager MANAGER Operations management
Operator OPERATOR Day-to-day operations
Viewer VIEWER Read-only access

Role Hierarchy

SUPER_ADMIN
    └── ADMIN
          └── MANAGER
                └── OPERATOR
                      └── VIEWER

Checking Roles

In controllers, use @PreAuthorize:

@GetMapping("/sensitive-data")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> getSensitiveData() { ... }

@PostMapping("/inspection")
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER', 'OPERATOR')")
public ResponseEntity<?> createInspection() { ... }

Permission System

Granular Permissions

Permissions follow the pattern: {resource}:{action}

Action Description
create Create new records
read View records
update Modify records
delete Remove records
publish Publish/approve records

Permission Examples

Permission Description
inspections:create Create inspections
inspections:read View inspections
work_orders:update Update work orders
users:delete Delete users

Checking Permissions

@GetMapping("/inspections")
@PreAuthorize("hasAuthority('inspections:read')")
public ResponseEntity<?> listInspections() { ... }

Permission Service

@Autowired
private PermissionService permissionService;

public void someMethod() {
    if (permissionService.canCreate("inspections")) {
        // User can create inspections
    }

    if (permissionService.canRead("work_orders")) {
        // User can view work orders
    }
}

Role-Based Permissions

Roles have default permission sets:

Super Admin

  • All permissions on all resources

Admin

  • All permissions except system configuration

Manager

  • Full access to operational resources
  • Read-only for user management

Operator

  • Create/update operational data
  • Read-only for configuration

Viewer

  • Read-only access to all resources

Security Best Practices

1. Use HTTPS

All API communication should use HTTPS.

2. Secure Token Storage

  • Store tokens securely (HttpOnly cookies, secure storage)
  • Never expose tokens in URLs or logs

3. Token Rotation

  • Implement regular token rotation
  • Revoke tokens when no longer needed

4. Enable 2FA

Encourage or require 2FA for admin accounts.

5. Audit Logging

All authentication events are logged: - Login attempts (success/failure) - Password changes - 2FA enable/disable - API token usage

6. Rate Limiting

Endpoint Limit
/auth/local 5/minute
/auth/forgot-password 3/hour
/auth/2fa/authenticate 5/minute

7. Session Management

  • Sessions expire after inactivity
  • Concurrent session limits apply
  • Force logout on password change

Error Responses

Authentication Errors

Status Code Description
401 UNAUTHORIZED No token provided
401 TOKEN_EXPIRED Token has expired
401 INVALID_TOKEN Token is invalid
401 INVALID_CREDENTIALS Wrong username/password
403 FORBIDDEN Insufficient permissions
403 2FA_REQUIRED 2FA code required
423 ACCOUNT_LOCKED Account is locked

Error Response Format

{
  "error": {
    "status": 401,
    "code": "INVALID_CREDENTIALS",
    "message": "Invalid email or password"
  }
}