GateIn authentication proposal

Version 2

    Status

     

    Initial design

     

     

    Introduction

     

    Current GateIn authentication

     

    Authentication in GateIn 3.2 is based on JAAS and by default it's standard J2EE FORM based authentication. However authentication workflow is not so easy and straightforward, also because we support many different authentication use cases. Result of successful authentication is:

    1. JAAS Subject created during JAAS authentication with login modules
    2. Identity object bound into IdentityRegistry, which encapsulates username, groups and memberships of particular user.

     

    Authentication can be triggered by accessing private URL declared in web.xml in security-constraints part, or by showing login dialog where user can add his username and password. By default we have FORM based authentication with RememberMe feature. We support other authentication types with various SSO solutions (CAS, JOSSO, OpenSSO, SPNEGO).

     

    Currently authentication related code is spread among more different components:

    • WCI - It contains WCILoginModule and WCILoginController and API for saving WCI tickets. WCI is used mainly because for abstracting login workflow for FORM based login, which is different for Servlet 3.0 servers (Here we can programmaticaly call HttpServletRequest.login() method) and for 2.5 servers (here we need to redirect into URL http://localhost:8080/portla/j_security_check?j_username=john&j_password=wci-ticket-456 to enforce JAAS)

     

    • exo.core.component.security - Some parts are in exo.core module (The whole former security API with Identity, IdentityRegistry, ConversationStateRegistry etc. and the login modules for integration with this API)

     

    • SSO - module which contains code for integration with SSO authentication solutions

     

    • Portal - Some parts are directly in portal codebase, mainly in modules component/web/security and component/identity .

     

    Potential improvements

    It should be mainly about remove all painpoints in current authentication. Namely:

    • Introduce separate component for authentication instead of having the code spread among many various components
    • Authentication API, which will simplify authentication process and it will allow to simply use different types of authentication (Form, RememberMe, cluster login, SSO,OpenID, ...).
      It should also simplify things from portal administrator perspective. It can be seen in  reference guide that current SSO integration is quite pain as portal administrators need to edit really big number of various files and do steps, which are obviously workarounds.

     

     

     

    Specification

    Authentication component

     

    • It will be good to have majority of things related to authentication in one place, instead of having them spread in different modules.
      There was initial idea to use WCI module for handle authentication. I don't think that this module is ideal candidate as it's name (Web Container Integration) means that it's main purpose is to unify access to portal from various types of application servers. And this is not directly related to authentication. Also WCI is quite isolated and don't have dependency on kernel, which is required, as you may need to obtain instances of various components and inject them in your authentication classes.

     

    Authentication API

     

    What I am thinking about, is having standard Authentication API/SPI, which will allow to easily change and intercept authentication process according to various needs. My proposal is:

     

    1) AuthenticationContext - Introduce AuthenticationContext as object, which will encapsulate whole state of current user authentication including credentials, tokens and other things (Username, password, value of RememberMe cookie, SSO tokens, initialURI to redirect after authentication...). Point of this object is to wrap all authentication related data like credentials, urls, tokens etc. and store them between various redirects, which are usually needed for various kinds of authentication.

     

    2) Authentication interceptors - Set of authentication interceptors, where each interceptor can be used to deal with some aspect of authentication. API can look like this

     

    public interface AuthenticationInterceptor
    {
    
        public void invoke(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context);
    }
    

     

    NOTE: We can use standard HTTP filters, but interceptors will allow us to have AuthenticationContext directly in particular interceptor without need to obtain it in some alternative way, like ThreadLocal variable. Alternatively we can inject AuthenticationContext via CDI @Inject annotation if CDI support will be available and allow it.

     

    Examples of interceptors:

    • RememberMeInterceptor to deal with RememberMe cookie,
    • UsernamePasswordInterceptor, which will redirect user to login screen if his credentials are not already found in AuthenticationContext
    • CASSSOInterceptor, which will redirect user to CAS (or other SSO server) if CAS ticket not found, or it will perform validation of SSO ticket if CAS ticket is found
    • JAASRedirectInterceptor to redirect to JAAS login when all informations from HttpRequest are collected and AuthenticationContext is properly set. This interceptor should be probably close to end of interceptor chain and it should redirect to WCI login, which will actually choose the way for redirection of JAAS through Servlet 3.0 or Servlet 2.5 flow
    • InitialURIInterceptor - to redirect to initialURI after authentication is finished

    ...

     

    Interceptor chain can be configured via eXo kernel configuration file with possibility to disable some interceptors (like CASSSOInterceptor won't be needed if CAS is not used, or RememberMeInterceptor won't be needed if clients don't want RememberMe feature to be enabled). Alternatively will be good if administrators have possibility to configure authentication flow from UI to make it as simple as possible.

    There should be Http filter AuthenticationDelegateFilter for authentication, which will create AuthenticationContext and then delegate invocation to interceptors.

     

    3) Interface Authenticator with signature like:

     

    public interface Authenticator
    {  
       public AuthenticationStatus authenticate(AuthenticationContext authContext);
    }
    

     

    where AuthenticationStatus can be enum indicating in which stage authentication process is:

     

    public enum AuthenticationStatus 
    {
      SUCCESS, FAILURE, DEFERRED
    }
    

     

    We will have various authenticators for various usecases. Like RememberMeAuthenticator for deal with RememberMe authentication, UsernamePasswordAuthenticator for deal with traditional username/password authentication, OpenIDAuthenticator to deal with OpenID etc. Authenticators will be invoked by the corresponding interceptors (like RememberMeAuthenticator will be likely invoked by RememberMeInterceptor). Responsibility of Authenticator will be to:

    • retrive relevant credentials, attributes, tokens from AuthenticationContext
    • verify correctness of these credentials and attributes (real authentication)
    • set AuthenticationStatus (or possibly other state attributes) into AuthenticationContext and return this AuthenticationStatus

     

    We assume that during call of method "authenticate" will be AuthenticationStatus with the result of authentication process saved as attribute into AuthenticationContext. So teoretically we don't need public AuthenticationStatus authenticate(AuthenticationContext authContext) but we can end with public void authenticate(AuthenticationContext authContext) . I would recomment to use signature with AuthenticationStatus as result because caller will likely always need to deal with AuthenticationStatus object.

     

    4) End of interceptor chain

    At the end of the interceptor chain should be either:

    • continue with filter chain if login is not required for this HTTP request, and it's not possible to obtain informations to automatically trigger authentication (RememberMe cookie is not found, SSO tickets are not presented etc.)
    • invocation of WCI login if AuthenticationContext is properly set, which will in next turn invoke programmatic login with servlet API or redirection to j_security_check URL in case of Servlet 2.5 to invoke JAAS redirection. I am assuming that most of the things will be moved out from WCI (WCILoginModule, WCILoginController, GenericAuthentication...) and only the DefaultServletContainer.login and ServletContainerContext.login will remain (same for logout where we still need to keep some parts in WCI to enforce crossContextLogout from all HTTP sessions, as value of JSESSIONID cookie is shared in portal environment).
    • very first HTTP request after successful JAAS authentication will have special meaning as it needs to perform some post-authentication actions, which are usually required. Examples of some actions will be:
      • set RememberMe cookie if user selected that he wants RememberMe
      • invoke AuthenticationListeners after successful Authentication
      • redirect to initialURI, which is original URL which initiates authentication process (This task may be performed by InitialURIInterceptor, which will be last interceptor in the chain)

     

    Authenticator is the key component, which is doing the real authentication. So JAAS login (login modules) will have only symbolic function and it's purpose will be only to correctly setup authentication in context of Application Server and save Identity object to IdentityRegistry, like it is now.

     

    5) JAAS Login Modules

    JAAS authentications is invoked from WCI. Note that "real" authentication is already finished at this stage. So JAAS login will have only symbolic function to:

    1. Check in AuthenticationContext that login has been correctly performed and context is correctly established ( AuthenticationContext.getAuthenticationStatus() == SUCCESS )
    2. Establish JAAS subject and assign roles to him.
    3. Save Identity into IdentityRegistry.

    In this case j_username will be name of user and j_password will be key of AuthenticationContext of current user in AuthenticationRegistry. So AuthenticationRegistry will be single shared component between all clients, which will be storage of AuthenticationContext objects for those clients. Key of AuthenticationContext in AuthenticationRegistry can be ID of HttpSession of current client. We should still support JAAS login with real password of client, like used by current DefaultLoginModule class. It is needed for some usecases like Crash login.

     

    6) Authentication listeners

    Also will be good and useful for people to have listeners, which will be able to listen to various events related to authentication (PreAuthenticationEvent, PostAuthenticationEvent, PreLogoutListener, PostLogoutListener).
    Currently we have AuthenticationListener introduced in WCI, but I don't think that it's usable as listeners are executed after JAAS authentication in case of Servlet 3.0 authentication but before JAAS authentication in case of Servlet 2.5. It's because 3.0 is using programmatic authentication of HttpServletRequest but Servlet 2.5 requires redirection to JAAS login URL (localhost:8080/j_security_check?j_username=root&j_password=wci-ticket-46465464849). And these WCI listeners are not executed at all in case of SSO or cluster login workflows, as these workflows omit to login through WCI.

     

    Use Case examples

     

    1. Basic username/password case

    Let's assume basic case with classic login form. We won't configure anything special. So chain of authenticator interceptors can look as follows:

     

    • LoginRequiredInterceptor
    • RememberMeInterceptor
    • UsernamePasswordInterceptor
    • WCIRedirectInterceptor
    • AuthenticationListenersInterceptor
    • InitialUriInterceptor

     

    1.1: User starts his session by going to http://localhost:8080/portal/classic

    • AuthenticationDelegateFilter declared in web.xml will invoke AuthenticationRegistry.getAuthenticationContext(String sessionId) to obtain AuthenticationContext of current client. This is new client so AuthenticationContext is not found and we will create one and save it into AuthenticationRegistry .
    • AuthenticationDelegateFilter will forward request to chain of interceptors
    • LoginRequiredInterceptor will try to look if current HttpServletRequest requires authentication. It can recognize it from set of secure URI (/private, /login, /dologin ...) or from having some special attribute in HttpSession (So portal clients or individual portlets can initiate authentication by adding some attribute into HttpSession which may be quite cool feature). LoginRequiredInterceptor will recognize that this particular request don't require authentication, so it will set AuthenticationContext.setLoginRequired(false).
      NOTE: Chain continues as sometimes user can still be automatically authenticated even if current request don't require authentication (case with RememberMe cookie or with valid SSO token etc.)
    • RememberMeInterceptor will try to look if RememberMe cookie exists but it doesn't exist. Forwarding request
      NOTE: If it exists, it will try to perform RememberMe login even if authentication is not required for this request. In this case user will be logged automatically.
    • UsernamePasswordInterceptor will try to look if it's login request (request like: http://localhost:8080/portal/login?username=root&password=gtn ) but it's not. Then it founds that AuthenticationContext.isLoginRequired() is false. So simply forwarding this request
    • WCIRedirectInterceptor, AuthenticationListenersInterceptor, InitialUriInterceptor - nothing to do. Forwarding request to rest of servlet filter chain

     

    1.2: Now user goes to http://localhost:8080/portal/private/classic

    Workflow will differ from previous in this:

    • LoginRequiredInterceptor will now recognize that login is required. It will set AuthenticationContext.setLoginRequired(true)
    • UsernamePasswordInterceptor will recognize that AuthenticationContext.isLoginRequired() is true. So it will call redirecting to login form ( httpResponse.sendRedirect("/portal/jsp/login.jsp") )
    • Before real redirection to login form will interceptor chain continue, so InitialURIInterceptor will save AuthenticationContext.setInitialURI("/portal/private/classic")
    • At the end of interceptor chain, will AuthenticationDelegateFilter recognize that redirection has been called (either from HttpServletResponse.isCommitted() == true or we can also introduce some flag on AuthenticationContext to really have everything on AuthenticationContext and not depend on other objects) and it will finish this HttpServletRequest and return so that redirection to login form can be performed.

     

    1.3: User submits login form http://localhost:8080/portal/login?username=john&password=gtn

    • UsernamePasswordInterceptor will recognize that it's login request. It will call: authenticationContext.setUsername("john") and authenticationContext.setPassword("gtn") and then calling usernamePasswordAuthenticator.authenticate(authenticationContext)
    • UsernamePasswordAuthenticator will read username and password from provided AuthenticationContext and it will perform real authentication (usage of former OrganizationAuthenticatorImpl from exo.core). Authentication is successful, so calling AuthenticationContext.setAuthenticatioStatus(SUCCESS) and returning this status
    • WCIRedirectInterceptor will recognize that AuthenticationContext is successfuly established with SUCCESS AuthenticationResult. So calling WCI login: DefaultServletContainer.login(request, response);
    • JAAS login will be performed successfuly and redirecting again to chain (in case of Servlet 2.5) or continue with current chain (in case of Servlet 3.0)

     

    1.4: First request after redirection: http://localhost:8080/portal/private/classic

    • AuthentiationListenersInterceptor will call listeners after successful authentication
    • InitialURIInterceptor will send redirection to initialURI if needed (different from current URI) but it's not the case now

     

    2. OpenSSO Login example

    In this case we have interceptors like:

    • LoginRequiredInterceptor
    • OpenSSOInterceptor
    • WCIRedirectInterceptor
    • AuthenticationListenersInterceptor
    • InitialUriInterceptor

     

    2.1: User will send request to secure uri: http://localhost:8080/portal/private/classic

    • LoginRequiredInterceptor will recognize that it's private URI and set AuthenticationContext.setLoginRequired(true)
    • OpenSSOInterceptor will try to find SSO token (in OpenSSO, it's saved in cookie iplanetDirectoryPro). Token not found, so calling redirection to OpenSSO console.
    • InitialURIInterceptor will save initialURI to AuthenticationContext (similar case like basic form workflow)

     

    2.2: OpenSSO redirects user back to portal with SSO token set: http://localhost:8080/portal/initiatessologin

    • OpenSSOInterceptor will find SSO token in request. It will save it into AuthenticationContext and then call OpenSSOAuthenticator.authenticate(authenticationContext)
    • OpenSSOAuthenticator will do verification of SSO token by communicating with OpenSSO server via REST api. It will return AuthenticationStatus.SUCCESS and save it into AuthenticationContext if SSO token verification is successful

     

    2.x: Now the workflow continues through WCILogin in same way like with basic form case.

    TODO

     

    Logout

     

    I think that logout workflow should also go through the chain of interceptors. Question is if reuse same chain of interceptors or use different chain for logout (or alternatively invoke different method for logout on each interceptor). It will again be quite clear and each interceptor will have single responsibility to perform.

     

    Examples:

    • RememberMeInterceptor - to clear RememberMe cookie from HttpServletResponse
    • OpenSSOLogoutInterceptor - to redirect to SSO console and enforce SSO logout
    • WCILogoutInterceptor - calling WCI logout (and in next turn JAAS logout)
    • InitialURIInterceptor - redirection to correct URI after logout is finished

    etc.

     

    Configuration

    Question is how exactly configuration should look. Maybe user can directly edit chain of interceptors. Alternatively user won't directly edit chain of interceptors but it will be created automatically according to configured properties from property file, UI or XML configuration. Examples:

    - Property authentication.sso.cas.enabled=true will automatically add CASSSOInterceptor into chain and remove UsernamePasswordInterceptor

    - Property authentication.rememberme.enabled=false will automatcically remove RememberMeInterceptor from the chain

    etc.

     

     

    Conclusion

     

    Overally I think that authentication API seems to me more like a long-term task as proposed changes require redesign of current stuff, and it requires moving stuff into separate authentication component instead of having it spread among many other components. It will also require adaption from other products like WCM. With respect to this, I really can't say when/if are these changes applicable. Please note that nothing said in current proposal is final. Everything is in only suggestion and can be subject to change or subject for discussion.