org.sakaiproject.nakamura.auth.trusted.TrustedTokenServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.nakamura.auth.trusted.TrustedTokenServiceImpl.java

Source

/**
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF licenses this file
 * to you 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.sakaiproject.nakamura.auth.trusted;

import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.sakaiproject.nakamura.api.auth.trusted.TokenTrustValidator;
import org.sakaiproject.nakamura.api.auth.trusted.TrustedTokenService;
import org.sakaiproject.nakamura.api.auth.trusted.TrustedTokenTypes;
import org.sakaiproject.nakamura.api.cluster.ClusterTrackingService;
import org.sakaiproject.nakamura.api.memory.CacheManagerService;
import org.sakaiproject.nakamura.api.servlet.HttpOnlyCookie;
import org.sakaiproject.nakamura.auth.trusted.TokenStore.SecureCookie;
import org.sakaiproject.nakamura.auth.trusted.TokenStore.SecureCookieException;
import org.sakaiproject.nakamura.util.Signature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.jcr.Credentials;
import javax.jcr.SimpleCredentials;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 *
 */
@Component(immediate = true, metatype = true)
@Service
public final class TrustedTokenServiceImpl implements TrustedTokenService {

    private static final Logger LOG = LoggerFactory.getLogger(TrustedTokenServiceImpl.class);

    /** Property to invalidate if the session should be used. */
    @Property(boolValue = false)
    public static final String USE_SESSION = "sakai.auth.trusted.token.usesession";

    /** Property to indicate if only cookies should be secure */
    @Property(boolValue = false)
    public static final String SECURE_COOKIE = "sakai.auth.trusted.token.securecookie";

    /** Property to indicate the TTL on cookies */
    @Property(longValue = 1200000)
    public static final String TTL = "sakai.auth.trusted.token.ttl";

    /** Property to indicate the name of the cookie. */
    @Property(value = "sakai-trusted-authn")
    public static final String COOKIE_NAME = "sakai.auth.trusted.token.name";

    /** Property to point to keystore file */
    @Property(value = "sling/cookie-keystore.bin")
    public static final String TOKEN_FILE_NAME = "sakai.auth.trusted.token.storefile";

    /** Property to contain the shared secret used by all trusted servers */
    @Property(value = "default-setting-change-before-use")
    public static final String SERVER_TOKEN_SHARED_SECRET = "sakai.auth.trusted.server.secret";

    /** True if server tokens are enabled. */
    @Property(boolValue = true)
    public static final String SERVER_TOKEN_ENABLED = "sakai.auth.trusted.server.enabled";

    /** A list of all the known safe hosts to trust as servers */
    @Property(value = "localhost;127.0.0.1;0:0:0:0:0:0:0:1%0")
    public static final String SERVER_TOKEN_SAFE_HOSTS_ADDR = "sakai.auth.trusted.server.safe-hostsaddress";

    private static final String DEFAULT_WRAPPERS = "org.sakaiproject.nakamura.formauth.FormAuthenticationTokenServiceWrapper;org.sakaiproject.nakamura.auth.saml.SamlAuthenticationTokenServiceWrapper;org.sakaiproject.nakamura.auth.rest.RestAuthenticationTokenServiceWrapper;org.sakaiproject.nakamura.auth.cas.CasAuthenticationTokenServiceWrapper;org.sakaiproject.nakamura.http.usercontent.UserContentAuthenticationTokenServiceWrapper";
    @Property(value = DEFAULT_WRAPPERS)
    public static final String SERVER_TOKEN_SAFE_WRAPPERS = "sakai.auth.trusted.wrapper.class.names";

    @Property(value = "")
    public static final String TRUSTED_HEADER_NAME = "sakai.auth.trusted.header";

    @Property(value = "")
    public static final String TRUSTED_PARAMETER_NAME = "sakai.auth.trusted.request-parameter";

    /** A list of all the known safe hosts to trust for authentication purposes, ie front end proxies */
    @Property(value = "")
    public static final String TRUSTED_PROXY_SERVER_ADDR = "sakai.auth.trusted.server.safe-authentication-addresses";

    @Property(boolValue = false)
    public static final String DEBUG_COOKIES = "sakai.auth.trusted.token.debugcookies";

    /**
     * the name of the header to be trusted, if null or "" then don't trust headers.
     */
    private String trustedHeaderName;

    /**
     * the name of the parameter to be trusted, if null or "" then don't trust request parameters.
     */
    private String trustedParameterName;

    /**
     * set of trusted IP address to use as proxies.
     */
    private Set<String> trustedProxyServerAddrSet = new HashSet<String>(5);

    /**
     * If True, sessions will be used, if false cookies.
     */
    private boolean usingSession = false;

    /**
     * Should the cookies go over ssl.
     */
    private boolean secureCookie = false;

    /**
     * The name of the authN token.
     */
    private String trustedAuthCookieName;

    /**
     * An optional cookie server can be used in a cluster to centralize the management of
     * authN tokens. This is for situations where session storage and replication is not
     * desired, and session affinity can't be tolerated. Without this clients must come back
     * to the same host where they were authenticated as the cookie encode decode has
     * entropy associated with the instance of the server they are operating on.
     */
    @Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY)
    private ClusterCookieServer clusterCookieServer;

    private TokenStore tokenStore;

    private Long ttl;

    @Reference
    protected ClusterTrackingService clusterTrackingService;

    @Reference
    protected CacheManagerService cacheManager;

    @Reference
    protected EventAdmin eventAdmin;

    /**
     * If this is true the implementation is in test mode to enable external components to
     * test, without compromising the protection of the class.
     */
    private boolean testing = false;

    /**
     * Contains the calls made during testing.
     */
    private ArrayList<Object[]> calls;

    private String sharedSecret;

    private boolean trustedTokenEnabled;

    private Set<String> safeHostAddrSet = new HashSet<String>(16); // 16 way cluster is about as big as we will get.

    private String[] safeWrappers;

    private boolean debugCookies;

    private Map<String, TokenTrustValidator> registeredTypes = new ConcurrentHashMap<String, TokenTrustValidator>();

    /**
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws UnsupportedEncodingException
     * @throws IllegalStateException
     *
     */
    public TrustedTokenServiceImpl() throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException,
            UnsupportedEncodingException {
        tokenStore = new TokenStore();

    }

    @SuppressWarnings("rawtypes")
    protected void activate(ComponentContext context) {
        Dictionary props = context.getProperties();
        usingSession = PropertiesUtil.toBoolean(props.get(USE_SESSION), false);
        secureCookie = PropertiesUtil.toBoolean(props.get(SECURE_COOKIE), false);
        ttl = PropertiesUtil.toLong(props.get(TTL), 1200000);
        trustedAuthCookieName = PropertiesUtil.toString(props.get(COOKIE_NAME), "");
        sharedSecret = PropertiesUtil.toString(props.get(SERVER_TOKEN_SHARED_SECRET),
                "default-setting-change-before-use");
        trustedTokenEnabled = PropertiesUtil.toBoolean(props.get(SERVER_TOKEN_ENABLED), true);
        debugCookies = PropertiesUtil.toBoolean(props.get(DEBUG_COOKIES), false);
        tokenStore.setDebugCookies(debugCookies);
        String safeHostsAddr = PropertiesUtil.toString(props.get(SERVER_TOKEN_SAFE_HOSTS_ADDR), "");
        safeHostAddrSet.clear();
        if (safeHostsAddr != null) {
            for (String address : StringUtils.split(safeHostsAddr, ';')) {
                safeHostAddrSet.add(address);
            }
        }
        String trustedProxyServerAddr = PropertiesUtil.toString(props.get(TRUSTED_PROXY_SERVER_ADDR), "");
        trustedProxyServerAddrSet.clear();
        if (trustedProxyServerAddr != null) {
            for (String address : StringUtils.split(trustedProxyServerAddr, ';')) {
                trustedProxyServerAddrSet.add(address);
            }
        }
        String wrappers = PropertiesUtil.toString(props.get(SERVER_TOKEN_SAFE_WRAPPERS), "");
        if (wrappers == null || wrappers.length() == 0) {
            wrappers = DEFAULT_WRAPPERS;
        }
        safeWrappers = StringUtils.split(wrappers, ";");

        String tokenFile = PropertiesUtil.toString(props.get(TOKEN_FILE_NAME), "");
        String serverId = clusterTrackingService.getCurrentServerId();
        tokenStore.doInit(cacheManager, tokenFile, serverId, ttl);

        trustedHeaderName = PropertiesUtil.toString(props.get(TRUSTED_HEADER_NAME), "");
        trustedParameterName = PropertiesUtil.toString(props.get(TRUSTED_PARAMETER_NAME), "");
    }

    public void activateForTesting() {
        testing = true;
        calls = new ArrayList<Object[]>();
        safeWrappers = StringUtils.split(DEFAULT_WRAPPERS, ";");
    }

    /**
     * @return the calls used in testing.
     */
    public ArrayList<Object[]> getCalls() {
        return calls;
    }

    /**
     * Extract credentials from the request.
     *
     * @param req
     * @return credentials associated with the request.
     */
    public Credentials getCredentials(HttpServletRequest req, HttpServletResponse response) {
        if (testing) {
            calls.add(new Object[] { "getCredentials", req, response });
            return new SimpleCredentials("testing", "testing".toCharArray());
        }
        Credentials cred = null;
        String userId = null;
        String sakaiTrustedHeader = req.getHeader("x-sakai-token");
        if (trustedTokenEnabled && sakaiTrustedHeader != null && sakaiTrustedHeader.trim().length() > 0) {
            String host = req.getRemoteAddr();
            if (!safeHostAddrSet.contains(host)) {
                LOG.warn("Ignoring Trusted Token request from {} ", host);
            } else {
                // we have a HMAC based token, we should see if it is valid against the key we
                // have
                // and if so create some credentials.
                String[] parts = sakaiTrustedHeader.split(";");
                if (parts.length == 3) {
                    try {
                        String hash = parts[0];
                        String user = parts[1];
                        String timestamp = parts[2];
                        String hmac = Signature.calculateRFC2104HMAC(user + ";" + timestamp, sharedSecret);
                        if (hmac.equals(hash)) {
                            // the user is Ok, we will trust it.
                            userId = user;
                            cred = createCredentials(userId, TrustedTokenTypes.TRUSTED_TOKEN);
                        } else {
                            LOG.debug("HMAC Match Failed {} != {} ", hmac, hash);
                        }
                    } catch (SignatureException e) {
                        LOG.warn("Failed to validate server token : {} {} ", sakaiTrustedHeader, e.getMessage());
                    }
                } else {
                    LOG.warn("Illegal number of elements in trusted server token:{} {}  ", sakaiTrustedHeader,
                            parts.length);
                }
            }
        }
        if (userId == null) {
            if (usingSession) {
                HttpSession session = req.getSession(false);
                if (session != null) {
                    Credentials testCredentials = (Credentials) session.getAttribute(SA_AUTHENTICATION_CREDENTIALS);
                    if (testCredentials instanceof SimpleCredentials) {
                        SimpleCredentials sc = (SimpleCredentials) testCredentials;
                        Object o = sc.getAttribute(CA_AUTHENTICATION_USER);
                        if (o instanceof TrustedUser) {
                            TrustedUser tu = (TrustedUser) o;
                            if (tu.getUser() != null) {
                                userId = tu.getUser();
                                cred = testCredentials;
                            }
                        }
                    }
                } else {
                    cred = null;
                }
            } else {
                Cookie[] cookies = req.getCookies();
                if (cookies != null) {
                    for (Cookie c : cookies) {
                        if (trustedAuthCookieName.equals(c.getName())) {
                            if (secureCookie && !c.getSecure()) {
                                continue;
                            }
                            String cookieValue = c.getValue();
                            String[] decodedToken = decodeCookie(c.getValue());
                            if (decodedToken != null) {
                                userId = decodedToken[0];
                                String tokenType = decodedToken[1];
                                TokenTrustValidator ttv = registeredTypes.get(tokenType);
                                if (ttv == null || ttv.isTrusted(req)) {
                                    LOG.debug("Token is valid and decoded to {} ", userId);
                                    cred = createCredentials(userId, tokenType);
                                    refreshToken(response, c.getValue(), userId, tokenType);
                                    break;
                                } else {
                                    LOG.debug("Cookie cant be trusted for this request {} ", cookieValue);
                                }
                            } else {
                                LOG.debug("Invalid Cookie {} ", cookieValue);
                                clearCookie(response);
                            }
                        }
                    }
                }
            }
        }
        if (userId != null) {
            LOG.debug("Trusted Authentication for {} with credentials {}  ", userId, cred);
        }

        return cred;
    }

    /**
     * Remove credentials so that subsequent request don't contain credentials.
     *
     * @param request
     * @param response
     */
    public void dropCredentials(HttpServletRequest request, HttpServletResponse response) {
        if (testing) {
            calls.add(new Object[] { "dropCredentials", request, response });
            return;
        }
        if (usingSession) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                session.setAttribute(SA_AUTHENTICATION_CREDENTIALS, null);
            }
        } else {
            clearCookie(response);
        }
    }

    /**
     * Inject a token into the request/response, this assumes htat the getUserPrincipal() of the request
     * or the request.getRemoteUser() contain valid user ID's from which to generate the request.
     *
     *
     * @param req
     * @param resp
     * @param readOnlyToken if true, the session or cookie will only allow read only operations in the server.
     */
    public String injectToken(HttpServletRequest request, HttpServletResponse response, String tokenType,
            UserValidator userValidator) {
        if (testing) {
            calls.add(new Object[] { "injectToken", request, response });
            return "testing";
        }
        String userId = null;
        String remoteAddress = request.getRemoteAddr();
        if (trustedProxyServerAddrSet.contains(remoteAddress)) {
            if (trustedHeaderName.length() > 0) {
                userId = request.getHeader(trustedHeaderName);
                if (userId != null) {
                    LOG.debug("Injecting Trusted Token from request: Header [{}] indicated user was [{}] ", 0,
                            userId);
                }
            }
            if (userId == null && trustedParameterName.length() > 0) {
                userId = request.getParameter(trustedParameterName);
                if (userId != null) {
                    LOG.debug("Injecting Trusted Token from request: Parameter [{}] indicated user was [{}] ",
                            trustedParameterName, userId);
                }
            }
        }
        if (userId == null) {
            Principal p = request.getUserPrincipal();
            if (p != null) {
                userId = p.getName();
                if (userId != null) {
                    LOG.debug("Injecting Trusted Token from request: User Principal indicated user was [{}] ",
                            userId);
                }
            }
        }
        if (userId == null) {
            userId = request.getRemoteUser();
            if (userId != null) {
                LOG.debug("Injecting Trusted Token from request: Remote User indicated user was [{}] ", userId);
            }
        }

        if (userValidator != null) {
            userId = userValidator.validate(userId);
        }
        if (userId != null) {
            if (usingSession) {
                HttpSession session = request.getSession(true);
                if (session != null) {
                    LOG.debug("Injecting Credentials into Session for " + userId);
                    session.setAttribute(SA_AUTHENTICATION_CREDENTIALS, createCredentials(userId, tokenType));
                }
            } else {
                addCookie(response, userId, tokenType);
            }
            Dictionary<String, Object> eventDictionary = new Hashtable<String, Object>();
            eventDictionary.put(TrustedTokenService.EVENT_USER_ID, userId);

            // send an async event to indicate that the user has been trusted, things that want to create users can hook into this.
            eventAdmin.sendEvent(new Event(TrustedTokenService.TRUST_USER_TOPIC, eventDictionary));
            return userId;
        } else {
            LOG.warn("Unable to inject token; unable to determine user from request.");
        }
        return null;
    }

    /**
     * @param userId
     * @param response
     */
    void addCookie(HttpServletResponse response, String userId, String tokenType) {
        Cookie c = new HttpOnlyCookie(trustedAuthCookieName, encodeCookie(userId, tokenType));
        c.setMaxAge(-1);
        c.setPath("/");
        c.setSecure(secureCookie);
        response.addCookie(c);
        // rfc 2109 section 4.5. stop http 1.1 caches caching the response
        response.addHeader("Cache-Control", "no-cache=\"set-cookie\" ");
        // and stop http 1.0 caches caching the response
        response.addDateHeader("Expires", 0);
    }

    /**
     * @param response
     */
    void clearCookie(HttpServletResponse response) {
        Cookie c = new HttpOnlyCookie(trustedAuthCookieName, "");
        c.setMaxAge(0);
        c.setPath("/");
        c.setSecure(secureCookie);
        response.addCookie(c);
    }

    /**
     * Refresh the token, assumes that the cookie is valid.
     *
     * @param req
     * @param value
     * @param userId
     * @param tokenType 
     */
    void refreshToken(HttpServletResponse response, String value, String userId, String tokenType) {
        String[] parts = StringUtils.split(value, "@");
        if (parts != null && parts.length == 5) {
            long cookieTime = Long.parseLong(parts[1].substring(1));
            if (System.currentTimeMillis() + (ttl / 2) > cookieTime) {
                if (debugCookies) {
                    LOG.info("Refreshing Token for {} cookieTime {} ttl {} CurrentTime {} ",
                            new Object[] { userId, cookieTime, ttl, System.currentTimeMillis() });
                }
                addCookie(response, userId, tokenType);
            }
        }

    }

    /**
     * Encode the user ID in a secure cookie.
     *
     * @param userId
     * @return
     */
    String encodeCookie(String userId, String tokenType) {
        if (userId == null) {
            return null;
        }
        if (clusterCookieServer != null) {
            return clusterCookieServer.encodeCookie(userId);
        } else {
            long expires = System.currentTimeMillis() + ttl;
            SecureCookie secretKeyHolder = tokenStore.getActiveToken();

            try {
                return secretKeyHolder.encode(expires, userId, tokenType);
            } catch (NoSuchAlgorithmException e) {
                LOG.error(e.getMessage(), e);
            } catch (InvalidKeyException e) {
                LOG.error(e.getMessage(), e);
            } catch (IllegalStateException e) {
                LOG.error(e.getMessage(), e);
            } catch (UnsupportedEncodingException e) {
                LOG.error(e.getMessage(), e);
            } catch (SecureCookieException e) {
                if (e.isError()) {
                    LOG.error(e.getMessage(), e);
                } else {
                    LOG.info(e.getMessage(), e);
                }
            }
            return null;
        }
    }

    /**
     * Decode the user ID.
     *
     * @param value
     * @return
     */
    String[] decodeCookie(String value) {
        if (value == null) {
            return null;
        }
        if (clusterCookieServer != null) {
            return clusterCookieServer.decodeCookie(value);
        } else {
            try {
                SecureCookie secureCookie = tokenStore.getSecureCookie();
                return secureCookie.decode(value);
            } catch (SecureCookieException e) {
                if (e.isError()) {
                    LOG.error(e.getMessage());
                } else {
                    LOG.info(e.getMessage());
                }
            }
        }
        return null;

    }

    /**
     * Create credentials from a validated userId.
     *
     * @param req
     *          The request to sniff for a user.
     * @return
     */
    private Credentials createCredentials(String userId, String tokenType) {
        SimpleCredentials sc = new SimpleCredentials(userId, new char[0]);
        TrustedUser user = new TrustedUser(userId);
        sc.setAttribute(CA_AUTHENTICATION_USER, user);
        sc.setAttribute(CA_AUTHENTICATION_ATTRIBUTES, tokenType);
        return sc;
    }

    /**
     * "Trusted" inner class for passing the user on to the authentication handler.<br/>
     * <br/>
     * By being a static, inner class with a private constructor, it is harder for an
     * external source to inject into the authentication chain.
     */
    static final class TrustedUser {
        private final String user;

        /**
         * Constructor.
         *
         * @param user
         *          The user to represent.
         */
        private TrustedUser(String user) {
            this.user = user;
        }

        /**
         * Get the user that is being represented.
         *
         * @return The represented user.
         */
        String getUser() {
            return user;
        }
    }

    /**
     * @return
     */
    public String[] getAuthorizedWrappers() {
        return safeWrappers;
    }

    public void registerType(String type, TokenTrustValidator tokenTrustValidator) {
        registeredTypes.put(type, tokenTrustValidator);
    }

    public void deregisterType(String type, TokenTrustValidator tokenTrustValidator) {
        TokenTrustValidator ttv = registeredTypes.get(type);
        if (tokenTrustValidator.equals(ttv)) {
            registeredTypes.remove(type);
        }
    }

}