Merge pull request #3115 from mposolda/master
KEYCLOAK-2169 KEYCLOAK-3286 Support for at_hash and c_hash
This commit is contained in:
commit
64c2077c0b
14 changed files with 471 additions and 93 deletions
|
@ -108,43 +108,29 @@ public class JWSBuilder {
|
||||||
return encodeAll(buffer, null);
|
return encodeAll(buffer, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String rsa256(PrivateKey privateKey) {
|
public String sign(Algorithm algorithm, PrivateKey privateKey) {
|
||||||
StringBuffer buffer = new StringBuffer();
|
StringBuffer buffer = new StringBuffer();
|
||||||
byte[] data = marshalContent();
|
byte[] data = marshalContent();
|
||||||
encode(Algorithm.RS256, data, buffer);
|
encode(algorithm, data, buffer);
|
||||||
byte[] signature = null;
|
byte[] signature = null;
|
||||||
try {
|
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) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
return encodeAll(buffer, signature);
|
return encodeAll(buffer, signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String rsa256(PrivateKey privateKey) {
|
||||||
|
return sign(Algorithm.RS256, privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
public String rsa384(PrivateKey privateKey) {
|
public String rsa384(PrivateKey privateKey) {
|
||||||
StringBuffer buffer = new StringBuffer();
|
return sign(Algorithm.RS384, privateKey);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String rsa512(PrivateKey privateKey) {
|
public String rsa512(PrivateKey privateKey) {
|
||||||
StringBuffer buffer = new StringBuffer();
|
return sign(Algorithm.RS512, privateKey);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -27,6 +27,8 @@ public class IDToken extends JsonWebToken {
|
||||||
public static final String NONCE = "nonce";
|
public static final String NONCE = "nonce";
|
||||||
public static final String AUTH_TIME = "auth_time";
|
public static final String AUTH_TIME = "auth_time";
|
||||||
public static final String SESSION_STATE = "session_state";
|
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 NAME = "name";
|
||||||
public static final String GIVEN_NAME = "given_name";
|
public static final String GIVEN_NAME = "given_name";
|
||||||
public static final String FAMILY_NAME = "family_name";
|
public static final String FAMILY_NAME = "family_name";
|
||||||
|
@ -60,6 +62,12 @@ public class IDToken extends JsonWebToken {
|
||||||
@JsonProperty(SESSION_STATE)
|
@JsonProperty(SESSION_STATE)
|
||||||
protected String sessionState;
|
protected String sessionState;
|
||||||
|
|
||||||
|
@JsonProperty(AT_HASH)
|
||||||
|
protected String accessTokenHash;
|
||||||
|
|
||||||
|
@JsonProperty(C_HASH)
|
||||||
|
protected String codeHash;
|
||||||
|
|
||||||
@JsonProperty(NAME)
|
@JsonProperty(NAME)
|
||||||
protected String name;
|
protected String name;
|
||||||
|
|
||||||
|
@ -147,6 +155,22 @@ public class IDToken extends JsonWebToken {
|
||||||
this.sessionState = sessionState;
|
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() {
|
public String getName() {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
49
core/src/test/java/org/keycloak/AtHashTest.java
Normal file
49
core/src/test/java/org/keycloak/AtHashTest.java
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -165,10 +165,24 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
// Implicit or hybrid flow
|
// Implicit or hybrid flow
|
||||||
if (responseType.isImplicitOrHybridFlow()) {
|
if (responseType.isImplicitOrHybridFlow()) {
|
||||||
TokenManager tokenManager = new TokenManager();
|
TokenManager tokenManager = new TokenManager();
|
||||||
AccessTokenResponse res = tokenManager.responseBuilder(realm, clientSession.getClient(), event, session, userSession, clientSession)
|
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, clientSession.getClient(), event, session, userSession, clientSession)
|
||||||
.generateAccessToken()
|
.generateAccessToken();
|
||||||
.generateIDToken()
|
|
||||||
.build();
|
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)) {
|
if (responseType.hasResponseType(OIDCResponseType.ID_TOKEN)) {
|
||||||
redirectUri.addParam(OAuth2Constants.ID_TOKEN, res.getIdToken());
|
redirectUri.addParam(OAuth2Constants.ID_TOKEN, res.getIdToken());
|
||||||
|
|
|
@ -24,9 +24,11 @@ import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.jose.jws.Algorithm;
|
||||||
import org.keycloak.jose.jws.JWSBuilder;
|
import org.keycloak.jose.jws.JWSBuilder;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.JWSInputException;
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
|
import org.keycloak.jose.jws.crypto.HashProvider;
|
||||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientSessionModel;
|
import org.keycloak.models.ClientSessionModel;
|
||||||
|
@ -81,6 +83,9 @@ public class TokenManager {
|
||||||
protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||||
private static final String JWT = "JWT";
|
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) {
|
public static void applyScope(RoleModel role, RoleModel scope, Set<RoleModel> visited, Set<RoleModel> requested) {
|
||||||
if (visited.contains(scope)) return;
|
if (visited.contains(scope)) return;
|
||||||
visited.add(scope);
|
visited.add(scope);
|
||||||
|
@ -619,7 +624,7 @@ public class TokenManager {
|
||||||
.type(JWT)
|
.type(JWT)
|
||||||
.kid(realm.getKeyId())
|
.kid(realm.getKeyId())
|
||||||
.jsonContent(token)
|
.jsonContent(token)
|
||||||
.rsa256(realm.getPrivateKey());
|
.sign(jwsAlgorithm, realm.getPrivateKey());
|
||||||
return encodedToken;
|
return encodedToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -639,6 +644,9 @@ public class TokenManager {
|
||||||
RefreshToken refreshToken;
|
RefreshToken refreshToken;
|
||||||
IDToken idToken;
|
IDToken idToken;
|
||||||
|
|
||||||
|
boolean generateAccessTokenHash = false;
|
||||||
|
String codeHash;
|
||||||
|
|
||||||
public AccessTokenResponseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
|
public AccessTokenResponseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
@ -712,6 +720,15 @@ public class TokenManager {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AccessTokenResponseBuilder generateAccessTokenHash() {
|
||||||
|
generateAccessTokenHash = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessTokenResponseBuilder generateCodeHash(String code) {
|
||||||
|
codeHash = HashProvider.oidcHash(jwsAlgorithm, code);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public AccessTokenResponse build() {
|
public AccessTokenResponse build() {
|
||||||
|
@ -729,12 +746,8 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
AccessTokenResponse res = new AccessTokenResponse();
|
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) {
|
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.setToken(encodedToken);
|
||||||
res.setTokenType("bearer");
|
res.setTokenType("bearer");
|
||||||
res.setSessionState(accessToken.getSessionState());
|
res.setSessionState(accessToken.getSessionState());
|
||||||
|
@ -742,8 +755,21 @@ public class TokenManager {
|
||||||
res.setExpiresIn(accessToken.getExpiration() - Time.currentTime());
|
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) {
|
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);
|
res.setRefreshToken(encodedToken);
|
||||||
if (refreshToken.getExpiration() != 0) {
|
if (refreshToken.getExpiration() != 0) {
|
||||||
res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime());
|
res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime());
|
||||||
|
|
|
@ -103,6 +103,8 @@ public class OAuthClient {
|
||||||
|
|
||||||
private String responseMode;
|
private String responseMode;
|
||||||
|
|
||||||
|
private String nonce;
|
||||||
|
|
||||||
private Map<String, PublicKey> publicKeys = new HashMap<>();
|
private Map<String, PublicKey> publicKeys = new HashMap<>();
|
||||||
|
|
||||||
public void init(Keycloak adminClient, WebDriver driver) {
|
public void init(Keycloak adminClient, WebDriver driver) {
|
||||||
|
@ -521,9 +523,12 @@ public class OAuthClient {
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
b.queryParam(OAuth2Constants.STATE, state);
|
b.queryParam(OAuth2Constants.STATE, state);
|
||||||
}
|
}
|
||||||
if(uiLocales != null){
|
if (uiLocales != null){
|
||||||
b.queryParam(OAuth2Constants.UI_LOCALES_PARAM, uiLocales);
|
b.queryParam(OAuth2Constants.UI_LOCALES_PARAM, uiLocales);
|
||||||
}
|
}
|
||||||
|
if (nonce != null){
|
||||||
|
b.queryParam(OIDCLoginProtocol.NONCE_PARAM, nonce);
|
||||||
|
}
|
||||||
|
|
||||||
String scopeParam = TokenUtil.attachOIDCScope(scope);
|
String scopeParam = TokenUtil.attachOIDCScope(scope);
|
||||||
b.queryParam(OAuth2Constants.SCOPE, scopeParam);
|
b.queryParam(OAuth2Constants.SCOPE, scopeParam);
|
||||||
|
@ -634,6 +639,11 @@ public class OAuthClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OAuthClient nonce(String nonce) {
|
||||||
|
this.nonce = nonce;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public String getRealm() {
|
public String getRealm() {
|
||||||
return realm;
|
return realm;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,14 +15,16 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.testsuite.oidc.resptype;
|
package org.keycloak.testsuite.oidc.flows;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.jose.jws.Algorithm;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
@ -30,10 +32,8 @@ import org.keycloak.testsuite.Assert;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.TestRealmKeycloakTest;
|
import org.keycloak.testsuite.TestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||||
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
|
|
||||||
import org.keycloak.testsuite.pages.AppPage;
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.pages.OAuthGrantPage;
|
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
@ -46,6 +46,9 @@ import static org.junit.Assert.assertTrue;
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractOIDCResponseTypeTest extends TestRealmKeycloakTest {
|
public abstract class AbstractOIDCResponseTypeTest extends TestRealmKeycloakTest {
|
||||||
|
|
||||||
|
// Harcoded for now
|
||||||
|
Algorithm jwsAlgorithm = Algorithm.RS256;
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public AssertEvents events = new AssertEvents(this);
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@ -55,12 +58,6 @@ public abstract class AbstractOIDCResponseTypeTest extends TestRealmKeycloakTest
|
||||||
@Page
|
@Page
|
||||||
protected LoginPage loginPage;
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
@Page
|
|
||||||
protected AccountUpdateProfilePage profilePage;
|
|
||||||
|
|
||||||
@Page
|
|
||||||
protected OAuthGrantPage grantPage;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
}
|
}
|
||||||
|
@ -73,14 +70,9 @@ public abstract class AbstractOIDCResponseTypeTest extends TestRealmKeycloakTest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void nonceMatches() {
|
@Test
|
||||||
driver.navigate().to(oauth.getLoginFormUrl() + "&nonce=abcdef123456");
|
public void nonceMatches() {
|
||||||
|
EventRepresentation loginEvent = loginUser("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();
|
|
||||||
List<IDToken> idTokens = retrieveIDTokens(loginEvent);
|
List<IDToken> idTokens = retrieveIDTokens(loginEvent);
|
||||||
|
|
||||||
for (IDToken idToken : idTokens) {
|
for (IDToken idToken : idTokens) {
|
||||||
|
@ -89,23 +81,8 @@ public abstract class AbstractOIDCResponseTypeTest extends TestRealmKeycloakTest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void nonceNotUsed() {
|
protected void validateNonceNotUsedErrorExpected() {
|
||||||
driver.navigate().to(oauth.getLoginFormUrl());
|
oauth.nonce(null);
|
||||||
|
|
||||||
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<IDToken> idTokens = retrieveIDTokens(loginEvent);
|
|
||||||
for (IDToken idToken : idTokens) {
|
|
||||||
Assert.assertNull(idToken.getNonce());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected void nonceNotUsedErrorExpected() {
|
|
||||||
driver.navigate().to(oauth.getLoginFormUrl());
|
driver.navigate().to(oauth.getLoginFormUrl());
|
||||||
|
|
||||||
assertFalse(loginPage.isCurrent());
|
assertFalse(loginPage.isCurrent());
|
||||||
|
@ -119,5 +96,20 @@ public abstract class AbstractOIDCResponseTypeTest extends TestRealmKeycloakTest
|
||||||
Assert.assertEquals("Missing parameter: nonce", resp.getErrorDescription());
|
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<IDToken> retrieveIDTokens(EventRepresentation loginEvent);
|
protected abstract List<IDToken> retrieveIDTokens(EventRepresentation loginEvent);
|
||||||
}
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.testsuite.oidc.resptype;
|
package org.keycloak.testsuite.oidc.flows;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -61,12 +61,11 @@ public class OIDCBasicResponseTypeCodeTest extends AbstractOIDCResponseTypeTest
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nonceNotUsed() {
|
public void nonceNotUsed() {
|
||||||
super.nonceNotUsed();
|
EventRepresentation loginEvent = loginUser(null);
|
||||||
}
|
|
||||||
|
|
||||||
|
List<IDToken> idTokens = retrieveIDTokens(loginEvent);
|
||||||
@Test
|
for (IDToken idToken : idTokens) {
|
||||||
public void nonceMatches() {
|
Assert.assertNull(idToken.getNonce());
|
||||||
super.nonceMatches();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,15 +15,15 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.testsuite.oidc.resptype;
|
package org.keycloak.testsuite.oidc.flows;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.jose.jws.crypto.HashProvider;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
|
@ -56,6 +56,11 @@ public class OIDCHybridResponseTypeCodeIDTokenTest extends AbstractOIDCResponseT
|
||||||
String idTokenStr = authzResponse.getIdToken();
|
String idTokenStr = authzResponse.getIdToken();
|
||||||
IDToken idToken = oauth.verifyIDToken(idTokenStr);
|
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 exchanged for the code
|
||||||
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
|
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
|
||||||
|
|
||||||
|
@ -65,12 +70,6 @@ public class OIDCHybridResponseTypeCodeIDTokenTest extends AbstractOIDCResponseT
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nonceNotUsedErrorExpected() {
|
public void nonceNotUsedErrorExpected() {
|
||||||
super.nonceNotUsedErrorExpected();
|
super.validateNonceNotUsedErrorExpected();
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void nonceMatches() {
|
|
||||||
super.nonceMatches();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
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<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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
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<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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.testsuite.oidc.resptype;
|
package org.keycloak.testsuite.oidc.flows;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -54,19 +54,16 @@ public class OIDCImplicitResponseTypeIDTokenTest extends AbstractOIDCResponseTyp
|
||||||
String idTokenStr = authzResponse.getIdToken();
|
String idTokenStr = authzResponse.getIdToken();
|
||||||
IDToken idToken = oauth.verifyIDToken(idTokenStr);
|
IDToken idToken = oauth.verifyIDToken(idTokenStr);
|
||||||
|
|
||||||
|
Assert.assertNull(idToken.getAccessTokenHash());
|
||||||
|
Assert.assertNull(idToken.getCodeHash());
|
||||||
|
|
||||||
return Collections.singletonList(idToken);
|
return Collections.singletonList(idToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nonceNotUsedErrorExpected() {
|
public void nonceNotUsedErrorExpected() {
|
||||||
super.nonceNotUsedErrorExpected();
|
super.validateNonceNotUsedErrorExpected();
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void nonceMatches() {
|
|
||||||
super.nonceMatches();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
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<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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue