Add request/auth package to Nomulus release

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=147087621
This commit is contained in:
mcilwain 2017-02-09 15:11:17 -08:00 committed by Ben McIlwain
parent c41f5bb31c
commit dc66cef8ae
13 changed files with 808 additions and 1 deletions

View file

@ -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.
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* 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:
*
* <ul>
* <li>1. Always include the result of UserService.getCurrentUser() as the active user</li>
* <li>2. Validate that the requests came from special AppEngine internal IPs</li>
* </ul>
*
* <p>See <a href=
* "https://cloud.google.com/appengine/docs/java/taskqueue/push/creating-handlers#reading_request_headers">task
* handler request header documentation</a>
*/
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);
}
}
}

View file

@ -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().
*
* <p>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;
}

View file

@ -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.
*
* <p>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.
*
* <p>In Auth: Authentication is required, but app-internal authentication (which isn't associated
* with a specific user) is permitted.
*
* <p>In AuthResult: App-internal authentication was successful.
*/
APP,
/**
* Authentication required, user required.
*
* <p>In Auth: Authentication is required, and app-internal authentication is forbidden, meaning
* that a valid authentication result will contain specific user information.
*
* <p>In AuthResult: A valid user was authenticated.
*/
USER
}

View file

@ -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<AuthenticationMechanism> provideApiAuthenticationMechanisms(
OAuthService oauthService,
@Config("availableOauthScopes") ImmutableSet<String> availableOauthScopes,
@Config("requiredOauthScopes") ImmutableSet<String> requiredOauthScopes,
@Config("allowedOauthClientIds") ImmutableSet<String> allowedOauthClientIds) {
return ImmutableList.<AuthenticationMechanism>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();
}
}

View file

@ -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> userAuthInfo();
public boolean isAuthenticated() {
return authLevel() != AuthLevel.NONE;
}
static AuthResult create(AuthLevel authLevel) {
return new AutoValue_AuthResult(authLevel, Optional.<UserAuthInfo>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.
*
* <p>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);
}

View file

@ -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}.
*
* <p>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);
}

View file

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

View file

@ -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.
*
* <p>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()));
}
}
}

View file

@ -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<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)));
}
}

View file

@ -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<String> 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.
*
* <p>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<String> authorizedScopes, String oauthClientId, String rawAccessToken) {
return new AutoValue_OAuthTokenInfo(authorizedScopes, oauthClientId, rawAccessToken);
}
}

View file

@ -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<AuthenticationMechanism> apiAuthenticationMechanisms;
private final LegacyAuthenticationMechanism legacyAuthenticationMechanism;
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
@VisibleForTesting
@Inject
public RequestAuthenticator(
AppEngineInternalAuthenticationMechanism appEngineInternalAuthenticationMechanism,
ImmutableList<AuthenticationMechanism> 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<AuthResult> 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<Auth.AuthMethod> 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");
}
}
}

View file

@ -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.
*
* <p>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> oauthTokenInfo();
static UserAuthInfo create(
User user, boolean isUserAdmin) {
return new AutoValue_UserAuthInfo(user, isUserAdmin, Optional.<OAuthTokenInfo>absent());
}
static UserAuthInfo create(
User user, boolean isUserAdmin, OAuthTokenInfo oauthTokenInfo) {
return new AutoValue_UserAuthInfo(user, isUserAdmin, Optional.of(oauthTokenInfo));
}
}

View file

@ -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.<AuthenticationMechanism>of(
new OAuthAuthenticationMechanism(
OAuthServiceFactory.getOAuthService(),
ImmutableSet.of("https://www.googleapis.com/auth/userinfo.email"),