/* * Copyright 2013 OmniFaces. * * 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.omnifaces.security.jaspic.core; import static java.lang.Boolean.TRUE; import static javax.security.auth.message.AuthStatus.SUCCESS; import static org.omnifaces.security.jaspic.Utils.isEmpty; import static org.omnifaces.security.jaspic.Utils.isOneOf; import static org.omnifaces.security.jaspic.config.ControlFlag.REQUIRED; import java.io.IOException; import java.util.List; import javax.faces.context.FacesContext; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.message.AuthStatus; import javax.security.auth.message.MessageInfo; import javax.security.auth.message.callback.CallerPrincipalCallback; import javax.security.auth.message.callback.GroupPrincipalCallback; import javax.security.auth.message.config.AuthConfigFactory; import javax.security.auth.message.module.ServerAuthModule; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.omnifaces.security.jaspic.config.AuthStacks; import org.omnifaces.security.jaspic.config.AuthStacksBuilder; import org.omnifaces.security.jaspic.factory.OmniAuthConfigProvider; /** * A set of utility methods for using the JASPIC API, specially in combination with * the OmniServerAuthModule. * <p> * Note that this contains various methods that assume being called from a JSF context. * * @author Arjan Tijms * */ public final class Jaspic { public static final String IS_AUTHENTICATION = "org.omnifaces.security.message.request.authentication"; public static final String IS_AUTHENTICATION_FROM_FILTER = "org.omnifaces.security.message.request.authenticationFromFilter"; public static final String IS_SECURE_RESPONSE = "org.omnifaces.security.message.request.secureResponse"; public static final String IS_REFRESH = "org.omnifaces.security.message.request.isRefresh"; public static final String DID_AUTHENTICATION = "org.omnifaces.security.message.request.didAuthentication"; public static final String AUTH_PARAMS = "org.omnifaces.security.message.request.authParams"; public static final String LOGGEDIN_USERNAME = "org.omnifaces.security.message.loggedin.username"; public static final String LOGGEDIN_ROLES = "org.omnifaces.security.message.loggedin.roles"; public static final String LAST_AUTH_STATUS = "org.omnifaces.security.message.authStatus"; public static final String CONTEXT_REGISTRATION_ID = "org.omnifaces.security.message.registrationId"; // Key in the MessageInfo Map that when present AND set to true indicated a protected resource is being accessed. // When the resource is not protected, GlassFish omits the key altogether. WebSphere does insert the key and sets // it to false. private static final String IS_MANDATORY = "javax.security.auth.message.MessagePolicy.isMandatory"; private static final String REGISTER_SESSION = "javax.servlet.http.registerSession"; private Jaspic() {} public static boolean authenticate(HttpServletRequest request, HttpServletResponse response, AuthParameters authParameters) { try { request.setAttribute(IS_AUTHENTICATION, true); if (authParameters != null) { request.setAttribute(AUTH_PARAMS, authParameters); } return request.authenticate(response); } catch (ServletException | IOException e) { throw new IllegalArgumentException(e); } finally { request.removeAttribute(IS_AUTHENTICATION); if (authParameters != null) { request.removeAttribute(AUTH_PARAMS); } } } public static boolean authenticateFromFilter(HttpServletRequest request, HttpServletResponse response) { try { request.setAttribute(IS_AUTHENTICATION_FROM_FILTER, true); return request.authenticate(response); } catch (ServletException e) { // Really problematic case, some servers (particularly JBoss Undertow since 1.1.0) throw a // ServletException when there's isn't actually an error, but just to indicate "nothing" has happened. return false; } catch (IOException e) { throw new IllegalArgumentException(e); } finally { request.removeAttribute(IS_AUTHENTICATION_FROM_FILTER); } } public static boolean refreshAuthentication(HttpServletRequest request, HttpServletResponse response, AuthParameters authParameters) { try { request.setAttribute(IS_REFRESH, true); // Doing an explicit logout is actually not really nice, as it has some side-effects that we need to counter // (like a SAM supporting remember-me clearing its remember-me cookie, etc). But there doesn't seem to be another // way in JASPIC request.logout(); return authenticate(request, response, authParameters); } catch (ServletException e) { throw new IllegalArgumentException(e); } finally { request.removeAttribute(IS_REFRESH); } } public static AuthParameters getAuthParameters(HttpServletRequest request) { AuthParameters authParameters = (AuthParameters) request.getAttribute(AUTH_PARAMS); if (authParameters == null) { authParameters = new AuthParameters(); } return authParameters; } public static void logout(HttpServletRequest request, HttpServletResponse response) { try { request.logout(); // Need to invalidate the session to really logout - request.logout only logs the user out for the *current request* // This is nearly always unwanted. Although the SAM's cleanSubject method can clear any session data too if needed, // invalidating the session is pretty much the safest way. request.getSession().invalidate(); } catch (ServletException e) { throw new IllegalArgumentException(e); } } public static AuthResult validateRequest(ServerAuthModule serverAuthModule, MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) { AuthResult authResult = new AuthResult(); try { AuthStatus status = serverAuthModule.validateRequest(messageInfo, clientSubject, serviceSubject); // TODO: not 100% sure about this; need mechanism for wrappers to abort the chain and signal to "do nothing" // TODO: use handler for "do nothing here"? if (status == null) { status = SUCCESS; } authResult.setAuthStatus(status); } catch (Exception exception) { authResult.setException(exception); } return authResult; } public static void cleanSubject(Subject subject) { if (subject != null) { subject.getPrincipals().clear(); } } public static boolean isRegisterSession(MessageInfo messageInfo) { return Boolean.valueOf((String)messageInfo.getMap().get(REGISTER_SESSION)); } public static boolean isProtectedResource(MessageInfo messageInfo) { return Boolean.valueOf((String) messageInfo.getMap().get(IS_MANDATORY)); } @SuppressWarnings("unchecked") public static void setRegisterSession(MessageInfo messageInfo, String username, List<String> roles) { messageInfo.getMap().put("javax.servlet.http.registerSession", TRUE.toString()); HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage(); request.setAttribute(LOGGEDIN_USERNAME, username); // TODO: check for existing roles and add request.setAttribute(LOGGEDIN_ROLES, roles); } public static boolean isAuthenticationRequest(HttpServletRequest request) { return TRUE.equals(request.getAttribute(IS_AUTHENTICATION)); } public static boolean isAuthenticationFromFilterRequest(HttpServletRequest request) { return TRUE.equals(request.getAttribute(IS_AUTHENTICATION_FROM_FILTER)); } public static boolean isSecureResponseRequest(HttpServletRequest request) { return TRUE.equals(request.getAttribute(IS_SECURE_RESPONSE)); } public static boolean isRefresh(HttpServletRequest request) { return TRUE.equals(request.getAttribute(IS_REFRESH)); } /** * Returns true if authorization was explicitly called for via this class (e.g. by calling {@link Jaspic#authenticate()}, * false if authorization was called automatically by the runtime at the start of the request or directly via e.g. * {@link HttpServletRequest#authenticate(HttpServletResponse)} * * @param request * @return true if authorization was initiated via this class, false otherwise */ public static boolean isExplicitAuthCall(HttpServletRequest request) { return isOneOf(TRUE, request.getAttribute(IS_AUTHENTICATION), request.getAttribute(IS_AUTHENTICATION_FROM_FILTER), request.getAttribute(IS_SECURE_RESPONSE) ); } public static void notifyContainerAboutLogin(Subject clientSubject, CallbackHandler handler, String username, List<String> roles) { try { // 1. Create a handler (kind of directive) to add the caller principal (AKA user principal =basically user name, or user id) that // the authenticator provides. // // This will be the name of the principal returned by e.g. HttpServletRequest#getUserPrincipal // // 2 Execute the handler right away // // This will typically eventually (NOT right away) add the provided principal in an application server specific way to the JAAS // Subject. // (it could become entries in a hash table inside the subject, or individual principles, or nested group principles etc.) handler.handle(new Callback[] { new CallerPrincipalCallback(clientSubject, username) }); if (!isEmpty(roles)) { // 1. Create a handler to add the groups (AKA roles) that the authenticator provides. // // This is what e.g. HttpServletRequest#isUserInRole and @RolesAllowed for // // 2. Execute the handler right away // // This will typically eventually (NOT right away) add the provided roles in an application server specific way to the JAAS // Subject. // (it could become entries in a hash table inside the subject, or individual principles, or nested group principles etc.) handler.handle(new Callback[] { new GroupPrincipalCallback(clientSubject, roles.toArray(new String[roles.size()])) }); } } catch (IOException | UnsupportedCallbackException e) { // Should not happen throw new IllegalStateException(e); } } public static void setLastStatus(HttpServletRequest request, AuthStatus status) { request.setAttribute(LAST_AUTH_STATUS, status); } public static AuthStatus getLastStatus(HttpServletRequest request) { return (AuthStatus) request.getAttribute(LAST_AUTH_STATUS); } /** * Should be called when the callback handler is used with the intention that an actual * user is going to be authenticated (as opposed to using the handler for the "do nothing" protocol * which uses the unauthenticated identity). * */ public static void setDidAuthentication(HttpServletRequest request) { request.setAttribute(DID_AUTHENTICATION, TRUE); } /** * Returns true if a SAM has indicated that it intended authentication to be happening during * the current request. * Does not necessarily mean that authentication has indeed succeeded, for this * the actual user/caller principal should be checked as well. * */ public static boolean isDidAuthentication(HttpServletRequest request) { return TRUE.equals(request.getAttribute(DID_AUTHENTICATION)); } public static boolean isDidAuthenticationAndSucceeded(HttpServletRequest request) { return TRUE.equals(request.getAttribute(DID_AUTHENTICATION)) && request.getUserPrincipal() != null; } /** * Gets the app context ID from the servlet context. * * <p> * The app context ID is the ID that JASPIC associates with the given application. * In this case that given application is the web application corresponding to the * ServletContext. * * @param context the servlet context for which to obtain the JASPIC app context ID * @return the app context ID for the web application corresponding to the given context */ public static String getAppContextID(ServletContext context) { return context.getVirtualServerName() + " " + context.getContextPath(); } /** * Registers a server auth module as the one and only module for the application corresponding to * the given servlet context. * * <p> * This will override any other modules that have already been registered, either via proprietary * means or using the standard API. * * @param serverAuthModule the server auth module to be registered * @param servletContext the context of the app for which the module is registered * @return A String identifier assigned by an underlying factory corresponding to an underlying factory-factory-factory registration */ public static String registerServerAuthModule(ServerAuthModule serverAuthModule, ServletContext servletContext) { AuthStacks stacks = new AuthStacksBuilder() .stack() .name(serverAuthModule.getClass().getSimpleName()) .setDefault() .module() .serverAuthModule(serverAuthModule) .controlFlag(REQUIRED) .add() .add() .build(); // Register the factory-factory-factory for the SAM String registrationId = AuthConfigFactory.getFactory().registerConfigProvider( new OmniAuthConfigProvider(stacks), "HttpServlet", getAppContextID(servletContext), "OmniSecurity authentication config provider" ); // Remember the registration ID returned by the factory, so we can unregister the JASPIC module when the web module // is undeployed. JASPIC being the low level API that it is won't do this automatically. servletContext.setAttribute(CONTEXT_REGISTRATION_ID, registrationId); return registrationId; } /** * Deregisters the server auth module (and encompassing wrappers/factories) that was previously registered via a call * to registerServerAuthModule. * * @param servletContext the context of the app for which the module is deregistered */ public static void deregisterServerAuthModule(ServletContext servletContext) { String registrationId = (String) servletContext.getAttribute(CONTEXT_REGISTRATION_ID); if (!isEmpty(registrationId)) { AuthConfigFactory.getFactory().removeRegistration(registrationId); } } // Couple of convenience methods for usage in JSF - may remove these as its too tightly coupled public static boolean authenticate() { return authenticate(getRequest(), getResponse(), null); } public static boolean authenticate(AuthParameters authParameters) { return authenticate(getRequest(), getResponse(), authParameters); } public static boolean refreshAuthentication(AuthParameters authParameters) { return refreshAuthentication(getRequest(), getResponse(), authParameters); } public static void logout() { logout(getRequest(), getResponse()); } // End Couple of convenience methods for usage in JSF - may remove these as its too tightly coupled public static HttpServletRequest getRequest() { return (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); } public static HttpServletResponse getResponse() { return (HttpServletResponse) FacesContext.getCurrentInstance().getExternalContext().getResponse(); } }