google-nomulus/javatests/google/registry/request/auth/RequestAuthenticatorTest.java
nickfelt 2e969d6ed1 Change XsrfTokenManager to support new HMAC token format
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
2017-03-07 13:31:46 -05:00

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