keycloak-uncached

Changes

Details

diff --git a/core/src/main/java/org/keycloak/jose/jws/crypto/HashProvider.java b/core/src/main/java/org/keycloak/jose/jws/crypto/HashProvider.java
new file mode 100644
index 0000000..c8fa714
--- /dev/null
+++ b/core/src/main/java/org/keycloak/jose/jws/crypto/HashProvider.java
@@ -0,0 +1,66 @@
+/*
+ * 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.jose.jws.crypto;
+
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.jose.jws.Algorithm;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class HashProvider {
+
+    // See "at_hash" and "c_hash" in OIDC specification
+    public static String oidcHash(Algorithm jwtAlgorithm, String input) {
+        byte[] digest = digest(jwtAlgorithm, input);
+
+        int hashLength = digest.length / 2;
+        byte[] hashInput = Arrays.copyOf(digest, hashLength);
+
+        return Base64Url.encode(hashInput);
+    }
+
+    private static byte[] digest(Algorithm algorithm, String input) {
+        String digestAlg = getJavaDigestAlgorithm(algorithm);
+
+        try {
+            MessageDigest md = MessageDigest.getInstance(digestAlg);
+            md.update(input.getBytes("UTF-8"));
+            return md.digest();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static String getJavaDigestAlgorithm(Algorithm alg) {
+        switch (alg) {
+            case RS256:
+                return "SHA-256";
+            case RS384:
+                return "SHA-384";
+            case RS512:
+                return "SHA-512";
+            default:
+                throw new IllegalArgumentException("Not an RSA Algorithm");
+        }
+    }
+
+}
diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java b/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java
index e4a9805..68ee65d 100755
--- a/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java
+++ b/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java
@@ -108,43 +108,29 @@ public class JWSBuilder {
             return encodeAll(buffer, null);
         }
 
-        public String rsa256(PrivateKey privateKey) {
+        public String sign(Algorithm algorithm, PrivateKey privateKey) {
             StringBuffer buffer = new StringBuffer();
             byte[] data = marshalContent();
-            encode(Algorithm.RS256, data, buffer);
+            encode(algorithm, data, buffer);
             byte[] signature = null;
             try {
-                signature = RSAProvider.sign(buffer.toString().getBytes("UTF-8"), Algorithm.RS256, privateKey);
+                signature = RSAProvider.sign(buffer.toString().getBytes("UTF-8"), algorithm, privateKey);
             } catch (UnsupportedEncodingException e) {
                 throw new RuntimeException(e);
             }
             return encodeAll(buffer, signature);
         }
 
+        public String rsa256(PrivateKey privateKey) {
+            return sign(Algorithm.RS256, privateKey);
+        }
+
         public String rsa384(PrivateKey privateKey) {
-            StringBuffer buffer = new StringBuffer();
-            byte[] data = marshalContent();
-            encode(Algorithm.RS384, data, buffer);
-            byte[] signature = null;
-            try {
-                signature = RSAProvider.sign(buffer.toString().getBytes("UTF-8"), Algorithm.RS384, privateKey);
-            } catch (UnsupportedEncodingException e) {
-                throw new RuntimeException(e);
-            }
-            return encodeAll(buffer, signature);
+            return sign(Algorithm.RS384, privateKey);
         }
 
         public String rsa512(PrivateKey privateKey) {
-            StringBuffer buffer = new StringBuffer();
-            byte[] data = marshalContent();
-            encode(Algorithm.RS512, data, buffer);
-            byte[] signature = null;
-            try {
-                signature = RSAProvider.sign(buffer.toString().getBytes("UTF-8"), Algorithm.RS512, privateKey);
-            } catch (UnsupportedEncodingException e) {
-                throw new RuntimeException(e);
-            }
-            return encodeAll(buffer, signature);
+            return sign(Algorithm.RS512, privateKey);
         }
 
 
diff --git a/core/src/main/java/org/keycloak/representations/IDToken.java b/core/src/main/java/org/keycloak/representations/IDToken.java
index 4d595f5..51776f0 100755
--- a/core/src/main/java/org/keycloak/representations/IDToken.java
+++ b/core/src/main/java/org/keycloak/representations/IDToken.java
@@ -27,6 +27,8 @@ public class IDToken extends JsonWebToken {
     public static final String NONCE = "nonce";
     public static final String AUTH_TIME = "auth_time";
     public static final String SESSION_STATE = "session_state";
+    public static final String AT_HASH = "at_hash";
+    public static final String C_HASH = "c_hash";
     public static final String NAME = "name";
     public static final String GIVEN_NAME = "given_name";
     public static final String FAMILY_NAME = "family_name";
@@ -60,6 +62,12 @@ public class IDToken extends JsonWebToken {
     @JsonProperty(SESSION_STATE)
     protected String sessionState;
 
+    @JsonProperty(AT_HASH)
+    protected String accessTokenHash;
+
+    @JsonProperty(C_HASH)
+    protected String codeHash;
+
     @JsonProperty(NAME)
     protected String name;
 
@@ -147,6 +155,22 @@ public class IDToken extends JsonWebToken {
         this.sessionState = sessionState;
     }
 
+    public String getAccessTokenHash() {
+        return accessTokenHash;
+    }
+
+    public void setAccessTokenHash(String accessTokenHash) {
+        this.accessTokenHash = accessTokenHash;
+    }
+
+    public String getCodeHash() {
+        return codeHash;
+    }
+
+    public void setCodeHash(String codeHash) {
+        this.codeHash = codeHash;
+    }
+
     public String getName() {
         return this.name;
     }
diff --git a/core/src/test/java/org/keycloak/AtHashTest.java b/core/src/test/java/org/keycloak/AtHashTest.java
new file mode 100644
index 0000000..7015e7a
--- /dev/null
+++ b/core/src/test/java/org/keycloak/AtHashTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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;
+
+import java.security.Security;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.jose.jws.Algorithm;
+import org.keycloak.jose.jws.crypto.HashProvider;
+
+/**
+ * See "at_hash" in OIDC specification
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AtHashTest {
+
+    static {
+        if (Security.getProvider("BC") == null) Security.addProvider(new BouncyCastleProvider());
+    }
+
+    @Test
+    public void testAtHash() throws Exception {
+        verifyHash("jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y", "77QmUPtjPfzWtF2AnpK9RQ");
+        verifyHash("ya29.eQETFbFOkAs8nWHcmYXKwEi0Zz46NfsrUU_KuQLOLTwWS40y6Fb99aVzEXC0U14m61lcPMIr1hEIBA", "aUAkJG-u6x4RTWuILWy-CA");
+    }
+
+    private void verifyHash(String accessToken, String expectedAtHash) {
+        String atHash = HashProvider.oidcHash(Algorithm.RS256, accessToken);
+        Assert.assertEquals(expectedAtHash, atHash);
+    }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index b08d3d8..931b04c 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -271,12 +271,10 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
     }
 
     private Response checkResponseType() {
-        OIDCResponseMode defaultResponseMode = client.isImplicitFlowEnabled() ? OIDCResponseMode.FRAGMENT : OIDCResponseMode.QUERY;
-
         if (responseType == null) {
             logger.missingParameter(OAuth2Constants.RESPONSE_TYPE);
             event.error(Errors.INVALID_REQUEST);
-            return redirectErrorToClient(defaultResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: response_type");
+            return redirectErrorToClient(OIDCResponseMode.QUERY, OAuthErrorException.INVALID_REQUEST, "Missing parameter: response_type");
         }
 
         event.detail(Details.RESPONSE_TYPE, responseType);
@@ -289,7 +287,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
         } catch (IllegalArgumentException iae) {
             logger.error(iae.getMessage());
             event.error(Errors.INVALID_REQUEST);
-            return redirectErrorToClient(defaultResponseMode, OAuthErrorException.UNSUPPORTED_RESPONSE_TYPE, null);
+            return redirectErrorToClient(OIDCResponseMode.QUERY, OAuthErrorException.UNSUPPORTED_RESPONSE_TYPE, null);
         }
 
         OIDCResponseMode parsedResponseMode = null;
@@ -298,7 +296,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
         } catch (IllegalArgumentException iae) {
             logger.invalidParameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
             event.error(Errors.INVALID_REQUEST);
-            return redirectErrorToClient(defaultResponseMode, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: response_mode");
+            return redirectErrorToClient(OIDCResponseMode.QUERY, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: response_mode");
         }
 
         event.detail(Details.RESPONSE_MODE, parsedResponseMode.toString().toLowerCase());
@@ -307,7 +305,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
         if (parsedResponseType.isImplicitOrHybridFlow() && parsedResponseMode == OIDCResponseMode.QUERY) {
             logger.responseModeQueryNotAllowed();
             event.error(Errors.INVALID_REQUEST);
-            return redirectErrorToClient(defaultResponseMode, OAuthErrorException.INVALID_REQUEST, "Response_mode 'query' not allowed for implicit or hybrid flow");
+            return redirectErrorToClient(OIDCResponseMode.QUERY, OAuthErrorException.INVALID_REQUEST, "Response_mode 'query' not allowed for implicit or hybrid flow");
         }
 
         if ((parsedResponseType.hasResponseType(OIDCResponseType.CODE) || parsedResponseType.hasResponseType(OIDCResponseType.NONE)) && !client.isStandardFlowEnabled()) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index 8271cc7..7ad2354 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -165,10 +165,24 @@ public class OIDCLoginProtocol implements LoginProtocol {
         // Implicit or hybrid flow
         if (responseType.isImplicitOrHybridFlow()) {
             TokenManager tokenManager = new TokenManager();
-            AccessTokenResponse res = tokenManager.responseBuilder(realm, clientSession.getClient(), event, session, userSession, clientSession)
-                    .generateAccessToken()
-                    .generateIDToken()
-                    .build();
+            TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, clientSession.getClient(), event, session, userSession, clientSession)
+                    .generateAccessToken();
+
+            if (responseType.hasResponseType(OIDCResponseType.ID_TOKEN)) {
+
+                responseBuilder.generateIDToken();
+
+                if (responseType.hasResponseType(OIDCResponseType.TOKEN)) {
+                    responseBuilder.generateAccessTokenHash();
+                }
+
+                if (responseType.hasResponseType(OIDCResponseType.CODE)) {
+                    responseBuilder.generateCodeHash(accessCode.getCode());
+                }
+
+            }
+
+            AccessTokenResponse res = responseBuilder.build();
 
             if (responseType.hasResponseType(OIDCResponseType.ID_TOKEN)) {
                 redirectUri.addParam(OAuth2Constants.ID_TOKEN, res.getIdToken());
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index d55df61..ac1d8e1 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -24,9 +24,11 @@ import org.keycloak.OAuthErrorException;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
+import org.keycloak.jose.jws.Algorithm;
 import org.keycloak.jose.jws.JWSBuilder;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.jose.jws.crypto.HashProvider;
 import org.keycloak.jose.jws.crypto.RSAProvider;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
@@ -81,6 +83,9 @@ public class TokenManager {
     protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
     private static final String JWT = "JWT";
 
+    // Harcoded for now
+    Algorithm jwsAlgorithm = Algorithm.RS256;
+
     public static void applyScope(RoleModel role, RoleModel scope, Set<RoleModel> visited, Set<RoleModel> requested) {
         if (visited.contains(scope)) return;
         visited.add(scope);
@@ -619,7 +624,7 @@ public class TokenManager {
                 .type(JWT)
                 .kid(realm.getKeyId())
                 .jsonContent(token)
-                .rsa256(realm.getPrivateKey());
+                .sign(jwsAlgorithm, realm.getPrivateKey());
         return encodedToken;
     }
 
@@ -639,6 +644,9 @@ public class TokenManager {
         RefreshToken refreshToken;
         IDToken idToken;
 
+        boolean generateAccessTokenHash = false;
+        String codeHash;
+
         public AccessTokenResponseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
             this.realm = realm;
             this.client = client;
@@ -712,6 +720,15 @@ public class TokenManager {
             return this;
         }
 
+        public AccessTokenResponseBuilder generateAccessTokenHash() {
+            generateAccessTokenHash = true;
+            return this;
+        }
+
+        public AccessTokenResponseBuilder generateCodeHash(String code) {
+            codeHash = HashProvider.oidcHash(jwsAlgorithm, code);
+            return this;
+        }
 
 
         public AccessTokenResponse build() {
@@ -729,12 +746,8 @@ public class TokenManager {
             }
 
             AccessTokenResponse res = new AccessTokenResponse();
-            if (idToken != null) {
-                String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(idToken).rsa256(realm.getPrivateKey());
-                res.setIdToken(encodedToken);
-            }
             if (accessToken != null) {
-                String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(accessToken).rsa256(realm.getPrivateKey());
+                String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(accessToken).sign(jwsAlgorithm, realm.getPrivateKey());
                 res.setToken(encodedToken);
                 res.setTokenType("bearer");
                 res.setSessionState(accessToken.getSessionState());
@@ -742,8 +755,21 @@ public class TokenManager {
                     res.setExpiresIn(accessToken.getExpiration() - Time.currentTime());
                 }
             }
+
+            if (generateAccessTokenHash) {
+                String atHash = HashProvider.oidcHash(jwsAlgorithm, res.getToken());
+                idToken.setAccessTokenHash(atHash);
+            }
+            if (codeHash != null) {
+                idToken.setCodeHash(codeHash);
+            }
+
+            if (idToken != null) {
+                String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(idToken).sign(jwsAlgorithm, realm.getPrivateKey());
+                res.setIdToken(encodedToken);
+            }
             if (refreshToken != null) {
-                String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(refreshToken).rsa256(realm.getPrivateKey());
+                String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(refreshToken).sign(jwsAlgorithm, realm.getPrivateKey());
                 res.setRefreshToken(encodedToken);
                 if (refreshToken.getExpiration() != 0) {
                     res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime());
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 1d632c1..a964e07 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -103,6 +103,8 @@ public class OAuthClient {
 
     private String responseMode;
 
+    private String nonce;
+
     private Map<String, PublicKey> publicKeys = new HashMap<>();
 
     public void init(Keycloak adminClient, WebDriver driver) {
@@ -521,9 +523,12 @@ public class OAuthClient {
         if (state != null) {
             b.queryParam(OAuth2Constants.STATE, state);
         }
-        if(uiLocales != null){
+        if (uiLocales != null){
             b.queryParam(OAuth2Constants.UI_LOCALES_PARAM, uiLocales);
         }
+        if (nonce != null){
+            b.queryParam(OIDCLoginProtocol.NONCE_PARAM, nonce);
+        }
 
         String scopeParam = TokenUtil.attachOIDCScope(scope);
         b.queryParam(OAuth2Constants.SCOPE, scopeParam);
@@ -634,6 +639,11 @@ public class OAuthClient {
         return this;
     }
 
+    public OAuthClient nonce(String nonce) {
+        this.nonce = nonce;
+        return this;
+    }
+
     public String getRealm() {
         return realm;
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
index cf69f74..99ee7c6 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
@@ -145,33 +145,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
     }
 
     @Test
-    public void authorizationRequestImplicitFlowDisabled() throws IOException {
-        oauth.responseType("token id_token");
-        UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
-        driver.navigate().to(b.build().toURL());
-
-        OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth, true);
-        Assert.assertTrue(errorResponse.isRedirected());
-        Assert.assertEquals(errorResponse.getError(), OAuthErrorException.UNSUPPORTED_RESPONSE_TYPE);
-        Assert.assertEquals(errorResponse.getErrorDescription(), "Client is not allowed to initiate browser login with given response_type. Implicit flow is disabled for the client.");
-
-        events.expectLogin().error(Errors.NOT_ALLOWED).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "token id_token").assertEvent();
-    }
-
-    @Test
-    public void authorizationRequestMissingResponseType() throws IOException {
-        oauth.responseType(null);
-        UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
-        driver.navigate().to(b.build().toURL());
-
-        OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
-        Assert.assertTrue(errorResponse.isRedirected());
-        Assert.assertEquals(errorResponse.getError(), OAuthErrorException.INVALID_REQUEST);
-
-        events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent();
-    }
-
-    @Test
     public void authorizationRequestInvalidResponseType() throws IOException {
         oauth.responseType("tokenn");
         UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTokenTest.java
new file mode 100644
index 0000000..dd56c7d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTokenTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.flows;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.events.Details;
+import org.keycloak.jose.jws.crypto.HashProvider;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.util.OAuthClient;
+
+/**
+ * Tests with response_type=code id_token token
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OIDCHybridResponseTypeCodeIDTokenTokenTest extends AbstractOIDCResponseTypeTest {
+
+    @Before
+    public void clientConfiguration() {
+        clientManagerBuilder().standardFlow(true).implicitFlow(true);
+
+        oauth.clientId("test-app");
+        oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN + " "  + OIDCResponseType.TOKEN);
+    }
+
+
+    protected List<IDToken> retrieveIDTokens(EventRepresentation loginEvent) {
+        Assert.assertEquals(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN, loginEvent.getDetails().get(Details.RESPONSE_TYPE));
+
+        // IDToken from the authorization response
+        OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth, true);
+        Assert.assertNotNull(authzResponse.getAccessToken());
+        String idTokenStr = authzResponse.getIdToken();
+        IDToken idToken = oauth.verifyIDToken(idTokenStr);
+
+        // Validate "at_hash" and "c_hash"
+        Assert.assertNotNull(idToken.getAccessTokenHash());
+        Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getAccessToken()));
+        Assert.assertNotNull(idToken.getCodeHash());
+        Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getCode()));
+
+        // IDToken exchanged for the code
+        IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
+
+        return Arrays.asList(idToken, idToken2);
+    }
+
+
+    @Test
+    public void nonceNotUsedErrorExpected() {
+        super.validateNonceNotUsedErrorExpected();
+    }
+
+    @Test
+    public void errorStandardFlowNotAllowed() throws Exception {
+        super.validateErrorStandardFlowNotAllowed();
+    }
+
+    @Test
+    public void errorImplicitFlowNotAllowed() throws Exception {
+        super.validateErrorImplicitFlowNotAllowed();
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeTokenTest.java
new file mode 100644
index 0000000..4ce4a28
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeTokenTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.flows;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.events.Details;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.util.OAuthClient;
+
+/**
+ * Tests with response_type=code token
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OIDCHybridResponseTypeCodeTokenTest extends AbstractOIDCResponseTypeTest  {
+
+    @Before
+    public void clientConfiguration() {
+        clientManagerBuilder().standardFlow(true).implicitFlow(true);
+
+        oauth.clientId("test-app");
+        oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.TOKEN);
+    }
+
+
+    protected List<IDToken> retrieveIDTokens(EventRepresentation loginEvent) {
+        Assert.assertEquals(OIDCResponseType.CODE + " " + OIDCResponseType.TOKEN, loginEvent.getDetails().get(Details.RESPONSE_TYPE));
+
+        OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth, true);
+        Assert.assertNotNull(authzResponse.getAccessToken());
+        Assert.assertNull(authzResponse.getIdToken());
+
+        // IDToken exchanged for the code
+        IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
+
+        return Collections.singletonList(idToken2);
+    }
+
+
+    @Test
+    public void nonceNotUsedErrorExpected() {
+        super.validateNonceNotUsedErrorExpected();
+    }
+
+    @Test
+    public void errorStandardFlowNotAllowed() throws Exception {
+        super.validateErrorStandardFlowNotAllowed();
+    }
+
+    @Test
+    public void errorImplicitFlowNotAllowed() throws Exception {
+        super.validateErrorImplicitFlowNotAllowed();
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCImplicitResponseTypeIDTokenTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCImplicitResponseTypeIDTokenTokenTest.java
new file mode 100644
index 0000000..899d406
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCImplicitResponseTypeIDTokenTokenTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.flows;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.events.Details;
+import org.keycloak.jose.jws.crypto.HashProvider;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.util.OAuthClient;
+
+/**
+ * Tests with response_type=id_token token
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OIDCImplicitResponseTypeIDTokenTokenTest extends AbstractOIDCResponseTypeTest {
+
+    @Before
+    public void clientConfiguration() {
+        clientManagerBuilder().standardFlow(false).implicitFlow(true);
+
+        oauth.clientId("test-app");
+        oauth.responseType(OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN);
+    }
+
+
+    protected List<IDToken> retrieveIDTokens(EventRepresentation loginEvent) {
+        Assert.assertEquals(OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN, loginEvent.getDetails().get(Details.RESPONSE_TYPE));
+
+        OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth, true);
+        Assert.assertNotNull(authzResponse.getAccessToken());
+        String idTokenStr = authzResponse.getIdToken();
+        IDToken idToken = oauth.verifyIDToken(idTokenStr);
+
+        // Validate "at_hash"
+        Assert.assertNotNull(idToken.getAccessTokenHash());
+        Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getAccessToken()));
+        Assert.assertNull(idToken.getCodeHash());
+
+        return Collections.singletonList(idToken);
+    }
+
+
+    @Test
+    public void nonceNotUsedErrorExpected() {
+        super.validateNonceNotUsedErrorExpected();
+    }
+
+    @Test
+    public void errorImplicitFlowNotAllowed() throws Exception {
+        super.validateErrorImplicitFlowNotAllowed();
+    }
+}