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}