Return invalid_client for introspection client auth failures

Closes #48721

Signed-off-by: Palash Thakur <117917450+palasht75@users.noreply.github.com>
This commit is contained in:
Palash Thakur
2026-05-12 18:32:52 -04:00
committed by Marek Posolda
parent fc667a827a
commit 6d3dd321e7
3 changed files with 55 additions and 8 deletions
@@ -20,11 +20,13 @@ import java.util.List;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@@ -37,6 +39,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.AccessTokenIntrospectionProviderFactory;
import org.keycloak.protocol.oidc.TokenIntrospectionProvider;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.TokenIntrospectContext;
@@ -136,11 +139,23 @@ public class TokenIntrospectionEndpoint {
} catch (ErrorResponseException ere) {
throw ere;
} catch (WebApplicationException wae) {
throw convertClientAuthenticationException(wae);
} catch (Exception e) {
throw throwErrorResponseException(Errors.INVALID_REQUEST, "Authentication failed.", Status.UNAUTHORIZED);
}
}
private WebApplicationException convertClientAuthenticationException(WebApplicationException wae) {
Response response = wae.getResponse();
if (response != null && response.getStatus() == Status.UNAUTHORIZED.getStatusCode()
&& response.getEntity() instanceof OAuth2ErrorRepresentation error
&& OAuthErrorException.UNAUTHORIZED_CLIENT.equals(error.getError())) {
return throwErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client authentication failed.", Status.UNAUTHORIZED);
}
return wae;
}
private void checkSsl() {
if (!session.getContext().getUri().getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
throw new ErrorResponseException("invalid_request", "HTTPS required", Status.FORBIDDEN);
@@ -1815,7 +1815,7 @@ public class ClientPoliciesExecutorTest extends AbstractClientPoliciesTest {
// Send a token introspection request with invalid 'aud' . Should fail
signedJwt = createSignedRequestToken(clientId, privateKey, publicKey, Algorithm.RS256, getRealmInfoUrl() + "/protocol/openid-connect/introspect");
HttpResponse tokenIntrospectionResponse = doTokenIntrospectionWithSignedJWT("access_token", refreshedResponse.getAccessToken(), signedJwt);
assertEquals(401, tokenIntrospectionResponse.getStatusLine().getStatusCode());
assertEquals(400, tokenIntrospectionResponse.getStatusLine().getStatusCode());
// Send a token introspection request with valid 'aud' . Should succeed
signedJwt = createSignedRequestToken(clientId, privateKey, publicKey, Algorithm.RS256, getRealmInfoUrl());
@@ -200,15 +200,27 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
}
@Test
public void testInvalidClientCredentials() throws Exception {
oauth.doLogin("test-user@localhost", "password");
String code = oauth.parseLoginResponse().getCode();
AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code);
public void testInvalidClientCredentials() {
oauth.client("confidential-cli", "bad_credential");
IntrospectionResponse tokenResponse = oauth.doIntrospectionAccessTokenRequest(accessTokenResponse.getAccessToken());
IntrospectionResponse tokenResponse = oauth.doIntrospectionAccessTokenRequest("any-token");
Assertions.assertEquals("Authentication failed.", tokenResponse.getErrorDescription());
Assertions.assertEquals(OAuthErrorException.INVALID_REQUEST, tokenResponse.getError());
Assertions.assertEquals(401, tokenResponse.getStatusCode());
Assertions.assertEquals(OAuthErrorException.INVALID_CLIENT, tokenResponse.getError());
Assertions.assertEquals("Client authentication failed.", tokenResponse.getErrorDescription());
}
@Test
public void testInvalidClientCredentialsPostAuthentication() throws Exception {
try (CloseableHttpResponse response = introspectAccessTokenWithClientSecretPost("confidential-cli", "bad_credential", "any-token")) {
Assertions.assertEquals(401, response.getStatusLine().getStatusCode());
ByteArrayOutputStream out = new ByteArrayOutputStream();
response.getEntity().writeTo(out);
OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(
new String(out.toByteArray(), StandardCharsets.UTF_8), OAuth2ErrorRepresentation.class);
Assertions.assertEquals(OAuthErrorException.INVALID_CLIENT, errorRep.getError());
Assertions.assertEquals("Client authentication failed.", errorRep.getErrorDescription());
}
}
@Test
@@ -709,6 +721,26 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
}
}
private CloseableHttpResponse introspectAccessTokenWithClientSecretPost(String clientId, String clientSecret, String tokenToIntrospect) {
HttpPost post = new HttpPost(oauth.getEndpoints().getIntrospection());
List<NameValuePair> parameters = new LinkedList<>();
parameters.add(new BasicNameValuePair("client_id", clientId));
parameters.add(new BasicNameValuePair("client_secret", clientSecret));
parameters.add(new BasicNameValuePair("token", tokenToIntrospect));
parameters.add(new BasicNameValuePair("token_type_hint", "access_token"));
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8);
post.setEntity(formEntity);
try {
return HttpClientBuilder.create().build().execute(post);
} catch (Exception e) {
throw new RuntimeException("Failed to introspect access token", e);
}
}
private JsonNode introspectRevokedToken() throws Exception {
oauth.doLogin("test-user@localhost", "password");
String code = oauth.parseLoginResponse().getCode();