KEYCLOAK-6839 You took too long to login after SSO idle
This commit is contained in:
parent
334ca6e96b
commit
efdf0f1bd8
16 changed files with 436 additions and 11 deletions
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue