001/* 002 * Licensed under the Apache License, Version 2.0 (the "License"); 003 * you may not use this file except in compliance with the License. 004 * You may obtain a copy of the License at 005 * 006 * http://www.apache.org/licenses/LICENSE-2.0 007 * 008 * Unless required by applicable law or agreed to in writing, software 009 * distributed under the License is distributed on an "AS IS" BASIS, 010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 011 * See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014package org.gbif.ws.security; 015 016import org.gbif.ws.server.GbifHttpServletRequestWrapper; 017 018import java.net.URI; 019import java.util.Objects; 020import java.util.regex.Pattern; 021 022import org.apache.commons.lang3.StringUtils; 023import org.slf4j.Logger; 024import org.slf4j.LoggerFactory; 025import org.springframework.beans.factory.annotation.Autowired; 026import org.springframework.http.HttpHeaders; 027import org.springframework.stereotype.Service; 028 029import static org.gbif.ws.util.SecurityConstants.GBIF_SCHEME_PREFIX; 030import static org.gbif.ws.util.SecurityConstants.HEADER_CONTENT_MD5; 031import static org.gbif.ws.util.SecurityConstants.HEADER_GBIF_USER; 032import static org.gbif.ws.util.SecurityConstants.HEADER_ORIGINAL_REQUEST_METHOD; 033import static org.gbif.ws.util.SecurityConstants.HEADER_ORIGINAL_REQUEST_URL; 034 035/** 036 * The GBIF authentication scheme is modelled after the Amazon scheme on how to sign REST HTTP 037 * requests using a private key. It uses the standard HTTP Authorization header to transport the 038 * following information: Authorization: GBIF applicationKey:signature 039 * 040 * <p><br> 041 * The header starts with the authentication scheme (GBIF), followed by the plain applicationKey 042 * (the public key) and a unique signature for the very request which is generated using a fixed set 043 * of request attributes which are then encrypted by a standard HMAC-SHA1 algorithm. 044 * 045 * <p><br> 046 * A POST request with a GBIF header would look like this: 047 * 048 * <pre> 049 * POST /dataset HTTP/1.1 050 * Host: api.gbif.org 051 * Date: Mon, 26 Mar 2007 19:37:58 +0000 052 * x-gbif-user: trobertson 053 * Content-MD5: LiFThEP4Pj2TODQXa/oFPg== 054 * Authorization: GBIF gbif.portal:frJIUN8DYpKDtOLCwo//yllqDzg= 055 * </pre> 056 * 057 * <p>When signing an HTTP request in addition to the Authorization header some additional custom 058 * headers are added which are used to sign and digest the message. <br> x-gbif-user is added to 059 * transport a proxied user in which the application is acting. <br> Content-MD5 is added if a body 060 * entity exists. See Content-MD5 header specs: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.15 061 */ 062@Service 063public class GbifAuthServiceImpl implements GbifAuthService { 064 065 private static final Logger LOG = LoggerFactory.getLogger(GbifAuthServiceImpl.class); 066 067 private static final Pattern COLON_PATTERN = Pattern.compile(":"); 068 069 private final SigningService signingService; 070 private final Md5EncodeService md5EncodeService; 071 private final AppKeyProvider appKeyProvider; 072 073 public GbifAuthServiceImpl( 074 SigningService signingService, 075 Md5EncodeService md5EncodeService, 076 @Autowired(required = false) AppKeyProvider appKeyProvider) { 077 this.signingService = signingService; 078 this.md5EncodeService = md5EncodeService; 079 this.appKeyProvider = appKeyProvider; 080 } 081 082 @Override 083 public boolean isValidRequest(final GbifHttpServletRequestWrapper request) { 084 // parse auth header 085 final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); 086 if (StringUtils.isEmpty(authHeader) || !authHeader.startsWith(GBIF_SCHEME_PREFIX)) { 087 LOG.info("{} header is no GBIF scheme", HttpHeaders.AUTHORIZATION); 088 return false; 089 } 090 091 String[] values = COLON_PATTERN.split(authHeader.substring(5), 2); 092 if (values.length < 2) { 093 LOG.warn("Invalid syntax for application key and signature: {}", authHeader); 094 return false; 095 } 096 097 final String appKey = values[0]; 098 final String signatureFound = values[1]; 099 if (appKey == null || signatureFound == null) { 100 LOG.warn("Authentication header missing applicationKey or signature: {}", authHeader); 101 return false; 102 } 103 104 final RequestDataToSign requestDataToSign = buildRequestDataToSign(request); 105 LOG.debug("Request data to sign: {}", requestDataToSign.stringToSign()); 106 // sign 107 final String signature; 108 try { 109 signature = signingService.buildSignature(requestDataToSign, appKey); 110 } catch (PrivateKeyNotFoundException e) { 111 LOG.debug("Private key was not found for app key {}", appKey); 112 return false; 113 } 114 // compare signatures 115 if (signatureFound.equals(signature)) { 116 LOG.debug("Trusted application with matching signatures"); 117 return true; 118 } 119 LOG.info("Invalid signature: {}", authHeader); 120 121 return false; 122 } 123 124 private RequestDataToSign buildRequestDataToSign(final GbifHttpServletRequestWrapper request) { 125 final HttpHeaders headers = request.getHttpHeaders(); 126 final RequestDataToSign dataToSign = new RequestDataToSign(); 127 128 // custom header to keep the original method in the case the request is forwarded as the remote 129 // auth does 130 if (headers.containsKey(HEADER_ORIGINAL_REQUEST_METHOD)) { 131 dataToSign.setMethod(headers.getFirst(HEADER_ORIGINAL_REQUEST_METHOD)); 132 } else { 133 dataToSign.setMethod(request.getMethod()); 134 } 135 136 // custom header set by varnish overrides real URI 137 // see http://dev.gbif.org/issues/browse/GBIFCOM-137 138 if (headers.containsKey(HEADER_ORIGINAL_REQUEST_URL)) { 139 dataToSign.setUrl(headers.getFirst(HEADER_ORIGINAL_REQUEST_URL)); 140 } else { 141 dataToSign.setUrl(getCanonicalizedPath(request.getRequestURI())); 142 } 143 dataToSign.setContentType(headers.getFirst(HttpHeaders.CONTENT_TYPE)); 144 dataToSign.setContentTypeMd5(headers.getFirst(HEADER_CONTENT_MD5)); 145 dataToSign.setUser(headers.getFirst(HEADER_GBIF_USER)); 146 147 return dataToSign; 148 } 149 150 /** 151 * @return an absolute uri of the resource path alone, excluding host, scheme and query parameters 152 */ 153 private String getCanonicalizedPath(final String strUri) { 154 return URI.create(strUri).normalize().getPath(); 155 } 156 157 /** 158 * Signs a request by adding a Content-MD5 and Authorization header. For PUT/POST requests that 159 * contain a body entity the Content-MD5 header is created using the same JSON mapper for 160 * serialization as the clients use. 161 * 162 * <p>Other formats than JSON are not supported currently !!! 163 */ 164 @Override 165 public GbifHttpServletRequestWrapper signRequest( 166 final String username, final GbifHttpServletRequestWrapper request) { 167 String appKey = appKeyProvider.get(); 168 Objects.requireNonNull(appKey, "To sign the request a single application key is required"); 169 // first add custom GBIF headers so we can use them to build the string to sign 170 // the proxied username 171 request.getHttpHeaders().add(HEADER_GBIF_USER, username); 172 173 // the canonical path header 174 request 175 .getHttpHeaders() 176 .add(HEADER_ORIGINAL_REQUEST_URL, getCanonicalizedPath(request.getRequestURI())); 177 178 String content = null; 179 if (request.getContent() != null) { 180 content = request.getContent(); 181 } 182 183 // adds content md5 184 if (StringUtils.isNotEmpty(content)) { 185 request.getHttpHeaders().add(HEADER_CONTENT_MD5, md5EncodeService.encode(content)); 186 } 187 188 // build the unique request data object to sign 189 final RequestDataToSign requestDataToSign = buildRequestDataToSign(request); 190 191 // sign 192 final String signature; 193 try { 194 signature = signingService.buildSignature(requestDataToSign, appKey); 195 } catch (PrivateKeyNotFoundException e) { 196 LOG.warn("Skip signing request with unknown application key: {}", appKey); 197 return request; 198 } 199 200 // build authorization header string 201 final String header = buildAuthHeader(appKey, signature); 202 // add authorization header 203 LOG.debug( 204 "Adding authentication header to request {} for proxied user {} : {}", 205 request.getRequestURI(), 206 username, 207 header); 208 request.getHttpHeaders().add(HttpHeaders.AUTHORIZATION, header); 209 210 return request; 211 } 212 213 private static String buildAuthHeader(String applicationKey, String signature) { 214 return GBIF_SCHEME_PREFIX + applicationKey + ':' + signature; 215 } 216}