keycloak-memoizeit

Merge pull request #3014 from mposolda/KEYCLOAK-3222 OIDC

7/8/2016 1:16:43 PM

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));
+        }
+    }
+}