// Copyright 2017 The Nomulus Authors. 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 google.registry.request.auth;

import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static google.registry.request.auth.AuthLevel.NONE;
import static google.registry.request.auth.AuthLevel.USER;

import com.google.appengine.api.oauth.OAuthRequestException;
import com.google.appengine.api.oauth.OAuthService;
import com.google.appengine.api.oauth.OAuthServiceFailureException;
import com.google.appengine.api.users.User;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import google.registry.config.RegistryConfig.Config;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;

/**
 * OAuth authentication mechanism, using the OAuthService interface.
 *
 * Only OAuth version 2 is supported.
 */
public class OAuthAuthenticationMechanism implements AuthenticationMechanism {

  private static final String BEARER_PREFIX = "Bearer ";

  private final OAuthService oauthService;

  /** The available OAuth scopes for which {@link OAuthService} should check. */
  private final ImmutableSet<String> availableOauthScopes;

  /** The OAuth scopes which must all be present for authentication to succeed. */
  private final ImmutableSet<String> requiredOauthScopes;

  private final ImmutableSet<String> allowedOauthClientIds;

  @VisibleForTesting
  @Inject
  public OAuthAuthenticationMechanism(
      OAuthService oauthService,
      @Config("availableOauthScopes") ImmutableSet<String> availableOauthScopes,
      @Config("requiredOauthScopes") ImmutableSet<String> requiredOauthScopes,
      @Config("allowedOauthClientIds") ImmutableSet<String> allowedOauthClientIds) {
    this.oauthService = oauthService;
    this.availableOauthScopes = availableOauthScopes;
    this.requiredOauthScopes = requiredOauthScopes;
    this.allowedOauthClientIds = allowedOauthClientIds;
  }

  @Override
  public AuthResult authenticate(HttpServletRequest request) {

    // Make sure that there is an Authorization header in Bearer form. OAuthService also accepts
    // tokens in the request body and URL string, but we should not use those, since they are more
    // likely to be logged than the Authorization header. Checking to make sure there's a token also
    // avoids unnecessary RPCs, since OAuthService itself does not check whether the header is
    // present. In theory, there could be more than one Authorization header, but we only check the
    // first one, because there's not a legitimate use case for having more than one, and
    // OAuthService itself only looks at the first one anyway.
    String header = request.getHeader(AUTHORIZATION);
    if ((header == null) || !header.startsWith(BEARER_PREFIX)) {
      return AuthResult.create(NONE);
    }
    // Assume that, if a bearer token is found, it's what OAuthService will use to attempt
    // authentication. This is not technically guaranteed by the contract of OAuthService; see
    // OAuthTokenInfo for more information.
    String rawAccessToken = header.substring(BEARER_PREFIX.length());

    // Get the OAuth information. The various oauthService method calls use a single cached
    // authentication result, we can call them one by one. Unfortunately, the calls have a
    // single-scope form, and a varargs scope list form, but no way to call them with a collection
    // of scopes, so it will not be easy to configure multiple scopes.
    User currentUser;
    boolean isUserAdmin;
    String clientId;
    ImmutableSet<String> authorizedScopes;
    try {
      currentUser = oauthService.getCurrentUser(availableOauthScopes.toArray(new String[0]));
      isUserAdmin = oauthService.isUserAdmin(availableOauthScopes.toArray(new String[0]));
      clientId = oauthService.getClientId(availableOauthScopes.toArray(new String[0]));
      authorizedScopes = ImmutableSet
          .copyOf(oauthService.getAuthorizedScopes(availableOauthScopes.toArray(new String[0])));
    } catch (OAuthRequestException | OAuthServiceFailureException e) {
      return AuthResult.create(NONE);
    }
    if ((currentUser == null) || (clientId == null) || (authorizedScopes == null)) {
      return AuthResult.create(NONE);
    }

    // Make sure that the client ID matches, to avoid a confused deputy attack; see:
    // http://stackoverflow.com/a/17439317/1179226
    if (!allowedOauthClientIds.contains(clientId)) {
      return AuthResult.create(NONE);
    }

    // Make sure that all required scopes are present.
    if (!authorizedScopes.containsAll(requiredOauthScopes)) {
      return AuthResult.create(NONE);
    }

    // Create the {@link AuthResult}, including the OAuth token info.
    return AuthResult.create(
        USER,
        UserAuthInfo.create(
            currentUser,
            isUserAdmin,
            OAuthTokenInfo.create(
                ImmutableSet.copyOf(authorizedScopes),
                clientId,
                rawAccessToken)));
  }
}

@@ -1214,16 +1214,62 @@
 Config;%0A
+import google.registry.util.FormattingLogger;%0A
 import j
@@ -1288,16 +1288,16 @@
 Inject;%0A
-
 import j
@@ -1591,16 +1591,110 @@
 rer %22;%0A%0A
+  private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();%0A%0A
   privat
@@ -3462,24 +3462,89 @@
 _PREFIX)) %7B%0A
+      logger.infofmt(%22missing or invalid authorization header%22);%0A
       return
@@ -4531,24 +4531,121 @@
 tring%5B0%5D));%0A
+      logger.infofmt(%22current user: %25s (%25s)%22, currentUser, isUserAdmin ? %22admin%22 : %22not admin%22);%0A
       client
@@ -4716,24 +4716,73 @@
 tring%5B0%5D));%0A
+      logger.infofmt(%22client ID: %25s%22, clientId);%0A
       author
@@ -4901,24 +4901,91 @@
 ring%5B0%5D)));%0A
+      logger.infofmt(%22authorized scope(s): %25s%22, authorizedScopes);%0A
     %7D catch 
@@ -5039,24 +5039,84 @@
 eption e) %7B%0A
+      logger.infofmt(e, %22unable to get OAuth information%22);%0A
       return
@@ -5462,24 +5462,71 @@
 lientId)) %7B%0A
+      logger.info(%22client ID is not allowed%22);%0A
       return
@@ -5613,16 +5613,16 @@
 resent.%0A
-
     if (
@@ -5671,24 +5671,72 @@
 hScopes)) %7B%0A
+      logger.info(%22required scope(s) missing%22);%0A
       return

Add logging to OAuth authentication mechanism