From 3bfd9995901a498f3dac9e088b6c107bdc644c24 Mon Sep 17 00:00:00 2001 From: mposolda Date: Fri, 8 Jul 2016 15:39:13 +0200 Subject: [PATCH] KEYCLOAK-3222 extend WellKnown to return supported types of client authentications. More tests --- .../java/org/keycloak/OAuth2Constants.java | 2 + .../ClientIdAndSecretAuthenticator.java | 2 +- .../protocol/oidc/OIDCWellKnownProvider.java | 11 +- .../oidc/OIDCWellKnownProviderFactory.java | 4 +- .../OIDCConfigurationRepresentation.java | 22 +++ .../services/resources/RealmsResource.java | 4 + .../oauth/ClientAuthPostMethodTest.java | 126 ++++++++++++++++++ .../oidc/OIDCWellKnownProviderTest.java | 91 +++++++++++++ 8 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthPostMethodTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index bb3ccd7134..75a227742d 100644 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -26,6 +26,8 @@ public interface OAuth2Constants { String CLIENT_ID = "client_id"; + String CLIENT_SECRET = "client_secret"; + String ERROR = "error"; String ERROR_DESCRIPTION = "error_description"; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java index c92ed062bd..957e35dea8 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java @@ -86,7 +86,7 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator if (formData != null && client_id == null) { client_id = formData.getFirst(OAuth2Constants.CLIENT_ID); - clientSecret = formData.getFirst("client_secret"); + clientSecret = formData.getFirst(OAuth2Constants.CLIENT_SECRET); } if (client_id == null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index d9471f8d08..3dbbbee60e 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -18,6 +18,7 @@ package org.keycloak.protocol.oidc; import org.keycloak.OAuth2Constants; +import org.keycloak.jose.jws.Algorithm; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.protocol.oidc.endpoints.TokenEndpoint; @@ -39,7 +40,7 @@ import java.util.List; */ public class OIDCWellKnownProvider implements WellKnownProvider { - public static final List DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list("RS256"); + public static final List DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString()); public static final List DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS); @@ -49,6 +50,11 @@ public class OIDCWellKnownProvider implements WellKnownProvider { public static final List DEFAULT_RESPONSE_MODES_SUPPORTED = list("query", "fragment", "form_post"); + // Should be rather retrieved dynamically based on available ClientAuthenticator providers? + public static final List DEFAULT_CLIENT_AUTH_METHODS_SUPPORTED = list("client_secret_basic", "client_secret_post", "private_key_jwt"); + + public static final List DEFAULT_CLIENT_AUTH_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString()); + private KeycloakSession session; public OIDCWellKnownProvider(KeycloakSession session) { @@ -78,6 +84,9 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED); config.setGrantTypesSupported(DEFAULT_GRANT_TYPES_SUPPORTED); + config.setTokenEndpointAuthMethodsSupported(DEFAULT_CLIENT_AUTH_METHODS_SUPPORTED); + config.setTokenEndpointAuthSigningAlgValuesSupported(DEFAULT_CLIENT_AUTH_SIGNING_ALG_VALUES_SUPPORTED); + return config; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProviderFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProviderFactory.java index 3135047852..91ef686e3b 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProviderFactory.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProviderFactory.java @@ -28,6 +28,8 @@ import org.keycloak.wellknown.WellKnownProviderFactory; */ public class OIDCWellKnownProviderFactory implements WellKnownProviderFactory { + public static final String PROVIDER_ID = "openid-configuration"; + @Override public WellKnownProvider create(KeycloakSession session) { return new OIDCWellKnownProvider(session); @@ -47,7 +49,7 @@ public class OIDCWellKnownProviderFactory implements WellKnownProviderFactory { @Override public String getId() { - return "openid-configuration"; + return PROVIDER_ID; } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java index 138a58e39a..1fc349affe 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -70,6 +70,12 @@ public class OIDCConfigurationRepresentation { @JsonProperty("registration_endpoint") private String registrationEndpoint; + @JsonProperty("token_endpoint_auth_methods_supported") + private List tokenEndpointAuthMethodsSupported; + + @JsonProperty("token_endpoint_auth_signing_alg_values_supported") + private List tokenEndpointAuthSigningAlgValuesSupported; + protected Map otherClaims = new HashMap(); public String getIssuer() { @@ -176,6 +182,22 @@ public class OIDCConfigurationRepresentation { this.registrationEndpoint = registrationEndpoint; } + public List getTokenEndpointAuthMethodsSupported() { + return tokenEndpointAuthMethodsSupported; + } + + public void setTokenEndpointAuthMethodsSupported(List tokenEndpointAuthMethodsSupported) { + this.tokenEndpointAuthMethodsSupported = tokenEndpointAuthMethodsSupported; + } + + public List getTokenEndpointAuthSigningAlgValuesSupported() { + return tokenEndpointAuthSigningAlgValuesSupported; + } + + public void setTokenEndpointAuthSigningAlgValuesSupported(List tokenEndpointAuthSigningAlgValuesSupported) { + this.tokenEndpointAuthSigningAlgValuesSupported = tokenEndpointAuthSigningAlgValuesSupported; + } + @JsonAnyGetter public Map getOtherClaims() { return otherClaims; diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 53c9aaf14a..1e42e7b740 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -99,6 +99,10 @@ public class RealmsResource { return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getBrokerService"); } + public static UriBuilder wellKnownProviderUrl(UriBuilder builder) { + return builder.path(RealmsResource.class).path(RealmsResource.class, "getWellKnown"); + } + @Path("{realm}/protocol/{protocol}") public Object getProtocol(final @PathParam("realm") String name, final @PathParam("protocol") String protocol) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthPostMethodTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthPostMethodTest.java new file mode 100644 index 0000000000..a7d395adae --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthPostMethodTest.java @@ -0,0 +1,126 @@ +/* + * 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.oauth; + +import java.io.UnsupportedEncodingException; +import java.util.LinkedList; +import java.util.List; + +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.events.Details; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.EventRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.AbstractAdminTest; +import org.keycloak.testsuite.util.OAuthClient; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.junit.Assert.assertEquals; + +/** + * Test for "client_secret_post" client authentication (clientID + clientSecret sent in the POST body instead of in "Authorization: Basic" header) + * + * @author Marek Posolda + */ +public class ClientAuthPostMethodTest extends AbstractKeycloakTest { + + @Rule + public AssertEvents events = new AssertEvents(this); + + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation realm = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); + testRealms.add(realm); + } + + + @Test + public void testPostAuthentication() { + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response = doAccessTokenRequestPostAuth(code, "password"); + + assertEquals(200, response.getStatusCode()); + + Assert.assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300))); + Assert.assertThat(response.getRefreshExpiresIn(), allOf(greaterThanOrEqualTo(1750), lessThanOrEqualTo(1800))); + + AccessToken token = oauth.verifyToken(response.getAccessToken()); + + EventRepresentation event = events.expectCodeToToken(codeId, sessionId).assertEvent(); + assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID)); + assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID)); + assertEquals(sessionId, token.getSessionState()); + } + + + private OAuthClient.AccessTokenResponse doAccessTokenRequestPostAuth(String code, String clientSecret) { + CloseableHttpClient client = new DefaultHttpClient(); + try { + HttpPost post = new HttpPost(oauth.getAccessTokenUrl()); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code)); + parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri())); + + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId())); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_SECRET, clientSecret)); + + + UrlEncodedFormEntity formEntity; + try { + formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + post.setEntity(formEntity); + + try { + return new OAuthClient.AccessTokenResponse(client.execute(post)); + } catch (Exception e) { + throw new RuntimeException("Failed to retrieve access token", e); + } + } finally { + oauth.closeClient(client); + } + } + + + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java new file mode 100644 index 0000000000..61d8c877b4 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java @@ -0,0 +1,91 @@ +/* + * 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.oidc; + +import java.net.URI; +import java.util.List; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory; +import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; +import org.keycloak.protocol.oidc.utils.OIDCResponseType; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.resources.RealmsResource; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.admin.AbstractAdminTest; +import org.keycloak.testsuite.util.OAuthClient; + +/** + * @author Marek Posolda + */ +public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation realm = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); + testRealms.add(realm); + } + + + @Test + public void testDiscovery() { + Client client = ClientBuilder.newClient(); + try { + OIDCConfigurationRepresentation oidcConfig = getOIDCDiscoveryConfiguration(client); + + // Support standard + implicit + hybrid flow + assertContains(oidcConfig.getResponseTypesSupported(), OAuth2Constants.CODE, OIDCResponseType.ID_TOKEN, "id_token token", "code id_token", "code token", "code id_token token"); + assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT); + assertContains(oidcConfig.getResponseModesSupported(), "query", "fragment"); + + Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "public"); + Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.RS256.toString()); + + // Client authentication + Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt"); + Assert.assertNames(oidcConfig.getTokenEndpointAuthSigningAlgValuesSupported(), Algorithm.RS256.toString()); + System.out.println("Fopo"); + } finally { + client.close(); + } + } + + private OIDCConfigurationRepresentation getOIDCDiscoveryConfiguration(Client client) { + UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT); + URI oidcDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder).build("test", OIDCWellKnownProviderFactory.PROVIDER_ID); + WebTarget oidcDiscoveryTarget = client.target(oidcDiscoveryUri); + + Response response = oidcDiscoveryTarget.request().get(); + return response.readEntity(OIDCConfigurationRepresentation.class); + } + + private void assertContains(List actual, String... expected) { + for (String exp : expected) { + Assert.assertTrue(actual.contains(exp)); + } + } +}