All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.api.auth.Authenticator Maven / Gradle / Ivy

/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.api.auth;

import com.google.api.AuthProvider;
import com.google.api.Authentication;
import com.google.api.Service;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.util.Clock;
import com.google.api.client.util.Maps;
import com.google.api.config.ServiceConfigFetcher;
import com.google.api.control.model.MethodRegistry.AuthInfo;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.HttpHeaders;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.NumericDate;
import org.jose4j.jwt.ReservedClaimNames;

/**
 * An authenticator that extracts the auth token from the HTTP request and
 * constructs a {@link UserInfo} object based on the claims contained in the
 * auth token.
 *
 * @author [email protected]
 *
 */
public class Authenticator {

  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  private static final String ACCESS_TOKEN_PARAM_NAME = "access_token";
  private static final String BEARER_TOKEN_PREFIX = "Bearer ";
  private static final String EMAIL_CLAIM_NAME = "email";

  private final AuthTokenDecoder authTokenDecoder;
  private final Clock clock;
  private final Map issuersToProviderIds;

  /**
   * Constructor.
   *
   * @param authTokenDecoder decodes auth tokens into {@link UserInfo} objects.
   * @param clock provides the time.
   * @param issuersToProviderIds maps from issuers to provider IDs.
   */
  @VisibleForTesting
  Authenticator(
      AuthTokenDecoder authTokenDecoder,
      Clock clock,
      Map issuersToProviderIds) {
    this.authTokenDecoder = authTokenDecoder;
    this.clock = clock;
    this.issuersToProviderIds = issuersToProviderIds;
  }

  /**
   * Authenticate the current HTTP request.
   *
   * @param httpServletRequest is the incoming HTTP request object.
   * @param authInfo contains authentication configurations of the API method being called.
   * @param serviceName is the name of this service.
   * @return a constructed {@link UserInfo} object representing the identity of the caller.
   */
  public UserInfo authenticate(
      HttpServletRequest httpServletRequest,
      AuthInfo authInfo,
      String serviceName) {

    Preconditions.checkNotNull(httpServletRequest);
    Preconditions.checkNotNull(authInfo);

    Optional maybeAuthToken = extractAuthToken(httpServletRequest);
    if (!maybeAuthToken.isPresent()) {
      throw new UnauthenticatedException(
          "No auth token is contained in the HTTP request");
    }

    JwtClaims jwtClaims = this.authTokenDecoder.decode(maybeAuthToken.get());
    UserInfo userInfo = toUserInfo(jwtClaims);
    String issuer = userInfo.getIssuer();

    if (!this.issuersToProviderIds.containsKey(issuer)) {
      throw new UnauthenticatedException("Unknown issuer: " + issuer);
    }
    String providerId = this.issuersToProviderIds.get(issuer);

    // Check whether the provider id is allowed.
    if (!authInfo.isProviderIdAllowed(providerId)) {
      String message = "The requested method does not allowed this provider id: " + providerId;
      throw new UnauthenticatedException(message);
    }

    checkJwtClaims(jwtClaims);

    // Check the audiences decoded from the auth token. The auth token is allowed when
    // 1) an audience is equal to the service name,
    // or 2) at least one audience is allowed in the method configuration.
    Set audiences = userInfo.getAudiences();
    boolean hasServiceName = audiences.contains(serviceName);
    Set allowedAudiences = authInfo.getAudiencesForProvider(providerId);
    if (!hasServiceName && Sets.intersection(audiences, allowedAudiences).isEmpty()) {
      throw new UnauthenticatedException("Audiences not allowed");
    }

    return userInfo;
  }

  // Check whether the JWT claims should be accepted.
  private void checkJwtClaims(JwtClaims jwtClaims) {
    Optional expiration = getDateClaim(ReservedClaimNames.EXPIRATION_TIME, jwtClaims);
    if (!expiration.isPresent()) {
      throw new UnauthenticatedException("Missing expiration field");
    }
    Optional notBefore = getDateClaim(ReservedClaimNames.NOT_BEFORE, jwtClaims);

    NumericDate currentTime = NumericDate.fromMilliseconds(clock.currentTimeMillis());
    if (expiration.get().isBefore(currentTime)) {
      throw new UnauthenticatedException("The auth token has already expired");
    }

    if (notBefore.isPresent() && notBefore.get().isAfter(currentTime)) {
      String message = "Current time is earlier than the \"nbf\" time";
      throw new UnauthenticatedException(message);
    }
  }

  /**
   * Create an instance of {@link Authenticator} using the service configuration fetched from Google
   * Service Management APIs.
   *
   * @return an {@code Authenticator}
   * @throws java.lang.IllegalArgumentException if the authentication message is not defined in the
   *         fetched service config.
   */
  public static Authenticator create() {
    ServiceConfigFetcher fetcher = ServiceConfigFetcher.create();
    Service service = fetcher.fetch();
    if (!service.hasAuthentication()) {
      throw new IllegalArgumentException("Authentication is not defined in service config");
    }
    return create(service.getAuthentication(), Clock.SYSTEM);
  }

  @VisibleForTesting
  static Authenticator create(Authentication authentication, Clock clock) {
    List providersList = authentication.getProvidersList();
    if (providersList == null || providersList.isEmpty()) {
      throw new IllegalArgumentException("No auth providers are defined in the config.");
    }
    Map issuerKeyConfigs = generateIssuerKeyConfig(providersList);

    Map issuersToProviderIds = Maps.newHashMap();
    for (AuthProvider authProvider : providersList) {
      issuersToProviderIds.put(authProvider.getIssuer(), authProvider.getId());
    }

    HttpRequestFactory httpRequestFactory = new NetHttpTransport().createRequestFactory();
    KeyUriSupplier defaultKeyUriSupplier =
        new DefaultKeyUriSupplier(httpRequestFactory, issuerKeyConfigs);
    JwksSupplier jwksSupplier = new DefaultJwksSupplier(httpRequestFactory, defaultKeyUriSupplier);
    JwksSupplier cachingJwksSupplier = new CachingJwksSupplier(jwksSupplier);
    AuthTokenVerifier authTokenVerifier = new DefaultAuthTokenVerifier(cachingJwksSupplier);
    AuthTokenDecoder authTokenDecoder = new DefaultAuthTokenDecoder(authTokenVerifier);
    AuthTokenDecoder cachingAuthTokenDecoder = new CachingAuthTokenDecoder(authTokenDecoder);

    return new Authenticator(
        cachingAuthTokenDecoder,
        clock,
        ImmutableMap.copyOf(issuersToProviderIds));
  }

  private static Map generateIssuerKeyConfig(
      List authProviders) {

    ImmutableMap.Builder issuerConfigBuilder = ImmutableMap.builder();
    Set issuers = Sets.newHashSet();
    for (AuthProvider authProvider : authProviders) {
      String issuer = authProvider.getIssuer();
      if (Strings.isNullOrEmpty(issuer)) {
        logger.atWarning().log("The 'issuer' field is not set in AuthProvider (%s)", authProvider);
        continue;
      }

      if (issuers.contains(issuer)) {
        throw new IllegalArgumentException(
            "Configuration contains multiple auth provider for the same issuer: " + issuer);
      }
      issuers.add(issuer);

      String jwksUri = authProvider.getJwksUri();
      IssuerKeyUrlConfig config = Strings.isNullOrEmpty(jwksUri)
          ? new IssuerKeyUrlConfig(true, Optional.absent())
          : new IssuerKeyUrlConfig(false, Optional.of(new GenericUrl(jwksUri)));
      issuerConfigBuilder.put(issuer, config);
    }
    return issuerConfigBuilder.build();
  }

  private static Optional getDateClaim(String claimName, JwtClaims jwtClaims) {
    try {
      NumericDate dateClaim = jwtClaims.getNumericDateClaimValue(claimName);
      return Optional.fromNullable(dateClaim);
    } catch (MalformedClaimException exception) {
      String message = String.format("The \"%s\" claim is malformed", claimName);
      throw new UnauthenticatedException(message);
    }
  }

  private static Optional extractAuthToken(HttpServletRequest request) {
    String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
    if (authHeader != null) {
      // When the authorization header is present, extract the token from the
      // header.
      if (authHeader.startsWith(BEARER_TOKEN_PREFIX)) {
        return Optional.of(authHeader.substring(BEARER_TOKEN_PREFIX.length()));
      }
      return Optional.absent();
    }

    String accessToken = request.getParameter(ACCESS_TOKEN_PARAM_NAME);
    if (accessToken != null) {
      return Optional.of(accessToken);
    }

    return Optional.absent();
  }

  private static UserInfo toUserInfo(JwtClaims jwtClaims) {
    try {
      List audiences = jwtClaims.getAudience();
      if (audiences == null || audiences.isEmpty()) {
        throw new UnauthenticatedException("Missing audience field");
      }

      String email = jwtClaims.getClaimValue(EMAIL_CLAIM_NAME, String.class);

      String subject = jwtClaims.getSubject();
      if (subject == null) {
        throw new UnauthenticatedException("Missing subject field");
      }

      String issuer = jwtClaims.getIssuer();
      if (issuer == null) {
        throw new UnauthenticatedException("Missing issuer field");
      }

      return new UserInfo(audiences, email, subject, issuer);
    } catch (MalformedClaimException exception) {
      throw new UnauthenticatedException("Cannot read malformed claim", exception);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy