KEYCLOAK-6839 You took too long to login after SSO idle

This commit is contained in:
Martin Kanis 2019-04-15 14:25:21 +02:00 committed by Hynek Mlnařík
parent 334ca6e96b
commit efdf0f1bd8
16 changed files with 436 additions and 11 deletions

View file

@ -121,6 +121,19 @@ public class RootAuthenticationSessionAdapter implements RootAuthenticationSessi
return new AuthenticationSessionAdapter(session, this, tabId, authSessionEntity);
}
@Override
public void removeAuthenticationSessionByTabId(String tabId) {
if (entity.getAuthenticationSessions().remove(tabId) != null) {
if (entity.getAuthenticationSessions().isEmpty()) {
provider.tx.remove(cache, entity.getId());
} else {
entity.setTimestamp(Time.currentTime());
update();
}
}
}
@Override
public void restartSession(RealmModel realm) {
entity.getAuthenticationSessions().clear();

View file

@ -58,6 +58,12 @@ public interface RootAuthenticationSessionModel {
*/
AuthenticationSessionModel createAuthenticationSession(ClientModel client);
/**
* Removes authentication session from root authentication session.
* If there's no child authentication session left in the root authentication session, it's removed as well.
* @param tabId String
*/
void removeAuthenticationSessionByTabId(String tabId);
/**
* Will completely restart whole state of authentication session. It will just keep same ID. It will setup it with provided realm.

View file

@ -138,6 +138,12 @@ public class TokenManager {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled");
}
if (oldToken.getIssuedAt() + 1 < userSession.getStarted()) {
logger.debug("Refresh toked issued before the user session started");
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh toked issued before the user session started");
}
ClientModel client = session.getContext().getClient();
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
@ -245,6 +251,9 @@ public class TokenManager {
if (token.getIssuedAt() < session.users().getNotBeforeOfUser(realm, user)) {
return false;
}
if (token.getIssuedAt() + 1 < userSession.getStarted()) {
return false;
}
return true;
}

View file

@ -120,6 +120,10 @@ public class LogoutEndpoint {
try {
IDToken idToken = tokenManager.verifyIDTokenSignature(session, encodedIdToken);
userSession = session.sessions().getUserSession(realm, idToken.getSessionState());
if (userSession != null) {
checkTokenIssuedAt(idToken, userSession);
}
} catch (OAuthErrorException e) {
event.event(EventType.LOGOUT);
event.error(Errors.INVALID_TOKEN);
@ -198,6 +202,7 @@ public class LogoutEndpoint {
}
if (userSessionModel != null) {
checkTokenIssuedAt(token, userSessionModel);
logout(userSessionModel, offline);
}
} catch (OAuthErrorException e) {
@ -235,4 +240,9 @@ public class LogoutEndpoint {
}
}
private void checkTokenIssuedAt(IDToken token, UserSessionModel userSession) throws OAuthErrorException {
if (token.getIssuedAt() + 1 < userSession.getStarted()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh toked issued before the user session started");
}
}
}

View file

@ -233,11 +233,13 @@ public class UserInfoEndpoint {
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
UserSessionModel offlineUserSession = null;
if (AuthenticationManager.isSessionValid(realm, userSession)) {
checkTokenIssuedAt(token, userSession, event);
event.session(userSession);
return userSession;
} else {
offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId());
if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) {
checkTokenIssuedAt(token, offlineUserSession, event);
event.session(offlineUserSession);
return offlineUserSession;
}
@ -258,4 +260,10 @@ public class UserInfoEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED);
}
private void checkTokenIssuedAt(AccessToken token, UserSessionModel userSession, EventBuilder event) throws ErrorResponseException {
if (token.getIssuedAt() + 1 < userSession.getStarted()) {
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Stale token", Response.Status.UNAUTHORIZED);
}
}
}

View file

@ -242,7 +242,8 @@ public class AuthenticationManager {
backchannelLogoutAll(session, realm, userSession, logoutAuthSession, uriInfo, headers, logoutBroker);
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
} finally {
asm.removeAuthenticationSession(realm, logoutAuthSession, false);
RootAuthenticationSessionModel rootAuthSession = logoutAuthSession.getParentSession();
rootAuthSession.removeAuthenticationSessionByTabId(logoutAuthSession.getTabId());
}
userSession.setState(UserSessionModel.State.LOGGED_OUT);
@ -707,7 +708,7 @@ public class AuthenticationManager {
}
public static void expireCookie(RealmModel realm, String cookieName, String path, boolean httpOnly, ClientConnection connection) {
logger.debugv("Expiring cookie: {0} path: {1}", cookieName, path);
logger.debugf("Expiring cookie: %s path: %s", cookieName, path);
boolean secureOnly = realm.getSslRequired().isRequired(connection);;
CookieHelper.addCookie(cookieName, "", path, null, "Expiring cookie", 0, secureOnly, httpOnly);
}

View file

@ -17,6 +17,8 @@
package org.keycloak.testsuite.auth.page.login;
import org.keycloak.testsuite.util.DroneUtils;
/**
*
* @author tkyjovsk
@ -27,4 +29,14 @@ public class OIDCLogin extends Login {
setProtocol(OIDC);
}
@Override
public boolean isCurrent() {
String realm = "test";
return isCurrent(realm);
}
public boolean isCurrent(String realm) {
return DroneUtils.getCurrentDriver().getTitle().equals("Log in to " + realm) || DroneUtils.getCurrentDriver().getTitle().equals("Anmeldung bei " + realm);
}
}

View file

@ -18,7 +18,9 @@
package org.keycloak.testsuite.page;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.logging.Logger;
import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.URLUtils;
import org.openqa.selenium.WebDriver;
@ -105,7 +107,7 @@ public abstract class AbstractPage {
public void assertCurrent() {
String name = getClass().getSimpleName();
Assert.assertTrue("Expected " + name + " but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")",
Assert.assertTrue("Expected " + name + " but was " + DroneUtils.getCurrentDriver().getTitle() + " (" + DroneUtils.getCurrentDriver().getCurrentUrl() + ")",
isCurrent());
}
}

View file

@ -21,6 +21,7 @@ import org.jboss.arquillian.test.api.ArquillianResource;
import org.junit.Assert;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
@ -44,7 +45,7 @@ public abstract class AbstractPage {
public void assertCurrent() {
String name = getClass().getSimpleName();
Assert.assertTrue("Expected " + name + " but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")",
Assert.assertTrue("Expected " + name + " but was " + DroneUtils.getCurrentDriver().getTitle() + " (" + DroneUtils.getCurrentDriver().getCurrentUrl() + ")",
isCurrent());
}

View file

@ -17,6 +17,7 @@
package org.keycloak.testsuite.pages;
import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
@ -40,7 +41,7 @@ public abstract class LanguageComboboxAwarePage extends AbstractPage {
public void openLanguage(String language){
WebElement langLink = localeDropdown.findElement(By.xpath("//a[text()='" + language + "']"));
String url = langLink.getAttribute("href");
driver.navigate().to(url);
DroneUtils.getCurrentDriver().navigate().to(url);
WaitUtils.waitForPageToLoad();
}
}

View file

@ -18,6 +18,7 @@
package org.keycloak.testsuite.pages;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
@ -145,7 +146,7 @@ public class LoginPage extends LanguageComboboxAwarePage {
}
public boolean isCurrent(String realm) {
return driver.getTitle().equals("Log in to " + realm) || driver.getTitle().equals("Anmeldung bei " + realm);
return DroneUtils.getCurrentDriver().getTitle().equals("Log in to " + realm) || DroneUtils.getCurrentDriver().getTitle().equals("Anmeldung bei " + realm);
}
public void clickRegister() {
@ -159,7 +160,7 @@ public class LoginPage extends LanguageComboboxAwarePage {
public WebElement findSocialButton(String providerId) {
String id = "zocial-" + providerId;
return this.driver.findElement(By.id(id));
return DroneUtils.getCurrentDriver().findElement(By.id(id));
}
public void resetPassword() {

View file

@ -16,12 +16,14 @@
*/
package org.keycloak.testsuite.forms;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.util.Retry;
import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@ -40,24 +42,32 @@ import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.console.page.AdminConsole;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.JavascriptBrowser;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.TokenSignatureUtil;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
@ -68,6 +78,7 @@ import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -94,9 +105,19 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
.build();
user2Id = user2.getId();
UserRepresentation admin = UserBuilder.create()
.username("admin")
.password("admin")
.enabled(true)
.build();
HashMap<String, List<String>> clientRoles = new HashMap<>();
clientRoles.put("realm-management", Arrays.asList("realm-admin"));
admin.setClientRoles(clientRoles);
RealmBuilder.edit(testRealm)
.user(user)
.user(user2);
.user(user2)
.user(admin);
}
@Rule
@ -105,9 +126,21 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
@Page
protected AppPage appPage;
@Page
@JavascriptBrowser
protected AdminConsole jsAdminConsole;
@Drone
@JavascriptBrowser
protected WebDriver jsDriver;
@Page
protected LoginPage loginPage;
@Page
@JavascriptBrowser
protected LoginPage jsLoginPage;
@Page
protected ErrorPage errorPage;
@ -696,6 +729,35 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
.assertEvent();
}
@Test
public void loginAfterExpiredTimeout() throws Exception {
try (AutoCloseable c = new RealmAttributeUpdater(adminClient.realm("test"))
.updateWith(r -> {
r.setSsoSessionMaxLifespan(5);
})
.update()) {
DroneUtils.addWebDriver(jsDriver);
jsAdminConsole.setAdminRealm(testRealm().toRepresentation().getRealm());
jsAdminConsole.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(jsAdminConsole);
// login for the first time
jsLoginPage.login("admin", "admin");
// wait for a timeout
TimeUnit.SECONDS.sleep(5);
Retry.execute(() -> jsLoginPage.assertCurrent(), 20, 500);
// try to re-login immediately, it should be successful i.e without "You took too long to login. Login process starting from beginning." message
jsLoginPage.login("admin", "admin");
assertFalse(jsLoginPage.isCurrent());
}
}
@Test
public void loginExpiredCodeAndExpiredCookies() {

View file

@ -25,16 +25,19 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.util.*;
import java.util.List;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
@ -107,6 +110,47 @@ public class LogoutTest extends AbstractKeycloakTest {
}
}
@Test
public void postLogoutWithRefreshTokenAfterUserSessionLogoutAndLoginAgain() throws Exception {
// Login
OAuthClient.AccessTokenResponse accessTokenResponse = loginAndForceNewLoginPage();
String refreshToken1 = accessTokenResponse.getRefreshToken();
oauth.doLogout(refreshToken1, "password");
setTimeOffset(2);
oauth.fillLoginForm("test-user@localhost", "password");
Assert.assertFalse(loginPage.isCurrent());
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse2 = oauth.doAccessTokenRequest(code, "password");
// POST logout with token should fail
try (CloseableHttpResponse response = oauth.doLogout(refreshToken1, "password")) {
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatusLine().getStatusCode());
}
String logoutUrl = oauth.getLogoutUrl()
.idTokenHint(accessTokenResponse.getIdToken())
.postLogoutRedirectUri(oauth.APP_AUTH_ROOT)
.build();
// GET logout with ID token should fail as well
try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatusLine().getStatusCode());
}
// finally POST logout with VALID token should succeed
try (CloseableHttpResponse response = oauth.doLogout(tokenResponse2.getRefreshToken(), "password")) {
assertThat(response, Matchers.statusCodeIsHC(Status.NO_CONTENT));
assertNotNull(testingClient.testApp().getAdminLogoutAction());
}
}
@Test
public void postLogoutFailWithCredentialsOfDifferentClient() throws Exception {
@ -248,4 +292,23 @@ public class LogoutTest extends AbstractKeycloakTest {
}
}
private OAuthClient.AccessTokenResponse loginAndForceNewLoginPage() {
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.clientSessionState("client-session");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
setTimeOffset(1);
String loginFormUri = UriBuilder.fromUri(oauth.getLoginFormUrl())
.queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
.build().toString();
driver.navigate().to(loginFormUri);
loginPage.assertCurrent();
return tokenResponse;
}
}

View file

@ -16,12 +16,14 @@
*/
package org.keycloak.testsuite.oauth;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
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.admin.client.resource.UserResource;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Details;
@ -29,6 +31,7 @@ import org.keycloak.events.Errors;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.utils.SessionTimeoutHelper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
@ -37,6 +40,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
@ -63,6 +67,7 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
@ -74,6 +79,9 @@ import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
*/
public class RefreshTokenTest extends AbstractKeycloakTest {
@Page
protected LoginPage loginPage;
@Rule
public AssertEvents events = new AssertEvents(this);
@ -470,7 +478,6 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
private void processExpectedValidRefresh(String sessionId, RefreshToken requestToken, String refreshToken) {
OAuthClient.AccessTokenResponse response2 = oauth.doRefreshTokenRequest(refreshToken, "password");
RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
assertEquals(200, response2.getStatusCode());
@ -540,6 +547,93 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
events.clear();
}
@Test
public void refreshTokenAfterUserLogoutAndLoginAgain() {
String refreshToken1 = loginAndForceNewLoginPage();
oauth.doLogout(refreshToken1, "password");
events.clear();
// Set time offset to 2 (Just to simulate to be more close to real situation)
setTimeOffset(2);
// Continue with login
oauth.fillLoginForm("test-user@localhost", "password");
assertFalse(loginPage.isCurrent());
OAuthClient.AccessTokenResponse tokenResponse2 = null;
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
tokenResponse2 = oauth.doAccessTokenRequest(code, "password");
// Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
OAuthClient.AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1, "password");
assertEquals(400, responseReuseExceeded.getStatusCode());
// Finally try with valid refresh token
responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken(), "password");
assertEquals(200, responseReuseExceeded.getStatusCode());
}
@Test
public void refreshTokenAfterAdminLogoutAllAndLoginAgain() {
String refreshToken1 = loginAndForceNewLoginPage();
adminClient.realm("test").logoutAll();
events.clear();
// Set time offset to 2 (Just to simulate to be more close to real situation)
setTimeOffset(2);
// Continue with login
oauth.fillLoginForm("test-user@localhost", "password");
assertFalse(loginPage.isCurrent());
OAuthClient.AccessTokenResponse tokenResponse2 = null;
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
tokenResponse2 = oauth.doAccessTokenRequest(code, "password");
// Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
OAuthClient.AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1, "password");
assertEquals(400, responseReuseExceeded.getStatusCode());
// Finally try with valid refresh token
responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken(), "password");
assertEquals(200, responseReuseExceeded.getStatusCode());
}
@Test
public void refreshTokenAfterUserAdminLogoutEndpointAndLoginAgain() {
String refreshToken1 = loginAndForceNewLoginPage();
RefreshToken refreshTokenParsed1 = oauth.parseRefreshToken(refreshToken1);
String userId = refreshTokenParsed1.getSubject();
UserResource user = adminClient.realm("test").users().get(userId);
user.logout();
// Set time offset to 2 (Just to simulate to be more close to real situation)
setTimeOffset(2);
// Continue with login
oauth.fillLoginForm("test-user@localhost", "password");
assertFalse(loginPage.isCurrent());
OAuthClient.AccessTokenResponse tokenResponse2 = null;
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
tokenResponse2 = oauth.doAccessTokenRequest(code, "password");
// Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
OAuthClient.AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1, "password");
assertEquals(400, responseReuseExceeded.getStatusCode());
// Finally try with valid refresh token
responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken(), "password");
assertEquals(200, responseReuseExceeded.getStatusCode());
}
@Test
public void testUserSessionRefreshAndIdle() throws Exception {
oauth.doLogin("test-user@localhost", "password");
@ -1009,4 +1103,34 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
setTimeOffset(0);
}
private String loginAndForceNewLoginPage() {
oauth.doLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin().assertEvent();
String sessionId = loginEvent.getSessionId();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
events.poll();
// Assert refresh successful
String refreshToken = tokenResponse.getRefreshToken();
RefreshToken refreshTokenParsed1 = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
processExpectedValidRefresh(sessionId, refreshTokenParsed1, refreshToken);
// Set time offset to 1 (Just to simulate to be more close to real situation)
setTimeOffset(1);
// Open the tab with prompt=login. AuthenticationSession will be created with same ID like userSession
String loginFormUri = UriBuilder.fromUri(oauth.getLoginFormUrl())
.queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
.build().toString();
driver.navigate().to(loginFormUri);
loginPage.assertCurrent();
return refreshToken;
}
}

View file

@ -26,6 +26,8 @@ import org.keycloak.OAuthErrorException;
import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Errors;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
@ -40,10 +42,12 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.oidc.OIDCScopeTest;
import org.keycloak.testsuite.oidc.AbstractOIDCScopeTest;
import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
import org.keycloak.testsuite.util.TokenSignatureUtil;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.core.UriBuilder;
import java.util.ArrayList;
import java.util.List;
@ -179,6 +183,36 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
assertEquals(jsonNode.get("typ").asText(), "Refresh");
}
@Test
public void testIntrospectRefreshTokenAfterUserSessionLogoutAndLoginAgain() throws Exception {
AccessTokenResponse accessTokenResponse = loginAndForceNewLoginPage();
String refreshToken1 = accessTokenResponse.getRefreshToken();
oauth.doLogout(refreshToken1, "password");
events.clear();
setTimeOffset(2);
oauth.fillLoginForm("test-user@localhost", "password");
events.expectLogin().assertEvent();
Assert.assertFalse(loginPage.isCurrent());
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse2 = oauth.doAccessTokenRequest(code, "password");
String introspectResponse = oauth.introspectRefreshTokenWithClientCredential("confidential-cli", "secret1", tokenResponse2.getRefreshToken());
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(introspectResponse);
assertTrue(jsonNode.get("active").asBoolean());
introspectResponse = oauth.introspectRefreshTokenWithClientCredential("confidential-cli", "secret1", refreshToken1);
jsonNode = objectMapper.readTree(introspectResponse);
assertFalse(jsonNode.get("active").asBoolean());
}
@Test
public void testPublicClientCredentialsNotAllowed() throws Exception {
oauth.doLogin("test-user@localhost", "password");
@ -389,4 +423,24 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
assertEquals(Errors.INVALID_CLIENT, rep.getOtherClaims().get("error"));
assertNull(rep.getSubject());
}
private OAuthClient.AccessTokenResponse loginAndForceNewLoginPage() {
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.clientSessionState("client-session");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
setTimeOffset(1);
String loginFormUri = UriBuilder.fromUri(oauth.getLoginFormUrl())
.queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
.build().toString();
driver.navigate().to(loginFormUri);
loginPage.assertCurrent();
return tokenResponse;
}
}

View file

@ -32,6 +32,7 @@ import org.keycloak.events.EventType;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.testsuite.util.KeycloakModelUtils;
@ -342,6 +343,44 @@ public class UserInfoTest extends AbstractKeycloakTest {
}
}
@Test
public void testAccessTokenAfterUserSessionLogoutAndLoginAgain() {
OAuthClient.AccessTokenResponse accessTokenResponse = loginAndForceNewLoginPage();
String refreshToken1 = accessTokenResponse.getRefreshToken();
oauth.doLogout(refreshToken1, "password");
events.clear();
setTimeOffset(2);
oauth.fillLoginForm("test-user@localhost", "password");
events.expectLogin().assertEvent();
Assert.assertFalse(loginPage.isCurrent());
events.clear();
Client client = ClientBuilder.newClient();
try {
Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getAccessToken());
assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
response.close();
events.expect(EventType.USER_INFO_REQUEST_ERROR)
.error(Errors.INVALID_TOKEN)
.user(Matchers.nullValue(String.class))
.session(Matchers.nullValue(String.class))
.detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN)
.client("test-app")
.assertEvent();
} finally {
client.close();
}
}
@Test
public void testSessionExpiredOfflineAccess() throws Exception {
Client client = ClientBuilder.newClient();
@ -518,4 +557,23 @@ public class UserInfoTest extends AbstractKeycloakTest {
}
}
private OAuthClient.AccessTokenResponse loginAndForceNewLoginPage() {
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.clientSessionState("client-session");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
setTimeOffset(1);
String loginFormUri = UriBuilder.fromUri(oauth.getLoginFormUrl())
.queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
.build().toString();
driver.navigate().to(loginFormUri);
loginPage.assertCurrent();
return tokenResponse;
}
}