org.wildfly.security.http.util.sso.DefaultSingleSignOnSession Maven / Gradle / Ivy
The newest version!
/*
* JBoss, Home of Professional Open Source.
* Copyright 2017 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.wildfly.security.http.util.sso;
import static org.wildfly.common.Assert.checkNotNullParam;
import static org.wildfly.security.http.HttpScopeNotification.SessionNotificationType.INVALIDATED;
import static org.wildfly.security.http.util.sso.ElytronMessages.log;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import org.wildfly.security.auth.principal.NamePrincipal;
import org.wildfly.security.auth.server.SecurityIdentity;
import org.wildfly.security.cache.CachedIdentity;
import org.wildfly.security.http.HttpScope;
import org.wildfly.security.http.HttpServerRequest;
import org.wildfly.security.http.Scope;
/**
* {@link SingleSignOnSession} that delegates its persistence strategy to a {@link SingleSignOnManager}.
* {@link SingleSignOn} entries are created lazily in response to {@link #put(SecurityIdentity)}.
*
* This implementation supports single logout in order to invalidate local sessions for each participant of a single sign-on session, where participants
* represent the applications with active sessions associated with a given single sign-on session.
* @author Paul Ferraro
*/
public class DefaultSingleSignOnSession implements SingleSignOnSession {
private static final String LOGOUT_REQUEST_PARAMETER = "ely_logout_message";
private static final String SESSION_INVALIDATING_ATTRIBUTE = DefaultSingleSignOnSessionFactory.class.getName() + ".INVALIDATING";
private static final Boolean SINGLE_SIGN_ON_KEY = Boolean.TRUE;
private final HttpServerRequest request;
// Serves as a lazy initializable atomic reference
private final ConcurrentMap map = new ConcurrentHashMap<>(1);
private final SingleSignOnSessionContext context;
private final Function ssoFactory;
public DefaultSingleSignOnSession(SingleSignOnSessionContext context, HttpServerRequest request, String mechanismName, boolean programmatic) {
this.context = checkNotNullParam("context", context);
this.request = checkNotNullParam("request", request);
checkNotNullParam("mechanismName", mechanismName);
this.ssoFactory = identity -> context.getSingleSignOnManager().create(mechanismName, programmatic, identity);
}
public DefaultSingleSignOnSession(SingleSignOnSessionContext context, HttpServerRequest request, SingleSignOn sso) {
this.context = checkNotNullParam("context", context);
this.map.put(SINGLE_SIGN_ON_KEY, sso);
this.request = checkNotNullParam("request", request);
checkNotNullParam("sso", sso);
this.ssoFactory = identity -> sso;
}
@Override
public String getId() {
SingleSignOn sso = this.map.get(SINGLE_SIGN_ON_KEY);
return (sso != null) ? sso.getId() : null;
}
@Override
public CachedIdentity get() {
SingleSignOn sso = this.map.get(SINGLE_SIGN_ON_KEY);
return (sso != null) ? getCachedIdentity(sso) : null;
}
@Override
public void put(SecurityIdentity identity) {
SingleSignOn sso = this.map.computeIfAbsent(SINGLE_SIGN_ON_KEY, key -> this.ssoFactory.apply(identity));
sso.setIdentity(identity);
HttpScope scope = this.request.getScope(Scope.SESSION);
if (!scope.exists()) {
scope.create();
}
URI uri = this.request.getRequestURI();
String sessionId = scope.getID();
String applicationId = this.request.getScope(Scope.APPLICATION).getID();
if (sso.addParticipant(applicationId, sessionId, uri)) {
String id = sso.getId();
log.debugf("Updating local sessions for SSO [%s]. New local session [%s]. Local sessions: [%s]", id, sessionId, sso.getParticipants());
scope.registerForNotification(notification -> {
HttpScope sessionScope = notification.getScope(Scope.SESSION);
Map> logoutTargets = Collections.emptyMap();
try (SingleSignOn target = this.context.getSingleSignOnManager().find(id)) {
if (target != null) {
Map.Entry localParticipant = target.removeParticipant(applicationId);
if (localParticipant != null) {
log.debugf("Removed local session [%s] from SSO [%s]", localParticipant.getKey(), target.getId());
}
if (sessionScope.getAttachment(SESSION_INVALIDATING_ATTRIBUTE) == null) {
Map> participants = target.getParticipants();
if (participants.isEmpty()) {
log.debugf("Destroying SSO [%s]. SSO is not associated with participants", target.getId());
target.invalidate();
} else if (notification.isOfType(INVALIDATED)) {
logoutTargets = participants;
}
}
}
}
if (!logoutTargets.isEmpty()) {
logoutTargets.forEach((participantId, participant) -> {
String remoteSessionId = participant.getKey();
URI remoteURI = participant.getValue();
try {
URL participantUrl = remoteURI.toURL();
HttpURLConnection connection = (HttpURLConnection) participantUrl.openConnection();
this.context.configureLogoutConnection(connection);
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setAllowUserInteraction(false);
connection.setConnectTimeout(10000);
connection.setReadTimeout(10000);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
StringBuilder parameterBuilder = new StringBuilder();
parameterBuilder.append(LOGOUT_REQUEST_PARAMETER).append("=").append(this.context.createLogoutParameter(remoteSessionId));
connection.setRequestProperty("Content-Length", Integer.toString(parameterBuilder.length()));
try (
OutputStream outputStream = connection.getOutputStream();
DataOutputStream wr = new DataOutputStream(outputStream);
) {
wr.writeBytes(parameterBuilder.toString());
}
connection.getInputStream().close();
} catch (Exception cause) {
log.warnHttpMechSsoFailedLogoutParticipant(remoteURI.toString(), cause);
}
});
try (SingleSignOn target = this.context.getSingleSignOnManager().find(id)) {
if (target != null) {
// If all logout requests were successful, then there should be no participants, and we can invalidate the SSO
if (!target.getParticipants().isEmpty()) {
log.debugf("Destroying SSO [%s]. Participant list not empty.", target.getId());
} else {
log.debugf("Destroying SSO [%s]. SSO is no longer associated with any participants", target.getId());
}
target.invalidate();
}
}
}
});
}
}
@Override
public CachedIdentity remove() {
SingleSignOn sso = this.map.get(SINGLE_SIGN_ON_KEY);
if (sso == null) return null;
sso.invalidate();
HttpScope scope = this.request.getScope(Scope.SESSION);
if (scope.exists()) {
invalidateLocalSession(scope);
}
return getCachedIdentity(sso);
}
@Override
public boolean logout() {
String logoutMessage = this.request.getFirstParameterValue(LOGOUT_REQUEST_PARAMETER);
if (logoutMessage == null) {
return false;
}
try {
String localSessionId = this.context.verifyLogoutParameter(logoutMessage);
HttpScope scope = this.request.getScope(Scope.SESSION, localSessionId);
if (!scope.exists()) {
return false;
}
log.debugf("Invalidating local session [%s] from SSO [%s]", localSessionId, this.getId());
invalidateLocalSession(scope);
} catch (Exception e) {
log.errorHttpMechSsoFailedInvalidateLocalSession(e);
}
this.request.authenticationInProgress(response -> response.setStatusCode(200));
return true;
}
@Override
public void close() {
Optional.ofNullable(this.map.remove(SINGLE_SIGN_ON_KEY)).ifPresent(SingleSignOn::close);
}
void invalidateLocalSession(HttpScope scope) {
scope.setAttachment(SESSION_INVALIDATING_ATTRIBUTE, true);
scope.invalidate();
log.debugf("Local session [%s] invalidated for SSO [%s]", scope.getID(), this.getId());
}
private static CachedIdentity getCachedIdentity(SingleSignOn sso) {
String mechanism = sso.getMechanism();
boolean programmatic = sso.isProgrammatic();
SecurityIdentity identity = sso.getIdentity();
return (identity != null) ? new CachedIdentity(mechanism, programmatic, identity) : new CachedIdentity(mechanism, programmatic, new NamePrincipal(sso.getName()));
}
}