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;
import java.nio.charset.StandardCharsets;
@ -11,6 +27,8 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.KeycloakDeployment;
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.representations.JsonWebToken;
@ -18,63 +36,95 @@ import org.keycloak.representations.JsonWebToken;
* 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.
*
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class JWTClientSecretCredentialsProvider implements ClientCredentialsProvider {
private static final Logger logger = Logger.getLogger(JWTClientSecretCredentialsProvider.class);
private static final Logger logger = Logger.getLogger(JWTClientSecretCredentialsProvider.class);
public static final String PROVIDER_ID = "secret-jwt";
private SecretKey clientSecret;
private String clientSecretJwtAlg = Algorithm.HS256;
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void init(KeycloakDeployment deployment, Object config) {
if (!(config instanceof Map)) {
throw new RuntimeException("Configuration of jwt credentials by client secret is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration");
}
Map<String, Object> cfg = (Map<String, Object>) config;
String clientSecretString = (String) cfg.get("secret");
if (clientSecretString == null) {
throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName());
}
setClientSecret(clientSecretString);
String clientSecretJwtAlg = (String) cfg.get("algorithm");
if (clientSecretJwtAlg == null) {
// "algorithm" field is optional. fallback to HS256.
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
public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
String signedToken = createSignedRequestToken(deployment.getResourceName(), deployment.getRealmInfoUrl());
formParams.put(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
formParams.put(OAuth2Constants.CLIENT_ASSERTION, signedToken);
}
public void setClientSecret(String clientSecretString) {
// 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>
// 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) {
return createSignedRequestToken(clientId, realmInfoUrl, clientSecretJwtAlg);
}
public String createSignedRequestToken(String clientId, String realmInfoUrl, String algorithm) {
JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
// JOSE header {"alg":"HS256","typ" : "JWT"} no need "kid" due to using only one registered client secret.
// 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.
return new JWSBuilder().jsonContent(jwt).hmac256(clientSecret);
String signedRequestToken = null;
if (Algorithm.HS512.equals(algorithm)) {
signedRequestToken = new JWSBuilder().jsonContent(jwt).hmac512(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) {
// According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
// JWT claims is the same as one by private_key_jwt
JsonWebToken reqToken = new JsonWebToken();
reqToken.id(AdapterUtils.generateId());
reqToken.issuer(clientId);

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;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@ -10,8 +25,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@ -21,7 +34,6 @@ import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.SingleUseTokenStoreProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -42,13 +54,12 @@ import org.keycloak.services.Urls;
*
* 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 {
private static final Logger logger = Logger.getLogger(JWTClientSecretAuthenticator.class);
public static final String PROVIDER_ID = "client-secret-jwt";
private static final Logger logger = Logger.getLogger(JWTClientSecretAuthenticator.class);
public static final String PROVIDER_ID = "client-secret-jwt";
@Override
public void authenticateClient(ClientAuthenticationFlowContext context) {
@ -56,7 +67,7 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
String clientAssertionType = params.getFirst(OAuth2Constants.CLIENT_ASSERTION_TYPE);
String clientAssertion = params.getFirst(OAuth2Constants.CLIENT_ASSERTION);
if (clientAssertionType == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type is missing");
context.challenge(challengeResponse);
@ -75,7 +86,7 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
return;
}
try {
JWSInput jws = new JWSInput(clientAssertion);
JsonWebToken token = jws.readJsonContent(JsonWebToken.class);
@ -99,22 +110,17 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
context.failure(AuthenticationFlowError.CLIENT_DISABLED, null);
return;
}
String clientSecretString = client.getSecret();
if (clientSecretString == null) {
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, null);
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;
try {
signatureValid = HMACProvider.verify(jws, clientSecret);
JsonWebToken jwt = context.getSession().tokens().decodeClientJWT(clientAssertion, client, JsonWebToken.class);
signatureValid = jwt != null;
} catch (RuntimeException e) {
Throwable cause = e.getCause() != null ? e.getCause() : e;
throw new RuntimeException("Signature on JWT token by client secret failed validation", cause);
@ -124,7 +130,7 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
}
// According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
// JWT contents and verification in client_secret_jwt is the same as in private_key_jwt
// Allow both "issuer" or "token-endpoint" as audience
String issuerUrl = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName());
String tokenUrl = OIDCLoginProtocolService.tokenUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
@ -163,7 +169,7 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
}
}
@Override
public boolean isConfigurable() {
return false;
@ -177,14 +183,16 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
@Override
public Map<String, Object> getAdapterConfiguration(ClientModel client) {
// e.g.
// "credentials": {
// e.g. client adapter's keycloak.json
// "credentials": {
// "secret-jwt": {
// "secret": "234234-234234-234234"
// }
// "secret": "234234-234234-234234",
// "algorithm": "HS256"
// }
// }
Map<String, Object> props = new HashMap<>();
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<>();
config.put("secret-jwt", props);
@ -227,6 +235,5 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
public List<ProviderConfigProperty> getConfigProperties() {
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.PS384ClientSignatureVerifierProviderFactory
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.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.VersionRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
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.page.BasicAuth;
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.CustomerCookiePortalRoot;
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.console.page.events.Config;
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.JavascriptBrowser;
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.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
@ -183,6 +183,8 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
@Page
private ClientSecretJwtSecurePortal clientSecretJwtSecurePortal;
@Page
private ClientSecretJwtSecurePortalValidAlg clientSecretJwtSecurePortalValidAlg;
@Page
private CustomerCookiePortal customerCookiePortal;
@Page
private CustomerCookiePortalRoot customerCookiePortalRoot;
@ -270,6 +272,11 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
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)
protected static WebArchive customerCookiePortalRoot() {
return servletDeployment(CustomerCookiePortalRoot.DEPLOYMENT_NAME, AdapterActionsFilter.class, CustomerServlet.class, ErrorServlet.class, ServletTestUtils.class);
@ -1232,48 +1239,60 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
@Test
public void testClientAuthenticatedInClientSecretJwt() {
// test login to customer-portal which does a bearer request to customer-db
// JWS Client Assertion in client_secret_jwt
// http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
// JWS Client Assertion in client_secret_jwt
// http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
String targetClientId = "client-secret-jwt-secure-portal";
expectResultOfClientAuthenticatedInClientSecretJwt(targetClientId);
expectResultOfClientAuthenticatedInClientSecretJwt(targetClientId, clientSecretJwtSecurePortal);
// test logout
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
.queryParam(OAuth2Constants.REDIRECT_URI, clientSecretJwtSecurePortal.toString()).build("demo").toString();
driver.navigate().to(logoutUri);
}
@Test
public void testClientNotAuthenticatedInClientSecretJwtBySharedSecretOutOfSync() {
// JWS Client Assertion in client_secret_jwt
// http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
String targetClientId = "client-secret-jwt-secure-portal";
String expectedErrorString = "invalid_client_credentials";
// JWS Client Assertion in client_secret_jwt
// http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
String targetClientId = "client-secret-jwt-secure-portal";
String expectedErrorString = "invalid_client_credentials";
ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), targetClientId);
ClientRepresentation client = clientResource.toRepresentation();
client.setSecret("passwordChanged");
clientResource.update(client);
expectResultOfClientNotAuthenticatedInClientSecretJwt(targetClientId, expectedErrorString);
}
@Test
public void testClientNotAuthenticatedInClientSecretJwtByAuthnMethodOutOfSync() {
// JWS Client Assertion in client_secret_jwt
// http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
String targetClientId = "client-secret-jwt-secure-portal";
String expectedErrorString = "invalid_client_credentials";
// JWS Client Assertion in client_secret_jwt
// http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
String targetClientId = "client-secret-jwt-secure-portal";
String expectedErrorString = "invalid_client_credentials";
ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), targetClientId);
ClientRepresentation client = clientResource.toRepresentation();
client.setClientAuthenticatorType("client-secret");
clientResource.update(client);
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
public void testTokenInCookieSSORoot() {
// Login
@ -1321,20 +1340,20 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
customerCookiePortalRoot.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
}
private void expectResultOfClientAuthenticatedInClientSecretJwt(String targetClientId) {
private void expectResultOfClientAuthenticatedInClientSecretJwt(String targetClientId, AbstractPageWithInjectedUrl portal) {
RealmRepresentation realm = testRealmResource().toRepresentation();
realm.setEventsEnabled(true);
realm.setEnabledEventTypes(Arrays.asList("LOGIN", "CODE_TO_TOKEN"));
realm.setEventsListeners(Arrays.asList("jboss-logging", "event-queue"));
testRealmResource().update(realm);
clientSecretJwtSecurePortal.navigateTo();
portal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
testRealmLoginPage.form().login("bburke@redhat.com", "password");
String userId = ApiUtil.findUserByUsername(testRealmResource(), "bburke@redhat.com").getId();
assertEvents.expectLogin()
.realm(realm.getId())
.client(targetClientId)
@ -1342,11 +1361,11 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
.detail(Details.USERNAME, "bburke@redhat.com")
.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED)
.detail(Details.REDIRECT_URI,
org.hamcrest.Matchers.anyOf(org.hamcrest.Matchers.equalTo(clientSecretJwtSecurePortal.getInjectedUrl().toString()),
org.hamcrest.Matchers.equalTo(clientSecretJwtSecurePortal.getInjectedUrl().toString() + "/")))
org.hamcrest.Matchers.anyOf(org.hamcrest.Matchers.equalTo(portal.getInjectedUrl().toString()),
org.hamcrest.Matchers.equalTo(portal.getInjectedUrl().toString() + "/")))
.removeDetail(Details.CODE_ID)
.assertEvent();
assertEvents.expectCodeToToken(null, null)
.realm(realm.getId())
.client(targetClientId)
@ -1355,18 +1374,18 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
.clearDetails()
.assertEvent();
}
private void expectResultOfClientNotAuthenticatedInClientSecretJwt(String targetClientId, String expectedErrorString) {
RealmRepresentation realm = testRealmResource().toRepresentation();
realm.setEventsEnabled(true);
realm.setEnabledEventTypes(Arrays.asList("LOGIN", "CODE_TO_TOKEN_ERROR"));
realm.setEventsListeners(Arrays.asList("jboss-logging", "event-queue"));
testRealmResource().update(realm);
clientSecretJwtSecurePortal.navigateTo();
clientSecretJwtSecurePortal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
testRealmLoginPage.form().login("bburke@redhat.com", "password");
String userId = ApiUtil.findUserByUsername(testRealmResource(), "bburke@redhat.com").getId();
assertEvents.expectLogin()

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;
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.UriUtils;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Details;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@ -31,31 +48,43 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.util.OAuthClient;
/**
* @author Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>
*/
@AuthServerContainerExclude(AuthServer.REMOTE)
public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
private static final Logger logger = Logger.getLogger(ClientAuthSecretSignedJWTTest.class);
private static final Logger logger = Logger.getLogger(ClientAuthSecretSignedJWTTest.class);
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void beforeAbstractKeycloakTest() throws Exception {
super.beforeAbstractKeycloakTest();
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/client-auth-test/testrealm-jwt-client-secret.json"), RealmRepresentation.class);
testRealms.add(realm);
}
// TEST SUCCESS
@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.doLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin()
@ -63,8 +92,8 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
.assertEvent();
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());
oauth.verifyToken(response.getAccessToken());
oauth.parseRefreshToken(response.getRefreshToken());
@ -73,9 +102,9 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
.detail(Details.CLIENT_AUTH_METHOD, JWTClientSecretAuthenticator.PROVIDER_ID)
.assertEvent();
}
// TEST ERRORS
@Test
public void testAssertionInvalidSignature() throws Exception {
oauth.clientId("test-app");
@ -92,7 +121,6 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
assertEquals("unauthorized_client", response.getError());
}
@Test
public void testAssertionReuse() throws Exception {
oauth.clientId("test-app");
@ -132,18 +160,21 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
assertEquals("unauthorized_client", response.getError());
}
private String getClientSignedJWT(String secret, int timeout) {
JWTClientSecretCredentialsProvider jwtProvider = new JWTClientSecretCredentialsProvider();
jwtProvider.setClientSecret(secret);
return jwtProvider.createSignedRequestToken(oauth.getClientId(), getRealmInfoUrl());
return getClientSignedJWT(secret, timeout, Algorithm.HS256);
}
private String getClientSignedJWT(String secret, int timeout, String algorithm) {
JWTClientSecretCredentialsProvider jwtProvider = new JWTClientSecretCredentialsProvider();
jwtProvider.setClientSecret(secret, algorithm);
return jwtProvider.createSignedRequestToken(oauth.getClientId(), getRealmInfoUrl(), algorithm);
}
private String getRealmInfoUrl() {
String authServerBaseUrl = UriUtils.getOrigin(oauth.getRedirectUri()) + "/auth";
return KeycloakUriBuilder.fromUri(authServerBaseUrl).path(ServiceUrlConstants.REALM_INFO_PATH).build("test").toString();
}
private OAuthClient.AccessTokenResponse doAccessTokenRequest(String code, String signedJwt) throws Exception {
List<NameValuePair> parameters = new LinkedList<>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
@ -151,11 +182,11 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
CloseableHttpResponse response = sendRequest(oauth.getAccessTokenUrl(), parameters);
return new OAuthClient.AccessTokenResponse(response);
}
private CloseableHttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters) throws Exception {
CloseableHttpClient client = new DefaultHttpClient();
try {
@ -167,4 +198,5 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
oauth.closeClient(client);
}
}
}

View file

@ -129,7 +129,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
// 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.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
Assert.assertNames(oidcConfig.getIdTokenEncryptionAlgValuesSupported(), JWEConstants.RSA1_5, JWEConstants.RSA_OAEP);
@ -137,7 +137,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
// Client authentication
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
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/*"
],
"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"
}
]
}