diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java index 4e0a386cbe..282787d6b8 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java @@ -36,6 +36,7 @@ import org.keycloak.common.util.Time; import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.AuthenticationExecutionModel.Requirement; import org.keycloak.models.SingleUseTokenStoreProvider; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.models.ClientModel; @@ -111,6 +112,20 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator { return; } + String expectedSignatureAlg = OIDCAdvancedConfigWrapper.fromClientModel(client).getTokenEndpointAuthSigningAlg(); + if (jws.getHeader().getAlgorithm() == null || jws.getHeader().getAlgorithm().name() == null) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "invalid signature algorithm"); + context.challenge(challengeResponse); + return; + } + + String actualSignatureAlg = jws.getHeader().getAlgorithm().name(); + if (expectedSignatureAlg != null && !expectedSignatureAlg.equals(actualSignatureAlg)) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "invalid signature algorithm"); + context.challenge(challengeResponse); + return; + } + String clientSecretString = client.getSecret(); if (clientSecretString == null) { context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, null); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java index ea522bb661..9f62b21c96 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java @@ -33,17 +33,22 @@ import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider; +import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.UriUtils; import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.crypto.Algorithm; import org.keycloak.events.Details; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.AbstractAdminTest; +import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.util.OAuthClient; @@ -84,6 +89,60 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest { testCodeToTokenRequestSuccess(Algorithm.HS512); } + @Test + public void testCodeToTokenRequestFailureHS384Enforced() throws Exception { + ClientResource clientResource = null; + ClientRepresentation clientRep = null; + final String realmName = "test"; + final String clientId = "test-app"; + try { + clientResource = ApiUtil.findClientByClientId(adminClient.realm(realmName), clientId); + clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setTokenEndpointAuthSigningAlg(Algorithm.HS384); + clientResource.update(clientRep); + + testCodeToTokenRequestSuccess(Algorithm.HS384); + } catch (Exception e) { + Assert.fail(); + } finally { + clientResource = ApiUtil.findClientByClientId(adminClient.realm(realmName), clientId); + clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setTokenEndpointAuthSigningAlg(null); + clientResource.update(clientRep); + } + } + + @Test + public void testCodeToTokenRequestFailureHS512Enforced() throws Exception { + ClientResource clientResource = null; + ClientRepresentation clientRep = null; + final String realmName = "test"; + final String clientId = "test-app"; + final String clientSecret = "password"; + try { + clientResource = ApiUtil.findClientByClientId(adminClient.realm(realmName), clientId); + clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setTokenEndpointAuthSigningAlg(Algorithm.HS512); + clientResource.update(clientRep); + + oauth.clientId(clientId); + oauth.doLogin("test-user@localhost", clientSecret); + events.expectLogin().client(clientId).assertEvent(); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClientSignedJWT(clientSecret, 20, Algorithm.HS256)); + assertEquals(400, response.getStatusCode()); + assertEquals("invalid_client", response.getError()); + } catch (Exception e) { + Assert.fail(); + } finally { + clientResource = ApiUtil.findClientByClientId(adminClient.realm(realmName), clientId); + clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setTokenEndpointAuthSigningAlg(null); + clientResource.update(clientRep); + } + } + private void testCodeToTokenRequestSuccess(String algorithm) throws Exception { oauth.clientId("test-app"); oauth.doLogin("test-user@localhost", "password"); diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 2c4d2df9db..e823ff36ce 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -136,7 +136,7 @@ module.controller('ClientCredentialsCtrl', function($scope, $location, realm, cl }; }); -module.controller('ClientSecretCtrl', function($scope, $location, ClientSecret, Notifications) { +module.controller('ClientSecretCtrl', function($scope, $location, Client, ClientSecret, Notifications) { var secret = ClientSecret.get({ realm : $scope.realm.realm, client : $scope.client.id }, function() { $scope.secret = secret.value; @@ -156,6 +156,25 @@ module.controller('ClientSecretCtrl', function($scope, $location, ClientSecret, ); }; + $scope.tokenEndpointAuthSigningAlg = $scope.client.attributes['token.endpoint.auth.signing.alg']; + + $scope.switchChange = function() { + $scope.changed = true; + } + + $scope.save = function() { + $scope.client.attributes['token.endpoint.auth.signing.alg'] = $scope.tokenEndpointAuthSigningAlg; + + Client.update({ + realm : $scope.realm.realm, + client : $scope.client.id + }, $scope.client, function() { + $scope.changed = false; + $scope.clientCopy = angular.copy($scope.client); + Notifications.success("Client authentication configuration has been saved to the client."); + }); + }; + $scope.$watch(function() { return $location.path(); }, function() { diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret-jwt.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret-jwt.html index 7cf5bf1359..444bf02faf 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret-jwt.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret-jwt.html @@ -13,5 +13,27 @@ + +
+ +
+
+ +
+
+ {{:: 'token-endpoint-auth-signing-alg.tooltip' | translate}} +
+ +
+
+ +
+
+