[KEYCLOAK-9809] Support private_key_jwt authentication for external IdP

This commit is contained in:
madgaet 2019-09-11 00:08:34 +02:00 committed by Marek Posolda
parent 69359eab23
commit c35718cb87
7 changed files with 261 additions and 26 deletions

View file

@ -30,10 +30,17 @@ import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.AsymmetricSignatureProvider;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.MacSignatureSignerContext;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
@ -41,14 +48,18 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.vault.VaultStringSecret;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.GET;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
@ -127,20 +138,20 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
}
protected String extractTokenFromResponse(String response, String tokenName) {
if(response == null)
return null;
if(response == null)
return null;
if (response.startsWith("{")) {
try {
JsonNode node = mapper.readTree(response);
if(node.has(tokenName)){
String s = node.get(tokenName).textValue();
if(s == null || s.trim().isEmpty())
return null;
return s;
} else {
return null;
}
JsonNode node = mapper.readTree(response);
if(node.has(tokenName)){
String s = node.get(tokenName).textValue();
if(s == null || s.trim().isEmpty())
return null;
return s;
} else {
return null;
}
} catch (IOException e) {
throw new IdentityBrokerException("Could not extract token [" + tokenName + "] from response [" + response + "] due: " + e.getMessage(), e);
}
@ -356,11 +367,11 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
*/
public String getJsonProperty(JsonNode jsonNode, String name) {
if (jsonNode.has(name) && !jsonNode.get(name).isNull()) {
String s = jsonNode.get(name).asText();
if(s != null && !s.isEmpty())
return s;
else
return null;
String s = jsonNode.get(name).asText();
if(s != null && !s.isEmpty())
return s;
else
return null;
}
return null;
@ -443,15 +454,50 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
}
public SimpleHttp generateTokenRequest(String authorizationCode) {
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
return SimpleHttp.doPost(getConfig().getTokenUrl(), session)
.param(OAUTH2_PARAMETER_CODE, authorizationCode)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret()))
.param(OAUTH2_PARAMETER_REDIRECT_URI, session.getContext().getUri().getAbsolutePath().toString())
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE);
SimpleHttp tokenRequest = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
.param(OAUTH2_PARAMETER_CODE, authorizationCode)
.param(OAUTH2_PARAMETER_REDIRECT_URI, session.getContext().getUri().getAbsolutePath().toString())
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE);
if (getConfig().isJWTAuthentication()) {
String jws = new JWSBuilder().type(OAuth2Constants.JWT).jsonContent(generateToken()).sign(getSignatureContext());
return tokenRequest
.param(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)
.param(OAuth2Constants.CLIENT_ASSERTION, jws);
} else {
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
return tokenRequest
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret()));
}
}
}
protected JsonWebToken generateToken() {
JsonWebToken jwt = new JsonWebToken();
jwt.id(KeycloakModelUtils.generateId());
jwt.type(OAuth2Constants.JWT);
jwt.issuer(getConfig().getClientId());
jwt.subject(getConfig().getClientId());
jwt.audience(getConfig().getTokenUrl());
jwt.issuedNow();
jwt.expiration(Time.currentTime() + realm.getAccessCodeLifespan());
return jwt;
}
protected SignatureSignerContext getSignatureContext() {
if (getConfig().getClientAuthMethod().equals(OIDCLoginProtocol.CLIENT_SECRET_JWT)) {
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
KeyWrapper key = new KeyWrapper();
key.setAlgorithm(Algorithm.HS256);
byte[] decodedSecret = vaultStringSecret.get().orElse(getConfig().getClientSecret()).getBytes();
SecretKey secret = new SecretKeySpec(decodedSecret, 0, decodedSecret.length, Algorithm.HS256);
key.setSecretKey(secret);
return new MacSignatureSignerContext(key);
}
}
return new AsymmetricSignatureProvider(session, Algorithm.RS256).signer();
}
}
protected String getProfileEndpointForValidation(EventBuilder event) {

View file

@ -17,6 +17,7 @@
package org.keycloak.broker.oidc;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
/**
* @author Pedro Igor
@ -59,6 +60,14 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {
getConfig().put("clientId", clientId);
}
public String getClientAuthMethod() {
return getConfig().getOrDefault("clientAuthMethod", OIDCLoginProtocol.CLIENT_SECRET_POST);
}
public void setClientAuthMethod(String clientAuth) {
getConfig().put("clientAuthMethod", clientAuth);
}
public String getClientSecret() {
return getConfig().get("clientSecret");
}
@ -82,6 +91,14 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {
public void setLoginHint(boolean loginHint) {
getConfig().put("loginHint", String.valueOf(loginHint));
}
public boolean isJWTAuthentication() {
if (getClientAuthMethod().equals(OIDCLoginProtocol.CLIENT_SECRET_JWT)
|| getClientAuthMethod().equals(OIDCLoginProtocol.PRIVATE_KEY_JWT)) {
return true;
}
return false;
}
public boolean isUiLocales() {
return Boolean.valueOf(getConfig().get("uiLocales"));

View file

@ -29,6 +29,7 @@ import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.utils.StripSecretsUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
@ -68,6 +69,7 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
@ -139,6 +141,34 @@ public class IdentityProviderTest extends AbstractAdminTest {
assertEquals(ComponentRepresentation.SECRET_VALUE, rep.getConfig().get("clientSecret"));
}
@Test
public void testCreateWithJWT() {
IdentityProviderRepresentation newIdentityProvider = createRep("new-identity-provider", "oidc");
newIdentityProvider.getConfig().put("clientId", "clientId");
newIdentityProvider.getConfig().put("clientAuthMethod", OIDCLoginProtocol.PRIVATE_KEY_JWT);
create(newIdentityProvider);
IdentityProviderResource identityProviderResource = realm.identityProviders().get("new-identity-provider");
assertNotNull(identityProviderResource);
IdentityProviderRepresentation representation = identityProviderResource.toRepresentation();
assertNotNull(representation);
assertNotNull(representation.getInternalId());
assertEquals("new-identity-provider", representation.getAlias());
assertEquals("oidc", representation.getProviderId());
assertEquals("clientId", representation.getConfig().get("clientId"));
assertNull(representation.getConfig().get("clientSecret"));
assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, representation.getConfig().get("clientAuthMethod"));
assertTrue(representation.isEnabled());
assertFalse(representation.isStoreToken());
assertFalse(representation.isTrustEmail());
}
@Test
public void testUpdate() {
IdentityProviderRepresentation newIdentityProvider = createRep("update-identity-provider", "oidc");

View file

@ -0,0 +1,48 @@
package org.keycloak.testsuite.broker;
import static org.keycloak.testsuite.broker.BrokerTestConstants.CLIENT_SECRET;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_PROVIDER_ID;
import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider;
import java.util.List;
import java.util.Map;
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.testsuite.arquillian.SuiteContext;
public class KcOidcBrokerClientSecretJwtTest extends KcOidcBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfigurationWithJWTAuthentication();
}
private class KcOidcBrokerConfigurationWithJWTAuthentication extends KcOidcBrokerConfiguration {
@Override
public List<ClientRepresentation> createProviderClients(SuiteContext suiteContext) {
List<ClientRepresentation> clientsRepList = super.createProviderClients(suiteContext);
log.info("Update provider clients to accept JWT authentication");
for (ClientRepresentation client: clientsRepList) {
client.setClientAuthenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID);
client.setSecret(CLIENT_SECRET);
}
return clientsRepList;
}
@Override
public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) {
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
Map<String, String> config = idp.getConfig();
applyDefaultConfiguration(suiteContext, config);
config.put("clientAuthMethod", OIDCLoginProtocol.CLIENT_SECRET_JWT);
return idp;
}
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.broker;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.crypto.Algorithm;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation.KeyMetadataRepresentation;
import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.testsuite.util.KeyUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_PROVIDER_ID;
import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider;
public class KcOidcBrokerPrivateKeyJwtTest extends KcOidcBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfigurationWithJWTAuthentication();
}
private class KcOidcBrokerConfigurationWithJWTAuthentication extends KcOidcBrokerConfiguration {
@Override
public List<ClientRepresentation> createProviderClients(SuiteContext suiteContext) {
List<ClientRepresentation> clientsRepList = super.createProviderClients(suiteContext);
log.info("Update provider clients to accept JWT authentication");
KeyMetadataRepresentation keyRep = KeyUtils.getActiveKey(adminClient.realm(consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256);
for (ClientRepresentation client: clientsRepList) {
client.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
if (client.getAttributes() == null) {
client.setAttributes(new HashMap<String, String>());
}
client.getAttributes().put(JWTClientAuthenticator.CERTIFICATE_ATTR, keyRep.getCertificate());
}
return clientsRepList;
}
@Override
public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) {
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
Map<String, String> config = idp.getConfig();
applyDefaultConfiguration(suiteContext, config);
config.put("clientSecret", null);
config.put("clientAuthMethod", OIDCLoginProtocol.PRIVATE_KEY_JWT);
return idp;
}
}
}

View file

@ -581,6 +581,12 @@ backchannel-logout=Backchannel Logout
backchannel-logout.tooltip=Does the external IDP support backchannel logout?
user-info-url=User Info URL
user-info-url.tooltip=The User Info Url. This is optional.
client-auth=Client Authentication
client-auth.tooltip=The client authentication methdod (cfr. https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication).
client-auth.client_secret_post=Client secret sent as post
client-auth.client_secret_basic=Client secret sent as basic auth
client-auth.client_secret_jwt=Client secret as jwt
client-auth.private_key_jwt=JWT signed with private key
identity-provider.client-id.tooltip=The client or client identifier registered within the identity provider.
client-secret=Client Secret
show-secret=Show secret

View file

@ -166,6 +166,22 @@
</div>
<kc-tooltip>{{:: 'user-info-url.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="clientAuth"><span class="required">*</span> {{:: 'client-auth' | translate}}</label>
<div class="col-md-6">
<div>
<select class="form-control" id="clientAuth"
ng-model="identityProvider.config.clientAuthMethod"
required>
<option id="clientAuth_post" name="clientAuth" value="client_secret_post" selected>{{:: 'client-auth.client_secret_post' | translate}}</option>
<!-- <option id="clientAuth_basic" name="clientAuth" value="client_secret_basic">{{:: 'client-auth.client_secret_basic' | translate}}</option> -->
<option id="clientAuth_secret_jwt" name="clientAuth" value="client_secret_jwt">{{:: 'client-auth.client_secret_jwt' | translate}}</option>
<option id="clientAuth_privatekey_jwt" name="clientAuth" value="private_key_jwt">{{:: 'client-auth.private_key_jwt' | translate}}</option>
</select>
</div>
</div>
<kc-tooltip>{{:: 'client-auth.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="clientId"><span class="required">*</span> {{:: 'client-id' | translate}}</label>
<div class="col-md-6">
@ -174,9 +190,9 @@
<kc-tooltip>{{:: 'identity-provider.client-id.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="clientSecret"><span class="required">*</span> {{:: 'client-secret' | translate}}</label>
<label class="col-md-2 control-label" for="clientSecret"><span data-ng-show="identityProvider.config.clientAuthMethod != 'private_key_jwt'" class="required">*</span> {{:: 'client-secret' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="clientSecret" kc-password ng-model="identityProvider.config.clientSecret" required>
<input class="form-control" id="clientSecret" kc-password ng-model="identityProvider.config.clientSecret" ng-required="identityProvider.config.clientAuthMethod != 'private_key_jwt'">
</div>
<kc-tooltip>{{:: 'client-secret.tooltip' | translate}}</kc-tooltip>
</div>