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 e4a98050a6..68ee65d9b3 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/jose/jws/crypto/HashProvider.java b/core/src/main/java/org/keycloak/jose/jws/crypto/HashProvider.java
new file mode 100644
index 0000000000..c8fa714841
--- /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 Marek Posolda
+ */
+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/representations/IDToken.java b/core/src/main/java/org/keycloak/representations/IDToken.java
index 4d595f5178..51776f0d17 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 0000000000..7015e7a93b
--- /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 Marek Posolda
+ */
+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/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index 8271cc70c7..7ad235407b 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 d55df61fbe..ac1d8e1f32 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 visited, Set 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 1d632c1051..a964e07803 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 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/oidc/resptype/AbstractOIDCResponseTypeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/AbstractOIDCResponseTypeTest.java
similarity index 75%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/resptype/AbstractOIDCResponseTypeTest.java
rename to testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/AbstractOIDCResponseTypeTest.java
index 047a62d4c9..f4d806a986 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/resptype/AbstractOIDCResponseTypeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/AbstractOIDCResponseTypeTest.java
@@ -15,14 +15,16 @@
* limitations under the License.
*/
-package org.keycloak.testsuite.oidc.resptype;
+package org.keycloak.testsuite.oidc.flows;
import java.util.List;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Rule;
+import org.junit.Test;
import org.keycloak.OAuthErrorException;
import org.keycloak.events.Details;
+import org.keycloak.jose.jws.Algorithm;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@@ -30,10 +32,8 @@ import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.TestRealmKeycloakTest;
import org.keycloak.testsuite.admin.AbstractAdminTest;
-import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
-import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.util.OAuthClient;
import static org.junit.Assert.assertFalse;
@@ -46,6 +46,9 @@ import static org.junit.Assert.assertTrue;
*/
public abstract class AbstractOIDCResponseTypeTest extends TestRealmKeycloakTest {
+ // Harcoded for now
+ Algorithm jwsAlgorithm = Algorithm.RS256;
+
@Rule
public AssertEvents events = new AssertEvents(this);
@@ -55,12 +58,6 @@ public abstract class AbstractOIDCResponseTypeTest extends TestRealmKeycloakTest
@Page
protected LoginPage loginPage;
- @Page
- protected AccountUpdateProfilePage profilePage;
-
- @Page
- protected OAuthGrantPage grantPage;
-
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@@ -73,14 +70,9 @@ public abstract class AbstractOIDCResponseTypeTest extends TestRealmKeycloakTest
}
- protected void nonceMatches() {
- driver.navigate().to(oauth.getLoginFormUrl() + "&nonce=abcdef123456");
-
- loginPage.assertCurrent();
- loginPage.login("test-user@localhost", "password");
- Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
-
- EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
+ @Test
+ public void nonceMatches() {
+ EventRepresentation loginEvent = loginUser("abcdef123456");
List idTokens = retrieveIDTokens(loginEvent);
for (IDToken idToken : idTokens) {
@@ -89,23 +81,8 @@ public abstract class AbstractOIDCResponseTypeTest extends TestRealmKeycloakTest
}
- protected void nonceNotUsed() {
- driver.navigate().to(oauth.getLoginFormUrl());
-
- loginPage.assertCurrent();
- loginPage.login("test-user@localhost", "password");
- Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
-
- EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
-
- List idTokens = retrieveIDTokens(loginEvent);
- for (IDToken idToken : idTokens) {
- Assert.assertNull(idToken.getNonce());
- }
- }
-
-
- protected void nonceNotUsedErrorExpected() {
+ protected void validateNonceNotUsedErrorExpected() {
+ oauth.nonce(null);
driver.navigate().to(oauth.getLoginFormUrl());
assertFalse(loginPage.isCurrent());
@@ -119,5 +96,20 @@ public abstract class AbstractOIDCResponseTypeTest extends TestRealmKeycloakTest
Assert.assertEquals("Missing parameter: nonce", resp.getErrorDescription());
}
+
+ protected EventRepresentation loginUser(String nonce) {
+ if (nonce != null) {
+ oauth.nonce(nonce);
+ }
+
+ driver.navigate().to(oauth.getLoginFormUrl());
+
+ loginPage.assertCurrent();
+ loginPage.login("test-user@localhost", "password");
+ Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ return events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
+ }
+
protected abstract List retrieveIDTokens(EventRepresentation loginEvent);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/resptype/OIDCBasicResponseTypeCodeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCBasicResponseTypeCodeTest.java
similarity index 89%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/resptype/OIDCBasicResponseTypeCodeTest.java
rename to testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCBasicResponseTypeCodeTest.java
index a8f51fb96e..7f042f5dc6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/resptype/OIDCBasicResponseTypeCodeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCBasicResponseTypeCodeTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.keycloak.testsuite.oidc.resptype;
+package org.keycloak.testsuite.oidc.flows;
import java.util.Collections;
import java.util.List;
@@ -61,12 +61,11 @@ public class OIDCBasicResponseTypeCodeTest extends AbstractOIDCResponseTypeTest
@Test
public void nonceNotUsed() {
- super.nonceNotUsed();
- }
+ EventRepresentation loginEvent = loginUser(null);
-
- @Test
- public void nonceMatches() {
- super.nonceMatches();
+ List idTokens = retrieveIDTokens(loginEvent);
+ for (IDToken idToken : idTokens) {
+ Assert.assertNull(idToken.getNonce());
+ }
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/resptype/OIDCHybridResponseTypeCodeIDTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTest.java
similarity index 85%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/resptype/OIDCHybridResponseTypeCodeIDTokenTest.java
rename to testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTest.java
index ff16927a52..f210b715d5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/resptype/OIDCHybridResponseTypeCodeIDTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTest.java
@@ -15,15 +15,15 @@
* limitations under the License.
*/
-package org.keycloak.testsuite.oidc.resptype;
+package org.keycloak.testsuite.oidc.flows;
import java.util.Arrays;
-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;
@@ -56,6 +56,11 @@ public class OIDCHybridResponseTypeCodeIDTokenTest extends AbstractOIDCResponseT
String idTokenStr = authzResponse.getIdToken();
IDToken idToken = oauth.verifyIDToken(idTokenStr);
+ // Validate "c_hash"
+ Assert.assertNull(idToken.getAccessTokenHash());
+ Assert.assertNotNull(idToken.getCodeHash());
+ Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getCode()));
+
// IDToken exchanged for the code
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
@@ -65,12 +70,6 @@ public class OIDCHybridResponseTypeCodeIDTokenTest extends AbstractOIDCResponseT
@Test
public void nonceNotUsedErrorExpected() {
- super.nonceNotUsedErrorExpected();
- }
-
-
- @Test
- public void nonceMatches() {
- super.nonceMatches();
+ super.validateNonceNotUsedErrorExpected();
}
}
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 0000000000..354db5a46d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTokenTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.ClientManager;
+import org.keycloak.testsuite.util.OAuthClient;
+
+/**
+ * Tests with response_type=code id_token token
+ *
+ * @author Marek Posolda
+ */
+public class OIDCHybridResponseTypeCodeIDTokenTokenTest extends AbstractOIDCResponseTypeTest {
+
+ @Before
+ public void clientConfiguration() {
+ ClientManager.realm(adminClient.realm("test")).clientId("test-app").standardFlow(true).implicitFlow(true);
+
+ oauth.clientId("test-app");
+ oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN);
+ }
+
+
+ protected List 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();
+ }
+}
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 0000000000..9490a7ecf6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeTokenTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.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.ClientManager;
+import org.keycloak.testsuite.util.OAuthClient;
+
+/**
+ * Tests with response_type=code token
+ *
+ * @author Marek Posolda
+ */
+public class OIDCHybridResponseTypeCodeTokenTest extends AbstractOIDCResponseTypeTest {
+
+ @Before
+ public void clientConfiguration() {
+ ClientManager.realm(adminClient.realm("test")).clientId("test-app").standardFlow(true).implicitFlow(true);
+
+ oauth.clientId("test-app");
+ oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.TOKEN);
+ }
+
+
+ protected List 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();
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/resptype/OIDCImplicitResponseTypeIDTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCImplicitResponseTypeIDTokenTest.java
similarity index 91%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/resptype/OIDCImplicitResponseTypeIDTokenTest.java
rename to testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCImplicitResponseTypeIDTokenTest.java
index 4bcdc6508c..f5284f353c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/resptype/OIDCImplicitResponseTypeIDTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCImplicitResponseTypeIDTokenTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.keycloak.testsuite.oidc.resptype;
+package org.keycloak.testsuite.oidc.flows;
import java.util.Collections;
import java.util.List;
@@ -54,19 +54,16 @@ public class OIDCImplicitResponseTypeIDTokenTest extends AbstractOIDCResponseTyp
String idTokenStr = authzResponse.getIdToken();
IDToken idToken = oauth.verifyIDToken(idTokenStr);
+ Assert.assertNull(idToken.getAccessTokenHash());
+ Assert.assertNull(idToken.getCodeHash());
+
return Collections.singletonList(idToken);
}
@Test
public void nonceNotUsedErrorExpected() {
- super.nonceNotUsedErrorExpected();
- }
-
-
- @Test
- public void nonceMatches() {
- super.nonceMatches();
+ super.validateNonceNotUsedErrorExpected();
}
}
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 0000000000..1ce2e666eb
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCImplicitResponseTypeIDTokenTokenTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.ClientManager;
+import org.keycloak.testsuite.util.OAuthClient;
+
+/**
+ * Tests with response_type=id_token token
+ *
+ * @author Marek Posolda
+ */
+public class OIDCImplicitResponseTypeIDTokenTokenTest extends AbstractOIDCResponseTypeTest {
+
+ @Before
+ public void clientConfiguration() {
+ ClientManager.realm(adminClient.realm("test")).clientId("test-app").standardFlow(false).implicitFlow(true);
+
+ oauth.clientId("test-app");
+ oauth.responseType(OIDCResponseType.ID_TOKEN + " " + OIDCResponseType.TOKEN);
+ }
+
+
+ protected List 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();
+ }
+}