OAuth 2.1 represents the next evolution of the authorization framework, consolidating years of best practices and security improvements into a single, coherent specification. For Java developers building modern applications—from microservices to mobile backends—understanding and implementing OAuth 2.1 compliance is essential for creating secure, maintainable authorization systems that align with current security standards.
What is OAuth 2.1?
OAuth 2.1 is not a new protocol but rather a consolidation and simplification of OAuth 2.0 with security enhancements from extensions developed over the past decade. It combines:
- OAuth 2.0 Core (RFC 6749) with security best practices
- OAuth 2.0 for Native Apps (RFC 8252) - PKCE requirement
- OAuth 2.0 Security Best Practices (BCP) - removing implicit flow
- OAuth 2.0 Authorization Server Metadata (RFC 8414)
- JWT Profile for OAuth 2.0 Access Tokens (RFC 9068)
Key Differences: OAuth 2.0 vs OAuth 2.1
| Feature | OAuth 2.0 | OAuth 2.1 |
|---|---|---|
| Implicit Flow | Allowed | Removed |
| Resource Owner Password Credentials | Allowed | Removed |
| PKCE Requirement | Optional for public clients | Required for all clients |
| Bearer Token Usage | Simple bearer | Stronger recommendations |
| Redirect URI Registration | Partial | Exact match required |
| Scope Validation | Optional | Required |
| Client Authentication | Various methods | Stronger recommendations |
Why OAuth 2.1 Compliance Matters for Java Applications
- Security by Default: Eliminates insecure flows (implicit, password) that have led to numerous vulnerabilities.
- Simplified Implementation: Single specification instead of multiple extensions to combine.
- Future-Proofing: Aligns with modern security expectations and regulatory requirements.
- Interoperability: Consistent behavior across different authorization servers and clients.
- Mobile-Friendly: Built-in PKCE ensures secure authorization for mobile and single-page applications.
Implementing an OAuth 2.1 Authorization Server in Java
1. Maven Dependencies
<dependencies> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Spring OAuth2 Authorization Server --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> <version>1.2.0</version> </dependency> <!-- JWT Support --> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.37.2</version> </dependency> <!-- For OAuth 2.1 client implementation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <!-- Database support --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies>
2. Authorization Server Configuration (OAuth 2.1 Compliant)
@Configuration
@EnableWebSecurity
public class OAuth2AuthorizationServerConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(
HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint
.consentPage("/oauth2/consent")
.authorizationResponseHandler(new AuthorizationResponseHandler())
.errorResponseHandler(new AuthorizationErrorHandler())
)
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenRequestConverter(new PkceAwareAuthenticationConverter())
.authenticationProvider(new OAuth2AuthorizationCodeAuthenticationProvider())
)
.clientAuthentication(clientAuthentication ->
clientAuthentication
.authenticationProviders(providers -> {
providers.add(new X509ClientAuthenticationProvider());
providers.add(new PrivateKeyJwtClientAuthenticationProvider());
})
)
.oidc(OpenIDConnectConfigurer::init);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
List<RegisteredClient> clients = new ArrayList<>();
// OAuth 2.1 compliant client - requires PKCE
clients.add(createOAuth21Client());
// Legacy client with stronger validation
clients.add(createMigratedLegacyClient());
return new InMemoryRegisteredClientRepository(clients);
}
private RegisteredClient createOAuth21Client() {
return RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("oauth21-client")
.clientSecret("{noop}client-secret") // In production, use proper encoding
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("https://client.example.com/callback")
.redirectUri("https://client.example.com/callback-native")
.postLogoutRedirectUri("https://client.example.com/logout")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")
.scope("message.write")
.scope("finance.transactions")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true)
.requireProofKey(true) // PKCE required for OAuth 2.1
.jwkSetUrl("https://client.example.com/jwks")
.build()
)
.tokenSettings(TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.accessTokenTimeToLive(Duration.ofMinutes(5))
.refreshTokenTimeToLive(Duration.ofHours(24))
.reuseRefreshTokens(false)
.build()
)
.build();
}
private RegisteredClient createMigratedLegacyClient() {
return RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("migrated-client")
.clientSecret("{noop}legacy-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("https://legacy.example.com/callback")
.scope("read")
.scope("write")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true)
.requireProofKey(false) // Legacy client exception
.build()
)
.build();
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsaKey();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static RSAKey generateRsaKey() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer("https://auth.example.com")
.authorizationEndpoint("/oauth2/authorize")
.tokenEndpoint("/oauth2/token")
.tokenIntrospectionEndpoint("/oauth2/introspect")
.tokenRevocationEndpoint("/oauth2/revoke")
.jwkSetEndpoint("/oauth2/jwks")
.oidcUserInfoEndpoint("/connect/userinfo")
.oidcClientRegistrationEndpoint("/connect/register")
.build();
}
}
3. PKCE-Aware Authentication Converter
@Component
public class PkceAwareAuthenticationConverter implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
// Extract parameters
String grantType = request.getParameter("grant_type");
if (!"authorization_code".equals(grantType)) {
return null;
}
String code = request.getParameter("code");
String redirectUri = request.getParameter("redirect_uri");
String clientId = request.getParameter("client_id");
// OAuth 2.1 requires PKCE validation
String codeVerifier = request.getParameter("code_verifier");
if (codeVerifier == null) {
// In OAuth 2.1, PKCE is REQUIRED for all clients
// Return null to trigger proper error response
return null;
}
// Validate code verifier format
if (!isValidCodeVerifier(codeVerifier)) {
throw new OAuth2AuthenticationException(
new OAuth2Error(
OAuth2ErrorCodes.INVALID_REQUEST,
"Invalid code_verifier format",
null
)
);
}
// Proceed with token request
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put("code_verifier", codeVerifier);
return new OAuth2AuthorizationCodeAuthenticationToken(
clientId,
null, // clientSecret (handled by other authentication methods)
code,
redirectUri,
null, // clientPrincipal
additionalParameters
);
}
private boolean isValidCodeVerifier(String codeVerifier) {
if (codeVerifier == null) {
return false;
}
// PKCE code verifier requirements:
// - Length between 43 and 128 characters
// - Characters: [A-Z], [a-z], [0-9], "-", ".", "_", "~"
if (codeVerifier.length() < 43 || codeVerifier.length() > 128) {
return false;
}
return codeVerifier.matches("^[A-Za-z0-9\\-\\._~]+$");
}
}
4. Enhanced Token Service with OAuth 2.1 Features
@Service
public class OAuth21TokenService {
private final JwtEncoder jwtEncoder;
private final OAuth2AuthorizationService authorizationService;
private final TokenBlacklistService blacklistService;
public OAuth21TokenService(
JwtEncoder jwtEncoder,
OAuth2AuthorizationService authorizationService,
TokenBlacklistService blacklistService) {
this.jwtEncoder = jwtEncoder;
this.authorizationService = authorizationService;
this.blacklistService = blacklistService;
}
public OAuth2AccessToken createAccessToken(
OAuth2Authorization authorization,
OAuth2TokenContext context) {
RegisteredClient client = authorization.getRegisteredClient();
String issuer = context.getProviderContext().getIssuer();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(
client.getTokenSettings().getAccessTokenTimeToLive());
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder()
.issuer(issuer)
.subject(authorization.getPrincipalName())
.audience(List.of("api.example.com"))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.id(UUID.randomUUID().toString())
.claim("client_id", client.getClientId());
// Add scopes
Set<String> scopes = authorization.getAuthorizedScopes();
if (!scopes.isEmpty()) {
claimsBuilder.claim("scope", String.join(" ", scopes));
}
// OAuth 2.1 recommended claims
claimsBuilder.claim("auth_time", authorization.getAttribute(
OAuth2ParameterNames.AUTH_TIME));
String nonce = authorization.getAttribute("nonce");
if (nonce != null) {
claimsBuilder.claim("nonce", nonce);
}
// Add client certificate thumbprint if bound (OAuth 2.1 recommendation)
String certThumbprint = authorization.getAttribute("cert_thumbprint");
if (certThumbprint != null) {
claimsBuilder.claim("cnf", Map.of("x5t#S256", certThumbprint));
}
// Add authorization details if present (RFC 9396)
String authorizationDetails = authorization.getAttribute("authorization_details");
if (authorizationDetails != null) {
claimsBuilder.claim("authorization_details", authorizationDetails);
}
Jwt jwt = jwtEncoder.encode(
JwtEncoderParameters.from(claimsBuilder.build()));
return new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(),
jwt.getIssuedAt(),
jwt.getExpiresAt(),
scopes
);
}
public OAuth2RefreshToken createRefreshToken(
OAuth2Authorization authorization) {
RegisteredClient client = authorization.getRegisteredClient();
// OAuth 2.1 recommends not reusing refresh tokens
if (!client.getTokenSettings().isReuseRefreshTokens()) {
// Invalidate old refresh token if exists
OAuth2RefreshToken currentRefreshToken =
authorization.getRefreshToken();
if (currentRefreshToken != null) {
blacklistService.blacklistToken(
currentRefreshToken.getTokenValue(),
"refresh-token-replaced"
);
}
}
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(
client.getTokenSettings().getRefreshTokenTimeToLive());
return new OAuth2RefreshToken(
UUID.randomUUID().toString(),
issuedAt,
expiresAt
);
}
}
OAuth 2.1 Client Implementation
1. OAuth 2.1 Compliant Client Configuration
@Configuration
public class OAuth21ClientConfig {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken(configurer ->
configurer.clockSkew(Duration.ofSeconds(60)))
.clientCredentials()
.password() // Only if absolutely necessary (legacy systems)
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Add PKCE support
authorizedClientManager.setContextAttributesMapper(
this::createContextAttributes);
return authorizedClientManager;
}
private Map<String, Object> createContextAttributes(OAuth2AuthorizeRequest request) {
Map<String, Object> attributes = new HashMap<>();
// Generate PKCE code verifier and challenge
String codeVerifier = generateCodeVerifier();
String codeChallenge = generateCodeChallenge(codeVerifier);
attributes.put(OAuth2ParameterNames.CODE_VERIFIER, codeVerifier);
attributes.put("code_challenge", codeChallenge);
attributes.put("code_challenge_method", "S256");
return attributes;
}
private String generateCodeVerifier() {
SecureRandom secureRandom = new SecureRandom();
byte[] codeVerifier = new byte[32];
secureRandom.nextBytes(codeVerifier);
return Base64.getUrlEncoder().withoutPadding()
.encodeToString(codeVerifier);
}
private String generateCodeChallenge(String codeVerifier) {
try {
byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(bytes);
return Base64.getUrlEncoder().withoutPadding()
.encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 not available", e);
}
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(
oidcClientRegistration(),
apiClientRegistration()
);
}
private ClientRegistration oidcClientRegistration() {
return ClientRegistration.withRegistrationId("oidc-client")
.clientId("oauth21-client")
.clientSecret("client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.scope(OidcScopes.OPENID, OidcScopes.PROFILE, "email")
.authorizationUri("https://auth.example.com/oauth2/authorize")
.tokenUri("https://auth.example.com/oauth2/token")
.jwkSetUri("https://auth.example.com/oauth2/jwks")
.userInfoUri("https://auth.example.com/connect/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.clientName("OAuth 2.1 OIDC Client")
.build();
}
private ClientRegistration apiClientRegistration() {
return ClientRegistration.withRegistrationId("api-client")
.clientId("oauth21-client")
.clientSecret("client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("message.read", "message.write")
.tokenUri("https://auth.example.com/oauth2/token")
.clientName("OAuth 2.1 API Client")
.build();
}
}
2. PKCE-Aware Authorization Request
@Service
public class PkceAuthorizationService {
private final OAuth2AuthorizedClientManager authorizedClientManager;
private final CodeVerifierRepository codeVerifierRepository;
public String initiateAuthorization(String registrationId) {
// Generate PKCE parameters
String codeVerifier = generateCodeVerifier();
String codeChallenge = generateCodeChallenge(codeVerifier);
// Store code verifier for later use (in session or database)
codeVerifierRepository.store(registrationId, codeVerifier);
// Build authorization URL
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(registrationId)
.principal(createPrincipal())
.attributes(attrs -> {
attrs.put(OAuth2ParameterNames.CODE_CHALLENGE, codeChallenge);
attrs.put(OAuth2ParameterNames.CODE_CHALLENGE_METHOD, "S256");
})
.build();
OAuth2AuthorizationRequest authorizationRequest =
authorizedClientManager.authorize(authorizeRequest);
return authorizationRequest.getAuthorizationRequestUri();
}
public OAuth2AccessToken handleCallback(String code, String state, String registrationId) {
// Retrieve stored code verifier
String codeVerifier = codeVerifierRepository.retrieve(registrationId);
if (codeVerifier == null) {
throw new OAuth2AuthorizationException(
new OAuth2Error("invalid_request", "Missing code verifier", null)
);
}
// Exchange code for token with PKCE
OAuth2AuthorizeRequest tokenRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(registrationId)
.principal(createPrincipal())
.attributes(attrs -> {
attrs.put(OAuth2ParameterNames.CODE, code);
attrs.put(OAuth2ParameterNames.REDIRECT_URI,
"https://client.example.com/callback");
attrs.put(OAuth2ParameterNames.CODE_VERIFIER, codeVerifier);
})
.build();
OAuth2AuthorizedClient authorizedClient =
authorizedClientManager.authorize(tokenRequest);
return authorizedClient.getAccessToken();
}
}
Token Introspection and Revocation
@RestController
@RequestMapping("/oauth2")
public class OAuth2IntrospectionEndpoint {
private final OAuth2AuthorizationService authorizationService;
private final TokenBlacklistService blacklistService;
@PostMapping("/introspect")
public ResponseEntity<IntrospectionResponse> introspect(
@RequestParam("token") String token,
@RequestParam(value = "token_type_hint", required = false)
String tokenTypeHint) {
// OAuth 2.1 introspection endpoint
OAuth2Authorization authorization =
authorizationService.findByToken(token);
if (authorization == null) {
return ResponseEntity.ok(IntrospectionResponse.active(false));
}
boolean active = isTokenActive(authorization, token);
IntrospectionResponse response = IntrospectionResponse.builder()
.active(active)
.scope(String.join(" ", authorization.getAuthorizedScopes()))
.clientId(authorization.getRegisteredClient().getClientId())
.username(authorization.getPrincipalName())
.tokenType("Bearer")
.exp(authorization.getToken(token).getExpiresAt())
.iat(authorization.getToken(token).getIssuedAt())
.build();
return ResponseEntity.ok(response);
}
@PostMapping("/revoke")
public ResponseEntity<Void> revoke(
@RequestParam("token") String token,
@RequestParam(value = "token_type_hint", required = false)
String tokenTypeHint) {
// OAuth 2.1 token revocation
blacklistService.blacklistToken(token, "user-revoked");
return ResponseEntity.ok().build();
}
@Data
@Builder
public static class IntrospectionResponse {
private boolean active;
private String scope;
private String clientId;
private String username;
private String tokenType;
private Instant exp;
private Instant iat;
private String sub;
private List<String> aud;
private String iss;
private String jti;
public static IntrospectionResponse active(boolean active) {
return IntrospectionResponse.builder().active(active).build();
}
}
}
OAuth 2.1 Security Configuration
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class OAuth2ResourceServerConfig {
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http)
throws Exception {
http.securityMatcher("/api/**")
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
.requestMatchers("/api/finance/**").hasAuthority("SCOPE_finance")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
.opaqueToken(opaque -> opaque
.introspectionUri("https://auth.example.com/oauth2/introspect")
.introspectionClientCredentials("resource-server", "secret")
)
.bearerTokenResolver(new CookieBearerTokenResolver())
.authenticationEntryPoint(new OAuth2AuthenticationEntryPoint())
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
private Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter =
new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("SCOPE_");
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return jwtConverter;
}
// Custom bearer token resolver that can handle cookies (for SPAs)
private static class CookieBearerTokenResolver implements BearerTokenResolver {
@Override
public String resolve(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
// Check cookie for SPA applications
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("access_token".equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
}
}
OAuth 2.1 Compliance Testing
@SpringBootTest
@AutoConfigureMockMvc
public class OAuth21ComplianceTest {
@Autowired
private MockMvc mockMvc;
@Test
void testImplicitFlow_ShouldBeRejected() throws Exception {
// OAuth 2.1 prohibits implicit flow
mockMvc.perform(get("/oauth2/authorize")
.param("response_type", "token") // Implicit flow
.param("client_id", "oauth21-client")
.param("redirect_uri", "https://client.example.com/callback"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").value("unsupported_response_type"));
}
@Test
void testPasswordGrant_ShouldBeRejected() throws Exception {
// OAuth 2.1 prohibits password grant
mockMvc.perform(post("/oauth2/token")
.param("grant_type", "password")
.param("username", "user")
.param("password", "pass")
.param("client_id", "oauth21-client"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").value("unsupported_grant_type"));
}
@Test
void testAuthorizationCodeWithoutPKCE_ShouldBeRejected() throws Exception {
// First get authorization code
String authorizationCode = getAuthorizationCode();
// Try to exchange without PKCE
mockMvc.perform(post("/oauth2/token")
.param("grant_type", "authorization_code")
.param("code", authorizationCode)
.param("redirect_uri", "https://client.example.com/callback")
.param("client_id", "oauth21-client"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").value("invalid_request"))
.andExpect(jsonPath("$.error_description")
.value(containsString("code_verifier")));
}
@Test
void testValidPKCEFlow_ShouldSucceed() throws Exception {
// Generate PKCE parameters
String codeVerifier = generateCodeVerifier();
String codeChallenge = generateCodeChallenge(codeVerifier);
// Get authorization code with PKCE challenge
String authorizationCode = getAuthorizationCodeWithPkce(codeChallenge);
// Exchange with PKCE verifier
mockMvc.perform(post("/oauth2/token")
.param("grant_type", "authorization_code")
.param("code", authorizationCode)
.param("redirect_uri", "https://client.example.com/callback")
.param("client_id", "oauth21-client")
.param("code_verifier", codeVerifier))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").exists())
.andExpect(jsonPath("$.token_type").value("Bearer"))
.andExpect(jsonPath("$.refresh_token").exists());
}
@Test
void testTokenBindingWithCertificate() throws Exception {
// OAuth 2.1 recommends token binding (optional but recommended)
String token = getTokenWithCertificateBinding();
// Try to use token without certificate
mockMvc.perform(get("/api/protected")
.header("Authorization", "Bearer " + token))
.andExpect(status().isUnauthorized());
// Try with correct certificate
mockMvc.perform(get("/api/protected")
.header("Authorization", "Bearer " + token)
.header("X-Client-Certificate", getTestCertificate()))
.andExpect(status().isOk());
}
}
OAuth 2.1 Migration Strategy for Existing Applications
@Service
public class OAuth21MigrationService {
/**
* Strategy for migrating from OAuth 2.0 to OAuth 2.1
*/
public MigrationPlan createMigrationPlan() {
return MigrationPlan.builder()
.phase(1, "Enable PKCE for all clients")
.action("Update all public clients to use PKCE")
.action("Update authorization server to require PKCE")
.validation("Verify all new tokens require PKCE")
.phase(2, "Deprecate implicit flow")
.action("Identify clients using implicit flow")
.action("Migrate implicit clients to authorization code + PKCE")
.action("Update documentation to warn about deprecation")
.validation("Track implicit flow usage decreasing")
.phase(3, "Remove password grant")
.action("Identify password grant usage")
.action("Work with clients to migrate to alternative grants")
.action("Consider ROPC replacement strategies")
.validation("Zero password grant usage")
.phase(4, "Enhance token security")
.action("Implement token binding (recommended)")
.action("Add audience validation")
.action("Implement stricter scope validation")
.validation("Enhanced security metrics")
.phase(5, "Update all clients to OAuth 2.1 compliance")
.action("Client SDK updates")
.action("Documentation and training")
.action("Final compliance audit")
.build();
}
public void assessClientReadiness(String clientId) {
ClientRegistration client = clientRepository.findById(clientId);
ComplianceReport report = new ComplianceReport(clientId);
// Check PKCE readiness
report.addCheck("PKCE Support",
client.isPkceSupported() ? ComplianceStatus.COMPLIANT :
ComplianceStatus.ACTION_REQUIRED);
// Check grant types
if (client.getAuthorizationGrantTypes().contains("implicit")) {
report.addCheck("Implicit Flow",
ComplianceStatus.ACTION_REQUIRED,
"Must migrate to authorization code + PKCE");
}
if (client.getAuthorizationGrantTypes().contains("password")) {
report.addCheck("Password Grant",
ComplianceStatus.ACTION_REQUIRED,
"Must migrate to alternative grant types");
}
// Check redirect URI validation
boolean hasExactMatchUris = client.getRedirectUris().stream()
.allMatch(uri -> !uri.contains("*"));
report.addCheck("Exact Redirect URIs",
hasExactMatchUris ? ComplianceStatus.COMPLIANT :
ComplianceStatus.ACTION_REQUIRED);
// Check scope usage
boolean hasWellDefinedScopes = client.getScopes().stream()
.noneMatch(scope -> scope.equals("*") || scope.isEmpty());
report.addCheck("Well-Defined Scopes",
hasWellDefinedScopes ? ComplianceStatus.COMPLIANT :
ComplianceStatus.ACTION_REQUIRED);
return report;
}
}
Best Practices for OAuth 2.1 in Java
- Always Use PKCE: Even for confidential clients, PKCE adds defense in depth.
- Validate Redirect URIs Exactly: No wildcards, exact string matching only.
- Use Short-Lived Access Tokens: 5-15 minutes is typical; use refresh tokens for longer sessions.
- Implement Token Binding: Bind tokens to client certificates when possible.
- Validate Scopes: Always verify scopes match the client's authorized scopes.
- Use Pushed Authorization Requests (PAR): For additional security in authorization flows.
- Implement Sender-Constrained Tokens: Use MTLS or DPoP for token binding.
- Rotate Refresh Tokens: Issue new refresh tokens with each refresh to prevent replay.
Conclusion
OAuth 2.1 represents a significant step forward in authorization protocol security, consolidating years of operational experience and security research into a coherent specification. For Java developers, implementing OAuth 2.1 compliance means:
- Eliminating insecure flows (implicit, password)
- Mandating PKCE for all clients
- Implementing stronger token security
- Validating inputs more strictly
- Providing better introspection and revocation
Spring Security's OAuth2 Authorization Server project provides excellent support for OAuth 2.1 features, making Java an ideal platform for building compliant authorization servers and clients. By adopting OAuth 2.1 now, developers ensure their applications meet modern security expectations and are prepared for future regulatory requirements.
As the security landscape continues to evolve, OAuth 2.1 provides a solid foundation that balances security, usability, and interoperability for the next generation of applications.
Java Programming Basics – Variables, Loops, Methods, Classes, Files & Exception Handling (Related to Java Programming)
Variables and Data Types in Java:
This topic explains how variables store data in Java and how data types define the kind of values a variable can hold, such as numbers, characters, or text. Java includes primitive types like int, double, and boolean, which are essential for storing and managing data in programs. (GeeksforGeeks)
Read more: https://macronepal.com/blog/variables-and-data-types-in-java/
Basic Input and Output in Java:
This lesson covers how Java programs receive input from users and display output using tools like Scanner for input and System.out.println() for output. These operations allow interaction between the program and the user.
Read more: https://macronepal.com/blog/basic-input-output-in-java/
Arithmetic Operations in Java:
This guide explains mathematical operations such as addition, subtraction, multiplication, and division using operators like +, -, *, and /. These operations are used to perform calculations in Java programs.
Read more: https://macronepal.com/blog/arithmetic-operations-in-java/
If-Else Statement in Java:
The if-else statement allows programs to make decisions based on conditions. It helps control program flow by executing different blocks of code depending on whether a condition is true or false.
Read more: https://macronepal.com/blog/if-else-statement-in-java/
For Loop in Java:
A for loop is used to repeat a block of code a specific number of times. It is commonly used when the number of repetitions is known in advance.
Read more: https://macronepal.com/blog/for-loop-in-java/
Method Overloading in Java:
Method overloading allows multiple methods to have the same name but different parameters. It improves code readability and flexibility by allowing similar tasks to be handled using one method name.
Read more: https://macronepal.com/blog/method-overloading-in-java-a-complete-guide/
Basic Inheritance in Java:
Inheritance is an object-oriented concept that allows one class to inherit properties and methods from another class. It promotes code reuse and helps build hierarchical class structures.
Read more: https://macronepal.com/blog/basic-inheritance-in-java-a-complete-guide/
File Writing in Java:
This topic explains how to create and write data into files using Java. File writing is commonly used to store program data permanently.
Read more: https://macronepal.com/blog/file-writing-in-java-a-complete-guide/
File Reading in Java:
File reading allows Java programs to read stored data from files. It is useful for retrieving saved information and processing it inside applications.
Read more: https://macronepal.com/blog/file-reading-in-java-a-complete-guide/
Exception Handling in Java:
Exception handling helps manage runtime errors using tools like try, catch, and finally. It prevents programs from crashing and allows safe error handling.
Read more: https://macronepal.com/blog/exception-handling-in-java-a-complete-guide/
Constructors in Java:
Constructors are special methods used to initialize objects when they are created. They help assign initial values to object variables automatically.
Read more: https://macronepal.com/blog/constructors-in-java/
Classes and Objects in Java:
Classes are blueprints used to create objects, while objects are instances of classes. These concepts form the foundation of object-oriented programming in Java.
Read more: https://macronepal.com/blog/classes-and-object-in-java/
Methods in Java:
Methods are blocks of code that perform specific tasks. They help organize programs into smaller reusable sections and improve code readability.
Read more: https://macronepal.com/blog/methods-in-java/
Arrays in Java:
Arrays store multiple values of the same type in a single variable. They are useful for handling lists of data such as numbers or names.
Read more: https://macronepal.com/blog/arrays-in-java/
While Loop in Java:
A while loop repeats a block of code as long as a given condition remains true. It is useful when the number of repetitions is not known beforehand.
Read more: https://macronepal.com/blog/while-loop-in-java/