KEYCLOAK-12615 HS384 and HS512 support for Client Authentication by Client Secret Signed JWT (#6633)

This commit is contained in:
Takashi Norimatsu 2020-01-28 22:55:48 +09:00 committed by Marek Posolda
parent 3beef2a4c0
commit 993ba3179c
17 changed files with 582 additions and 101 deletions

View file

@ -1,3 +1,19 @@
/*
* Copyright 2018 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.adapters.authentication; package org.keycloak.adapters.authentication;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -11,6 +27,8 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.AdapterUtils; import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.JsonWebToken;
@ -18,7 +36,6 @@ import org.keycloak.representations.JsonWebToken;
* Client authentication based on JWT signed by client secret instead of private key . * Client authentication based on JWT signed by client secret instead of private key .
* See <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">specs</a> for more details. * See <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">specs</a> for more details.
* *
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/ */
public class JWTClientSecretCredentialsProvider implements ClientCredentialsProvider { public class JWTClientSecretCredentialsProvider implements ClientCredentialsProvider {
@ -28,6 +45,8 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv
private SecretKey clientSecret; private SecretKey clientSecret;
private String clientSecretJwtAlg = Algorithm.HS256;
@Override @Override
public String getId() { public String getId() {
return PROVIDER_ID; return PROVIDER_ID;
@ -44,7 +63,24 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv
if (clientSecretString == null) { if (clientSecretString == null) {
throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName()); throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName());
} }
String clientSecretJwtAlg = (String) cfg.get("algorithm");
if (clientSecretJwtAlg == null) {
// "algorithm" field is optional. fallback to HS256.
setClientSecret(clientSecretString); setClientSecret(clientSecretString);
} else if (isValidClientSecretJwtAlg(clientSecretJwtAlg)) {
setClientSecret(clientSecretString, clientSecretJwtAlg);
} else {
// invalid "algorithm" field
throw new RuntimeException("Invalid parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName());
}
}
private boolean isValidClientSecretJwtAlg(String clientSecretJwtAlg) {
boolean ret = false;
if (Algorithm.HS256.equals(clientSecretJwtAlg) || Algorithm.HS384.equals(clientSecretJwtAlg) || Algorithm.HS512.equals(clientSecretJwtAlg))
ret = true;
return ret;
} }
@Override @Override
@ -60,15 +96,29 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv
// The HMAC (Hash-based Message Authentication Code) is calculated using the octets of the UTF-8 representation of the client_secret as the shared key. // The HMAC (Hash-based Message Authentication Code) is calculated using the octets of the UTF-8 representation of the client_secret as the shared key.
// Use "HmacSHA256" consulting <a href="https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Mac.html">java8 api</a> // Use "HmacSHA256" consulting <a href="https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Mac.html">java8 api</a>
// because it must be implemented in every java platform. // because it must be implemented in every java platform.
clientSecret = new SecretKeySpec(clientSecretString.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); setClientSecret(clientSecretString, Algorithm.HS256);
}
public void setClientSecret(String clientSecretString, String algorithm) {
clientSecret = new SecretKeySpec(clientSecretString.getBytes(StandardCharsets.UTF_8), JavaAlgorithm.getJavaAlgorithm(algorithm));
clientSecretJwtAlg = algorithm;
} }
public String createSignedRequestToken(String clientId, String realmInfoUrl) { public String createSignedRequestToken(String clientId, String realmInfoUrl) {
return createSignedRequestToken(clientId, realmInfoUrl, clientSecretJwtAlg);
}
public String createSignedRequestToken(String clientId, String realmInfoUrl, String algorithm) {
JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl); JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
// JOSE header {"alg":"HS256","typ" : "JWT"} no need "kid" due to using only one registered client secret. String signedRequestToken = null;
// Use "HmacSHA256" consulting <a href="https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Mac.html">java8 api</a>. if (Algorithm.HS512.equals(algorithm)) {
// because it must be implemented in every java platform. signedRequestToken = new JWSBuilder().jsonContent(jwt).hmac512(clientSecret);
return new JWSBuilder().jsonContent(jwt).hmac256(clientSecret); } else if (Algorithm.HS384.equals(algorithm)) {
signedRequestToken = new JWSBuilder().jsonContent(jwt).hmac384(clientSecret);
} else {
signedRequestToken = new JWSBuilder().jsonContent(jwt).hmac256(clientSecret);
}
return signedRequestToken;
} }
private JsonWebToken createRequestToken(String clientId, String realmInfoUrl) { private JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {

View file

@ -1,6 +1,21 @@
/*
* Copyright 2018 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.authentication.authenticators.client; package org.keycloak.authentication.authenticators.client;
import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -10,8 +25,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -21,7 +34,6 @@ import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext; import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.models.AuthenticationExecutionModel.Requirement; import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.SingleUseTokenStoreProvider; import org.keycloak.models.SingleUseTokenStoreProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -42,14 +54,13 @@ import org.keycloak.services.Urls;
* *
* TODO: Try to create abstract superclass to be shared with {@link JWTClientAuthenticator}. Most of the code can be reused * TODO: Try to create abstract superclass to be shared with {@link JWTClientAuthenticator}. Most of the code can be reused
* *
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/ */
public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator { public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
private static final Logger logger = Logger.getLogger(JWTClientSecretAuthenticator.class); private static final Logger logger = Logger.getLogger(JWTClientSecretAuthenticator.class);
public static final String PROVIDER_ID = "client-secret-jwt"; public static final String PROVIDER_ID = "client-secret-jwt";
@Override @Override
public void authenticateClient(ClientAuthenticationFlowContext context) { public void authenticateClient(ClientAuthenticationFlowContext context) {
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
@ -106,15 +117,10 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
return; return;
} }
// Get client secret and validate signature
// According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
// The HMAC (Hash-based Message Authentication Code) is calculated using the octets of the UTF-8 representation of the client_secret as the shared key.
// Use "HmacSHA256" consulting <a href="https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Mac.html">java8 api</a>.
SecretKey clientSecret = new SecretKeySpec(clientSecretString.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
boolean signatureValid; boolean signatureValid;
try { try {
signatureValid = HMACProvider.verify(jws, clientSecret); JsonWebToken jwt = context.getSession().tokens().decodeClientJWT(clientAssertion, client, JsonWebToken.class);
signatureValid = jwt != null;
} catch (RuntimeException e) { } catch (RuntimeException e) {
Throwable cause = e.getCause() != null ? e.getCause() : e; Throwable cause = e.getCause() != null ? e.getCause() : e;
throw new RuntimeException("Signature on JWT token by client secret failed validation", cause); throw new RuntimeException("Signature on JWT token by client secret failed validation", cause);
@ -177,14 +183,16 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
@Override @Override
public Map<String, Object> getAdapterConfiguration(ClientModel client) { public Map<String, Object> getAdapterConfiguration(ClientModel client) {
// e.g. // e.g. client adapter's keycloak.json
// "credentials": { // "credentials": {
// "secret-jwt": { // "secret-jwt": {
// "secret": "234234-234234-234234" // "secret": "234234-234234-234234",
// "algorithm": "HS256"
// } // }
// } // }
Map<String, Object> props = new HashMap<>(); Map<String, Object> props = new HashMap<>();
props.put("secret", client.getSecret()); props.put("secret", client.getSecret());
// "algorithm" field is not saved because keycloak does not manage client's property of which algorithm is used for client secret signed JWT.
Map<String, Object> config = new HashMap<>(); Map<String, Object> config = new HashMap<>();
config.put("secret-jwt", props); config.put("secret-jwt", props);
@ -228,5 +236,4 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
return new LinkedList<>(); return new LinkedList<>();
} }
} }

View file

@ -0,0 +1,46 @@
/*
* Copyright 2020 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.crypto;
import java.nio.charset.StandardCharsets;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.keycloak.common.VerificationException;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
public class ClientMacSignatureVerifierContext extends MacSignatureVerifierContext {
public ClientMacSignatureVerifierContext(KeycloakSession session, ClientModel client, String algorithm) throws VerificationException {
super(getKey(session, client, algorithm));
}
private static KeyWrapper getKey(KeycloakSession session, ClientModel client, String algorithm) throws VerificationException {
if (algorithm == null) algorithm = Algorithm.HS256;
String clientSecretString = client.getSecret();
SecretKey clientSecret = new SecretKeySpec(clientSecretString.getBytes(StandardCharsets.UTF_8), JavaAlgorithm.getJavaAlgorithm(algorithm));
KeyWrapper key = new KeyWrapper();
key.setSecretKey(clientSecret);
key.setUse(KeyUse.SIG);
key.setType(KeyType.OCT);
key.setAlgorithm(algorithm);
return key;
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2020 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.crypto;
import org.keycloak.models.KeycloakSession;
public class HS256ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {
public static final String ID = Algorithm.HS256;
@Override
public String getId() {
return ID;
}
@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new MacSecretClientSignatureVerifierProvider(session, Algorithm.HS256);
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2020 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.crypto;
import org.keycloak.models.KeycloakSession;
public class HS384ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {
public static final String ID = Algorithm.HS384;
@Override
public String getId() {
return ID;
}
@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new MacSecretClientSignatureVerifierProvider(session, Algorithm.HS384);
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2020 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.crypto;
import org.keycloak.models.KeycloakSession;
public class HS512ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {
public static final String ID = Algorithm.HS512;
@Override
public String getId() {
return ID;
}
@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new MacSecretClientSignatureVerifierProvider(session, Algorithm.HS512);
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2020 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.crypto;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
public class MacSecretClientSignatureVerifierProvider implements ClientSignatureVerifierProvider {
private final KeycloakSession session;
private final String algorithm;
public MacSecretClientSignatureVerifierProvider(KeycloakSession session, String algorithm) {
this.session = session;
this.algorithm = algorithm;
}
@Override
public SignatureVerifierContext verifier(ClientModel client, JWSInput input) throws VerificationException {
return new ClientMacSignatureVerifierContext(session, client, algorithm);
}
}

View file

@ -7,3 +7,6 @@ org.keycloak.crypto.ES512ClientSignatureVerifierProviderFactory
org.keycloak.crypto.PS256ClientSignatureVerifierProviderFactory org.keycloak.crypto.PS256ClientSignatureVerifierProviderFactory
org.keycloak.crypto.PS384ClientSignatureVerifierProviderFactory org.keycloak.crypto.PS384ClientSignatureVerifierProviderFactory
org.keycloak.crypto.PS512ClientSignatureVerifierProviderFactory org.keycloak.crypto.PS512ClientSignatureVerifierProviderFactory
org.keycloak.crypto.HS256ClientSignatureVerifierProviderFactory
org.keycloak.crypto.HS384ClientSignatureVerifierProviderFactory
org.keycloak.crypto.HS512ClientSignatureVerifierProviderFactory

View file

@ -0,0 +1,39 @@
/*
* Copyright 2020 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.adapter.page;
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
import java.net.URL;
public class ClientSecretJwtSecurePortalValidAlg extends AbstractPageWithInjectedUrl {
public static final String DEPLOYMENT_NAME = "client-secret-jwt-secure-portal-valid-alg";
@ArquillianResource
@OperateOnDeployment(DEPLOYMENT_NAME)
private URL url;
@Override
public URL getInjectedUrl() {
return url;
}
}

View file

@ -39,7 +39,6 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.VersionRepresentation;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
@ -49,6 +48,7 @@ import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter; import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
import org.keycloak.testsuite.adapter.page.BasicAuth; import org.keycloak.testsuite.adapter.page.BasicAuth;
import org.keycloak.testsuite.adapter.page.ClientSecretJwtSecurePortal; import org.keycloak.testsuite.adapter.page.ClientSecretJwtSecurePortal;
import org.keycloak.testsuite.adapter.page.ClientSecretJwtSecurePortalValidAlg;
import org.keycloak.testsuite.adapter.page.CustomerCookiePortal; import org.keycloak.testsuite.adapter.page.CustomerCookiePortal;
import org.keycloak.testsuite.adapter.page.CustomerCookiePortalRoot; import org.keycloak.testsuite.adapter.page.CustomerCookiePortalRoot;
import org.keycloak.testsuite.adapter.page.CustomerDb; import org.keycloak.testsuite.adapter.page.CustomerDb;
@ -72,6 +72,7 @@ import org.keycloak.testsuite.auth.page.login.OAuthGrant;
import org.keycloak.testsuite.auth.page.login.OIDCLogin; import org.keycloak.testsuite.auth.page.login.OIDCLogin;
import org.keycloak.testsuite.console.page.events.Config; import org.keycloak.testsuite.console.page.events.Config;
import org.keycloak.testsuite.console.page.events.LoginEvents; import org.keycloak.testsuite.console.page.events.LoginEvents;
import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
import org.keycloak.testsuite.util.FollowRedirectsEngine; import org.keycloak.testsuite.util.FollowRedirectsEngine;
import org.keycloak.testsuite.util.JavascriptBrowser; import org.keycloak.testsuite.util.JavascriptBrowser;
import org.keycloak.testsuite.util.Matchers; import org.keycloak.testsuite.util.Matchers;
@ -112,7 +113,6 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -183,6 +183,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
@Page @Page
private ClientSecretJwtSecurePortal clientSecretJwtSecurePortal; private ClientSecretJwtSecurePortal clientSecretJwtSecurePortal;
@Page @Page
private ClientSecretJwtSecurePortalValidAlg clientSecretJwtSecurePortalValidAlg;
@Page
private CustomerCookiePortal customerCookiePortal; private CustomerCookiePortal customerCookiePortal;
@Page @Page
private CustomerCookiePortalRoot customerCookiePortalRoot; private CustomerCookiePortalRoot customerCookiePortalRoot;
@ -270,6 +272,11 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
return servletDeployment(ClientSecretJwtSecurePortal.DEPLOYMENT_NAME, CallAuthenticatedServlet.class); return servletDeployment(ClientSecretJwtSecurePortal.DEPLOYMENT_NAME, CallAuthenticatedServlet.class);
} }
@Deployment(name = ClientSecretJwtSecurePortalValidAlg.DEPLOYMENT_NAME)
protected static WebArchive clientSecretSecurePortalValidAlg() {
return servletDeployment(ClientSecretJwtSecurePortalValidAlg.DEPLOYMENT_NAME, CallAuthenticatedServlet.class);
}
@Deployment(name = CustomerCookiePortalRoot.DEPLOYMENT_NAME) @Deployment(name = CustomerCookiePortalRoot.DEPLOYMENT_NAME)
protected static WebArchive customerCookiePortalRoot() { protected static WebArchive customerCookiePortalRoot() {
return servletDeployment(CustomerCookiePortalRoot.DEPLOYMENT_NAME, AdapterActionsFilter.class, CustomerServlet.class, ErrorServlet.class, ServletTestUtils.class); return servletDeployment(CustomerCookiePortalRoot.DEPLOYMENT_NAME, AdapterActionsFilter.class, CustomerServlet.class, ErrorServlet.class, ServletTestUtils.class);
@ -1236,7 +1243,7 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
// http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication // http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
String targetClientId = "client-secret-jwt-secure-portal"; String targetClientId = "client-secret-jwt-secure-portal";
expectResultOfClientAuthenticatedInClientSecretJwt(targetClientId); expectResultOfClientAuthenticatedInClientSecretJwt(targetClientId, clientSecretJwtSecurePortal);
// test logout // test logout
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()) String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
@ -1274,6 +1281,18 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
expectResultOfClientNotAuthenticatedInClientSecretJwt(targetClientId, expectedErrorString); expectResultOfClientNotAuthenticatedInClientSecretJwt(targetClientId, expectedErrorString);
} }
@Test
public void testClientAuthenticatedInClientSecretJwtValidAlg() {
String targetClientId = "client-secret-jwt-secure-portal-valid-alg";
expectResultOfClientAuthenticatedInClientSecretJwt(targetClientId, clientSecretJwtSecurePortalValidAlg);
// test logout
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
.queryParam(OAuth2Constants.REDIRECT_URI, clientSecretJwtSecurePortalValidAlg.toString()).build("demo").toString();
driver.navigate().to(logoutUri);
}
@Test @Test
public void testTokenInCookieSSORoot() { public void testTokenInCookieSSORoot() {
// Login // Login
@ -1322,14 +1341,14 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage); assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
} }
private void expectResultOfClientAuthenticatedInClientSecretJwt(String targetClientId) { private void expectResultOfClientAuthenticatedInClientSecretJwt(String targetClientId, AbstractPageWithInjectedUrl portal) {
RealmRepresentation realm = testRealmResource().toRepresentation(); RealmRepresentation realm = testRealmResource().toRepresentation();
realm.setEventsEnabled(true); realm.setEventsEnabled(true);
realm.setEnabledEventTypes(Arrays.asList("LOGIN", "CODE_TO_TOKEN")); realm.setEnabledEventTypes(Arrays.asList("LOGIN", "CODE_TO_TOKEN"));
realm.setEventsListeners(Arrays.asList("jboss-logging", "event-queue")); realm.setEventsListeners(Arrays.asList("jboss-logging", "event-queue"));
testRealmResource().update(realm); testRealmResource().update(realm);
clientSecretJwtSecurePortal.navigateTo(); portal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage); assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
testRealmLoginPage.form().login("bburke@redhat.com", "password"); testRealmLoginPage.form().login("bburke@redhat.com", "password");
@ -1342,8 +1361,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
.detail(Details.USERNAME, "bburke@redhat.com") .detail(Details.USERNAME, "bburke@redhat.com")
.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED) .detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED)
.detail(Details.REDIRECT_URI, .detail(Details.REDIRECT_URI,
org.hamcrest.Matchers.anyOf(org.hamcrest.Matchers.equalTo(clientSecretJwtSecurePortal.getInjectedUrl().toString()), org.hamcrest.Matchers.anyOf(org.hamcrest.Matchers.equalTo(portal.getInjectedUrl().toString()),
org.hamcrest.Matchers.equalTo(clientSecretJwtSecurePortal.getInjectedUrl().toString() + "/"))) org.hamcrest.Matchers.equalTo(portal.getInjectedUrl().toString() + "/")))
.removeDetail(Details.CODE_ID) .removeDetail(Details.CODE_ID)
.assertEvent(); .assertEvent();

View file

@ -1,3 +1,19 @@
/*
* Copyright 2018 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.oauth; package org.keycloak.testsuite.oauth;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -21,6 +37,7 @@ import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenti
import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.UriUtils; import org.keycloak.common.util.UriUtils;
import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -31,11 +48,9 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
/**
* @author Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>
*/
@AuthServerContainerExclude(AuthServer.REMOTE) @AuthServerContainerExclude(AuthServer.REMOTE)
public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest { public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
private static final Logger logger = Logger.getLogger(ClientAuthSecretSignedJWTTest.class); private static final Logger logger = Logger.getLogger(ClientAuthSecretSignedJWTTest.class);
@Rule @Rule
@ -56,6 +71,20 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
@Test @Test
public void testCodeToTokenRequestSuccess() throws Exception { public void testCodeToTokenRequestSuccess() throws Exception {
testCodeToTokenRequestSuccess(Algorithm.HS256);
}
@Test
public void testCodeToTokenRequestSuccessHS384() throws Exception {
testCodeToTokenRequestSuccess(Algorithm.HS384);
}
@Test
public void testCodeToTokenRequestSuccessHS512() throws Exception {
testCodeToTokenRequestSuccess(Algorithm.HS512);
}
private void testCodeToTokenRequestSuccess(String algorithm) throws Exception {
oauth.clientId("test-app"); oauth.clientId("test-app");
oauth.doLogin("test-user@localhost", "password"); oauth.doLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin() EventRepresentation loginEvent = events.expectLogin()
@ -63,7 +92,7 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
.assertEvent(); .assertEvent();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClientSignedJWT("password", 20)); OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClientSignedJWT("password", 20, algorithm));
assertEquals(200, response.getStatusCode()); assertEquals(200, response.getStatusCode());
oauth.verifyToken(response.getAccessToken()); oauth.verifyToken(response.getAccessToken());
@ -92,7 +121,6 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
assertEquals("unauthorized_client", response.getError()); assertEquals("unauthorized_client", response.getError());
} }
@Test @Test
public void testAssertionReuse() throws Exception { public void testAssertionReuse() throws Exception {
oauth.clientId("test-app"); oauth.clientId("test-app");
@ -132,11 +160,14 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
assertEquals("unauthorized_client", response.getError()); assertEquals("unauthorized_client", response.getError());
} }
private String getClientSignedJWT(String secret, int timeout) { private String getClientSignedJWT(String secret, int timeout) {
return getClientSignedJWT(secret, timeout, Algorithm.HS256);
}
private String getClientSignedJWT(String secret, int timeout, String algorithm) {
JWTClientSecretCredentialsProvider jwtProvider = new JWTClientSecretCredentialsProvider(); JWTClientSecretCredentialsProvider jwtProvider = new JWTClientSecretCredentialsProvider();
jwtProvider.setClientSecret(secret); jwtProvider.setClientSecret(secret, algorithm);
return jwtProvider.createSignedRequestToken(oauth.getClientId(), getRealmInfoUrl()); return jwtProvider.createSignedRequestToken(oauth.getClientId(), getRealmInfoUrl(), algorithm);
} }
private String getRealmInfoUrl() { private String getRealmInfoUrl() {
@ -167,4 +198,5 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
oauth.closeClient(client); oauth.closeClient(client);
} }
} }
} }

View file

@ -129,7 +129,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
// Signature algorithms // Signature algorithms
Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512); Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512); Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512); Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
// Encryption algorithms // Encryption algorithms
Assert.assertNames(oidcConfig.getIdTokenEncryptionAlgValuesSupported(), JWEConstants.RSA1_5, JWEConstants.RSA_OAEP); Assert.assertNames(oidcConfig.getIdTokenEncryptionAlgValuesSupported(), JWEConstants.RSA1_5, JWEConstants.RSA_OAEP);
@ -137,7 +137,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
// Client authentication // Client authentication
Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt", "tls_client_auth"); Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt", "tls_client_auth");
Assert.assertNames(oidcConfig.getTokenEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512); Assert.assertNames(oidcConfig.getTokenEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
// Claims // Claims
assertContains(oidcConfig.getClaimsSupported(), IDToken.NAME, IDToken.EMAIL, IDToken.PREFERRED_USERNAME, IDToken.FAMILY_NAME, IDToken.ACR); assertContains(oidcConfig.getClaimsSupported(), IDToken.NAME, IDToken.EMAIL, IDToken.PREFERRED_USERNAME, IDToken.FAMILY_NAME, IDToken.ACR);

View file

@ -0,0 +1,20 @@
<!--
~ Copyright 2020 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.
-->
<Context path="/customer-portal">
<Valve className="org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve"/>
</Context>

View file

@ -0,0 +1,46 @@
<?xml version="1.0"?>
<!--
~ Copyright 2020 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.
-->
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
<Get name="securityHandler">
<Set name="authenticator">
<New class="org.keycloak.adapters.jetty.KeycloakJettyAuthenticator">
<!--
<Set name="adapterConfig">
<New class="org.keycloak.representations.adapters.config.AdapterConfig">
<Set name="realm">tomcat</Set>
<Set name="resource">customer-portal</Set>
<Set name="authServerUrl">http://localhost:8180/auth</Set>
<Set name="sslRequired">external</Set>
<Set name="credentials">
<Map>
<Entry>
<Item>secret</Item>
<Item>password</Item>
</Entry>
</Map>
</Set>
<Set name="realmKey">MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</Set>
</New>
</Set>
-->
</New>
</Set>
</Get>
</Configure>

View file

@ -0,0 +1,12 @@
{
"realm": "demo",
"auth-server-url": "http://localhost:8180/auth",
"ssl-required": "external",
"resource": "client-secret-jwt-secure-portal-valid-alg",
"credentials": {
"secret-jwt": {
"secret": "234234-234234-234234",
"algorithm": "HS512"
}
}
}

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2020 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.
-->
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>client-secret-jwt-secure-portal-valid-alg</module-name>
<servlet>
<servlet-name>Servlet</servlet-name>
<servlet-class>org.keycloak.testsuite.adapter.servlet.CallAuthenticatedServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Servlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>Permit all</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>*</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>KEYCLOAK</auth-method>
<realm-name>demo</realm-name>
</login-config>
<security-role>
<role-name>admin</role-name>
</security-role>
<security-role>
<role-name>user</role-name>
</security-role>
</web-app>

View file

@ -351,6 +351,17 @@
"/client-secret-jwt-secure-portal/*" "/client-secret-jwt-secure-portal/*"
], ],
"secret": "234234-234234-234234" "secret": "234234-234234-234234"
},
{
"clientId": "client-secret-jwt-secure-portal-valid-alg",
"enabled": true,
"adminUrl": "/client-secret-jwt-secure-portal-valid-alg",
"baseUrl": "/client-secret-jwt-secure-portal-valid-alg",
"clientAuthenticatorType": "client-secret-jwt",
"redirectUris": [
"/client-secret-jwt-secure-portal-valid-alg/*"
],
"secret": "234234-234234-234234"
} }
] ]
} }