/* * JBoss, Home of Professional Open Source. * Copyright 2017, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.wildfly.extension.undertow.security.jaspi; import io.undertow.security.api.AuthenticatedSessionManager; import io.undertow.security.api.AuthenticationMechanism; import io.undertow.security.api.SecurityContext; import io.undertow.security.idm.Account; import io.undertow.server.HttpServerExchange; import io.undertow.servlet.handlers.ServletRequestContext; import io.undertow.util.AttachmentKey; import io.undertow.util.StatusCodes; import org.jboss.security.SecurityConstants; import org.jboss.security.SimpleGroup; import org.jboss.security.SimplePrincipal; import org.jboss.security.auth.callback.JBossCallbackHandler; import org.jboss.security.auth.message.GenericMessageInfo; import org.jboss.security.identity.plugins.SimpleRole; import org.jboss.security.identity.plugins.SimpleRoleGroup; import org.jboss.security.plugins.auth.JASPIServerAuthenticationManager; import org.wildfly.extension.undertow.logging.UndertowLogger; import org.wildfly.extension.undertow.security.AccountImpl; import javax.security.auth.Subject; import javax.security.auth.message.AuthException; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import static org.jboss.security.SecurityConstants.ROLES_IDENTIFIER; import java.security.Principal; import java.security.acl.Group; import java.util.Collection; import java.util.HashSet; import java.util.Set; import org.jboss.security.auth.callback.JASPICallbackHandler; import org.jboss.security.identity.Role; import org.jboss.security.identity.RoleGroup; import org.wildfly.extension.undertow.security.UndertowSecurityAttachments; /** * <p> * {@link AuthenticationMechanism} implementation that enables JASPI-based authentication. * </p> * * @author Pedro Igor * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a> */ public class JASPICAuthenticationMechanism implements AuthenticationMechanism { static final String JASPI_HTTP_SERVLET_LAYER = "HttpServlet"; private static final String MECHANISM_NAME = "JASPIC"; private static final String JASPI_AUTH_TYPE = "javax.servlet.http.authType"; private static final String JASPI_REGISTER_SESSION = "javax.servlet.http.registerSession"; public static final AttachmentKey<HttpServerExchange> HTTP_SERVER_EXCHANGE_ATTACHMENT_KEY = AttachmentKey.create(HttpServerExchange.class); public static final AttachmentKey<SecurityContext> SECURITY_CONTEXT_ATTACHMENT_KEY = AttachmentKey.create(SecurityContext.class); public static final AttachmentKey<Boolean> AUTH_RUN = AttachmentKey.create(Boolean.class); public static final int DEFAULT_ERROR_CODE = StatusCodes.UNAUTHORIZED; private final String securityDomain; private final String configuredAuthMethod; public JASPICAuthenticationMechanism(final String securityDomain, final String configuredAuthMethod) { this.securityDomain = securityDomain; this.configuredAuthMethod = configuredAuthMethod; } @Override public AuthenticationMechanismOutcome authenticate(final HttpServerExchange exchange, final SecurityContext sc) { exchange.putAttachment(AUTH_RUN, true); final ServletRequestContext requestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); final JASPIServerAuthenticationManager sam = createJASPIAuthenticationManager(); final GenericMessageInfo messageInfo = createMessageInfo(exchange, sc); final String applicationIdentifier = buildApplicationIdentifier(requestContext); final JASPICallbackHandler cbh = new JASPICallbackHandler(); exchange.putAttachment(JASPICContext.ATTACHMENT_KEY, new JASPICContext(messageInfo, sam, cbh)); UndertowLogger.ROOT_LOGGER.debugf("validateRequest for layer [%s] and applicationContextIdentifier [%s]", JASPI_HTTP_SERVLET_LAYER, applicationIdentifier); Account cachedAccount = null; final JASPICSecurityContext jaspicSecurityContext = (JASPICSecurityContext) exchange.getSecurityContext(); final AuthenticatedSessionManager sessionManager = exchange.getAttachment(AuthenticatedSessionManager.ATTACHMENT_KEY); if (sessionManager != null) { AuthenticatedSessionManager.AuthenticatedSession authSession = sessionManager.lookupSession(exchange); if(authSession != null) { cachedAccount = authSession.getAccount(); // if there is a cached account we set it in the security context so that the principal is available to // SAM modules via request.getUserPrincipal(). if (cachedAccount != null) { jaspicSecurityContext.setCachedAuthenticatedAccount(cachedAccount); } } } AuthenticationMechanismOutcome outcome = AuthenticationMechanismOutcome.NOT_AUTHENTICATED; Account authenticatedAccount = null; boolean isValid = sam.isValid(messageInfo, new Subject(), JASPI_HTTP_SERVLET_LAYER, applicationIdentifier, cbh); jaspicSecurityContext.setCachedAuthenticatedAccount(null); if (isValid) { // The CBH filled in the JBOSS SecurityContext, we need to create an Undertow account based on that org.jboss.security.SecurityContext jbossSct = SecurityActions.getSecurityContext(); authenticatedAccount = createAccount(cachedAccount, jbossSct); updateSubjectRoles(jbossSct); } // authType resolution (check message info first, then check for the configured auth method, then use mech-specific name). String authType = (String) messageInfo.getMap().get(JASPI_AUTH_TYPE); if (authType == null) authType = this.configuredAuthMethod != null ? this.configuredAuthMethod : MECHANISM_NAME; if (isValid && authenticatedAccount != null) { outcome = AuthenticationMechanismOutcome.AUTHENTICATED; Object registerObj = messageInfo.getMap().get(JASPI_REGISTER_SESSION); boolean cache = false; if(registerObj != null && (registerObj instanceof String)) { cache = Boolean.valueOf((String)registerObj); } sc.authenticationComplete(authenticatedAccount, authType, cache); } else if (isValid && authenticatedAccount == null && !isMandatory(requestContext)) { outcome = AuthenticationMechanismOutcome.NOT_ATTEMPTED; } else { outcome = AuthenticationMechanismOutcome.NOT_AUTHENTICATED; sc.authenticationFailed("JASPIC authentication failed.", authType); // make sure we don't return status OK if the AuthException was thrown if (wasAuthExceptionThrown(exchange) && !statusIndicatesError(exchange)) { exchange.setResponseCode(DEFAULT_ERROR_CODE); } } // A SAM can wrap the HTTP request/response objects - update the servlet request context with the values found in the message info. ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); servletRequestContext.setServletRequest((HttpServletRequest) messageInfo.getRequestMessage()); servletRequestContext.setServletResponse((HttpServletResponse) messageInfo.getResponseMessage()); return outcome; } @Override public ChallengeResult sendChallenge(final HttpServerExchange exchange, final SecurityContext securityContext) { return new ChallengeResult(true); } private JASPIServerAuthenticationManager createJASPIAuthenticationManager() { return new JASPIServerAuthenticationManager(this.securityDomain, new JBossCallbackHandler()); } static String buildApplicationIdentifier(final ServletRequestContext attachment) { ServletRequest servletRequest = attachment.getServletRequest(); return servletRequest.getServletContext().getVirtualServerName() + " " + servletRequest.getServletContext().getContextPath(); } private GenericMessageInfo createMessageInfo(final HttpServerExchange exchange, final SecurityContext securityContext) { ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); GenericMessageInfo messageInfo = new GenericMessageInfo(); messageInfo.setRequestMessage(servletRequestContext.getServletRequest()); messageInfo.setResponseMessage(servletRequestContext.getServletResponse()); messageInfo.getMap().put("javax.security.auth.message.MessagePolicy.isMandatory", isMandatory(servletRequestContext).toString()); // additional context data, useful to provide access to Undertow resources during the modules processing messageInfo.getMap().put(SECURITY_CONTEXT_ATTACHMENT_KEY, securityContext); messageInfo.getMap().put(HTTP_SERVER_EXCHANGE_ATTACHMENT_KEY, exchange); return messageInfo; } private void updateSubjectRoles(final org.jboss.security.SecurityContext jbossSct){ if (jbossSct == null) { throw UndertowLogger.ROOT_LOGGER.nullParamter("org.jboss.security.SecurityContext"); } RoleGroup contextRoleGroup = jbossSct.getUtil().getRoles(); if(contextRoleGroup == null){ return; } Collection<Role> contextRoles = contextRoleGroup.getRoles(); if(contextRoles.isEmpty()){ return; } Subject subject = jbossSct.getUtil().getSubject(); Set<Group> groupPrincipals = subject.getPrincipals(Group.class); Group subjectRoleGroup = null; for (Group candidate : groupPrincipals) { if (candidate.getName().equals(ROLES_IDENTIFIER)) { subjectRoleGroup = candidate; break; } } if (subjectRoleGroup == null) { subjectRoleGroup = new SimpleGroup(ROLES_IDENTIFIER); subject.getPrincipals().add(subjectRoleGroup); } for (Role role : contextRoles) { Principal rolePrincipal = new SimplePrincipal(role.getRoleName()); subjectRoleGroup.addMember(rolePrincipal); } } private Account createAccount(final Account cachedAccount, final org.jboss.security.SecurityContext jbossSct) { if (jbossSct == null) { throw UndertowLogger.ROOT_LOGGER.nullParamter("org.jboss.security.SecurityContext"); } // null principal: SAM has opted out of the authentication process. Principal userPrincipal = jbossSct.getUtil().getUserPrincipal(); if (userPrincipal == null) { return null; } // SAM handled the same principal found in the cached account: indicates we must use the cached account. if (cachedAccount != null && cachedAccount.getPrincipal() == userPrincipal) { // populate the security context using the cached account data. jbossSct.getUtil().createSubjectInfo(userPrincipal, ((AccountImpl) cachedAccount).getCredential(), jbossSct.getUtil().getSubject()); RoleGroup roleGroup = new SimpleRoleGroup(SecurityConstants.ROLES_IDENTIFIER); for (String role : cachedAccount.getRoles()) roleGroup.addRole(new SimpleRole(role)); jbossSct.getUtil().setRoles(roleGroup); return cachedAccount; } // SAM handled a different principal or there is no cached account: build a new account. Set<String> stringRoles = new HashSet<String>(); RoleGroup roleGroup = jbossSct.getUtil().getRoles(); if (roleGroup != null) { for (Role role : roleGroup.getRoles()) { stringRoles.add(role.getRoleName()); } } Object credential = jbossSct.getUtil().getCredential(); Principal original = null; if(cachedAccount != null) { original = cachedAccount.getPrincipal(); } return new AccountImpl(userPrincipal, stringRoles, credential, original); } /** * <p>The authentication is mandatory if the servlet has http constraints (eg.: {@link * javax.servlet.annotation.HttpConstraint}).</p> * * @param attachment * @return */ private Boolean isMandatory(final ServletRequestContext attachment) { return attachment.getExchange().getSecurityContext() != null && attachment.getExchange().getSecurityContext().isAuthenticationRequired(); } private boolean statusIndicatesError(HttpServerExchange exchange) { return exchange.getResponseCode() != StatusCodes.OK; } static boolean wasAuthExceptionThrown(HttpServerExchange exchange) { return exchange.getAttachment(UndertowSecurityAttachments.SECURITY_CONTEXT_ATTACHMENT).getData().get(AuthException.class.getName()) != null; } }