Skip to content

IDP Server-Side Implementation Overview

Introduction

The Identity Provider (IDP) server-side implementation provides secure JWT-based authentication and authorization for headless commerce applications. It consists of two main modules:

  • idp-server - Validates and processes JWT tokens from external IDP services
  • idp-client - Handles OAuth2 client credentials flow and token caching

Architecture Overview

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   External IDP  │    │   IDP Server     │    │   IDP Client    │
│   (Auth Server) │◄───┤   (Validator)    │◄───┤   (Token Mgmt)  │
└─────────────────┘    └──────────────────┘    └─────────────────┘
        │                        │                        │
        ├─ JWT Tokens            ├─ Security Config       ├─ OAuth2 Flow
        ├─ JWK Keys              ├─ JWT Validation        ├─ Token Caching
        └─ Device Registration   ├─ Offline Support       └─ API Client
                                 └─ WebSocket Security

IDP Server Module (headless/core/idp-server)

Core Components

1. IdpAutoConfiguration

@AutoConfiguration
@EnableConfigurationProperties(IdpProperties.class)
@Import({SecurityConfig.class, IdpWebsocketInterceptor.class})
public class IdpAutoConfiguration
- Entry point for auto-configuration - Enables IDP properties binding - Imports security and WebSocket configurations

2. IdpProperties

Configuration properties for IDP server:

@ConfigurationProperties(prefix = "secured.idp")
public class IdpProperties {
    private boolean enabled = false;
    private String baseUrl;
    private String authPoolId;
    private String jwksCacheFile;
    private int httpTimeoutInMs = 2000;
    private int offlineExpirationLeniencyInMinutes = 4320; // 3 days
}

Security Framework

1. SecurityConfig

Main security configuration that: - Configures OAuth2 Resource Server with JWT validation - Sets up public endpoints (websockets, static assets) - Implements localhost bypass for development - Configures custom JWT decoder with ES256 algorithm support

Key features:

@Bean
public JwtDecoder jwtDecoder() throws Exception {
    // Custom JWT processor with ES256 support
    ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
    jwtProcessor.setJWTClaimsSetVerifier(new LenientJWTClaimsVerifier());

    // File-cached JWK source for offline support
    JWKSource<SecurityContext> jwkSource = new FileCachingJwkSource(
        new URL(jwkSetUri), cacheFile, httpTimeoutInMs);

    // Lenient expiration validation for offline scenarios
    jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
        new IgnoringTimestampFailuresValidator(JwtValidators.createDefault()),
        new LenientExpirationValidator(leewayDuration, idpAvailabilityChecker)
    ));
}

2. FileCachingJwkSource

Provides resilient JWK (JSON Web Key) management: - Primary: Fetches JWK sets from remote IDP - Fallback: Uses local file cache when remote is unavailable - Auto-caching: Saves successful fetches to local file system

@Override
public List<JWK> get(JWKSelector jwkSelector, SecurityContext context) {
    try {
        // Try remote first
        List<JWK> jwkSet = remoteJwkSet.get(jwkSelector, context);
        saveToCache(cachedSet); // Cache successful fetch
        return jwkSet;
    } catch (Exception e) {
        // Fallback to local cache
        return loadFromCache().select(jwkSelector);
    }
}

Offline Support & Resilience

1. IdpAvailabilityChecker

Determines if external IDP is reachable:

public boolean isIdpOnline() {
    try {
        HttpURLConnection connection = (HttpURLConnection) new URL(jwkSetUri).openConnection();
        connection.setRequestMethod("HEAD");
        connection.setConnectTimeout(timeout);
        return connection.getResponseCode() == 200;
    } catch (Exception e) {
        return false;
    }
}

2. LenientExpirationValidator

Implements smart token expiration handling: - Online Mode: Standard JWT expiration validation - Offline Mode: Allows tokens to be valid beyond expiration within configured leeway period - Hybrid: Automatically switches based on IDP availability

@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
    Instant now = clock.instant();
    Instant exp = jwt.getExpiresAt();

    if (now.isBefore(exp)) {
        return OAuth2TokenValidatorResult.success(); // Still valid
    }

    boolean idpOnline = idpChecker.isIdpOnline();
    if (!idpOnline && now.isBefore(exp.plus(leeway))) {
        return OAuth2TokenValidatorResult.success(); // Allow leeway if offline
    }

    return failure("JWT expired");
}

3. LenientJWTClaimsVerifier & IgnoringTimestampFailuresValidator

Additional validation components that provide flexible JWT processing for edge cases and offline scenarios.


IDP Client Module (headless/core/idp-client)

Core Components

1. IdpClientAutoConfiguration

@AutoConfiguration
@EnableConfigurationProperties(IdpClientProperties.class)
@Import({SecureClientConfig.class, NoSecurityConfig.class})
Auto-configuration for IDP client functionality.

2. IdpClientProperties

Configuration for client-side token management:

@ConfigurationProperties(prefix = "secured.idp")
public class IdpClientProperties {
    private int offlineExpirationLeniencyInMinutes = 4320;
}

OAuth2 Client Implementation

1. SecureClientConfig

Configures OAuth2 client credentials flow:

@Bean
public OAuth2AuthorizedClientManager oauth2AuthorizedClientManager(...) {
    OAuth2AuthorizedClientProvider provider = OAuth2AuthorizedClientProviderBuilder.builder()
        .clientCredentials() // Client credentials grant type
        .build();

    AuthorizedClientServiceOAuth2AuthorizedClientManager manager = 
        new AuthorizedClientServiceOAuth2AuthorizedClientManager(repo, service);
    manager.setAuthorizedClientProvider(provider);
    return manager;
}

2. JwtTokenRetriever

Simple token retrieval service:

public String retrieve() {
    OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest
        .withClientRegistrationId("server")
        .principal("system")
        .build();

    OAuth2AuthorizedClient client = clientManager.authorize(request);
    return client.getAccessToken().getTokenValue();
}

Token Caching Strategy

1. CachingOAuth2Interceptor

HTTP interceptor that provides intelligent token management: - Primary: Attempts to fetch fresh tokens from OAuth2 server - Fallback: Uses cached tokens when server is unavailable - Validation: Checks token expiration with configured leeway

private String fetchOrLoadToken() {
    try {
        // Try to get fresh token
        OAuth2AuthorizedClient client = delegate.authorize(authorizeRequest);
        if (client != null) {
            String tokenValue = client.getAccessToken().getTokenValue();
            tokenCacheStrategy.saveToken(tokenValue); // Cache it
            return tokenValue;
        }
    } catch (Exception e) {
        // Fall back to cached token
    }

    String cachedToken = tokenCacheStrategy.loadToken();
    if (cachedToken != null && isTokenStillValid(cachedToken)) {
        return cachedToken;
    }

    throw new IllegalStateException("No valid access token available");
}

2. Token Cache Strategies

FileTokenCacheStrategy: - Secure storage for production environments - Generates a new AES key and encrypts the cached jwt for long term storage - To enable set secured.idp-token-cache.type=file which is the default behavior - To secure set an environment variable for the password SECURED_IDP_TOKEN_CACHE_PASSWORD=your-secure-password

NoopTokenCacheStrategy: - No-op implementation for testing/development or non-IDP enabled environments - Tokens are not persisted between application restarts

Device Management API

1. IdpApiClient

RESTful client for device registration and management:

public class IdpApiClient {
    // Device registration
    public CreateDeviceResponse createDevice(String orgId, String envId, CreateDeviceRequest request)

    // Client app creation
    public CreateClientAppIdResponse createClientAppId(String orgId, String envId, String deviceId, CreateClientAppIdRequest request)

    // OTP generation
    public CreateOtpResponse createOneTimePassword(String orgId, String envId, String deviceId, String deviceClientAppId)

    // Device activation
    public ActivateResponse activate(ActivateRequest request)
}

2. API Models

Data transfer objects for device registration flow: - CreateDeviceRequest/Response - CreateClientAppIdRequest/Response - CreateOtpResponse - ActivateRequest/Response


Configuration Example

secured:
    api:
        enabled: false
    idp:
        enabled: true
        baseUrl: https://idp.env.jumpmind.cloud
        authPoolId: client-pool-id
        organizationId: org-id
        apiKey: api-key
        env: dev
        jwksCacheFile: ./work/jwks.json
        httpTimeoutInMs: 2000
        offlineExpirationLeniencyInMinutes: 4320
    idp-token-cache:
        type: file
        password: ${LOAD_SECURE_PASSWORD}
spring:
    security:
        oauth2:
            client:
                registration:
                    server:
                        client-id: client-id
                        client-secret: client-secret
                        authorization-grant-type: client_credentials
                        scope: device
                        provider: jmc-idp
                provider:
                    jmc-idp:
                        token-uri: ${secured.idp.baseUrl}/${secured.idp.authPoolId}/oauth2/token
            resourceserver:
                jwt:
                    jwk-set-uri: ${secured.idp.baseUrl}/${secured.idp.authPoolId}/.well-known/jwks.json