package io.undertow.security.impl;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.NotificationReceiver;
import io.undertow.security.api.SecurityContext;
import io.undertow.security.api.SecurityNotification;
import io.undertow.security.idm.Account;
import io.undertow.security.idm.IdentityManager;
import io.undertow.server.ConduitWrapper;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.Cookie;
import io.undertow.server.handlers.CookieImpl;
import io.undertow.server.session.Session;
import io.undertow.server.session.SessionListener;
import io.undertow.server.session.SessionManager;
import io.undertow.util.ConduitFactory;
import io.undertow.util.Sessions;
import org.jboss.logging.Logger;
import org.xnio.conduits.StreamSinkConduit;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
public class SingleSignOnAuthenticationMechanism implements AuthenticationMechanism {
private static final Logger log = Logger.getLogger(SingleSignOnAuthenticationMechanism.class);
private static final String SSO_SESSION_ATTRIBUTE = SingleSignOnAuthenticationMechanism.class.getName() + ".SSOID";
private final Set<SessionManager> seenSessionManagers = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<SessionManager, Boolean>()));
private String cookieName = "JSESSIONIDSSO";
private boolean httpOnly;
private boolean secure;
private String domain;
private String path;
private final SessionInvalidationListener listener = new SessionInvalidationListener();
private final ResponseListener responseListener = new ResponseListener();
private final SingleSignOnManager singleSignOnManager;
private final IdentityManager identityManager;
public SingleSignOnAuthenticationMechanism(SingleSignOnManager storage) {
this(storage, null);
}
public SingleSignOnAuthenticationMechanism(SingleSignOnManager storage, IdentityManager identityManager) {
this.singleSignOnManager = storage;
this.identityManager = identityManager;
}
@SuppressWarnings("deprecation")
private IdentityManager getIdentityManager(SecurityContext securityContext) {
return identityManager != null ? identityManager : securityContext.getIdentityManager();
}
@Override
public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) {
Cookie cookie = null;
for (Cookie c : exchange.requestCookies()) {
if (cookieName.equals(c.getName())) {
cookie = c;
}
}
if (cookie != null) {
final String ssoId = cookie.getValue();
log.tracef("Found SSO cookie %s", ssoId);
try (SingleSignOn sso = this.singleSignOnManager.findSingleSignOn(ssoId)) {
if (sso != null) {
if(log.isTraceEnabled()) {
log.tracef("SSO session with ID: %s found.", ssoId);
}
Account verified = getIdentityManager(securityContext).verify(sso.getAccount());
if (verified == null) {
if(log.isTraceEnabled()) {
log.tracef("Account not found. Returning 'not attempted' here.");
}
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
final Session session = getSession(exchange);
registerSessionIfRequired(sso, session);
securityContext.authenticationComplete(verified, sso.getMechanismName(), false);
securityContext.registerNotificationReceiver(new NotificationReceiver() {
@Override
public void handleNotification(SecurityNotification notification) {
if (notification.getEventType() == SecurityNotification.EventType.LOGGED_OUT) {
singleSignOnManager.removeSingleSignOn(sso);
}
}
});
log.tracef("Authenticated account %s using SSO", verified.getPrincipal().getName());
return AuthenticationMechanismOutcome.AUTHENTICATED;
}
}
clearSsoCookie(exchange);
}
exchange.addResponseWrapper(responseListener);
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
private void registerSessionIfRequired(SingleSignOn sso, Session session) {
if (!sso.contains(session)) {
if(log.isTraceEnabled()) {
log.tracef("Session %s added to SSO %s", session.getId(), sso.getId());
}
sso.add(session);
}
if(session.getAttribute(SSO_SESSION_ATTRIBUTE) == null) {
if(log.isTraceEnabled()) {
log.tracef("SSO_SESSION_ATTRIBUTE not found. Creating it with SSO ID %s as value.", sso.getId());
}
session.setAttribute(SSO_SESSION_ATTRIBUTE, sso.getId());
}
SessionManager manager = session.getSessionManager();
if (seenSessionManagers.add(manager)) {
manager.registerSessionListener(listener);
}
}
private void (HttpServerExchange exchange) {
exchange.setResponseCookie(new CookieImpl(cookieName).setMaxAge(0).setHttpOnly(httpOnly).setSecure(secure).setDomain(domain).setPath(path));
}
@Override
public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
return ChallengeResult.NOT_SENT;
}
protected Session getSession(final HttpServerExchange exchange) {
return Sessions.getOrCreateSession(exchange);
}
final class ResponseListener implements ConduitWrapper<StreamSinkConduit> {
@Override
public StreamSinkConduit wrap(ConduitFactory<StreamSinkConduit> factory, HttpServerExchange exchange) {
SecurityContext sc = exchange.getSecurityContext();
Account account = sc.getAuthenticatedAccount();
if (account != null) {
try (SingleSignOn sso = singleSignOnManager.createSingleSignOn(account, sc.getMechanismName())) {
Session session = getSession(exchange);
registerSessionIfRequired(sso, session);
exchange.setResponseCookie(new CookieImpl(cookieName, sso.getId()).setHttpOnly(httpOnly).setSecure(secure).setDomain(domain).setPath(path));
}
}
return factory.create();
}
}
final class SessionInvalidationListener implements SessionListener {
@Override
public void sessionCreated(Session session, HttpServerExchange exchange) {
}
@Override
public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) {
String ssoId = (String) session.getAttribute(SSO_SESSION_ATTRIBUTE);
if (ssoId != null) {
if(log.isTraceEnabled()) {
log.tracef("Removing SSO ID %s from destroyed session %s.", ssoId, session.getId());
}
List<Session> sessionsToRemove = new LinkedList<>();
try (SingleSignOn sso = singleSignOnManager.findSingleSignOn(ssoId)) {
if (sso != null) {
sso.remove(session);
if (reason == SessionDestroyedReason.INVALIDATED) {
for (Session associatedSession : sso) {
sso.remove(associatedSession);
sessionsToRemove.add(associatedSession);
}
}
if (!sso.iterator().hasNext()) {
singleSignOnManager.removeSingleSignOn(sso);
}
}
}
for (Session sessionToRemove : sessionsToRemove) {
sessionToRemove.invalidate(null);
}
}
}
@Override
public void attributeAdded(Session session, String name, Object value) {
}
@Override
public void attributeUpdated(Session session, String name, Object newValue, Object oldValue) {
}
@Override
public void attributeRemoved(Session session, String name, Object oldValue) {
}
@Override
public void sessionIdChanged(Session session, String oldSessionId) {
}
}
public String getCookieName() {
return cookieName;
}
public SingleSignOnAuthenticationMechanism setCookieName(String cookieName) {
this.cookieName = cookieName;
return this;
}
public boolean isHttpOnly() {
return httpOnly;
}
public SingleSignOnAuthenticationMechanism setHttpOnly(boolean httpOnly) {
this.httpOnly = httpOnly;
return this;
}
public boolean isSecure() {
return secure;
}
public SingleSignOnAuthenticationMechanism setSecure(boolean secure) {
this.secure = secure;
return this;
}
public String getDomain() {
return domain;
}
public SingleSignOnAuthenticationMechanism setDomain(String domain) {
this.domain = domain;
return this;
}
public String getPath() {
return path;
}
public SingleSignOnAuthenticationMechanism setPath(String path) {
this.path = path;
return this;
}
}