diff --git a/java/google/registry/request/auth/AppEngineInternalAuthenticationMechanism.java b/java/google/registry/request/auth/AppEngineInternalAuthenticationMechanism.java
new file mode 100644
index 000000000..0750cb215
--- /dev/null
+++ b/java/google/registry/request/auth/AppEngineInternalAuthenticationMechanism.java
@@ -0,0 +1,67 @@
+// 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 google.registry.request.auth.AuthLevel.APP;
+import static google.registry.request.auth.AuthLevel.NONE;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Authentication mechanism which uses the X-AppEngine-QueueName header set by App Engine for
+ * internal requests.
+ *
+ *
+ * Task queue push task requests set this header value to the actual queue name. Cron requests set
+ * this header value to __cron, since that's actually the name of the hidden queue used for cron
+ * requests. Cron also sets the header X-AppEngine-Cron, which we could check, but it's simpler just
+ * to check the one.
+ *
+ *
+ * App Engine allows app admins to set these headers for testing purposes. This means that this auth
+ * method is somewhat unreliable - any app admin can access any internal endpoint and pretend to be
+ * the app itself by setting these headers, which would circumvent any finer-grained authorization
+ * if we added it in the future (assuming we did not apply it to the app itself). And App Engine's
+ * concept of an "admin" includes all project owners, editors and viewers. So anyone with access to
+ * the project will be able to access anything the app itself can access.
+ *
+ *
+ * For now, it's probably okay to allow this behavior, especially since it could indeed be
+ * convenient for testing. If we wanted to revisit this decision in the future, we have a couple
+ * options for locking this down:
+ *
+ *
+ * - 1. Always include the result of UserService.getCurrentUser() as the active user
+ * - 2. Validate that the requests came from special AppEngine internal IPs
+ *
+ *
+ * See task
+ * handler request header documentation
+ */
+public class AppEngineInternalAuthenticationMechanism implements AuthenticationMechanism {
+
+ // As defined in the App Engine request header documentation.
+ private static final String QUEUE_NAME_HEADER = "X-AppEngine-QueueName";
+
+ @Override
+ public AuthResult authenticate(HttpServletRequest request) {
+ if (request.getHeader(QUEUE_NAME_HEADER) == null) {
+ return AuthResult.create(NONE);
+ } else {
+ return AuthResult.create(APP);
+ }
+ }
+}
diff --git a/java/google/registry/request/auth/Auth.java b/java/google/registry/request/auth/Auth.java
new file mode 100644
index 000000000..f1ed4b941
--- /dev/null
+++ b/java/google/registry/request/auth/Auth.java
@@ -0,0 +1,67 @@
+// 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 java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Annotation used to configure authentication settings for Actions. */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Auth {
+
+ /** Available methods for authentication. */
+ public enum AuthMethod {
+
+ /** App Engine internal authentication. Must always be provided as the first method. */
+ INTERNAL,
+
+ /** Authentication methods suitable for API-style access, such as OAuth 2. */
+ API,
+
+ /** Legacy authentication using cookie-based App Engine Users API. Must come last if present. */
+ LEGACY
+ }
+
+ /** User authorization policy options. */
+ public enum UserPolicy {
+
+ /** This action ignores end users; the only configured auth method must be INTERNAL. */
+ IGNORED,
+
+ /** No user policy is enforced; anyone can access this action. */
+ PUBLIC,
+
+ /**
+ * If there is a user, it must be an admin, as determined by isUserAdmin().
+ *
+ *
Note that, according to App Engine, anybody with access to the app in the GCP Console,
+ * including editors and viewers, is an admin.
+ */
+ ADMIN
+ }
+
+ /** Enabled authentication methods for this action. */
+ AuthMethod[] methods() default { AuthMethod.INTERNAL };
+
+ /** Required minimum level of authentication for this action. */
+ // TODO(mountford) This should probably default to APP eventually.
+ AuthLevel minimumLevel() default AuthLevel.NONE;
+
+ /** Required user authorization policy for this action. */
+ UserPolicy userPolicy() default UserPolicy.IGNORED;
+}
diff --git a/java/google/registry/request/auth/AuthLevel.java b/java/google/registry/request/auth/AuthLevel.java
new file mode 100644
index 000000000..45ddd2457
--- /dev/null
+++ b/java/google/registry/request/auth/AuthLevel.java
@@ -0,0 +1,52 @@
+// 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;
+
+/**
+ * Authentication level.
+ *
+ *
Used by {@link Auth} to specify what authentication is required, and by {@link AuthResult})
+ * to specify what authentication was found. These are a series of levels, from least to most
+ * authentication required. The lowest level of requirement, NONE, can be satisfied by any level
+ * of authentication, while the highest level, USER, can only be satisfied by the authentication of
+ * a specific user. The level returned may be higher than what was required, if more authentication
+ * turns out to be possible. For instance, if an authenticated user is found, USER will be returned
+ * even if no authentication was required.
+ */
+public enum AuthLevel {
+
+ /** No authentication was required/found. */
+ NONE,
+
+ /**
+ * Authentication required, but user not required.
+ *
+ *
In Auth: Authentication is required, but app-internal authentication (which isn't associated
+ * with a specific user) is permitted.
+ *
+ *
In AuthResult: App-internal authentication was successful.
+ */
+ APP,
+
+ /**
+ * Authentication required, user required.
+ *
+ *
In Auth: Authentication is required, and app-internal authentication is forbidden, meaning
+ * that a valid authentication result will contain specific user information.
+ *
+ *
In AuthResult: A valid user was authenticated.
+ */
+ USER
+}
diff --git a/java/google/registry/request/auth/AuthModule.java b/java/google/registry/request/auth/AuthModule.java
new file mode 100644
index 000000000..ee8eef21e
--- /dev/null
+++ b/java/google/registry/request/auth/AuthModule.java
@@ -0,0 +1,61 @@
+// 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 com.google.appengine.api.oauth.OAuthService;
+import com.google.appengine.api.oauth.OAuthServiceFactory;
+import com.google.appengine.api.users.UserService;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import dagger.Module;
+import dagger.Provides;
+import google.registry.config.RegistryConfig.Config;
+
+/**
+ * Dagger module for authentication routines.
+ */
+@Module
+public class AuthModule {
+
+ /** Provides the internal authentication mechanism. */
+ @Provides
+ AppEngineInternalAuthenticationMechanism provideAppEngineInternalAuthenticationMechanism() {
+ return new AppEngineInternalAuthenticationMechanism();
+ }
+
+ /** Provides the custom authentication mechanisms (including OAuth). */
+ @Provides
+ ImmutableList provideApiAuthenticationMechanisms(
+ OAuthService oauthService,
+ @Config("availableOauthScopes") ImmutableSet availableOauthScopes,
+ @Config("requiredOauthScopes") ImmutableSet requiredOauthScopes,
+ @Config("allowedOauthClientIds") ImmutableSet allowedOauthClientIds) {
+ return ImmutableList.of(
+ new OAuthAuthenticationMechanism(
+ oauthService, availableOauthScopes, requiredOauthScopes, allowedOauthClientIds));
+ }
+
+ /** Provides the legacy authentication mechanism. */
+ @Provides
+ LegacyAuthenticationMechanism provideLegacyAuthenticationMechanism(UserService userService) {
+ return new LegacyAuthenticationMechanism(userService);
+ }
+
+ /** Provides the OAuthService instance. */
+ @Provides
+ OAuthService provideOauthService() {
+ return OAuthServiceFactory.getOAuthService();
+ }
+}
diff --git a/java/google/registry/request/auth/AuthResult.java b/java/google/registry/request/auth/AuthResult.java
new file mode 100644
index 000000000..23029a8ab
--- /dev/null
+++ b/java/google/registry/request/auth/AuthResult.java
@@ -0,0 +1,61 @@
+// 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.base.Preconditions.checkNotNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import javax.annotation.Nullable;
+
+/**
+ * Results of authentication for a given HTTP request, as emitted by an
+ * {@link AuthenticationMechanism}.
+ */
+@AutoValue
+public abstract class AuthResult {
+
+ public abstract AuthLevel authLevel();
+
+ /** Information about the authenticated user, if there is one. */
+ public abstract Optional userAuthInfo();
+
+ public boolean isAuthenticated() {
+ return authLevel() != AuthLevel.NONE;
+ }
+
+ static AuthResult create(AuthLevel authLevel) {
+ return new AutoValue_AuthResult(authLevel, Optional.absent());
+ }
+
+ static AuthResult create(AuthLevel authLevel, @Nullable UserAuthInfo userAuthInfo) {
+ if (authLevel == AuthLevel.USER) {
+ checkNotNull(userAuthInfo);
+ }
+ return new AutoValue_AuthResult(authLevel, Optional.fromNullable(userAuthInfo));
+ }
+
+ /**
+ * No authentication was made.
+ *
+ * In the authentication step, this means that none of the configured authentication methods
+ * were able to authenticate the user. But the authorization settings may be such that it's
+ * perfectly fine not to be authenticated. The {@link RequestAuthenticator#authorize} method
+ * returns NOT_AUTHENTICATED in this case, as opposed to absent() if authentication failed and was
+ * required. So as a return from an authorization check, this can be treated as a success.
+ */
+ public static final AuthResult NOT_AUTHENTICATED =
+ AuthResult.create(AuthLevel.NONE);
+}
diff --git a/java/google/registry/request/auth/AuthenticationMechanism.java b/java/google/registry/request/auth/AuthenticationMechanism.java
new file mode 100644
index 000000000..8427c102e
--- /dev/null
+++ b/java/google/registry/request/auth/AuthenticationMechanism.java
@@ -0,0 +1,34 @@
+// 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 javax.servlet.http.HttpServletRequest;
+
+/**
+ * A particular way to authenticate an HTTP request, returning an {@link AuthResult}.
+ *
+ *
For instance, a request could be authenticated using OAuth, via special request headers, etc.
+ */
+public interface AuthenticationMechanism {
+
+ /**
+ * Attempt to authenticate an incoming request.
+ *
+ * @param request the request to be authenticated
+ * @return the results of the authentication check; if the request could not be authenticated,
+ * the mechanism should return AuthResult.NOT_AUTHENTICATED
+ */
+ AuthResult authenticate(HttpServletRequest request);
+}
diff --git a/java/google/registry/request/auth/BUILD b/java/google/registry/request/auth/BUILD
new file mode 100644
index 000000000..fb701cb33
--- /dev/null
+++ b/java/google/registry/request/auth/BUILD
@@ -0,0 +1,20 @@
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+licenses(["notice"]) # Apache 2.0
+
+java_library(
+ name = "auth",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/google/registry/config",
+ "//java/google/registry/util",
+ "@com_google_appengine_api_1_0_sdk",
+ "@com_google_auto_value",
+ "@com_google_code_findbugs_jsr305",
+ "@com_google_dagger",
+ "@com_google_guava",
+ "@javax_servlet_api",
+ ],
+)
diff --git a/java/google/registry/request/auth/LegacyAuthenticationMechanism.java b/java/google/registry/request/auth/LegacyAuthenticationMechanism.java
new file mode 100644
index 000000000..5f6259b0f
--- /dev/null
+++ b/java/google/registry/request/auth/LegacyAuthenticationMechanism.java
@@ -0,0 +1,51 @@
+// 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 google.registry.request.auth.AuthLevel.NONE;
+import static google.registry.request.auth.AuthLevel.USER;
+
+import com.google.appengine.api.users.UserService;
+import com.google.common.annotations.VisibleForTesting;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Authentication mechanism for legacy cookie-based App Engine authentication.
+ *
+ *
Just use the values returned by UserService.
+ */
+// TODO(mountford) Add XSRF protection here or elsewhere, from RequestHandler
+public class LegacyAuthenticationMechanism implements AuthenticationMechanism {
+
+ private final UserService userService;
+
+ @VisibleForTesting
+ @Inject
+ public LegacyAuthenticationMechanism(UserService userService) {
+ this.userService = userService;
+ }
+
+ @Override
+ public AuthResult authenticate(HttpServletRequest request) {
+ if (!userService.isUserLoggedIn()) {
+ return AuthResult.create(NONE);
+ } else {
+ return AuthResult.create(
+ USER,
+ UserAuthInfo.create(userService.getCurrentUser(), userService.isUserAdmin()));
+ }
+ }
+}
diff --git a/java/google/registry/request/auth/OAuthAuthenticationMechanism.java b/java/google/registry/request/auth/OAuthAuthenticationMechanism.java
new file mode 100644
index 000000000..9de788f38
--- /dev/null
+++ b/java/google/registry/request/auth/OAuthAuthenticationMechanism.java
@@ -0,0 +1,125 @@
+// 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 availableOauthScopes;
+
+ /** The OAuth scopes which must all be present for authentication to succeed. */
+ private final ImmutableSet requiredOauthScopes;
+
+ private final ImmutableSet allowedOauthClientIds;
+
+ @VisibleForTesting
+ @Inject
+ public OAuthAuthenticationMechanism(
+ OAuthService oauthService,
+ @Config("availableOauthScopes") ImmutableSet availableOauthScopes,
+ @Config("requiredOauthScopes") ImmutableSet requiredOauthScopes,
+ @Config("allowedOauthClientIds") ImmutableSet 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 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)));
+ }
+}
diff --git a/java/google/registry/request/auth/OAuthTokenInfo.java b/java/google/registry/request/auth/OAuthTokenInfo.java
new file mode 100644
index 000000000..6cb3d3950
--- /dev/null
+++ b/java/google/registry/request/auth/OAuthTokenInfo.java
@@ -0,0 +1,48 @@
+// 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 com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+
+/** Information provided by the OAuth authentication mechanism (only) about the session. */
+@AutoValue
+public abstract class OAuthTokenInfo {
+
+ /** Authorized OAuth scopes granted by the access token provided with the request. */
+ abstract ImmutableSet authorizedScopes();
+
+ /** OAuth client ID from the access token provided with the request. */
+ abstract String oauthClientId();
+
+ /**
+ * Raw OAuth access token value provided with the request, for passing along to downstream APIs as
+ * appropriate.
+ *
+ * Note that the request parsing code makes certain assumptions about whether the Authorization
+ * header was used as the source of the token. Because OAuthService could theoretically fall back
+ * to some other source of authentication, it might be possible for rawAccessToken not to have
+ * been the source of OAuth authentication. Looking at the code of OAuthService, that could not
+ * currently happen, but if OAuthService were modified in the future so that it tried the bearer
+ * token, and then when that failed, fell back to another, successful authentication path, then
+ * rawAccessToken might not be valid.
+ */
+ abstract String rawAccessToken();
+
+ static OAuthTokenInfo create(
+ ImmutableSet authorizedScopes, String oauthClientId, String rawAccessToken) {
+ return new AutoValue_OAuthTokenInfo(authorizedScopes, oauthClientId, rawAccessToken);
+ }
+}
diff --git a/java/google/registry/request/auth/RequestAuthenticator.java b/java/google/registry/request/auth/RequestAuthenticator.java
new file mode 100644
index 000000000..861f7d33f
--- /dev/null
+++ b/java/google/registry/request/auth/RequestAuthenticator.java
@@ -0,0 +1,171 @@
+// 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.base.Preconditions.checkArgument;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+import google.registry.util.FormattingLogger;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+
+/** Top-level authentication/authorization class; calls authentication mechanisms as needed. */
+public class RequestAuthenticator {
+
+ private final AppEngineInternalAuthenticationMechanism appEngineInternalAuthenticationMechanism;
+ private final ImmutableList apiAuthenticationMechanisms;
+ private final LegacyAuthenticationMechanism legacyAuthenticationMechanism;
+
+ private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
+
+ @VisibleForTesting
+ @Inject
+ public RequestAuthenticator(
+ AppEngineInternalAuthenticationMechanism appEngineInternalAuthenticationMechanism,
+ ImmutableList apiAuthenticationMechanisms,
+ LegacyAuthenticationMechanism legacyAuthenticationMechanism) {
+ this.appEngineInternalAuthenticationMechanism = appEngineInternalAuthenticationMechanism;
+ this.apiAuthenticationMechanisms = apiAuthenticationMechanisms;
+ this.legacyAuthenticationMechanism = legacyAuthenticationMechanism;
+ }
+
+ /**
+ * Attempts to authenticate and authorize the user, according to the settings of the action.
+ *
+ * @param auth the auth settings of the action, which determine what authentication and
+ * authorization are allowed
+ * @param req the {@link HttpServletRequest}; some authentication mechanisms use HTTP headers
+ * @return an authentication result if authentication/authorization was successful, or absent() if
+ * not; authentication can be "successful" even without any authentication if the action's
+ * auth settings are set to NONE -- in this case, NOT_AUTHENTICATED is returned
+ */
+ public Optional authorize(Auth auth, HttpServletRequest req) {
+ logger.infofmt("Action requires auth: %s", auth);
+ AuthResult authResult = authenticate(auth, req);
+ switch (auth.minimumLevel()) {
+ case NONE:
+ // Any authentication result is ok.
+ break;
+ case APP:
+ if (!authResult.isAuthenticated()) {
+ logger.info("Not authorized; no authentication found");
+ return Optional.absent();
+ }
+ break;
+ case USER:
+ if (authResult.authLevel() != AuthLevel.USER) {
+ logger.info("Not authorized; no authenticated user");
+ return Optional.absent();
+ }
+ break;
+ }
+ switch (auth.userPolicy()) {
+ case IGNORED:
+ if (authResult.authLevel() == AuthLevel.USER) {
+ logger.info("Not authorized; user policy is IGNORED, but a user was found");
+ return Optional.absent();
+ }
+ break;
+ case PUBLIC:
+ // Any user auth result is okay.
+ break;
+ case ADMIN:
+ if (authResult.userAuthInfo().isPresent()
+ && !authResult.userAuthInfo().get().isUserAdmin()) {
+ logger.info("Not authorized; user policy is ADMIN, but the user was not an admin");
+ return Optional.absent();
+ }
+ break;
+ }
+ logger.info("Authorized");
+ return Optional.of(authResult);
+ }
+
+ /**
+ * Attempts to authenticate the user, according to the settings of the action.
+ *
+ * @param auth the auth settings of the action, which determine what authentication is allowed
+ * @param req the {@link HttpServletRequest}; some authentication mechanisms use HTTP headers
+ * @return an authentication result; if no authentication was made, returns NOT_AUTHENTICATED
+ */
+ private AuthResult authenticate(Auth auth, HttpServletRequest req) {
+ checkAuthConfig(auth);
+ for (Auth.AuthMethod authMethod : auth.methods()) {
+ switch (authMethod) {
+ // App Engine internal authentication, using the queue name header
+ case INTERNAL:
+ logger.info("Checking internal auth");
+ // INTERNAL should be skipped if a user is required.
+ if (auth.minimumLevel() != AuthLevel.USER) {
+ AuthResult authResult = appEngineInternalAuthenticationMechanism.authenticate(req);
+ if (authResult.isAuthenticated()) {
+ logger.infofmt("Authenticated: %s", authResult);
+ return authResult;
+ }
+ }
+ break;
+ // API-based user authentication mechanisms, such as OAuth
+ case API:
+ // checkAuthConfig will have insured that the user policy is not IGNORED.
+ for (AuthenticationMechanism authMechanism : apiAuthenticationMechanisms) {
+ logger.infofmt("Checking %s", authMechanism);
+ AuthResult authResult = authMechanism.authenticate(req);
+ if (authResult.isAuthenticated()) {
+ logger.infofmt("Authenticated: %s", authResult);
+ return authResult;
+ }
+ }
+ break;
+ // Legacy authentication via UserService
+ case LEGACY:
+ // checkAuthConfig will have insured that the user policy is not IGNORED.
+ logger.info("Checking legacy auth");
+ AuthResult authResult = legacyAuthenticationMechanism.authenticate(req);
+ if (authResult.isAuthenticated()) {
+ logger.infofmt("Authenticated: %s", authResult);
+ return authResult;
+ }
+ break;
+ }
+ }
+ logger.info("No authentication found");
+ return AuthResult.NOT_AUTHENTICATED;
+ }
+
+ /** Validates an Auth object, checking for invalid setting combinations. */
+ void checkAuthConfig(Auth auth) {
+ ImmutableList authMethods = ImmutableList.copyOf(auth.methods());
+ checkArgument(!authMethods.isEmpty(), "Must specify at least one auth method");
+ checkArgument(
+ Ordering.explicit(Auth.AuthMethod.INTERNAL, Auth.AuthMethod.API, Auth.AuthMethod.LEGACY)
+ .isStrictlyOrdered(authMethods),
+ "Auth methods must be strictly in order - INTERNAL, API, LEGACY");
+ checkArgument(
+ authMethods.contains(Auth.AuthMethod.INTERNAL),
+ "Auth method INTERNAL must always be specified, and as the first auth method");
+ if (authMethods.equals(ImmutableList.of(Auth.AuthMethod.INTERNAL))) {
+ checkArgument(
+ !auth.minimumLevel().equals(AuthLevel.USER),
+ "Actions with only INTERNAL auth may not require USER auth level");
+ } else {
+ checkArgument(
+ !auth.userPolicy().equals(Auth.UserPolicy.IGNORED),
+ "Actions with auth methods beyond INTERNAL must not specify the IGNORED user policy");
+ }
+ }
+}
diff --git a/java/google/registry/request/auth/UserAuthInfo.java b/java/google/registry/request/auth/UserAuthInfo.java
new file mode 100644
index 000000000..64033109e
--- /dev/null
+++ b/java/google/registry/request/auth/UserAuthInfo.java
@@ -0,0 +1,49 @@
+// 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 com.google.appengine.api.users.User;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+
+/** Extra information provided by the authentication mechanism about the user. */
+@AutoValue
+public abstract class UserAuthInfo {
+
+ /** User object from the AppEngine Users API. */
+ public abstract User user();
+
+ /**
+ * Whether the user is an admin.
+ *
+ * Note that, in App Engine parlance, an admin is any user who is a project owner, editor, OR
+ * viewer (as well as the specific role App Engine Admin). So even users with read-only access to
+ * the App Engine product qualify as an "admin".
+ */
+ public abstract boolean isUserAdmin();
+
+ /** Used by the OAuth authentication mechanism (only) to return information about the session. */
+ public abstract Optional oauthTokenInfo();
+
+ static UserAuthInfo create(
+ User user, boolean isUserAdmin) {
+ return new AutoValue_UserAuthInfo(user, isUserAdmin, Optional.absent());
+ }
+
+ static UserAuthInfo create(
+ User user, boolean isUserAdmin, OAuthTokenInfo oauthTokenInfo) {
+ return new AutoValue_UserAuthInfo(user, isUserAdmin, Optional.of(oauthTokenInfo));
+ }
+}
diff --git a/javatests/google/registry/request/RequestHandlerTest.java b/javatests/google/registry/request/RequestHandlerTest.java
index 93cd9c2c2..f6b2c7ace 100644
--- a/javatests/google/registry/request/RequestHandlerTest.java
+++ b/javatests/google/registry/request/RequestHandlerTest.java
@@ -34,6 +34,7 @@ import google.registry.request.auth.AppEngineInternalAuthenticationMechanism;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthLevel;
import google.registry.request.auth.AuthResult;
+import google.registry.request.auth.AuthenticationMechanism;
import google.registry.request.auth.LegacyAuthenticationMechanism;
import google.registry.request.auth.OAuthAuthenticationMechanism;
import google.registry.request.auth.RequestAuthenticator;
@@ -238,7 +239,7 @@ public final class RequestHandlerTest {
public void before() throws Exception {
requestAuthenticator = new RequestAuthenticator(
new AppEngineInternalAuthenticationMechanism(),
- ImmutableList.of(
+ ImmutableList.of(
new OAuthAuthenticationMechanism(
OAuthServiceFactory.getOAuthService(),
ImmutableSet.of("https://www.googleapis.com/auth/userinfo.email"),