KEYCLOAK-14093 Specify Signature Algorithm in Signed JWT with Client Secret
This commit is contained in:
parent
c4a6f0830e
commit
3716bd96ad
4 changed files with 116 additions and 1 deletions
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -13,5 +13,27 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="tokenEndpointAuthSigningAlg">{{:: 'token-endpoint-auth-signing-alg' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<div>
|
||||
<select class="form-control" id="tokenEndpointAuthSigningAlg"
|
||||
ng-change="switchChange()"
|
||||
ng-model="tokenEndpointAuthSigningAlg">
|
||||
<option value=""></option>
|
||||
<option ng-repeat="provider in serverInfo.listProviderIds('clientSignature')" value="{{provider}}">{{provider}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'token-endpoint-auth-signing-alg.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="client.access.configure">
|
||||
<button kc-save data-ng-disabled="!changed" data-ng-click="save()">{{:: 'save' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue