KEYCLOAK-14093 Specify Signature Algorithm in Signed JWT with Client Secret

This commit is contained in:
Takashi Norimatsu 2020-05-07 10:43:35 +09:00 committed by Marek Posolda
parent c4a6f0830e
commit 3716bd96ad
4 changed files with 116 additions and 1 deletions

View file

@ -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);

View file

@ -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");

View file

@ -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() {

View file

@ -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>