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);
|
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
|
@Override
|
||||||
public void restartSession(RealmModel realm) {
|
public void restartSession(RealmModel realm) {
|
||||||
entity.getAuthenticationSessions().clear();
|
entity.getAuthenticationSessions().clear();
|
||||||
|
|
|
@ -58,6 +58,12 @@ public interface RootAuthenticationSessionModel {
|
||||||
*/
|
*/
|
||||||
AuthenticationSessionModel createAuthenticationSession(ClientModel client);
|
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.
|
* 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");
|
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();
|
ClientModel client = session.getContext().getClient();
|
||||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
|
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
|
||||||
|
|
||||||
|
@ -245,6 +251,9 @@ public class TokenManager {
|
||||||
if (token.getIssuedAt() < session.users().getNotBeforeOfUser(realm, user)) {
|
if (token.getIssuedAt() < session.users().getNotBeforeOfUser(realm, user)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (token.getIssuedAt() + 1 < userSession.getStarted()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -120,6 +120,10 @@ public class LogoutEndpoint {
|
||||||
try {
|
try {
|
||||||
IDToken idToken = tokenManager.verifyIDTokenSignature(session, encodedIdToken);
|
IDToken idToken = tokenManager.verifyIDTokenSignature(session, encodedIdToken);
|
||||||
userSession = session.sessions().getUserSession(realm, idToken.getSessionState());
|
userSession = session.sessions().getUserSession(realm, idToken.getSessionState());
|
||||||
|
|
||||||
|
if (userSession != null) {
|
||||||
|
checkTokenIssuedAt(idToken, userSession);
|
||||||
|
}
|
||||||
} catch (OAuthErrorException e) {
|
} catch (OAuthErrorException e) {
|
||||||
event.event(EventType.LOGOUT);
|
event.event(EventType.LOGOUT);
|
||||||
event.error(Errors.INVALID_TOKEN);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
|
@ -198,6 +202,7 @@ public class LogoutEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userSessionModel != null) {
|
if (userSessionModel != null) {
|
||||||
|
checkTokenIssuedAt(token, userSessionModel);
|
||||||
logout(userSessionModel, offline);
|
logout(userSessionModel, offline);
|
||||||
}
|
}
|
||||||
} catch (OAuthErrorException e) {
|
} 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 userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
|
||||||
UserSessionModel offlineUserSession = null;
|
UserSessionModel offlineUserSession = null;
|
||||||
if (AuthenticationManager.isSessionValid(realm, userSession)) {
|
if (AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||||
|
checkTokenIssuedAt(token, userSession, event);
|
||||||
event.session(userSession);
|
event.session(userSession);
|
||||||
return userSession;
|
return userSession;
|
||||||
} else {
|
} else {
|
||||||
offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId());
|
offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId());
|
||||||
if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) {
|
if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) {
|
||||||
|
checkTokenIssuedAt(token, offlineUserSession, event);
|
||||||
event.session(offlineUserSession);
|
event.session(offlineUserSession);
|
||||||
return offlineUserSession;
|
return offlineUserSession;
|
||||||
}
|
}
|
||||||
|
@ -258,4 +260,10 @@ public class UserInfoEndpoint {
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED);
|
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);
|
backchannelLogoutAll(session, realm, userSession, logoutAuthSession, uriInfo, headers, logoutBroker);
|
||||||
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
|
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
|
||||||
} finally {
|
} finally {
|
||||||
asm.removeAuthenticationSession(realm, logoutAuthSession, false);
|
RootAuthenticationSessionModel rootAuthSession = logoutAuthSession.getParentSession();
|
||||||
|
rootAuthSession.removeAuthenticationSessionByTabId(logoutAuthSession.getTabId());
|
||||||
}
|
}
|
||||||
|
|
||||||
userSession.setState(UserSessionModel.State.LOGGED_OUT);
|
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) {
|
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);;
|
boolean secureOnly = realm.getSslRequired().isRequired(connection);;
|
||||||
CookieHelper.addCookie(cookieName, "", path, null, "Expiring cookie", 0, secureOnly, httpOnly);
|
CookieHelper.addCookie(cookieName, "", path, null, "Expiring cookie", 0, secureOnly, httpOnly);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.auth.page.login;
|
package org.keycloak.testsuite.auth.page.login;
|
||||||
|
|
||||||
|
import org.keycloak.testsuite.util.DroneUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author tkyjovsk
|
* @author tkyjovsk
|
||||||
|
@ -27,4 +29,14 @@ public class OIDCLogin extends Login {
|
||||||
setProtocol(OIDC);
|
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;
|
package org.keycloak.testsuite.page;
|
||||||
|
|
||||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||||
|
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.testsuite.util.DroneUtils;
|
||||||
import org.keycloak.testsuite.util.URLUtils;
|
import org.keycloak.testsuite.util.URLUtils;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
|
@ -105,7 +107,7 @@ public abstract class AbstractPage {
|
||||||
|
|
||||||
public void assertCurrent() {
|
public void assertCurrent() {
|
||||||
String name = getClass().getSimpleName();
|
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());
|
isCurrent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.jboss.arquillian.test.api.ArquillianResource;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
import org.keycloak.testsuite.arquillian.SuiteContext;
|
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||||
|
import org.keycloak.testsuite.util.DroneUtils;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
|
@ -44,7 +45,7 @@ public abstract class AbstractPage {
|
||||||
|
|
||||||
public void assertCurrent() {
|
public void assertCurrent() {
|
||||||
String name = getClass().getSimpleName();
|
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());
|
isCurrent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.pages;
|
package org.keycloak.testsuite.pages;
|
||||||
|
|
||||||
|
import org.keycloak.testsuite.util.DroneUtils;
|
||||||
import org.keycloak.testsuite.util.WaitUtils;
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
|
@ -40,7 +41,7 @@ public abstract class LanguageComboboxAwarePage extends AbstractPage {
|
||||||
public void openLanguage(String language){
|
public void openLanguage(String language){
|
||||||
WebElement langLink = localeDropdown.findElement(By.xpath("//a[text()='" + language + "']"));
|
WebElement langLink = localeDropdown.findElement(By.xpath("//a[text()='" + language + "']"));
|
||||||
String url = langLink.getAttribute("href");
|
String url = langLink.getAttribute("href");
|
||||||
driver.navigate().to(url);
|
DroneUtils.getCurrentDriver().navigate().to(url);
|
||||||
WaitUtils.waitForPageToLoad();
|
WaitUtils.waitForPageToLoad();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.testsuite.pages;
|
package org.keycloak.testsuite.pages;
|
||||||
|
|
||||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||||
|
import org.keycloak.testsuite.util.DroneUtils;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
|
@ -145,7 +146,7 @@ public class LoginPage extends LanguageComboboxAwarePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCurrent(String realm) {
|
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() {
|
public void clickRegister() {
|
||||||
|
@ -159,7 +160,7 @@ public class LoginPage extends LanguageComboboxAwarePage {
|
||||||
|
|
||||||
public WebElement findSocialButton(String providerId) {
|
public WebElement findSocialButton(String providerId) {
|
||||||
String id = "zocial-" + providerId;
|
String id = "zocial-" + providerId;
|
||||||
return this.driver.findElement(By.id(id));
|
return DroneUtils.getCurrentDriver().findElement(By.id(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetPassword() {
|
public void resetPassword() {
|
||||||
|
|
|
@ -16,12 +16,14 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.forms;
|
package org.keycloak.testsuite.forms;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.admin.client.resource.UserResource;
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
|
import org.keycloak.common.util.Retry;
|
||||||
import org.keycloak.crypto.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
|
@ -40,24 +42,32 @@ import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
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;
|
||||||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||||
import org.keycloak.testsuite.pages.ErrorPage;
|
import org.keycloak.testsuite.pages.ErrorPage;
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
|
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.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.Matchers;
|
import org.keycloak.testsuite.util.Matchers;
|
||||||
import org.keycloak.testsuite.util.RealmBuilder;
|
import org.keycloak.testsuite.util.RealmBuilder;
|
||||||
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
import org.openqa.selenium.Cookie;
|
|
||||||
import org.openqa.selenium.NoSuchElementException;
|
import org.openqa.selenium.NoSuchElementException;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
import javax.ws.rs.client.Client;
|
import javax.ws.rs.client.Client;
|
||||||
import javax.ws.rs.client.ClientBuilder;
|
import javax.ws.rs.client.ClientBuilder;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
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.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
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.junit.Assert.assertTrue;
|
||||||
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
|
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
|
||||||
import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
|
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>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -94,9 +105,19 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
.build();
|
.build();
|
||||||
user2Id = user2.getId();
|
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)
|
RealmBuilder.edit(testRealm)
|
||||||
.user(user)
|
.user(user)
|
||||||
.user(user2);
|
.user(user2)
|
||||||
|
.user(admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
|
@ -105,9 +126,21 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
@Page
|
@Page
|
||||||
protected AppPage appPage;
|
protected AppPage appPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
@JavascriptBrowser
|
||||||
|
protected AdminConsole jsAdminConsole;
|
||||||
|
|
||||||
|
@Drone
|
||||||
|
@JavascriptBrowser
|
||||||
|
protected WebDriver jsDriver;
|
||||||
|
|
||||||
@Page
|
@Page
|
||||||
protected LoginPage loginPage;
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
@JavascriptBrowser
|
||||||
|
protected LoginPage jsLoginPage;
|
||||||
|
|
||||||
@Page
|
@Page
|
||||||
protected ErrorPage errorPage;
|
protected ErrorPage errorPage;
|
||||||
|
|
||||||
|
@ -696,6 +729,35 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
.assertEvent();
|
.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
|
@Test
|
||||||
public void loginExpiredCodeAndExpiredCookies() {
|
public void loginExpiredCodeAndExpiredCookies() {
|
||||||
|
|
|
@ -25,16 +25,19 @@ import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.jose.jws.JWSHeader;
|
import org.keycloak.jose.jws.JWSHeader;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.Assert;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.pages.AppPage;
|
|
||||||
import org.keycloak.testsuite.util.*;
|
import org.keycloak.testsuite.util.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.Response.Status;
|
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.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpGet;
|
import org.apache.http.client.methods.HttpGet;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
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
|
@Test
|
||||||
public void postLogoutFailWithCredentialsOfDifferentClient() throws Exception {
|
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;
|
package org.keycloak.testsuite.oauth;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.admin.client.resource.RealmResource;
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
import org.keycloak.common.enums.SslRequired;
|
import org.keycloak.common.enums.SslRequired;
|
||||||
import org.keycloak.crypto.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
import org.keycloak.events.Details;
|
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.JWSHeader;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.RefreshToken;
|
import org.keycloak.representations.RefreshToken;
|
||||||
|
@ -37,6 +40,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.util.ClientManager;
|
import org.keycloak.testsuite.util.ClientManager;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.RealmBuilder;
|
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.lessThan;
|
||||||
import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
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 {
|
public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public AssertEvents events = new AssertEvents(this);
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@ -470,7 +478,6 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
private void processExpectedValidRefresh(String sessionId, RefreshToken requestToken, String refreshToken) {
|
private void processExpectedValidRefresh(String sessionId, RefreshToken requestToken, String refreshToken) {
|
||||||
OAuthClient.AccessTokenResponse response2 = oauth.doRefreshTokenRequest(refreshToken, "password");
|
OAuthClient.AccessTokenResponse response2 = oauth.doRefreshTokenRequest(refreshToken, "password");
|
||||||
RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
|
|
||||||
|
|
||||||
assertEquals(200, response2.getStatusCode());
|
assertEquals(200, response2.getStatusCode());
|
||||||
|
|
||||||
|
@ -540,6 +547,93 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
events.clear();
|
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
|
@Test
|
||||||
public void testUserSessionRefreshAndIdle() throws Exception {
|
public void testUserSessionRefreshAndIdle() throws Exception {
|
||||||
oauth.doLogin("test-user@localhost", "password");
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
@ -1009,4 +1103,34 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
setTimeOffset(0);
|
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.crypto.Algorithm;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
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.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
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.OIDCScopeTest;
|
||||||
import org.keycloak.testsuite.oidc.AbstractOIDCScopeTest;
|
import org.keycloak.testsuite.oidc.AbstractOIDCScopeTest;
|
||||||
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
|
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
|
||||||
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -179,6 +183,36 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertEquals(jsonNode.get("typ").asText(), "Refresh");
|
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
|
@Test
|
||||||
public void testPublicClientCredentialsNotAllowed() throws Exception {
|
public void testPublicClientCredentialsNotAllowed() throws Exception {
|
||||||
oauth.doLogin("test-user@localhost", "password");
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
@ -389,4 +423,24 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertEquals(Errors.INVALID_CLIENT, rep.getOtherClaims().get("error"));
|
assertEquals(Errors.INVALID_CLIENT, rep.getOtherClaims().get("error"));
|
||||||
assertNull(rep.getSubject());
|
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.Algorithm;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.idm.RoleRepresentation;
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
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
|
@Test
|
||||||
public void testSessionExpiredOfflineAccess() throws Exception {
|
public void testSessionExpiredOfflineAccess() throws Exception {
|
||||||
Client client = ClientBuilder.newClient();
|
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