mirror of
https://github.com/google/nomulus.git
synced 2025-05-14 16:37:13 +02:00
Add request/auth package to Nomulus release
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147087621
This commit is contained in:
parent
c41f5bb31c
commit
dc66cef8ae
13 changed files with 808 additions and 1 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
67
java/google/registry/request/auth/Auth.java
Normal file
67
java/google/registry/request/auth/Auth.java
Normal 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;
|
||||
}
|
52
java/google/registry/request/auth/AuthLevel.java
Normal file
52
java/google/registry/request/auth/AuthLevel.java
Normal 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
|
||||
}
|
61
java/google/registry/request/auth/AuthModule.java
Normal file
61
java/google/registry/request/auth/AuthModule.java
Normal 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();
|
||||
}
|
||||
}
|
61
java/google/registry/request/auth/AuthResult.java
Normal file
61
java/google/registry/request/auth/AuthResult.java
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
}
|
20
java/google/registry/request/auth/BUILD
Normal file
20
java/google/registry/request/auth/BUILD
Normal 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",
|
||||
],
|
||||
)
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
48
java/google/registry/request/auth/OAuthTokenInfo.java
Normal file
48
java/google/registry/request/auth/OAuthTokenInfo.java
Normal 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);
|
||||
}
|
||||
}
|
171
java/google/registry/request/auth/RequestAuthenticator.java
Normal file
171
java/google/registry/request/auth/RequestAuthenticator.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
49
java/google/registry/request/auth/UserAuthInfo.java
Normal file
49
java/google/registry/request/auth/UserAuthInfo.java
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue