OIDC package notes and LinkedIn flow details: services/auth-service/.../oidc/README.md.
The Forge platform uses fully stateless JWT-based authentication across all modules. All
authentication flows return JWT tokens that are stored client-side and included in API requests via the
Authorization header.
See ADR-0011: Stateless JWT Authentication for the architectural decision.
@Secured annotation for all authenticationFrontend → POST /auth/login (backend-actor)
→ POST /auth/login (auth-service)
→ Cognito User Pool (authenticate)
→ JWT tokens returned (accessToken, idToken, refreshToken, expiresAt)
→ Frontend stores tokens in localStorage
→ Frontend includes Authorization: Bearer <token> in all API calls
Email and password sign-in uses POST /auth/login, which authenticates against the Cognito User Pool
via server-side APIs (for example InitiateAuth with username and password), not the OAuth2
authorization-code flow where the browser is redirected to Cognito’s authorize endpoint and returns with
a code. Cognito still issues OIDC-shaped JWTs; the difference is how the user proves their identity.
This approach was chosen because:
Authorization: Bearer.
Direct login returns tokens in the API response. Browser OIDC “web-app” flows typically add redirects,
callback URLs, and framework session or cookie behavior unless deliberately minimized.LinkedIn sign-in uses OAuth2 redirects and a server-side callback (see below). It is separate from Cognito email and password, which use form-based login only.
GET /auth/linkedin/login (auth-service)LinkedInLoginRedirectResource constructs OAuth2 authorization URL and redirects to LinkedIn/auth/linkedin/login/callback with authorization codeLinkedInLoginCallbackResource manually exchanges code for access token (LinkedIn doesn’t support Quarkus OIDC’s default flow)AuthUser domain modelTokenStore, user redirected to UI with tokenPOST /auth/tokens/exchangeSecurity Note: Temporary tokens are single-use and automatically invalidated after exchange. See Temporary Token Security section below.
Frontend → POST /auth/register (backend-actor)
→ POST /auth/register (auth-service)
→ Cognito User Pool (create user)
→ POST /actors/register (actor-service, directly from auth-service)
→ PostgreSQL (save user profile)
→ JWT tokens returned to frontend
All frontend requests route through backend-actor (port 8500), which proxies to appropriate services:
POST /auth/login → backend-actor → auth-servicePOST /auth/register → backend-actor → auth-servicePOST /auth/refresh-user-token → backend-actor → auth-serviceGET /auth/linkedin/login → backend-actor → auth-service (redirect)GET /actors/{id} → backend-actor → actor-servicePOST /resumes → backend-actor → document-serviceServices can make calls to other services using service JWTs:
auth-service → actor-service (with service JWT)actor-service → x-service (with service JWT)OAuth2/OIDC flows use temporary tokens as an intermediate step between OAuth callback and JWT token generation. These tokens provide an additional security layer:
How It Works:
TokenStore.generateToken() creates a cryptographically secure random token (32-byte UUID)AuthIdentity (TTL: 5 minutes)POST /auth/tokens/exchange with token in request body (not URL)TokenStore.exchangeToken() retrieves AuthIdentity from cacheSecurity Benefits:
Cache Implementation:
ServiceTokenCacheKeyGeneratorservice-token:${token}put (store), get (retrieve), invalidate (remove)Why Not Direct JWT Generation?:
All REST endpoints use JWT-based authentication supporting both user and service tokens:
Authorization: Bearer <token> headerTokenValidatorcustom:service_id claim) or user tokensUser or authenticatedServiceId in request context if valid@SecuredAuthenticationException if no user found@AllowedServicesAuthenticationException if service is not authorizedService accounts, service JWTs, client filters, and @AllowedServices are covered in
SERVICE_AUTHENTICATION.md (this document focuses on user-facing and
BFF auth flows).
Frontend applications handle authentication client-side:
/auth/login endpoint to get JWT tokensaccessToken, idToken, refreshToken, and expiresAt in localStorageAuthorization: Bearer <token> header in all API requestslibs/security)@Secured: Annotation to mark JAX-RS methods/classes requiring authentication@AllowedServices: Annotation to restrict endpoints to specific servicesTokenAuthenticationFilter: JAX-RS filter for token validation (supports both user and service tokens)UserTokenAuthorizationInterceptor: CDI interceptor that enforces @SecuredServiceTokenAuthorizationInterceptor: CDI interceptor that enforces @AllowedServicesUserTokenClientRequestFilter: Client filter that forwards user tokens to downstream servicesServiceTokenClientRequestFilter: Client filter that adds service tokens when no user token is presentTokenValidator: Interface for JWT token validation (validates both user and service tokens)ServiceAuthenticationProvider: Interface for service authenticationServiceTokenProvider: Interface for obtaining and caching service JWT tokensCognitoTokenValidator: Cognito implementation of TokenValidatorCachingServiceTokenProvider: Cognito implementation of ServiceTokenProvider with caching and automatic refreshservices/auth-service)AuthResource: Form-based login and registration endpointsTokenExchangeResource: Exchanges temporary tokens for user info (POST /auth/tokens/exchange)LinkedInLoginRedirectResource: Initiates LinkedIn OAuth2 flowLinkedInLoginCallbackResource: Handles LinkedIn OAuth2 login callback, generates temporary tokenTokenStore: Generates temporary tokens for OIDC flowsCognitoServiceAuthenticationProvider: Authenticates services with Cognito using service account credentialsapplications/backend-actor)AuthController: Proxies authentication requests to auth-serviceActorController: Actor profile endpointsDocumentController: Document endpointsLinkedInController: LinkedIn-related BFF routes| Variable | Description | Default |
|---|---|---|
COGNITO_ACTOR_POOL_ID |
AWS Cognito actor pool ID (for job seekers) | - |
COGNITO_ACTOR_CLIENT_ID |
AWS Cognito actor client ID | - |
COGNITO_ACTOR_CLIENT_SECRET |
AWS Cognito actor client secret | - |
COGNITO_SERVICE_POOL_ID |
AWS Cognito service pool ID (for service accounts) | - |
COGNITO_SERVICE_CLIENT_ID |
AWS Cognito service client ID | - |
COGNITO_SERVICE_CLIENT_SECRET |
AWS Cognito service client secret | - |
COGNITO_SERVICE_ACCOUNT_USERNAME |
Service account username (e.g., service-document-service) |
- |
COGNITO_SERVICE_ACCOUNT_PASSWORD |
Service account password | - |
AWS_REGION |
AWS region | us-west-2 |
LINKEDIN_OAUTH2_CLIENT_ID |
LinkedIn OAuth2 client ID (from LinkedIn developer app) | - |
LINKEDIN_OAUTH2_CLIENT_SECRET |
LinkedIn OAuth2 client secret (from LinkedIn developer app) | - |
LINKEDIN_REFRESH_TOKEN_ENCRYPTION_KEY |
Base64 AES-256 key for encrypting stored LinkedIn refresh tokens (app-generated; not from LinkedIn) | - |
The system uses Quarkus OIDC multi-tenant configuration for OAuth2 flows.
Authoritative copy:
config/src/main/resources/oidc.properties.
In short: application-type=service - no Quarkus OIDC authorization-code redirect; human login
is POST /auth/login (Cognito InitiateAuth); API JWT validation uses the security stack
(CompositeTokenValidator / TokenAuthenticationFilter), not a browser redirect to Cognito.
quarkus.oidc.linkedin.tenant-enabled=false. LinkedIn OAuth2 uses custom JAX-RS resources under
services/auth-service/.../oidc/linkedin/, not the Quarkus OIDC redirect flow. Remaining
quarkus.oidc.linkedin.* properties are read via @ConfigProperty where needed.
The default tenant points at the Cognito issuer with application-type=service (no Quarkus OIDC
web-app redirect flow). LinkedIn tenant is disabled (quarkus.oidc.linkedin.tenant-enabled=false)
because LinkedIn OAuth2 is handled manually via custom callback resources, not using Quarkus OIDC’s
automatic flow.
POST /auth/login - Form-based login (returns JWT tokens)POST /auth/register - Registration (returns JWT tokens)POST /auth/tokens/exchange - Exchange temporary token for user info (used in OIDC flows)POST /auth/tokens/refresh - Refresh access token using refresh tokenGET /auth/linkedin/login - Initiates LinkedIn OAuth2 flow (redirects to LinkedIn)@Secured)All other endpoints require @Secured annotation and valid JWT token in Authorization: Bearer <token> header.
The authentication system implements a zero-trust security model where:
✅ Every service call is authenticated - Services must have valid JWTs (user or service tokens)
✅ Service identity verification - Receiving services know which service is calling via custom:service_id claim
✅ Service-level authorization - Fine-grained control with @AllowedServices annotation
✅ No trusted network assumptions - Services verify each other’s identity regardless of network location
✅ Credential isolation - Service credentials are separate from user credentials
✅ Automatic token management - Service tokens are cached and refreshed automatically
@AllowedServices@Secured annotation everywhereFor detailed implementation instructions, see: