micronaut security has invalid IdTokenClaimsValidator logic on aud
Description
Micronaut Security is a security solution for applications. Prior to versions 3.1.2, 3.2.4, 3.3.2, 3.4.3, 3.5.3, 3.6.6, 3.7.4, 3.8.4, 3.9.6, 3.10.2, and 3.11.1, IdTokenClaimsValidator skips aud claim validation if token is issued by same identity issuer/provider. Any OIDC setup using Micronaut where multiple OIDC applications exists for the same issuer but token auth are not meant to be shared. This issue has been patched in versions 3.1.2, 3.2.4, 3.3.2, 3.4.3, 3.5.3, 3.6.6, 3.7.4, 3.8.4, 3.9.6, 3.10.2, and 3.11.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.micronaut.security:micronaut-security-oauth2Maven | >= 3.11.0, < 3.11.1 | 3.11.1 |
io.micronaut.security:micronaut-security-oauth2Maven | >= 3.10.0, < 3.10.2 | 3.10.2 |
io.micronaut.security:micronaut-security-oauth2Maven | >= 3.9.0, < 3.9.6 | 3.9.6 |
io.micronaut.security:micronaut-security-oauth2Maven | >= 3.8.0, < 3.8.4 | 3.8.4 |
io.micronaut.security:micronaut-security-oauth2Maven | >= 3.7.0, < 3.7.4 | 3.7.4 |
io.micronaut.security:micronaut-security-oauth2Maven | >= 3.6.0, < 3.6.6 | 3.6.6 |
io.micronaut.security:micronaut-security-oauth2Maven | >= 3.5.0, < 3.5.3 | 3.5.3 |
io.micronaut.security:micronaut-security-oauth2Maven | >= 3.4.0, < 3.4.3 | 3.4.3 |
io.micronaut.security:micronaut-security-oauth2Maven | >= 3.3.0, < 3.3.2 | 3.3.2 |
io.micronaut.security:micronaut-security-oauth2Maven | >= 3.2.0, < 3.2.4 | 3.2.4 |
io.micronaut.security:micronaut-security-oauth2Maven | >= 3.1.0, < 3.1.2 | 3.1.2 |
Affected products
1- Range: >= 3.11.0, < 3.11.1
Patches
19728b925221aMerge pull request from GHSA-qw22-8w9r-864h
2 files changed · +214 −1
security-oauth2/src/main/java/io/micronaut/security/oauth2/client/IdTokenClaimsValidator.java+1 −1 modified@@ -199,7 +199,7 @@ protected boolean validateIssuerAudienceAndAzp(@NonNull JwtClaims claims, Optional<URL> issuerOptional = openIdClientConfiguration.getIssuer(); if (issuerOptional.isPresent()) { String issuer = issuerOptional.get().toString(); - return issuer.equalsIgnoreCase(iss) || + return issuer.equalsIgnoreCase(iss) && audiences.contains(clientId) && validateAzp(claims, clientId, audiences); }
security-oauth2/src/test/groovy/io/micronaut/security/oauth2/endpoint/token/response/validation/AudienceClaimMatchesClientIdSpec.groovy+213 −0 added@@ -0,0 +1,213 @@ +package io.micronaut.security.oauth2.endpoint.token.response.validation + +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.KeyUse +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jose.jwk.gen.JWKGenerator +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.nimbusds.jwt.JWT +import com.nimbusds.jwt.JWTParser +import com.nimbusds.jwt.SignedJWT +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.core.util.StringUtils +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.http.client.BlockingHttpClient +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.server.util.HttpHostResolver +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.security.annotation.Secured +import io.micronaut.security.authentication.Authentication +import io.micronaut.security.rules.SecurityRule +import io.micronaut.security.token.Claims +import io.micronaut.security.token.generator.TokenGenerator +import io.micronaut.security.token.jwt.endpoints.JwkProvider +import io.micronaut.security.token.jwt.generator.AccessTokenConfiguration +import io.micronaut.security.token.jwt.generator.claims.ClaimsGenerator +import io.micronaut.security.token.jwt.signature.rsa.RSASignatureGeneratorConfiguration +import jakarta.inject.Named +import jakarta.inject.Singleton +import spock.lang.Specification + +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey + +class AudienceClaimMatchesClientIdSpec extends Specification { + + void "test same issuer two different client ids"() { + given: + EmbeddedServer authServer = ApplicationContext.run(EmbeddedServer, [ + "micronaut.security.reject-not-found": StringUtils.FALSE, + "spec.name": "AudienceClaimMatchesClientIdSpecAuthServer" + ]) + Map<String, Object> configuration = [ + "micronaut.security.authentication": "idtoken", + "micronaut.security.reject-not-found": StringUtils.FALSE, + "spec.name": "AudienceClaimMatchesClientIdSpec", + "micronaut.security.oauth2.clients.auth.client-id": "AAA", + "micronaut.security.oauth2.clients.auth.client-secret": "YYY", + 'micronaut.security.oauth2.clients.auth.openid.issuer': "http://localhost:${authServer.port}/oauth2/default"] + EmbeddedServer server = ApplicationContext.run(EmbeddedServer, configuration) + HttpClient authServerHttpClient = server.applicationContext.createBean(HttpClient, authServer.URL) + BlockingHttpClient authServerClient = authServerHttpClient.toBlocking() + + when: + String keys = authServerClient.retrieve(HttpRequest.GET("/keys")) + + then: + noExceptionThrown() + keys != '{"keys":[]}' + + when: + String clientAToken = authServerClient.retrieve(HttpRequest.GET("/token/clientA").accept(MediaType.TEXT_PLAIN)) + JWT clientAJWT = JWTParser.parse(clientAToken) + + then: + clientAJWT instanceof SignedJWT + clientAJWT.getJWTClaimsSet().getClaim("aud") == ["AAA"] + clientAJWT.getJWTClaimsSet().getClaim("iss") == "http://localhost:${authServer.port}/oauth2/default" + + when: + HttpClient httpClient = server.applicationContext.createBean(HttpClient, server.URL) + BlockingHttpClient client = httpClient.toBlocking() + client.retrieve(HttpRequest.GET("/hello").bearerAuth(clientAToken)) + + then: + noExceptionThrown() + + when: + String clientBToken = authServerClient.retrieve(HttpRequest.GET("/token/clientB").accept(MediaType.TEXT_PLAIN)) + JWT clientBJWT = JWTParser.parse(clientBToken) + + then: + clientBJWT instanceof SignedJWT + clientBJWT.getJWTClaimsSet().getClaim("aud") == ["BBB"] + clientBJWT.getJWTClaimsSet().getClaim("iss") == "http://localhost:${authServer.port}/oauth2/default" + + when: + client.retrieve(HttpRequest.GET("/hello").bearerAuth(clientBToken)) + + then: + HttpClientResponseException e = thrown() + HttpStatus.UNAUTHORIZED == e.status + + cleanup: + authServerClient.close() + authServerHttpClient.close() + httpClient.close() + client.close() + server.close() + authServer.close() + } + + @Requires(property = 'spec.name', value = 'AudienceClaimMatchesClientIdSpec') + @Controller("/hello") + static class HelloWorldController { + @Get + @Secured(SecurityRule.IS_AUTHENTICATED) + Map<String, String> index() { + [message: 'Hello World'] + } + } + + @Requires(property = "spec.name", value="AudienceClaimMatchesClientIdSpecAuthServer") + @Secured(SecurityRule.IS_ANONYMOUS) + @Controller + static class AuthServerController { + + private final HttpHostResolver httpHostResolver + private final TokenGenerator tokenGenerator + private final AccessTokenConfiguration accessTokenConfiguration + private final ClaimsGenerator claimsGenerator + + AuthServerController(HttpHostResolver httpHostResolver, + TokenGenerator tokenGenerator, + AccessTokenConfiguration accessTokenConfiguration, + ClaimsGenerator claimsGenerator) { + this.httpHostResolver = httpHostResolver + this.tokenGenerator = tokenGenerator + this.accessTokenConfiguration = accessTokenConfiguration + this.claimsGenerator = claimsGenerator + } + + @Produces(MediaType.TEXT_PLAIN) + @Get("/token/clientA") + HttpResponse<Object> clientA(HttpRequest<?> request) { + generateToken(request, "AAA") + } + + @Produces(MediaType.TEXT_PLAIN) + @Get("/token/clientB") + HttpResponse<Object> clientB(HttpRequest<?> request) { + generateToken(request, "BBB") + } + + private HttpResponse<Object> generateToken(HttpRequest<?> request, String clientId) { + String host = httpHostResolver.resolve(request) + Map<String, Object> claims = new HashMap<>(claimsGenerator.generateClaims(Authentication.build("sherlock"), accessTokenConfiguration.expiration)) + claims[Claims.ISSUER] = "${host}/oauth2/default".toString() + claims[Claims.AUDIENCE] = clientId + tokenGenerator.generateToken(claims).map(HttpResponse::ok).orElseGet(() -> HttpResponse.serverError()) + } + + @Get("/oauth2/default/.well-known/openid-configuration") + String openIdConfiguration(HttpRequest<?> request) { + String host = httpHostResolver.resolve(request) + '{"issuer":"' + host + '/oauth2/default","authorization_endpoint":"' + host + '/oauth2/default/v1/authorize","token_endpoint":"' + host + '/oauth2/default/v1/token","userinfo_endpoint":"' + host + '/oauth2/default/v1/userinfo","registration_endpoint":"' + host + '/oauth2/v1/clients","jwks_uri":"' + host + '/keys","response_types_supported":["code","id_token","code id_token","code token","id_token token","code id_token token"],"response_modes_supported":["query","fragment","form_post","okta_post_message"],"grant_types_supported":["authorization_code","implicit","refresh_token","password"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"scopes_supported":["openid","profile","email","address","phone","offline_access"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt","none"],"claims_supported":["iss","ver","sub","aud","iat","exp","jti","auth_time","amr","idp","nonce","name","nickname","preferred_username","given_name","middle_name","family_name","email","email_verified","profile","zoneinfo","locale","address","phone_number","picture","website","gender","birthdate","updated_at","at_hash","c_hash"],"code_challenge_methods_supported":["S256"],"introspection_endpoint":"' + host + '/oauth2/default/v1/introspect","introspection_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt","none"],"revocation_endpoint":"' + host + '/oauth2/default/v1/revoke","revocation_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt","none"],"end_session_endpoint":"' + host + '/oauth2/default/v1/logout","request_parameter_supported":true,"request_object_signing_alg_values_supported":["HS256","HS384","HS512","RS256","RS384","RS512","ES256","ES384","ES512"]}' + } + } + + @Requires(property = 'spec.name', value = 'AudienceClaimMatchesClientIdSpecAuthServer') + @Named("generator") + @Singleton + static class AuthServerSignatureConfiguration implements RSASignatureGeneratorConfiguration, JwkProvider { + private final static JWSAlgorithm ALG = JWSAlgorithm.RS256 + private List<JWK> jwks = null + private RSAKey rsaKey = null + + List<JWK> getJwks() { + if (jwks == null) { + this.jwks = Collections.singletonList(getRsaKey().toPublicJWK()) + } + return jwks + } + + RSAKey getRsaKey() { + if (rsaKey == null) { + JWKGenerator jwkGenerator = new RSAKeyGenerator(2048) + .algorithm(ALG) + .keyUse(KeyUse.SIGNATURE) + this.rsaKey = jwkGenerator.generate() + } + return rsaKey + } + + @Override + RSAPublicKey getPublicKey() { + getRsaKey().toRSAPublicKey() + } + + @Override + RSAPrivateKey getPrivateKey() { + getRsaKey().toRSAPrivateKey() + } + + @Override + JWSAlgorithm getJwsAlgorithm() { + ALG + } + + @Override + List<JWK> retrieveJsonWebKeys() { + getJwks() + } + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-qw22-8w9r-864hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-36820ghsaADVISORY
- github.com/micronaut-projects/micronaut-security/blob/master/security-oauth2/src/main/java/io/micronaut/security/oauth2/client/IdTokenClaimsValidator.javaghsaWEB
- github.com/micronaut-projects/micronaut-security/commit/9728b925221a0d87798ccf250657a3c214b7e980ghsax_refsource_MISCWEB
- github.com/micronaut-projects/micronaut-security/security/advisories/GHSA-qw22-8w9r-864hghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.