The system uses OpenID Connect (OIDC) for user authentication, providing secure login through an external identity provider. Users authenticate once and receive a JWT token for subsequent API requests.
We make use of the Authorization Code flow in OIDC for user authentication. The general flow for this is as follows:
- User initiates login with backend application.
- Backend application generates a redirect to the Identity Provider (IdP) which contains the following information:
- Client ID (to identify the application)
- Redirect URI (where to send the user back to after authentication)
- Requested Scopes (what details about the user the app needs)
- Response Type (identifies the flow type
codefor our purposes) - State value (to protect against CSRF)
- Optionally: Nonce value (to bind an authentication request to a specific user session)
- User follows the redirect url to the identity provider; completes authentication, and consents to sharing their information with the specified application.
- User is sent back to application with the authorization code.
- Backend application exchanges authorization code and client secret with the Identity Provider for an access token and id token.
This is generally where the code authentication flow ends, with the application (client) having confirmed that the user is authenticated and given some information about the user (from the ID token). In our application, we extend this to fit our client's requirements.
After our application obtains the id token, we expose it to the user so that the frontend of our application can store it and use it as a Bearer token for future requests. (ID tokens are JWTs signed by the Identity Provider which can be cryptographically verified given the Identity Provider's JSON Web Keys)
spring.security.oauth2.client:
provider.oidc:
issuer-uri: https://auth.example.com
authorization-uri: https://auth.example.com/api/oidc/authorization
token-uri: https://auth.example.com/api/oidc/token
user-info-uri: https://auth.example.com/api/oidc/userinfo
jwk-set-uri: https://auth.example.com/jwks.json
registration.oidc:
client-id: endpoint-insights
client-secret: your-secret-here
redirect-uri: http://localhost:8080/login/oauth2/code/oidc
authorization-grant-type: authorization_code
scope: openid,profile,email,groups
client-name: OIDCThe system expects these claims in the ID token:
sub- User identifierpreferred_username- Display usernameemail- User email addressgroups- User group memberships for authorizationaud- Audience claim (must match OIDC client ID or allowed audiences)
Normally, ID tokens only optionally contain user details (username, email, groups, etc) and such information should normally be fetched from the Identity Provider's userinfo endpoint. However in this case, due to the requirement of deploying our application without an external session provider, such as Redis, we are required to expect that the ID token contains our required claims.
This was done to maintain the simplicity of using the Identity Provider's JWKS to ensure the validity of bearer tokens without implementing token minting and signing ourselves.
In a production application, user details should be fetched from the user info endpoint on the first login and stored in a persistent data store. Then future authentications would use the combination of iss and sub claims to uniquely identify a user and correlate that user with their associated information in the persistent data store. This could be done in our application, but it was determined to add too much complexity for minimal benefit.
Handles successful OIDC authentication:
- Validates OIDC user and ID token
- Extracts required claims (username, email)
- Returns JSON response with token details
- Provides token expiration information
Validates bearer tokens from authenticated users (see JWT Authorization for details):
- Allows users to access public endpoints without authorization
- Validates Authorization header and extracts Bearer token
- Validates the Bearer token contains the proper claims (sub, username, email, aud)
- Validates audience claim matches OIDC client ID or allowed audiences
- Determines if the Bearer token is authorized to access the requested resource
- Validates that the Bearer token is signed by a trusted JSON Web Key
- Populates the CurrentUser class for request context
Configures Spring Security for OIDC integration:
- Client registration with provider details
- OAuth2 login flow with custom success handler
- Spring Security Filter Chain
- CSRF disabled for stateless JWT API
- Session creation only for OAuth2 authorization code flow
- ID token signature verified against OIDC provider
- Required claims presence validated
- Token expiration enforced
- Malformed authentication responses rejected with 400
These endpoints bypass authentication:
/login/**- Authentication flows/oauth2/**- OAuth2 callbacks/api/health- Health check (see JWT Authorization)
# This will redirect to the identity provider
curl -v http://localhost:8080/oauth2/authorization/oidcThe response will be a 302 redirect to your OIDC provider's authorization endpoint with query parameters:
client_idredirect_uriresponse_type=codescope=openid profile email groupsstate(CSRF protection)
Since the OIDC flow requires browser interaction:
- Open browser to
http://localhost:8080/oauth2/authorization/oidc - Authenticate with identity provider
- Grant consent if prompted
- You will be redirected back to the application
After successful authentication, the success handler returns JSON:
{
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresAt": 1234567890,
"username": "testuser",
"email": "test@example.com"
}You can decode the JWT token (without verification) to inspect claims:
# Extract the payload (second part) of the JWT
echo "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." | cut -d. -f2 | base64 -d | jqExpected claims:
{
"sub": "user-id-123",
"preferred_username": "testuser",
"email": "test@example.com",
"groups": ["ei:write"],
"aud": ["endpoint-insights"],
"iss": "https://auth.example.com",
"exp": 1234567890
}If the ID token is missing required claims, the success handler will return 400:
{
"error": "Bad Request",
"status": 400
}Check server logs for specific error messages:
- "ID token missing preferred_username"
- "ID token missing email"
- "ID token missing expiry time"
If the OIDC provider configuration is incorrect:
# Check logs for JWT decoder initialization
# Should see: "Initialized JWT decoder with JWKS URI: https://..."- Add Proof-Key Code Exchange (PKCE) to improve security between backend application and identity provider
- Add token refresh flow to improve user experience
- Add comprehensive audit logging (successful/failed auth attempts, token validations)
- Implement logout endpoint to invalidate sessions