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.Auth;
|
||||||
import google.registry.request.auth.AuthLevel;
|
import google.registry.request.auth.AuthLevel;
|
||||||
import google.registry.request.auth.AuthResult;
|
import google.registry.request.auth.AuthResult;
|
||||||
|
import google.registry.request.auth.AuthenticationMechanism;
|
||||||
import google.registry.request.auth.LegacyAuthenticationMechanism;
|
import google.registry.request.auth.LegacyAuthenticationMechanism;
|
||||||
import google.registry.request.auth.OAuthAuthenticationMechanism;
|
import google.registry.request.auth.OAuthAuthenticationMechanism;
|
||||||
import google.registry.request.auth.RequestAuthenticator;
|
import google.registry.request.auth.RequestAuthenticator;
|
||||||
|
@ -238,7 +239,7 @@ public final class RequestHandlerTest {
|
||||||
public void before() throws Exception {
|
public void before() throws Exception {
|
||||||
requestAuthenticator = new RequestAuthenticator(
|
requestAuthenticator = new RequestAuthenticator(
|
||||||
new AppEngineInternalAuthenticationMechanism(),
|
new AppEngineInternalAuthenticationMechanism(),
|
||||||
ImmutableList.of(
|
ImmutableList.<AuthenticationMechanism>of(
|
||||||
new OAuthAuthenticationMechanism(
|
new OAuthAuthenticationMechanism(
|
||||||
OAuthServiceFactory.getOAuthService(),
|
OAuthServiceFactory.getOAuthService(),
|
||||||
ImmutableSet.of("https://www.googleapis.com/auth/userinfo.email"),
|
ImmutableSet.of("https://www.googleapis.com/auth/userinfo.email"),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue