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: + * + *

+ * + *

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"),