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