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.api.model.common.GbifUser; 017import org.gbif.api.service.common.IdentityAccessService; 018import org.gbif.ws.WebApplicationException; 019import org.gbif.ws.server.GbifHttpServletRequestWrapper; 020 021import java.nio.charset.StandardCharsets; 022import java.security.Principal; 023import java.util.Base64; 024import java.util.List; 025import java.util.Objects; 026import java.util.UUID; 027import java.util.regex.Pattern; 028import java.util.stream.Collectors; 029 030import javax.annotation.Nullable; 031import javax.servlet.http.HttpServletRequest; 032import javax.validation.constraints.NotNull; 033 034import org.apache.commons.lang3.StringUtils; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037import org.springframework.http.HttpHeaders; 038import org.springframework.http.HttpStatus; 039import org.springframework.security.core.authority.SimpleGrantedAuthority; 040import org.springframework.stereotype.Component; 041 042import static org.gbif.ws.util.SecurityConstants.BASIC_AUTH; 043import static org.gbif.ws.util.SecurityConstants.BASIC_SCHEME_PREFIX; 044import static org.gbif.ws.util.SecurityConstants.GBIF_SCHEME; 045import static org.gbif.ws.util.SecurityConstants.GBIF_SCHEME_PREFIX; 046import static org.gbif.ws.util.SecurityConstants.HEADER_GBIF_USER; 047 048@Component 049public class GbifAuthenticationManagerImpl implements GbifAuthenticationManager { 050 051 private static final Logger LOG = LoggerFactory.getLogger(GbifAuthenticationManagerImpl.class); 052 053 private static final Pattern COLON_PATTERN = Pattern.compile(":"); 054 055 private final IdentityAccessService identityAccessService; 056 private final GbifAuthService authService; 057 058 /** 059 * In case {@link GbifAuthService} is not provided, this class will reject all authentications 060 * on the GBIF scheme prefix. 061 */ 062 public GbifAuthenticationManagerImpl( 063 @NotNull IdentityAccessService identityAccessService, @Nullable GbifAuthService authService) { 064 Objects.requireNonNull(identityAccessService, "identityAccessService shall be provided"); 065 this.identityAccessService = identityAccessService; 066 this.authService = authService; 067 } 068 069 /** 070 * Authenticate a provided request. 071 * There are two authentication types here: GBIF and Basic. 072 */ 073 @Override 074 public GbifAuthentication authenticate(final HttpServletRequest request) { 075 // Extract authentication credentials 076 final String authentication = request.getHeader(HttpHeaders.AUTHORIZATION); 077 078 if (authentication != null) { 079 if (authentication.startsWith(BASIC_SCHEME_PREFIX)) { 080 return basicAuthentication(authentication.substring(BASIC_SCHEME_PREFIX.length())); 081 } else if (authentication.startsWith(GBIF_SCHEME_PREFIX)) { 082 return gbifAuthentication(request); 083 } 084 } 085 return getAnonymous(); 086 } 087 088 /** 089 * Basic authentication (when the Authorization header scheme is 'BASIC'). 090 */ 091 private GbifAuthentication basicAuthentication(final String authentication) { 092 // As specified in RFC 7617, the auth header (if not ASCII) is in UTF-8. 093 byte[] decodedAuthentication = Base64.getDecoder().decode(authentication); 094 String[] values = 095 COLON_PATTERN.split(new String(decodedAuthentication, StandardCharsets.UTF_8), 2); 096 if (values.length < 2) { 097 LOG.warn("Invalid syntax for username and password: {}", authentication); 098 throw new WebApplicationException( 099 "Invalid syntax for username and password", HttpStatus.BAD_REQUEST); 100 } 101 102 String username = values[0]; 103 String password = values[1]; 104 if (username == null || password == null) { 105 LOG.warn("Missing basic authentication username or password: {}", authentication); 106 throw new WebApplicationException( 107 "Missing basic authentication username or password", HttpStatus.BAD_REQUEST); 108 } 109 110 // it's not a good approach to check UUID 111 // ignore usernames which are UUIDs - these are registry legacy IPT calls and handled by a 112 // special security filter 113 try { 114 UUID.fromString(username); 115 return getAnonymous(); 116 } catch (IllegalArgumentException e) { 117 // no UUID, continue with regular drupal authentication 118 } 119 120 GbifUser user = identityAccessService.authenticate(username, password); 121 if (user == null) { 122 throw new WebApplicationException( 123 "Failed to authenticate user " + username, HttpStatus.UNAUTHORIZED); 124 } 125 126 LOG.debug("Authenticating user {} via scheme {}", username, BASIC_AUTH); 127 return getAuthenticated(user, BASIC_AUTH); 128 } 129 130 /** 131 * GBIF authentication (when the Authorization header scheme is 'GBIF'). 132 */ 133 private GbifAuthentication gbifAuthentication(final HttpServletRequest request) { 134 String username = request.getHeader(HEADER_GBIF_USER); 135 if (StringUtils.isEmpty(username)) { 136 LOG.warn("Missing gbif username header {}", HEADER_GBIF_USER); 137 throw new WebApplicationException("Missing gbif username header", HttpStatus.BAD_REQUEST); 138 } 139 if (authService == null) { 140 LOG.warn("Missing GBIF Authentication Service"); 141 throw new WebApplicationException( 142 "Missing GBIF Authentication Service", HttpStatus.UNAUTHORIZED); 143 } 144 GbifHttpServletRequestWrapper requestObject = 145 request instanceof GbifHttpServletRequestWrapper 146 ? ((GbifHttpServletRequestWrapper) request) 147 : new GbifHttpServletRequestWrapper(request, false); 148 if (!authService.isValidRequest(requestObject)) { 149 LOG.warn("Invalid GBIF authenticated request"); 150 throw new WebApplicationException( 151 "Invalid GBIF authenticated request", HttpStatus.UNAUTHORIZED); 152 } 153 154 LOG.debug("Authenticating user {} via scheme {}", username, GBIF_SCHEME); 155 156 // check if we have a request that impersonates a user 157 GbifUser user = identityAccessService.get(username); 158 // Note: using an Anonymous Authorizer is probably not the best thing to do here 159 // we should consider simply return null to let another filter handle it 160 return user == null ? getAnonymous() : getAuthenticated(user, GBIF_SCHEME); 161 } 162 163 /** 164 * Get an anonymous user, it does not have {@link Principal}. 165 * 166 * @return authentication object for the anonymous user 167 */ 168 private GbifAuthentication getAnonymous() { 169 return GbifAuthenticationToken.anonymous(); 170 } 171 172 /** 173 * Construct GbifAuthentication by parameter. 174 * 175 * @param user user which has to be authenticated 176 * @param authenticationScheme authentication scheme (BASIC, GBIF etc.) 177 * @return authentication object for this user 178 */ 179 private GbifAuthentication getAuthenticated( 180 final GbifUser user, final String authenticationScheme) { 181 final List<SimpleGrantedAuthority> authorities = 182 user.getRoles().stream() 183 .map(Enum::name) 184 .map(SimpleGrantedAuthority::new) 185 .collect(Collectors.toList()); 186 187 return new GbifAuthenticationToken( 188 new GbifUserPrincipal(user), authenticationScheme, authorities); 189 } 190}