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 Methods
- JWT Authentication
- Single Sign-On (SSO)
- Two-Factor Authentication (2FA)
- API Token Authentication
- Role-Based Access Control
- Permission System
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¶
- User clicks "Login with SSO"
- Redirect to IdP
- User authenticates with IdP
- Redirect back with SAML assertion or OIDC token
- TGM Expert validates and creates session
- 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"
}
}