KEYCLOAK-12156 LogoutEndpoint does not verify token type of id_token_hint

Co-authored-by: Martin Kanis <mkanis@redhat.com>
Co-authored-by: Marek Posolda <mposolda@redhat.com>
This commit is contained in:
Martin Kanis 2020-02-07 16:52:35 +01:00 committed by Stian Thorgersen
parent 5e101d20ca
commit e6e0e6945d
10 changed files with 115 additions and 133 deletions

View file

@ -338,7 +338,7 @@ public class TokenVerifier<T extends JsonWebToken> {
}
/**
* @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
*
* @return This token verifier
*/
public TokenVerifier<T> tokenType(String tokenType) {

View file

@ -22,7 +22,9 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.TokenVerifier;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@ -120,12 +122,13 @@ public class LogoutEndpoint {
if (encodedIdToken != null) {
try {
idToken = tokenManager.verifyIDTokenSignature(session, encodedIdToken);
TokenVerifier.createWithoutSignature(idToken).tokenType(TokenUtil.TOKEN_TYPE_ID).verify();
userSession = session.sessions().getUserSession(realm, idToken.getSessionState());
if (userSession != null) {
checkTokenIssuedAt(idToken, userSession);
}
} catch (OAuthErrorException e) {
} catch (OAuthErrorException | VerificationException e) {
event.event(EventType.LOGOUT);
event.error(Errors.INVALID_TOKEN);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE);

View file

@ -549,11 +549,12 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
.param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
.param(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
));
Assert.assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String exchangedAccessToken = tokenResponse.getToken();
String idToken = tokenResponse.getIdToken();
JWSInput jws = new JWSInput(tokenResponse.getToken());
AccessToken token = jws.readJsonContent(AccessToken.class);
response.close();
@ -585,7 +586,7 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
// test logout
response = childLogoutWebTarget(httpClient)
.queryParam("id_token_hint", exchangedAccessToken)
.queryParam("id_token_hint", idToken)
.request()
.get();
response.close();
@ -606,11 +607,12 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
.param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
.param(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
));
Assert.assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String exchangedAccessToken = tokenResponse.getToken();
String idToken = tokenResponse.getIdToken();
JWSInput jws = new JWSInput(tokenResponse.getToken());
AccessToken token = jws.readJsonContent(AccessToken.class);
response.close();
@ -625,7 +627,7 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
// test logout
response = childLogoutWebTarget(httpClient)
.queryParam("id_token_hint", exchangedAccessToken)
.queryParam("id_token_hint", idToken)
.request()
.get();
response.close();
@ -645,11 +647,12 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
.param(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
));
Assert.assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String exchangedAccessToken = tokenResponse.getToken();
String idToken = tokenResponse.getIdToken();
JWSInput jws = new JWSInput(tokenResponse.getToken());
AccessToken token = jws.readJsonContent(AccessToken.class);
response.close();
@ -664,7 +667,7 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
// test logout
response = childLogoutWebTarget(httpClient)
.queryParam("id_token_hint", exchangedAccessToken)
.queryParam("id_token_hint", idToken)
.request()
.get();
response.close();
@ -733,21 +736,22 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
.param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
.param(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
));
Assert.assertEquals(statusCode, response.getStatus());
if (statusCode != Response.Status.NOT_IMPLEMENTED.getStatusCode()) {
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String exchangedAccessToken = tokenResponse.getToken();
Assert.assertNotNull(exchangedAccessToken);
String idToken = tokenResponse.getIdToken();
Assert.assertNotNull(idToken);
response.close();
Assert.assertEquals(1, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
// test logout
response = childLogoutWebTarget(httpClient)
.queryParam("id_token_hint", exchangedAccessToken)
.queryParam("id_token_hint", idToken)
.request()
.get();
response.close();

View file

@ -55,6 +55,7 @@ import org.keycloak.testsuite.util.MailServer;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.TimeoutException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriBuilderException;
import java.net.URI;
@ -162,6 +163,28 @@ public abstract class AbstractBaseBrokerTest extends AbstractKeycloakTest {
consumerRealm.update(master);
}
protected void addClientsToProviderAndConsumer() {
List<ClientRepresentation> clients = bc.createProviderClients(suiteContext);
final RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getClientId() + " to realm " + bc.providerRealmName());
final Response resp = providerRealm.clients().create(client);
resp.close();
}
clients = bc.createConsumerClients(suiteContext);
if (clients != null) {
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getClientId() + " to realm " + bc.consumerRealmName());
Response resp = consumerRealm.clients().create(client);
resp.close();
}
}
}
@Before
public void beforeBrokerTest() {
importRealm(bc.createConsumerRealm());
@ -195,24 +218,24 @@ public abstract class AbstractBaseBrokerTest extends AbstractKeycloakTest {
protected void logInAsUserInIDP() {
driver.navigate().to(getAccountUrl(bc.consumerRealmName()));
logInWithBroker(bc);
}
protected void logInWithBroker(BrokerConfiguration bc) {
log.debug("Clicking social " + bc.getIDPAlias());
loginPage.clickSocial(bc.getIDPAlias());
waitForPage(driver, "log in to", true);
Assert.assertTrue("Driver should be on the provider realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
log.debug("Logging in");
loginPage.login(bc.getUserLogin(), bc.getUserPassword());
}
/** Logs in the IDP and updates account information */
protected void logInAsUserInIDPForFirstTime() {
logInAsUserInIDP();
updateAccountInformation();
}
protected void updateAccountInformation() {
waitForPage(driver, "update account information", false);
Assert.assertTrue(updateAccountInformationPage.isCurrent());

View file

@ -23,14 +23,11 @@ import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import java.util.List;
import java.util.function.BiConsumer;
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
/**
* @author Stan Silvert ssilvert@redhat.com (C) 2019 Red Hat Inc.
@ -61,38 +58,11 @@ public abstract class AbstractInitializedBaseBrokerTest extends AbstractBaseBrok
realm.identityProviders().create(bc.setUpIdentityProvider(suiteContext)).close();
identityProviderResource = realm.identityProviders().get(bc.getIDPAlias());
// addClients
List<ClientRepresentation> clients = bc.createProviderClients(suiteContext);
if (clients != null) {
RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getClientId()+ " to realm " + bc.providerRealmName());
providerRealm.clients().create(client).close();
}
}
clients = bc.createConsumerClients(suiteContext);
if (clients != null) {
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getClientId() + " to realm " + bc.consumerRealmName());
consumerRealm.clients().create(client).close();
}
}
addClientsToProviderAndConsumer();
testContext.setInitialized(true);
}
protected void logInWithBroker(BrokerConfiguration bc) {
log.debug("Clicking social " + bc.getIDPAlias());
loginPage.clickSocial(bc.getIDPAlias());
waitForPage(driver, "log in to", true);
log.debug("Logging in");
loginPage.login(bc.getUserLogin(), bc.getUserPassword());
}
protected void updateExecutions(BiConsumer<AuthenticationExecutionInfoRepresentation, AuthenticationManagementResource> action) {
AuthenticationManagementResource flows = adminClient.realm(bc.consumerRealmName()).flows();

View file

@ -3,7 +3,6 @@ package org.keycloak.testsuite.broker;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
@ -63,27 +62,7 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker
@Before
public void addClients() {
List<ClientRepresentation> clients = bc.createProviderClients(suiteContext);
if (clients != null) {
RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getName() + " to realm " + bc.providerRealmName());
Response resp = providerRealm.clients().create(client);
resp.close();
}
}
clients = bc.createConsumerClients(suiteContext);
if (clients != null) {
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getName() + " to realm " + bc.consumerRealmName());
Response resp = consumerRealm.clients().create(client);
resp.close();
}
}
addClientsToProviderAndConsumer();
}
protected void createUserInProviderRealm(Map<String, List<String>> attributes) {

View file

@ -17,8 +17,6 @@
package org.keycloak.testsuite.broker;
import java.util.List;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
@ -32,7 +30,6 @@ import org.keycloak.crypto.Algorithm;
import org.keycloak.keys.KeyProvider;
import org.keycloak.keys.PublicKeyStorageUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
@ -86,27 +83,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
@Before
public void addClients() {
List<ClientRepresentation> clients = bc.createProviderClients(suiteContext);
if (clients != null) {
RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getClientId() + " to realm " + bc.providerRealmName());
Response resp = providerRealm.clients().create(client);
resp.close();
}
}
clients = bc.createConsumerClients(suiteContext);
if (clients != null) {
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getClientId() + " to realm " + bc.consumerRealmName());
Response resp = consumerRealm.clients().create(client);
resp.close();
}
}
addClientsToProviderAndConsumer();
}

View file

@ -1,22 +1,29 @@
package org.keycloak.testsuite.broker;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
import org.openqa.selenium.Cookie;
import javax.ws.rs.core.Response;
import java.util.List;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.util.OAuthClient;
import static org.junit.Assert.assertEquals;
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_CONS_NAME;
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME;
import static org.keycloak.testsuite.broker.BrokerTestTools.getAuthRoot;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
public class KcOidcBrokerLogoutTest extends AbstractBaseBrokerTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return KcOidcBrokerConfiguration.INSTANCE;
@ -48,14 +55,7 @@ public class KcOidcBrokerLogoutTest extends AbstractBaseBrokerTest {
@Before
public void addClients() {
final List<ClientRepresentation> clients = bc.createProviderClients(suiteContext);
final RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
for (final ClientRepresentation client : clients) {
log.debug("adding client " + client.getClientId() + " to realm " + bc.providerRealmName());
final Response resp = providerRealm.clients().create(client);
resp.close();
}
addClientsToProviderAndConsumer();
}
@Test
@ -91,15 +91,25 @@ public class KcOidcBrokerLogoutTest extends AbstractBaseBrokerTest {
@Test
public void logoutAfterBrowserRestart() {
logInAsUserInIDPForFirstTime();
assertLoggedInAccountManagement();
driver.navigate().to(getLoginUrl(bc.consumerRealmName(), "broker-app"));
logInWithBroker(bc);
updateAccountInformation();
Cookie identityCookie = driver.manage().getCookieNamed(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE);
String idToken = identityCookie.getValue();
// Exchange code from "broker-app" client of "consumer" realm for the tokens
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.realm(bc.consumerRealmName())
.clientId("broker-app")
.redirectUri(getAuthRoot(suiteContext) + "/auth/realms/" + REALM_CONS_NAME + "/app")
.doAccessTokenRequest(code, "broker-app-secret");
assertEquals(200, response.getStatusCode());
String idToken = response.getIdToken();
// simulate browser restart by deleting an identity cookie
log.debugf("Deleting %s cookie", AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE);
log.debugf("Deleting %s and %s cookies", AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE,
AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE + CookieHelper.LEGACY_COOKIE);
driver.manage().deleteCookieNamed(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE);
driver.manage().deleteCookieNamed(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE + CookieHelper.LEGACY_COOKIE);
logoutFromRealm(bc.consumerRealmName(), null, idToken);
driver.navigate().to(getAccountUrl(REALM_PROV_NAME));

View file

@ -71,27 +71,7 @@ public class OidcAdvancedClaimToRoleMapperTest extends AbstractBaseBrokerTest {
@Before
public void addClients() {
List<ClientRepresentation> clients = bc.createProviderClients(suiteContext);
if (clients != null) {
RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getName() + " to realm " + bc.providerRealmName());
Response resp = providerRealm.clients().create(client);
resp.close();
}
}
clients = bc.createConsumerClients(suiteContext);
if (clients != null) {
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
for (ClientRepresentation client : clients) {
log.debug("adding client " + client.getName() + " to realm " + bc.consumerRealmName());
Response resp = consumerRealm.clients().create(client);
resp.close();
}
}
addClientsToProviderAndConsumer();
}
@Test

View file

@ -22,7 +22,9 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -46,6 +48,7 @@ import org.apache.http.impl.client.HttpClientBuilder;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -110,6 +113,39 @@ public class LogoutTest extends AbstractKeycloakTest {
}
}
@Test
public void logoutIDTokenHint() {
oauth.doLogin("test-user@localhost", "password");
String sessionId = events.expectLogin().assertEvent().getSessionId();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
String idToken = tokenResponse.getIdToken();
events.clear();
driver.navigate().to(oauth.getLogoutUrl().redirectUri(oauth.APP_AUTH_ROOT).idTokenHint(idToken).build());
events.expectLogout(sessionId).detail(Details.REDIRECT_URI, oauth.APP_AUTH_ROOT).assertEvent();
assertCurrentUrlEquals(oauth.APP_AUTH_ROOT);
}
@Test
public void browserLogoutWithAccessToken() {
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
String accessToken = tokenResponse.getAccessToken();
events.clear();
driver.navigate().to(oauth.getLogoutUrl().redirectUri(oauth.APP_AUTH_ROOT).idTokenHint(accessToken).build());
events.expectLogoutError(OAuthErrorException.INVALID_TOKEN).assertEvent();
}
@Test
public void postLogoutWithRefreshTokenAfterUserSessionLogoutAndLoginAgain() throws Exception {
// Login