keycloak-memoizeit
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java 4(+4 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java 2(+1 -1)
services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java 6(+6 -0)
services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java 22(+22 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthPostMethodTest.java 126(+126 -0)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
index 747137d..2490c0e 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
@@ -54,6 +54,10 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
this.tokenTimeout = tokenTimeout;
}
+ protected int getTokenTimeout() {
+ return tokenTimeout;
+ }
+
@Override
public void init(KeycloakDeployment deployment, Object config) {
if (config == null || !(config instanceof Map)) {
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index bb3ccd7..75a2277 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 c92ed06..957e35d 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/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
index f97dce0..2ee2fcb 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
@@ -32,6 +32,7 @@ import javax.ws.rs.core.Response;
import org.keycloak.OAuth2Constants;
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.RSAProvider;
import org.keycloak.models.AuthenticationExecutionModel;
@@ -145,6 +146,11 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
throw new RuntimeException("Token is not active");
}
+ // KEYCLOAK-2986
+ if (token.getExpiration() == 0 && token.getIssuedAt() + 10 < Time.currentTime()) {
+ throw new RuntimeException("Token is not active");
+ }
+
context.success();
} catch (Exception e) {
logger.errorValidatingAssertion(e);
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 d9471f8..3dbbbee 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<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list("RS256");
+ public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
public static final List<String> 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<String> DEFAULT_RESPONSE_MODES_SUPPORTED = list("query", "fragment", "form_post");
+ // Should be rather retrieved dynamically based on available ClientAuthenticator providers?
+ public static final List<String> DEFAULT_CLIENT_AUTH_METHODS_SUPPORTED = list("client_secret_basic", "client_secret_post", "private_key_jwt");
+
+ public static final List<String> 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 3135047..91ef686 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 138a58e..1fc349a 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<String> tokenEndpointAuthMethodsSupported;
+
+ @JsonProperty("token_endpoint_auth_signing_alg_values_supported")
+ private List<String> tokenEndpointAuthSigningAlgValuesSupported;
+
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
public String getIssuer() {
@@ -176,6 +182,22 @@ public class OIDCConfigurationRepresentation {
this.registrationEndpoint = registrationEndpoint;
}
+ public List<String> getTokenEndpointAuthMethodsSupported() {
+ return tokenEndpointAuthMethodsSupported;
+ }
+
+ public void setTokenEndpointAuthMethodsSupported(List<String> tokenEndpointAuthMethodsSupported) {
+ this.tokenEndpointAuthMethodsSupported = tokenEndpointAuthMethodsSupported;
+ }
+
+ public List<String> getTokenEndpointAuthSigningAlgValuesSupported() {
+ return tokenEndpointAuthSigningAlgValuesSupported;
+ }
+
+ public void setTokenEndpointAuthSigningAlgValuesSupported(List<String> tokenEndpointAuthSigningAlgValuesSupported) {
+ this.tokenEndpointAuthSigningAlgValuesSupported = tokenEndpointAuthSigningAlgValuesSupported;
+ }
+
@JsonAnyGetter
public Map<String, Object> 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 53c9aaf..1e42e7b 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 0000000..a7d395a
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAuthPostMethodTest extends AbstractKeycloakTest {
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
index 8a890df..087a1e6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -613,7 +613,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
}
@Test
- @Ignore // Waiting for KEYCLOAK-2986 to be implemented
+ // KEYCLOAK-2986
public void testMissingExpirationClaim() throws Exception {
// Missing only exp; the lifespan should be calculated from issuedAt
OAuthClient.AccessTokenResponse response = testMissingClaim("expiration");
@@ -840,9 +840,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
int now = Time.currentTime();
if (isClaimEnabled("issuedAt")) reqToken.issuedAt(now);
- // For the time being there's no getter for tokenTimeout in JWTClientCredentialsProvider
- // This is fine because KC doesn't care when exp claim is missing (see KEYCLOAK-2986)
- /*if (isClaimEnabled("expiration")) reqToken.expiration(now + getTokenTimeout());*/
+ if (isClaimEnabled("expiration")) reqToken.expiration(now + getTokenTimeout());
if (isClaimEnabled("notBefore")) reqToken.notBefore(now);
return reqToken;
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 0000000..61d8c87
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> 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<String> actual, String... expected) {
+ for (String exp : expected) {
+ Assert.assertTrue(actual.contains(exp));
+ }
+ }
+}