001/*
002 * (C) Copyright 2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.jwt;
020
021import static java.lang.Boolean.FALSE;
022import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION;
023import static org.nuxeo.ecm.jwt.JWTClaims.CLAIM_AUDIENCE;
024import static org.nuxeo.ecm.jwt.JWTClaims.CLAIM_SUBJECT;
025
026import java.util.List;
027import java.util.Map;
028
029import javax.servlet.http.HttpServletRequest;
030import javax.servlet.http.HttpServletResponse;
031
032import org.apache.commons.lang3.StringUtils;
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
036import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin;
037import org.nuxeo.runtime.api.Framework;
038
039/**
040 * JSON Web Token (JWT) Authentication Plugin.
041 * <p>
042 * The Authorization Bearer token from the headers is checked with the {@link JWTService} for validity, and if it is
043 * valid the authentication is done for the token's subject.
044 * <p>
045 * If an "aud" claim ({@link JWTClaims#CLAIM_AUDIENCE}) is present in the token, it must be a prefix of the request HTTP
046 * path info (excluding the web context). This allows limiting tokens for specific URL patterns.
047 *
048 * @since 10.3
049 */
050public class JWTAuthenticator implements NuxeoAuthenticationPlugin {
051
052    private static final Log log = LogFactory.getLog(JWTAuthenticator.class);
053
054    protected static final String BEARER_SP = "Bearer ";
055
056    protected static final String ACCESS_TOKEN = "access_token";
057
058    @Override
059    public void initPlugin(Map<String, String> parameters) {
060        // nothing to init
061    }
062
063    @Override
064    public List<String> getUnAuthenticatedURLPrefix() {
065        return null; // NOSONAR
066    }
067
068    @Override
069    public Boolean needLoginPrompt(HttpServletRequest httpRequest) {
070        return FALSE;
071    }
072
073    @Override
074    public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) {
075        return FALSE;
076    }
077
078    @Override
079    public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest request, HttpServletResponse response) {
080        String token = retrieveToken(request);
081        if (token == null) {
082            log.trace("No JWT token");
083            return null;
084        }
085        JWTService service = Framework.getService(JWTService.class);
086        Map<String, Object> claims = service.verifyToken(token);
087        if (claims == null) {
088            log.trace("JWT token invalid");
089            return null;
090        }
091        Object sub = claims.get(CLAIM_SUBJECT);
092        if (!(sub instanceof String)) {
093            log.trace("JWT token contains non-String subject claim");
094            return null;
095        }
096        String username = (String) sub;
097        if (log.isTraceEnabled()) {
098            log.trace("JWT token valid for username: " + username);
099        }
100        // check Audience
101        Object aud = claims.get(CLAIM_AUDIENCE);
102        if (aud != null) {
103            if (!(aud instanceof String)) {
104                log.trace("JWT token contains non-String audience claim");
105                return null;
106            }
107            String audience = StringUtils.strip((String) aud, "/");
108            String path = getRequestPath(request);
109            if (!isEqualOrPathPrefix(path, audience)) {
110                if (log.isTraceEnabled()) {
111                    log.trace("JWT token for audience: " + audience + " but used with path: " + path);
112                }
113                return null;
114            }
115        }
116        return new UserIdentificationInfo(username);
117    }
118
119    protected String retrieveToken(HttpServletRequest request) {
120        String auth = request.getHeader(AUTHORIZATION);
121        if (auth == null) {
122            String token = request.getParameter(ACCESS_TOKEN);
123            if (StringUtils.isNotEmpty(token)) {
124                log.trace("Access token available from URI");
125                return token;
126            }
127            log.trace("No Authorization header or URI access token");
128        } else if (auth.startsWith(BEARER_SP)) {
129            String token = auth.substring(BEARER_SP.length()).trim();
130            if (!token.isEmpty()) {
131                log.trace("Bearer token available");
132                return token;
133            }
134            log.trace("Bearer token empty");
135        } else {
136            log.trace("Authorization header without Bearer token");
137        }
138        return null;
139    }
140
141    /**
142     * Gets the request path. The returned value never starts nor ends with a slash.
143     */
144    protected static String getRequestPath(HttpServletRequest request) {
145        String path = request.getServletPath(); // use decoded and normalized servlet path
146        String info = request.getPathInfo();
147        if (info != null) {
148            path = path + info;
149        }
150        if (!path.isEmpty()) {
151            path = path.substring(1); // strip initial /
152        }
153        return path;
154    }
155
156    /**
157     * Compares path-wise a path with a prefix.
158     */
159    protected static boolean isEqualOrPathPrefix(String path, String prefix) {
160        return path.equals(prefix) || path.startsWith(prefix + '/');
161    }
162
163}