mirror of
https://github.com/google/nomulus.git
synced 2025-05-06 23:17:51 +02:00
This follows up on Brian's work to transition not just to a new format with an empty scope value, but instead to replace the existing format entirely with a new one that: 1) includes a version number to support future format migrations 2) doesn't include a field for the scope at all, since scoping the tokens adds no real security benefit and just makes verification more difficult 3) replaces the raw SHA-256 hash with a SHA-256 HMAC instead, as a best practice to avoid length-extension attacks [1], even though in our particular case they would only be able to extend the timestamp and would thus be relatively innocuous The new format will be produced by calling generateToken(), and the scope-accepting version is renamed to generateLegacyToken() in addition to its existing deprecation, for maximum clarity. I changed the validateToken() logic to stop accepting a scope entirely; when validating a legacy-style token, we'll test it against the two existing legacy scope values ("admin" and "console") and accept it if it matches either one. Note that this means the xsrfScope parameter in @Action is now wholly obsolete; I'll remove it in a follow-up to avoid bringing extra files into this CL. After this CL hits production, the next one will replace all calls to generateLegacyToken() with generateToken(). Once that CL is deployed, the last step will be removing the legacy fallback in validateToken(). [1] See https://en.wikipedia.org/wiki/Length_extension_attack ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148936805
459 lines
18 KiB
Java
459 lines
18 KiB
Java
// 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 com.google.common.truth.Truth.assertThat;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.verifyZeroInteractions;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
import com.google.appengine.api.users.User;
|
|
import com.google.appengine.api.users.UserService;
|
|
import com.google.common.base.Optional;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import google.registry.request.Action;
|
|
import google.registry.security.XsrfTokenManager;
|
|
import google.registry.testing.AppEngineRule;
|
|
import google.registry.testing.ExceptionRule;
|
|
import google.registry.testing.FakeClock;
|
|
import google.registry.testing.FakeOAuthService;
|
|
import google.registry.testing.FakeUserService;
|
|
import javax.servlet.http.HttpServletRequest;
|
|
import org.junit.Before;
|
|
import org.junit.Rule;
|
|
import org.junit.Test;
|
|
import org.junit.runner.RunWith;
|
|
import org.junit.runners.JUnit4;
|
|
|
|
/** Unit tests for {@link RequestAuthenticator}. */
|
|
@RunWith(JUnit4.class)
|
|
public class RequestAuthenticatorTest {
|
|
|
|
@Rule
|
|
public final AppEngineRule appEngine = AppEngineRule.builder().build();
|
|
|
|
@Rule
|
|
public final ExceptionRule thrown = new ExceptionRule();
|
|
|
|
@Action(
|
|
path = "/auth/none",
|
|
auth = @Auth(
|
|
methods = {Auth.AuthMethod.INTERNAL},
|
|
minimumLevel = AuthLevel.NONE,
|
|
userPolicy = Auth.UserPolicy.IGNORED),
|
|
method = Action.Method.GET)
|
|
public static class AuthNone {}
|
|
|
|
@Action(
|
|
path = "/auth/internalOnly",
|
|
auth = @Auth(minimumLevel = AuthLevel.APP),
|
|
method = Action.Method.GET)
|
|
public static class AuthInternalOnly {}
|
|
|
|
@Action(
|
|
path = "/auth/anyUserAnyMethod",
|
|
auth = @Auth(
|
|
methods = {Auth.AuthMethod.INTERNAL, Auth.AuthMethod.API, Auth.AuthMethod.LEGACY},
|
|
minimumLevel = AuthLevel.USER,
|
|
userPolicy = Auth.UserPolicy.PUBLIC),
|
|
method = Action.Method.GET)
|
|
public static class AuthAnyUserAnyMethod {}
|
|
|
|
@Action(
|
|
path = "/auth/anyUserNoLegacy",
|
|
auth = @Auth(
|
|
methods = {Auth.AuthMethod.INTERNAL, Auth.AuthMethod.API},
|
|
minimumLevel = AuthLevel.USER,
|
|
userPolicy = Auth.UserPolicy.PUBLIC),
|
|
method = Action.Method.GET)
|
|
public static class AuthAnyUserNoLegacy {}
|
|
|
|
@Action(
|
|
path = "/auth/adminUserAnyMethod",
|
|
auth = @Auth(
|
|
methods = {Auth.AuthMethod.INTERNAL, Auth.AuthMethod.API, Auth.AuthMethod.LEGACY},
|
|
minimumLevel = AuthLevel.USER,
|
|
userPolicy = Auth.UserPolicy.ADMIN),
|
|
method = Action.Method.GET)
|
|
public static class AuthAdminUserAnyMethod {}
|
|
|
|
@Action(
|
|
path = "/auth/noMethods",
|
|
auth = @Auth(methods = {}))
|
|
public static class AuthNoMethods {}
|
|
|
|
@Action(
|
|
path = "/auth/missingInternal",
|
|
auth = @Auth(methods = {Auth.AuthMethod.API, Auth.AuthMethod.LEGACY}))
|
|
public static class AuthMissingInternal {}
|
|
|
|
@Action(
|
|
path = "/auth/wrongMethodOrdering",
|
|
auth = @Auth(methods = {Auth.AuthMethod.API, Auth.AuthMethod.INTERNAL}))
|
|
public static class AuthWrongMethodOrdering {}
|
|
|
|
@Action(
|
|
path = "/auth/duplicateMethods",
|
|
auth = @Auth(methods = {Auth.AuthMethod.INTERNAL, Auth.AuthMethod.API, Auth.AuthMethod.API}))
|
|
public static class AuthDuplicateMethods {}
|
|
|
|
@Action(
|
|
path = "/auth/internalWithUser",
|
|
auth = @Auth(methods = {Auth.AuthMethod.INTERNAL}, minimumLevel = AuthLevel.USER))
|
|
public static class AuthInternalWithUser {}
|
|
|
|
@Action(
|
|
path = "/auth/wronglyIgnoringUser",
|
|
auth = @Auth(
|
|
methods = {Auth.AuthMethod.INTERNAL, Auth.AuthMethod.API},
|
|
userPolicy = Auth.UserPolicy.IGNORED))
|
|
public static class AuthWronglyIgnoringUser {}
|
|
|
|
private final UserService mockUserService = mock(UserService.class);
|
|
private final HttpServletRequest req = mock(HttpServletRequest.class);
|
|
|
|
private final User testUser = new User("test@google.com", "test@google.com");
|
|
private final FakeUserService fakeUserService = new FakeUserService();
|
|
private final XsrfTokenManager xsrfTokenManager =
|
|
new XsrfTokenManager(new FakeClock(), fakeUserService);
|
|
private final FakeOAuthService fakeOAuthService = new FakeOAuthService(
|
|
false /* isOAuthEnabled */,
|
|
testUser,
|
|
false /* isUserAdmin */,
|
|
"test-client-id",
|
|
ImmutableList.of("test-scope1", "test-scope2", "nontest-scope"));
|
|
|
|
@Before
|
|
public void before() throws Exception {
|
|
when(req.getMethod()).thenReturn("POST");
|
|
}
|
|
|
|
private RequestAuthenticator createRequestAuthenticator(UserService userService) {
|
|
return new RequestAuthenticator(
|
|
new AppEngineInternalAuthenticationMechanism(),
|
|
ImmutableList.<AuthenticationMechanism>of(
|
|
new OAuthAuthenticationMechanism(
|
|
fakeOAuthService,
|
|
ImmutableSet.of("test-scope1", "test-scope2", "test-scope3"),
|
|
ImmutableSet.of("test-scope1", "test-scope2"),
|
|
ImmutableSet.of("test-client-id", "other-test-client-id"))),
|
|
new LegacyAuthenticationMechanism(userService, xsrfTokenManager));
|
|
}
|
|
|
|
private Optional<AuthResult> runTest(UserService userService, Class<?> clazz) {
|
|
return createRequestAuthenticator(userService)
|
|
.authorize(clazz.getAnnotation(Action.class).auth(), req);
|
|
}
|
|
|
|
@Test
|
|
public void testNoAuthNeeded_noneFound() throws Exception {
|
|
Optional<AuthResult> authResult = runTest(mockUserService, AuthNone.class);
|
|
|
|
verifyZeroInteractions(mockUserService);
|
|
assertThat(authResult).isPresent();
|
|
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.NONE);
|
|
}
|
|
|
|
@Test
|
|
public void testNoAuthNeeded_internalFound() throws Exception {
|
|
when(req.getHeader("X-AppEngine-QueueName")).thenReturn("__cron");
|
|
|
|
Optional<AuthResult> authResult = runTest(mockUserService, AuthNone.class);
|
|
|
|
verifyZeroInteractions(mockUserService);
|
|
assertThat(authResult).isPresent();
|
|
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.APP);
|
|
assertThat(authResult.get().userAuthInfo()).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testInternalAuth_notInvokedInternally() throws Exception {
|
|
Optional<AuthResult> authResult = runTest(mockUserService, AuthInternalOnly.class);
|
|
|
|
verifyZeroInteractions(mockUserService);
|
|
assertThat(authResult).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testInternalAuth_success() throws Exception {
|
|
when(req.getHeader("X-AppEngine-QueueName")).thenReturn("__cron");
|
|
|
|
Optional<AuthResult> authResult = runTest(mockUserService, AuthInternalOnly.class);
|
|
|
|
verifyZeroInteractions(mockUserService);
|
|
assertThat(authResult).isPresent();
|
|
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.APP);
|
|
assertThat(authResult.get().userAuthInfo()).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testAnyUserAnyMethod_notLoggedIn() throws Exception {
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserAnyMethod.class);
|
|
|
|
assertThat(authResult).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testAnyUserAnyMethod_xsrfFailure() throws Exception {
|
|
fakeUserService.setUser(testUser, false);
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserAnyMethod.class);
|
|
|
|
assertThat(authResult).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testAnyUserAnyMethod_success() throws Exception {
|
|
fakeUserService.setUser(testUser, false /* isAdmin */);
|
|
when(req.getHeader(XsrfTokenManager.X_CSRF_TOKEN))
|
|
.thenReturn(xsrfTokenManager.generateLegacyToken("console", testUser.getEmail()));
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserAnyMethod.class);
|
|
|
|
assertThat(authResult).isPresent();
|
|
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
|
|
assertThat(authResult.get().userAuthInfo()).isPresent();
|
|
assertThat(authResult.get().userAuthInfo().get().user()).isEqualTo(testUser);
|
|
assertThat(authResult.get().userAuthInfo().get().isUserAdmin()).isFalse();
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testAnyUserAnyMethod_xsrfNotRequiredForGet() throws Exception {
|
|
fakeUserService.setUser(testUser, false);
|
|
when(req.getMethod()).thenReturn("GET");
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserAnyMethod.class);
|
|
|
|
assertThat(authResult).isPresent();
|
|
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
|
|
assertThat(authResult.get().userAuthInfo()).isPresent();
|
|
assertThat(authResult.get().userAuthInfo().get().user()).isEqualTo(testUser);
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testAdminUserAnyMethod_notLoggedIn() throws Exception {
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAdminUserAnyMethod.class);
|
|
|
|
assertThat(authResult).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testAdminUserAnyMethod_notAdminUser() throws Exception {
|
|
fakeUserService.setUser(testUser, false /* isAdmin */);
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAdminUserAnyMethod.class);
|
|
|
|
assertThat(authResult).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testAdminUserAnyMethod_xsrfFailure() throws Exception {
|
|
fakeUserService.setUser(testUser, true);
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAdminUserAnyMethod.class);
|
|
|
|
assertThat(authResult).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testAdminUserAnyMethod_success() throws Exception {
|
|
fakeUserService.setUser(testUser, true /* isAdmin */);
|
|
when(req.getHeader(XsrfTokenManager.X_CSRF_TOKEN))
|
|
.thenReturn(xsrfTokenManager.generateLegacyToken("console", testUser.getEmail()));
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAdminUserAnyMethod.class);
|
|
|
|
assertThat(authResult).isPresent();
|
|
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
|
|
assertThat(authResult.get().userAuthInfo()).isPresent();
|
|
assertThat(authResult.get().userAuthInfo().get().user()).isEqualTo(testUser);
|
|
assertThat(authResult.get().userAuthInfo().get().isUserAdmin()).isTrue();
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testOAuth_success() throws Exception {
|
|
fakeOAuthService.setUser(testUser);
|
|
fakeOAuthService.setOAuthEnabled(true);
|
|
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserNoLegacy.class);
|
|
|
|
assertThat(authResult).isPresent();
|
|
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
|
|
assertThat(authResult.get().userAuthInfo()).isPresent();
|
|
assertThat(authResult.get().userAuthInfo().get().user()).isEqualTo(testUser);
|
|
assertThat(authResult.get().userAuthInfo().get().isUserAdmin()).isFalse();
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isPresent();
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().authorizedScopes())
|
|
.containsAllOf("test-scope1", "test-scope2");
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().oauthClientId())
|
|
.isEqualTo("test-client-id");
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().rawAccessToken())
|
|
.isEqualTo("TOKEN");
|
|
}
|
|
|
|
@Test
|
|
public void testOAuthAdmin_success() throws Exception {
|
|
fakeOAuthService.setUser(testUser);
|
|
fakeOAuthService.setUserAdmin(true);
|
|
fakeOAuthService.setOAuthEnabled(true);
|
|
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserNoLegacy.class);
|
|
|
|
assertThat(authResult).isPresent();
|
|
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
|
|
assertThat(authResult.get().userAuthInfo()).isPresent();
|
|
assertThat(authResult.get().userAuthInfo().get().user()).isEqualTo(testUser);
|
|
assertThat(authResult.get().userAuthInfo().get().isUserAdmin()).isTrue();
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isPresent();
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().authorizedScopes())
|
|
.containsAllOf("test-scope1", "test-scope2");
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().oauthClientId())
|
|
.isEqualTo("test-client-id");
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().rawAccessToken())
|
|
.isEqualTo("TOKEN");
|
|
}
|
|
|
|
@Test
|
|
public void testOAuthMissingAuthenticationToken_failure() throws Exception {
|
|
fakeOAuthService.setUser(testUser);
|
|
fakeOAuthService.setOAuthEnabled(true);
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserNoLegacy.class);
|
|
|
|
assertThat(authResult).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testOAuthClientIdMismatch_failure() throws Exception {
|
|
fakeOAuthService.setUser(testUser);
|
|
fakeOAuthService.setOAuthEnabled(true);
|
|
fakeOAuthService.setClientId("wrong-client-id");
|
|
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserNoLegacy.class);
|
|
|
|
assertThat(authResult).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testOAuthNoScopes_failure() throws Exception {
|
|
fakeOAuthService.setUser(testUser);
|
|
fakeOAuthService.setOAuthEnabled(true);
|
|
fakeOAuthService.setAuthorizedScopes();
|
|
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserNoLegacy.class);
|
|
|
|
assertThat(authResult).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testOAuthMissingScope_failure() throws Exception {
|
|
fakeOAuthService.setUser(testUser);
|
|
fakeOAuthService.setOAuthEnabled(true);
|
|
fakeOAuthService.setAuthorizedScopes("test-scope1", "test-scope3");
|
|
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserNoLegacy.class);
|
|
|
|
assertThat(authResult).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testOAuthExtraScope_success() throws Exception {
|
|
fakeOAuthService.setUser(testUser);
|
|
fakeOAuthService.setOAuthEnabled(true);
|
|
fakeOAuthService.setAuthorizedScopes("test-scope1", "test-scope2", "test-scope3");
|
|
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserNoLegacy.class);
|
|
|
|
assertThat(authResult).isPresent();
|
|
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
|
|
assertThat(authResult.get().userAuthInfo()).isPresent();
|
|
assertThat(authResult.get().userAuthInfo().get().user()).isEqualTo(testUser);
|
|
assertThat(authResult.get().userAuthInfo().get().isUserAdmin()).isFalse();
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isPresent();
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().authorizedScopes())
|
|
.containsAllOf("test-scope1", "test-scope2", "test-scope3");
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().oauthClientId())
|
|
.isEqualTo("test-client-id");
|
|
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().rawAccessToken())
|
|
.isEqualTo("TOKEN");
|
|
}
|
|
|
|
@Test
|
|
public void testAnyUserNoLegacy_failureWithLegacyUser() throws Exception {
|
|
fakeUserService.setUser(testUser, false /* isAdmin */);
|
|
|
|
Optional<AuthResult> authResult = runTest(fakeUserService, AuthAnyUserNoLegacy.class);
|
|
|
|
assertThat(authResult).isAbsent();
|
|
}
|
|
|
|
@Test
|
|
public void testNoMethods_failure() throws Exception {
|
|
thrown.expect(IllegalArgumentException.class, "Must specify at least one auth method");
|
|
|
|
runTest(fakeUserService, AuthNoMethods.class);
|
|
}
|
|
|
|
@Test
|
|
public void testMissingInternal_failure() throws Exception {
|
|
thrown.expect(IllegalArgumentException.class,
|
|
"Auth method INTERNAL must always be specified, and as the first auth method");
|
|
|
|
runTest(fakeUserService, AuthMissingInternal.class);
|
|
}
|
|
|
|
@Test
|
|
public void testWrongMethodOrdering_failure() throws Exception {
|
|
thrown.expect(IllegalArgumentException.class,
|
|
"Auth methods must be unique and strictly in order - INTERNAL, API, LEGACY");
|
|
|
|
runTest(fakeUserService, AuthWrongMethodOrdering.class);
|
|
}
|
|
|
|
@Test
|
|
public void testDuplicateMethods_failure() throws Exception {
|
|
thrown.expect(IllegalArgumentException.class,
|
|
"Auth methods must be unique and strictly in order - INTERNAL, API, LEGACY");
|
|
|
|
runTest(fakeUserService, AuthDuplicateMethods.class);
|
|
}
|
|
|
|
@Test
|
|
public void testInternalWithUser_failure() throws Exception {
|
|
thrown.expect(IllegalArgumentException.class,
|
|
"Actions with only INTERNAL auth may not require USER auth level");
|
|
|
|
runTest(fakeUserService, AuthInternalWithUser.class);
|
|
}
|
|
|
|
@Test
|
|
public void testWronglyIgnoringUser_failure() throws Exception {
|
|
thrown.expect(IllegalArgumentException.class,
|
|
"Actions with auth methods beyond INTERNAL must not specify the IGNORED user policy");
|
|
|
|
runTest(fakeUserService, AuthWronglyIgnoringUser.class);
|
|
}
|
|
}
|