Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,25 @@

import com.google.common.collect.Sets;
import org.jboss.logging.Logger;
import org.keycloak.authentication.actiontoken.ActionTokenContext;
import org.keycloak.authentication.actiontoken.DefaultActionToken;
import org.keycloak.common.ClientConnection;
import org.keycloak.http.HttpRequest;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.utils.StringUtil;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static org.keycloak.services.managers.AuthenticationManager.FORCED_REAUTHENTICATION;
import static org.keycloak.services.managers.AuthenticationManager.SSO_AUTH;
Expand Down Expand Up @@ -110,4 +119,35 @@ public static List<AuthenticationExecutionModel> getExecutionsByType(RealmModel
return executions;
}

/**
* Logouts all sessions that are different to the current authentication session
* managed in the action context.
*
* @param context The required action context
*/
public static void logoutOtherSessions(RequiredActionContext context) {
logoutOtherSessions(context.getSession(), context.getRealm(), context.getUser(),
context.getAuthenticationSession(), context.getConnection(), context.getHttpRequest());
}

/**
* Logouts all sessions that are different to the current authentication session
* managed in the action token context.
*
* @param context The required action token context
*/
public static void logoutOtherSessions(ActionTokenContext<? extends DefaultActionToken> context) {
logoutOtherSessions(context.getSession(), context.getRealm(), context.getAuthenticationSession().getAuthenticatedUser(),
context.getAuthenticationSession(), context.getClientConnection(), context.getRequest());
}

private static void logoutOtherSessions(KeycloakSession session, RealmModel realm, UserModel user,
AuthenticationSessionModel authSession, ClientConnection conn, HttpRequest req) {
session.sessions().getUserSessionsStream(realm, user)
.filter(s -> !Objects.equals(s.getId(), authSession.getParentSession().getId()))
.collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions.
.forEach(s -> AuthenticationManager.backchannelLogout(session, realm, s, session.getContext().getUri(),
conn, req.getHttpHeaders(), true)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,19 @@ public class UpdateEmailActionToken extends DefaultActionToken {
private String oldEmail;
@JsonProperty("newEmail")
private String newEmail;
@JsonProperty("logoutSessions")
private Boolean logoutSessions;

public UpdateEmailActionToken(String userId, int absoluteExpirationInSecs, String oldEmail, String newEmail, String clientId){
public UpdateEmailActionToken(String userId, int absoluteExpirationInSecs, String oldEmail, String newEmail, String clientId) {
this(userId, absoluteExpirationInSecs, oldEmail, newEmail, clientId, null);
}

public UpdateEmailActionToken(String userId, int absoluteExpirationInSecs, String oldEmail, String newEmail, String clientId, Boolean logoutSessions){
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null);
this.oldEmail = oldEmail;
this.newEmail = newEmail;
this.issuedFor = clientId;
this.logoutSessions = Boolean.TRUE.equals(logoutSessions)? true : null;
}

private UpdateEmailActionToken(){
Expand All @@ -55,4 +62,12 @@ public String getNewEmail() {
public void setNewEmail(String newEmail) {
this.newEmail = newEmail;
}

public Boolean getLogoutSessions() {
return this.logoutSessions;
}

public void setLogoutSessions(Boolean logoutSessions) {
this.logoutSessions = Boolean.TRUE.equals(logoutSessions)? true : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Objects;
import jakarta.ws.rs.core.Response;
import org.keycloak.TokenVerifier;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.actiontoken.AbstractActionTokenHandler;
import org.keycloak.authentication.actiontoken.ActionTokenContext;
import org.keycloak.authentication.actiontoken.TokenUtils;
Expand Down Expand Up @@ -74,6 +75,10 @@ public Response handleToken(UpdateEmailActionToken token, ActionTokenContext<Upd

UpdateEmail.updateEmailNow(tokenContext.getEvent(), user, emailUpdateValidationResult);

if (Boolean.TRUE.equals(token.getLogoutSessions())) {
AuthenticatorUtil.logoutOtherSessions(tokenContext);
}

tokenContext.getEvent().success();

// verify user email as we know it is valid as this entry point would never have gotten here.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Arrays;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.InitiatedActionSupport;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
Expand Down Expand Up @@ -92,6 +93,10 @@ public void processAction(RequiredActionContext reqActionContext) {

RecoveryAuthnCodesCredentialModel credentialModel = createFromValues(generatedCodes, generatedAtTime, generatedUserLabel);

if ("on".equals(httpReqParamsMap.getFirst("logout-sessions"))) {
AuthenticatorUtil.logoutOtherSessions(reqActionContext);
}

recoveryCodeCredentialProvider.createCredential(reqActionContext.getRealm(), reqActionContext.getUser(),
credentialModel);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import jakarta.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.InitiatedActionSupport;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
Expand Down Expand Up @@ -97,16 +98,20 @@ public void processAction(RequiredActionContext context) {
return;
}

final boolean logoutSessions = "on".equals(formData.getFirst("logout-sessions"));
if (!realm.isVerifyEmail() || Validation.isBlank(newEmail)
|| Objects.equals(user.getEmail(), newEmail) && user.isEmailVerified()) {
if (logoutSessions) {
AuthenticatorUtil.logoutOtherSessions(context);
}
updateEmailWithoutConfirmation(context, emailUpdateValidationResult);
return;
}

sendEmailUpdateConfirmation(context);
sendEmailUpdateConfirmation(context, logoutSessions);
}

private void sendEmailUpdateConfirmation(RequiredActionContext context) {
private void sendEmailUpdateConfirmation(RequiredActionContext context, boolean logoutSessions) {
UserModel user = context.getUser();
String oldEmail = user.getEmail();
String newEmail = context.getHttpRequest().getDecodedFormParameters().getFirst(UserModel.EMAIL);
Expand All @@ -119,7 +124,7 @@ private void sendEmailUpdateConfirmation(RequiredActionContext context) {
AuthenticationSessionModel authenticationSession = context.getAuthenticationSession();

UpdateEmailActionToken actionToken = new UpdateEmailActionToken(user.getId(), Time.currentTime() + validityInSecs,
oldEmail, newEmail, authenticationSession.getClient().getClientId());
oldEmail, newEmail, authenticationSession.getClient().getClientId(), logoutSessions);

String link = Urls
.actionTokenBuilder(uriInfo.getBaseUri(), actionToken.serialize(session, realm, uriInfo),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,19 @@
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;
import org.keycloak.sessions.AuthenticationSessionModel;

import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
* @author <a href="mailto:[email protected]">Bill Burke</a>
Expand Down Expand Up @@ -95,9 +90,7 @@ public void requiredActionChallenge(RequiredActionContext context) {
public void processAction(RequiredActionContext context) {
EventBuilder event = context.getEvent();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
RealmModel realm = context.getRealm();
UserModel user = context.getUser();
KeycloakSession session = context.getSession();
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
event.event(EventType.UPDATE_PASSWORD);
String passwordNew = formData.getFirst("password-new");
Expand Down Expand Up @@ -125,13 +118,8 @@ public void processAction(RequiredActionContext context) {
return;
}

if ("on".equals(formData.getFirst("logout-sessions")))
{
session.sessions().getUserSessionsStream(realm, user)
.filter(s -> !Objects.equals(s.getId(), authSession.getParentSession().getId()))
.collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions.
.forEach(s -> AuthenticationManager.backchannelLogout(session, realm, s, session.getContext().getUri(),
context.getConnection(), context.getHttpRequest().getHttpHeaders(), true));
if ("on".equals(formData.getFirst("logout-sessions"))) {
AuthenticatorUtil.logoutOtherSessions(context);
}

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.keycloak.authentication.requiredactions;

import org.keycloak.Config;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.CredentialRegistrator;
import org.keycloak.authentication.InitiatedActionSupport;
import org.keycloak.authentication.RequiredActionContext;
Expand Down Expand Up @@ -105,6 +106,10 @@ public void processAction(RequiredActionContext context) {
return;
}

if ("on".equals(formData.getFirst("logout-sessions"))) {
AuthenticatorUtil.logoutOtherSessions(context);
}

if (!CredentialHelper.createOTPCredential(context.getSession(), context.getRealm(), context.getUser(), challengeResponse, credentialModel)) {
Response challenge = context.form()
.setAttribute("mode", mode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.jboss.logging.Logger;
import org.keycloak.http.HttpRequest;
import org.keycloak.WebAuthnConstants;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.CredentialRegistrator;
import org.keycloak.authentication.InitiatedActionSupport;
import org.keycloak.authentication.RequiredActionContext;
Expand All @@ -52,6 +53,8 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
import org.keycloak.models.WebAuthnPolicy;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.utils.StringUtil;

import com.webauthn4j.converter.util.ObjectConverter;
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
Expand All @@ -73,8 +76,6 @@
import com.webauthn4j.validator.attestation.statement.u2f.FIDOU2FAttestationStatementValidator;
import com.webauthn4j.validator.attestation.trustworthiness.certpath.CertPathTrustworthinessValidator;
import com.webauthn4j.validator.attestation.trustworthiness.self.DefaultSelfAttestationTrustworthinessValidator;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.utils.StringUtil;

import static org.keycloak.WebAuthnConstants.REG_ERR_DETAIL_LABEL;
import static org.keycloak.WebAuthnConstants.REG_ERR_LABEL;
Expand Down Expand Up @@ -225,6 +226,10 @@ public void processAction(RequiredActionContext context) {

RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, isUserVerificationRequired);

if ("on".equals(params.getFirst("logout-sessions"))) {
AuthenticatorUtil.logoutOtherSessions(context);
}

WebAuthnRegistrationManager webAuthnRegistrationManager = createWebAuthnRegistrationManager();
try {
// parse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
import static org.keycloak.testsuite.util.UIUtils.getTextFromElement;

import org.keycloak.models.UserModel;
import org.keycloak.testsuite.pages.LogoutSessionsPage;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;

public class UpdateEmailPage extends RequiredActions {
public class UpdateEmailPage extends LogoutSessionsPage {

@FindBy(id = "email")
private WebElement emailInput;
Expand All @@ -38,22 +39,20 @@ public class UpdateEmailPage extends RequiredActions {
@FindBy(css = "input[type='submit']")
private WebElement submitActionButton;

@Override
public String getActionId() {
return UserModel.RequiredAction.UPDATE_EMAIL.name();
}
@FindBy(css = "input[type='submit']")
private WebElement submitButton;

@Override
public boolean isCurrent() {
return driver.getCurrentUrl().contains("login-actions/required-action")
&& driver.getCurrentUrl().contains("execution=" + getActionId());
&& driver.getCurrentUrl().contains("execution=" + UserModel.RequiredAction.UPDATE_EMAIL.name());
}

public void changeEmail(String email){
emailInput.clear();
emailInput.sendKeys(email);

submit();
clickLink(submitButton);
}

public String getEmail() {
Expand Down Expand Up @@ -84,4 +83,8 @@ public void clickSubmitAction() {
clickLink(submitActionButton);
}

@Override
public void open() throws Exception {
throw new UnsupportedOperationException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
/**
* @author <a href="mailto:[email protected]">Stian Thorgersen</a>
*/
public class LoginConfigTotpPage extends AbstractPage {
public class LoginConfigTotpPage extends LogoutSessionsPage {

@FindBy(id = "totpSecret")
private WebElement totpSecret;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.util.UIUtils.isElementVisible;

/**
* @author <a href="mailto:[email protected]">Stian Thorgersen</a>
*/
public class LoginPasswordUpdatePage extends LanguageComboboxAwarePage {
public class LoginPasswordUpdatePage extends LogoutSessionsPage {

@FindBy(id = "password-new")
private WebElement newPasswordInput;
Expand All @@ -42,9 +40,6 @@ public class LoginPasswordUpdatePage extends LanguageComboboxAwarePage {

@FindBy(className = "kc-feedback-text")
private WebElement feedbackMessage;

@FindBy(id = "logout-sessions")
private WebElement logoutSessionsCheckbox;

@FindBy(name = "cancel-aia")
private WebElement cancelAIAButton;
Expand Down Expand Up @@ -76,24 +71,6 @@ public String getFeedbackMessage() {
return feedbackMessage.getText();
}

public boolean isLogoutSessionDisplayed() {
return isElementVisible(logoutSessionsCheckbox);
}

public boolean isLogoutSessionsChecked() {
return logoutSessionsCheckbox.isSelected();
}

public void checkLogoutSessions() {
assertFalse("Logout sessions is checked", isLogoutSessionsChecked());
logoutSessionsCheckbox.click();
}

public void uncheckLogoutSessions() {
assertTrue("Logout sessions is not checked", isLogoutSessionsChecked());
logoutSessionsCheckbox.click();
}

public boolean isCancelDisplayed() {
return isElementVisible(cancelAIAButton);
}
Expand Down
Loading