Complete guide for using the best-in-class OAuth2 client helper in the Firefly Common Client Library.
- Overview
- When to Use OAuth2 Helper
- Quick Start
- Advanced Configuration
- Client Credentials Flow
- Password Grant Flow
- Refresh Token Flow
- Token Caching
- Automatic Retry Logic
- Integration with REST Client
- Best Practices
- Complete Examples
The OAuth2ClientHelper provides a production-ready, enterprise-grade API for OAuth2 authentication flows with advanced features like automatic retry, intelligent token caching, and seamless integration with REST clients.
Key Features:
- ✅ Client Credentials Flow - Machine-to-machine authentication
- ✅ Password Grant Flow - User authentication (legacy systems)
- ✅ Refresh Token Flow - Automatic token refresh
- ✅ NEW: Multi-scope token caching - Cache tokens per scope
- ✅ NEW: Automatic retry with exponential backoff - Resilient token requests
- ✅ NEW: Configurable timeout and retry policies - Production-ready configuration
- ✅ NEW: Automatic refresh token management - Store and reuse refresh tokens
- ✅ Reactive programming with
Mono<T>- Non-blocking operations - ✅ Seamless integration with RestClient - Easy authentication
- ✅ Thread-safe token management - Concurrent request support
- ✅ ID Token support - OpenID Connect compatibility
Note: For production applications with complex OAuth2/OIDC needs (Authorization Code Flow, PKCE, Social Login), use Spring Security OAuth2 Client.
- You need machine-to-machine authentication (Client Credentials)
- You're authenticating users with username/password (Password Grant)
- You need to refresh access tokens
- You want simple OAuth2 integration with REST clients
- You're building microservice-to-microservice authentication
- You need Authorization Code Flow (use Spring Security OAuth2)
- You need PKCE support (use Spring Security OAuth2)
- You need OpenID Connect (use Spring Security OAuth2)
- You need social login (Google, Facebook, etc.)
import org.fireflyframework.client.oauth2.OAuth2ClientHelper;
import reactor.core.publisher.Mono;
// Create OAuth2 helper with default configuration
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(
"https://auth.example.com/oauth/token",
"your-client-id",
"your-client-secret"
);
// Get access token (automatically cached)
Mono<String> tokenMono = oauth2.getClientCredentialsToken();
tokenMono.subscribe(token -> {
System.out.println("Access Token: " + token);
});import org.fireflyframework.client.oauth2.OAuth2ClientHelper;
import org.fireflyframework.client.oauth2.OAuth2ClientHelper.OAuth2Config;
import java.time.Duration;
// Create advanced configuration
OAuth2Config config = OAuth2Config.builder()
.timeout(Duration.ofMinutes(2)) // Request timeout
.enableRetry(true) // Enable automatic retry
.maxRetries(3) // Max retry attempts
.retryBackoff(Duration.ofSeconds(1)) // Backoff between retries
.tokenExpirationBuffer(120) // Refresh 2 min before expiration
.defaultHeader("User-Agent", "MyApp/1.0") // Custom headers
.build();
// Create OAuth2 helper with advanced config
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(
"https://auth.example.com/oauth/token",
"your-client-id",
"your-client-secret",
config
);
// Get token with automatic retry on failures
oauth2.getClientCredentialsToken("api.read api.write")
.subscribe(token -> System.out.println("Token: " + token));import org.fireflyframework.client.RestClient;
import org.fireflyframework.client.ServiceClient;
RestClient restClient = ServiceClient.rest("api-service")
.baseUrl("https://api.example.com")
.build();
// Get token and make authenticated request
oauth2.getClientCredentialsToken().flatMap(token ->
restClient.get("/protected/resource", Resource.class)
.withHeader("Authorization", "Bearer " + token)
.execute()
).subscribe(resource -> {
System.out.println("Resource: " + resource);
});The OAuth2Config class provides advanced configuration options for production environments:
OAuth2Config config = OAuth2Config.builder()
.timeout(Duration.ofMinutes(2)) // Request timeout (default: 30s)
.enableRetry(true) // Enable automatic retry (default: false)
.maxRetries(3) // Max retry attempts (default: 3)
.retryBackoff(Duration.ofSeconds(1)) // Backoff between retries (default: 500ms)
.tokenExpirationBuffer(120) // Refresh buffer in seconds (default: 60s)
.defaultHeader("User-Agent", "MyApp/1.0") // Add custom headers
.defaultHeader("X-Custom", "value") // Multiple headers supported
.build();
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(
"https://auth.example.com/oauth/token",
"client-id",
"client-secret",
config
);| Option | Type | Default | Description |
|---|---|---|---|
timeout |
Duration |
30s |
Request timeout for token requests |
enableRetry |
boolean |
false |
Enable automatic retry on failures |
maxRetries |
int |
3 |
Maximum number of retry attempts |
retryBackoff |
Duration |
500ms |
Initial backoff duration (exponential) |
tokenExpirationBuffer |
int |
60 |
Seconds before expiration to refresh token |
defaultHeader |
String, String |
- | Add custom headers to all requests |
// Simple configuration (no retry, default timeout)
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(
"https://auth.example.com/oauth/token", // Token endpoint
"client-id", // Client ID
"client-secret" // Client secret
);The helper validates all required parameters:
// ✅ Valid configuration
new OAuth2ClientHelper("https://auth.example.com/token", "client-id", "secret");
// ❌ Invalid configurations (will throw IllegalArgumentException)
new OAuth2ClientHelper(null, "client-id", "secret"); // Null endpoint
new OAuth2ClientHelper("", "client-id", "secret"); // Empty endpoint
new OAuth2ClientHelper("https://...", null, "secret"); // Null client ID
new OAuth2ClientHelper("https://...", "", "secret"); // Empty client ID
new OAuth2ClientHelper("https://...", "client-id", null); // Null secret
new OAuth2ClientHelper("https://...", "client-id", ""); // Empty secretThe Client Credentials flow is used for machine-to-machine authentication.
// Get token without scope
Mono<String> token = oauth2.getClientCredentialsToken();
token.subscribe(accessToken -> {
System.out.println("Token: " + accessToken);
});// Get token with specific scope
Mono<String> token = oauth2.getClientCredentialsToken("read:users write:users");
token.subscribe(accessToken -> {
System.out.println("Token with scope: " + accessToken);
});// Get complete token response (includes expires_in, token_type, etc.)
Mono<TokenResponse> response = oauth2.getTokenResponse(
Map.of("grant_type", "client_credentials")
);
response.subscribe(tokenResponse -> {
System.out.println("Access Token: " + tokenResponse.getAccessToken());
System.out.println("Token Type: " + tokenResponse.getTokenType());
System.out.println("Expires In: " + tokenResponse.getExpiresIn());
System.out.println("Scope: " + tokenResponse.getScope());
});The Password Grant flow authenticates users with username and password.
Warning: This flow is considered less secure. Use Authorization Code Flow when possible.
// Authenticate user
Mono<String> token = oauth2.getPasswordGrantToken(
"john.doe@example.com", // Username
"password123", // Password
null // No scope
);
token.subscribe(accessToken -> {
System.out.println("User token: " + accessToken);
});// Authenticate with specific scope
Mono<String> token = oauth2.getPasswordGrantToken(
"john.doe@example.com",
"password123",
"profile email"
);
token.subscribe(accessToken -> {
System.out.println("User token: " + accessToken);
});Use refresh tokens to obtain new access tokens without re-authentication.
// Refresh access token with explicit refresh token
Mono<String> newToken = oauth2.refreshToken("refresh-token-here");
newToken.subscribe(accessToken -> {
System.out.println("New access token: " + accessToken);
});The helper automatically stores refresh tokens from password grant responses:
// Password grant returns a refresh token
oauth2.getPasswordGrantToken("user@example.com", "password", null)
.subscribe(token -> {
System.out.println("Access token: " + token);
// Refresh token is automatically stored internally
});
// Later, use automatic refresh
oauth2.autoRefreshToken()
.subscribe(newToken -> {
System.out.println("Refreshed token: " + newToken);
});
// Or get the cached refresh token
String refreshToken = oauth2.getCachedRefreshToken();
if (refreshToken != null) {
oauth2.refreshToken(refreshToken).subscribe(...);
}// Initial authentication
oauth2.getPasswordGrantToken("user@example.com", "password", null)
.flatMap(token -> {
// Use token for API calls
return makeApiCall(token);
})
.onErrorResume(error -> {
// If token expired, automatically refresh it
return oauth2.autoRefreshToken()
.flatMap(newToken -> makeApiCall(newToken));
})
.subscribe();The OAuth2 helper automatically caches tokens per scope to reduce unnecessary token requests.
Tokens are cached separately for each scope combination:
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(endpoint, clientId, clientSecret);
// Request token with scope "read"
oauth2.getClientCredentialsToken("read").subscribe(token1 -> {
System.out.println("Read token: " + token1);
});
// Request token with scope "write" - different cache entry
oauth2.getClientCredentialsToken("write").subscribe(token2 -> {
System.out.println("Write token: " + token2);
});
// Request token with scope "read" again - returns cached token
oauth2.getClientCredentialsToken("read").subscribe(token3 -> {
System.out.println("Cached read token: " + token3); // Same as token1
});OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(endpoint, clientId, clientSecret);
// First call - requests token from server
oauth2.getClientCredentialsToken().subscribe(token1 -> {
System.out.println("Token 1: " + token1);
});
// Second call - returns cached token (if not expired)
oauth2.getClientCredentialsToken().subscribe(token2 -> {
System.out.println("Token 2: " + token2); // Same as token1
});// Clear all cached tokens
oauth2.clearCache();
// Clear specific cached token by key
oauth2.clearCache("client_credentials:read write");
// Check cache size
int cacheSize = oauth2.getCacheSize();
System.out.println("Cached tokens: " + cacheSize);
// Check if a valid token is cached
boolean hasToken = oauth2.hasValidToken("client_credentials:");
if (!hasToken) {
// Request new token
oauth2.getClientCredentialsToken().subscribe(...);
}Tokens are considered expired before their actual expiration time:
OAuth2Config config = OAuth2Config.builder()
.tokenExpirationBuffer(120) // Refresh 2 minutes before expiration
.build();
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(endpoint, clientId, clientSecret, config);
// If token expires in 90 seconds, it's considered expired
// A new token will be requested automaticallyTokens are automatically invalidated based on the expires_in field from the OAuth2 server:
// Token cached with expiration
oauth2.getClientCredentialsToken().subscribe(token -> {
// Token is cached for duration specified by server (e.g., 3600 seconds)
});
// After expiration, new token is automatically requested
Thread.sleep(3700000); // Wait for expiration
oauth2.getClientCredentialsToken().subscribe(token -> {
// New token is fetched automatically
});The OAuth2 helper supports automatic retry with exponential backoff for resilient token requests.
The helper automatically retries on the following errors:
- 5xx Server Errors - Server-side failures (500, 502, 503, 504)
- 429 Too Many Requests - Rate limiting
- Timeout Exceptions - Request timeouts
- Connection Errors - Network connectivity issues
Non-retryable errors (fail immediately):
- 4xx Client Errors (except 429) - Invalid credentials, bad request
- OAuth2 Errors - Invalid grant, unsupported grant type
OAuth2Config config = OAuth2Config.builder()
.enableRetry(true) // Enable automatic retry
.maxRetries(3) // Max 3 retry attempts
.retryBackoff(Duration.ofSeconds(1)) // Start with 1s backoff (exponential)
.build();
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(
"https://auth.example.com/oauth/token",
"client-id",
"client-secret",
config
);
// Token requests will automatically retry on failures
oauth2.getClientCredentialsToken()
.subscribe(
token -> System.out.println("Token: " + token),
error -> System.err.println("Failed after retries: " + error)
);Exponential Backoff:
- Attempt 1: Immediate
- Attempt 2: Wait 1 second
- Attempt 3: Wait 2 seconds
- Attempt 4: Wait 4 seconds
OAuth2Config config = OAuth2Config.builder()
.enableRetry(true)
.maxRetries(5) // Up to 5 retries
.retryBackoff(Duration.ofMillis(500)) // Start with 500ms
.build();
// Retry schedule: 0ms, 500ms, 1000ms, 2000ms, 4000ms, 8000msFor advanced retry scenarios, use the getTokenWithRetry method:
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(endpoint, clientId, clientSecret);
// Custom retry logic
Mono<String> tokenWithRetry = oauth2.getTokenWithRetry(() ->
oauth2.getClientCredentialsToken("custom.scope")
);
tokenWithRetry.subscribe(token -> {
System.out.println("Token obtained with retry: " + token);
});// Production-ready configuration with retry
OAuth2Config config = OAuth2Config.builder()
.timeout(Duration.ofMinutes(2))
.enableRetry(true)
.maxRetries(3)
.retryBackoff(Duration.ofSeconds(1))
.tokenExpirationBuffer(120)
.build();
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(
"https://auth.example.com/oauth/token",
"client-id",
"client-secret",
config
);
// Resilient token request with automatic retry
oauth2.getClientCredentialsToken("api.read api.write")
.timeout(Duration.ofMinutes(5)) // Overall timeout including retries
.subscribe(
token -> {
// Success - use token
System.out.println("Token: " + token);
},
error -> {
// Failed after all retries
log.error("Failed to obtain token after retries", error);
}
);import org.fireflyframework.client.RestClient;
import org.fireflyframework.client.ServiceClient;
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(tokenEndpoint, clientId, clientSecret);
RestClient restClient = ServiceClient.rest("api").baseUrl(apiUrl).build();
// Make authenticated request
oauth2.getClientCredentialsToken().flatMap(token ->
restClient.get("/users", UserList.class)
.withHeader("Authorization", "Bearer " + token)
.execute()
).subscribe(users -> {
System.out.println("Users: " + users);
});@Component
public class ProtectedApiService {
private final OAuth2ClientHelper oauth2;
private final RestClient restClient;
public ProtectedApiService(
@Value("${oauth2.token-endpoint}") String tokenEndpoint,
@Value("${oauth2.client-id}") String clientId,
@Value("${oauth2.client-secret}") String clientSecret,
@Value("${api.base-url}") String apiBaseUrl
) {
this.oauth2 = new OAuth2ClientHelper(tokenEndpoint, clientId, clientSecret);
this.restClient = ServiceClient.rest("protected-api")
.baseUrl(apiBaseUrl)
.build();
}
public Mono<User> getUser(String userId) {
return oauth2.getClientCredentialsToken().flatMap(token ->
restClient.get("/users/{id}", User.class)
.withPathParam("id", userId)
.withHeader("Authorization", "Bearer " + token)
.execute()
);
}
public Mono<User> createUser(User user) {
return oauth2.getClientCredentialsToken().flatMap(token ->
restClient.post("/users", User.class)
.withBody(user)
.withHeader("Authorization", "Bearer " + token)
.execute()
);
}
}oauth2.getClientCredentialsToken()
.flatMap(token ->
restClient.get("/protected/resource", Resource.class)
.withHeader("Authorization", "Bearer " + token)
.execute()
)
.doOnError(error -> {
if (error instanceof UnauthorizedException) {
// Token might be invalid, clear cache
oauth2.clearCache();
}
})
.retry(1) // Retry once with fresh token
.subscribe(resource -> {
System.out.println("Resource: " + resource);
});❌ Bad:
// No retry, default timeout, no custom configuration
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(
"https://auth.example.com/token",
"client-id",
"client-secret"
);✅ Good:
// Production-ready with retry, custom timeout, and buffer
OAuth2Config config = OAuth2Config.builder()
.timeout(Duration.ofMinutes(2))
.enableRetry(true)
.maxRetries(3)
.retryBackoff(Duration.ofSeconds(1))
.tokenExpirationBuffer(120)
.defaultHeader("User-Agent", "MyApp/1.0")
.build();
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(
"https://auth.example.com/token",
"client-id",
"client-secret",
config
);❌ Bad:
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(
"https://auth.example.com/token",
"hardcoded-client-id", // Hardcoded credentials
"hardcoded-secret"
);✅ Good:
@Configuration
public class OAuth2Configuration {
@Bean
public OAuth2ClientHelper oauth2Helper(
@Value("${oauth2.token-endpoint}") String tokenEndpoint,
@Value("${oauth2.client-id}") String clientId,
@Value("${oauth2.client-secret}") String clientSecret
) {
OAuth2Config config = OAuth2Config.builder()
.timeout(Duration.ofMinutes(2))
.enableRetry(true)
.maxRetries(3)
.retryBackoff(Duration.ofSeconds(1))
.tokenExpirationBuffer(120)
.build();
return new OAuth2ClientHelper(tokenEndpoint, clientId, clientSecret, config);
}
}❌ Bad:
public Mono<User> getUser(String id) {
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(...); // New instance each time
return oauth2.getClientCredentialsToken().flatMap(...);
}✅ Good:
@Component
public class UserService {
private final OAuth2ClientHelper oauth2; // Singleton instance
public UserService(OAuth2ClientHelper oauth2) {
this.oauth2 = oauth2;
}
public Mono<User> getUser(String id) {
return oauth2.getClientCredentialsToken().flatMap(...);
}
}❌ Bad:
// No error handling
public Mono<Resource> getResource() {
return oauth2.getClientCredentialsToken()
.flatMap(token -> makeApiCall(token));
}✅ Good:
// Automatic retry with fresh token on 401
public Mono<Resource> getResource() {
return oauth2.getClientCredentialsToken()
.flatMap(token -> makeApiCall(token))
.onErrorResume(UnauthorizedException.class, error -> {
// Clear cache and retry with fresh token
oauth2.clearCache();
return oauth2.getClientCredentialsToken()
.flatMap(token -> makeApiCall(token));
});
}- Client Credentials: For service-to-service communication
- Password Grant: Only when Authorization Code flow is not possible
- Refresh Token: To extend user sessions without re-authentication
❌ Bad:
// Requesting new token for each scope
oauth2.getClientCredentialsToken("read").subscribe(...);
oauth2.getClientCredentialsToken("write").subscribe(...);
oauth2.getClientCredentialsToken("read").subscribe(...); // Requests new token again✅ Good:
// Tokens are cached per scope automatically
oauth2.getClientCredentialsToken("read").subscribe(...); // Requests from server
oauth2.getClientCredentialsToken("write").subscribe(...); // Requests from server
oauth2.getClientCredentialsToken("read").subscribe(...); // Returns cached token// Check cache metrics
int cacheSize = oauth2.getCacheSize();
log.info("OAuth2 cache size: {}", cacheSize);
// Clear cache periodically if needed
@Scheduled(cron = "0 0 * * * *") // Every hour
public void clearOAuth2Cache() {
oauth2.clearCache();
log.info("OAuth2 cache cleared");
}oauth2.getClientCredentialsToken()
.timeout(Duration.ofMinutes(5)) // Overall timeout
.doOnError(error -> {
log.error("Failed to obtain OAuth2 token", error);
// Send alert, metrics, etc.
})
.onErrorResume(error -> {
// Fallback or circuit breaker logic
return Mono.error(new ServiceUnavailableException("Auth service down"));
})
.subscribe();import org.fireflyframework.client.oauth2.OAuth2ClientHelper;
import org.fireflyframework.client.oauth2.OAuth2ClientHelper.OAuth2Config;
import org.fireflyframework.client.RestClient;
import org.fireflyframework.client.ServiceClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.time.Duration;
@Service
public class OrderService {
private final OAuth2ClientHelper oauth2;
private final RestClient paymentClient;
private final RestClient inventoryClient;
public OrderService(
@Value("${oauth2.token-endpoint}") String tokenEndpoint,
@Value("${oauth2.client-id}") String clientId,
@Value("${oauth2.client-secret}") String clientSecret
) {
// Production-ready OAuth2 configuration
OAuth2Config config = OAuth2Config.builder()
.timeout(Duration.ofMinutes(2))
.enableRetry(true)
.maxRetries(3)
.retryBackoff(Duration.ofSeconds(1))
.tokenExpirationBuffer(120)
.defaultHeader("User-Agent", "OrderService/1.0")
.build();
this.oauth2 = new OAuth2ClientHelper(tokenEndpoint, clientId, clientSecret, config);
this.paymentClient = ServiceClient.rest("payment-service")
.baseUrl("https://payment.example.com")
.build();
this.inventoryClient = ServiceClient.rest("inventory-service")
.baseUrl("https://inventory.example.com")
.build();
}
public Mono<Order> createOrder(OrderRequest request) {
return oauth2.getClientCredentialsToken("orders:write")
.flatMap(token -> {
// Check inventory
return inventoryClient.get("/inventory/{productId}", InventoryStatus.class)
.withPathParam("productId", request.getProductId())
.withHeader("Authorization", "Bearer " + token)
.execute()
.flatMap(inventory -> {
if (!inventory.isAvailable()) {
return Mono.error(new OutOfStockException());
}
// Process payment
return paymentClient.post("/payments", PaymentResponse.class)
.withBody(new PaymentRequest(request.getAmount()))
.withHeader("Authorization", "Bearer " + token)
.execute();
})
.map(payment -> new Order(request, payment));
});
}
}✅ Client Credentials Flow: Machine-to-machine authentication
✅ Password Grant Flow: User authentication with credentials
✅ Refresh Token Flow: Token refresh without re-authentication
✅ Token Caching: Automatic caching with expiration
✅ Reactive API: Full Mono<T> support
✅ Thread-Safe: Safe for concurrent use
❌ Authorization Code Flow: Use Spring Security OAuth2
❌ PKCE Support: Use Spring Security OAuth2
❌ OpenID Connect: Use Spring Security OAuth2
❌ Social Login: Use Spring Security OAuth2
❌ Token Introspection: Implement custom logic
❌ JWT Parsing: Use dedicated JWT libraries
For Production: Use Spring Security OAuth2 Client for full OAuth2/OIDC support.
Next Steps: