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}