Skip to content

Commit

Permalink
Add a fallback token verifier (#2216)
Browse files Browse the repository at this point in the history
This allows us to switch the proxy to a different client ID without
disrupting the service. This is a temporary measure and will be removed
once the switch is complete.
  • Loading branch information
jianglai authored Nov 9, 2023
1 parent 2855944 commit 779d0c9
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 9 deletions.
6 changes: 6 additions & 0 deletions core/src/main/java/google/registry/config/RegistryConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,12 @@ public static String provideOauthClientId(RegistryConfigSettings config) {
return config.auth.oauthClientId;
}

@Provides
@Config("fallbackOauthClientId")
public static String provideFallbackOauthClientId(RegistryConfigSettings config) {
return config.auth.fallbackOauthClientId;
}

/**
* Provides the OAuth scopes required for accessing Google APIs using the default credential.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public static class GcpProject {
public static class Auth {
public List<String> allowedServiceAccountEmails;
public String oauthClientId;
public String fallbackOauthClientId;
}

/** Configuration options for accessing Google APIs. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,10 @@ auth:
# the same as this one.
oauthClientId: iap-oauth-clientid

# Same as above, but serve as a fallback, so we can switch the client ID of
# the proxy without downtime.
fallbackOauthClientId: fallback-oauth-clientid

credentialOAuth:
# OAuth scopes required for accessing Google APIs using the default
# credential.
Expand Down
11 changes: 11 additions & 0 deletions core/src/main/java/google/registry/request/auth/AuthModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ ImmutableList<AuthenticationMechanism> provideApiAuthenticationMechanisms(
@Qualifier
@interface RegularOidc {}

@Qualifier
@interface RegularOidcFallback {}

@Provides
@IapOidc
@Singleton
Expand All @@ -71,6 +74,14 @@ TokenVerifier provideRegularTokenVerifier(@Config("oauthClientId") String client
return TokenVerifier.newBuilder().setAudience(clientId).setIssuer(REGULAR_ISSUER_URL).build();
}

@Provides
@RegularOidcFallback
@Singleton
TokenVerifier provideFallbackRegularTokenVerifier(
@Config("fallbackOauthClientId") String clientId) {
return TokenVerifier.newBuilder().setAudience(clientId).setIssuer(REGULAR_ISSUER_URL).build();
}

@Provides
@IapOidc
@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import google.registry.model.console.UserDao;
import google.registry.request.auth.AuthModule.IapOidc;
import google.registry.request.auth.AuthModule.RegularOidc;
import google.registry.request.auth.AuthModule.RegularOidcFallback;
import google.registry.request.auth.AuthSettings.AuthLevel;
import java.util.Optional;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -53,16 +54,20 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication

protected final TokenVerifier tokenVerifier;

protected final Optional<TokenVerifier> fallbackTokenVerifier;

protected final TokenExtractor tokenExtractor;

private final ImmutableSet<String> serviceAccountEmails;

protected OidcTokenAuthenticationMechanism(
ImmutableSet<String> serviceAccountEmails,
TokenVerifier tokenVerifier,
@Nullable TokenVerifier fallbackTokenVerifier,
TokenExtractor tokenExtractor) {
this.serviceAccountEmails = serviceAccountEmails;
this.tokenVerifier = tokenVerifier;
this.fallbackTokenVerifier = Optional.ofNullable(fallbackTokenVerifier);
this.tokenExtractor = tokenExtractor;
}

Expand All @@ -77,7 +82,7 @@ public AuthResult authenticate(HttpServletRequest request) {
if (rawIdToken == null) {
return AuthResult.NOT_AUTHENTICATED;
}
JsonWebSignature token;
JsonWebSignature token = null;
try {
token = tokenVerifier.verify(rawIdToken);
} catch (Exception e) {
Expand All @@ -86,8 +91,25 @@ public AuthResult authenticate(HttpServletRequest request) {
RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)
? "Raw token redacted in prod"
: rawIdToken);
return AuthResult.NOT_AUTHENTICATED;
}

if (token == null) {
if (fallbackTokenVerifier.isPresent()) {
try {
token = fallbackTokenVerifier.get().verify(rawIdToken);
} catch (Exception e) {
logger.atInfo().withCause(e).log(
"Failed OIDC fallback verification attempt:\n%s",
RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)
? "Raw token redacted in prod"
: rawIdToken);
return AuthResult.NOT_AUTHENTICATED;
}
} else {
return AuthResult.NOT_AUTHENTICATED;
}
}

String email = (String) token.getPayload().get("email");
if (email == null) {
logger.atWarning().log("No email address from the OIDC token:\n%s", token.getPayload());
Expand Down Expand Up @@ -141,7 +163,7 @@ protected IapOidcAuthenticationMechanism(
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
@IapOidc TokenVerifier tokenVerifier,
@IapOidc TokenExtractor tokenExtractor) {
super(serviceAccountEmails, tokenVerifier, tokenExtractor);
super(serviceAccountEmails, tokenVerifier, null, tokenExtractor);
}
}

Expand All @@ -161,8 +183,9 @@ static class RegularOidcAuthenticationMechanism extends OidcTokenAuthenticationM
protected RegularOidcAuthenticationMechanism(
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
@RegularOidc TokenVerifier tokenVerifier,
@RegularOidcFallback TokenVerifier fallbackTokenVerifier,
@RegularOidc TokenExtractor tokenExtractor) {
super(serviceAccountEmails, tokenVerifier, tokenExtractor);
super(serviceAccountEmails, tokenVerifier, fallbackTokenVerifier, tokenExtractor);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import static google.registry.request.auth.AuthModule.BEARER_PREFIX;
import static google.registry.request.auth.AuthModule.IAP_HEADER_NAME;
import static google.registry.testing.DatabaseHelper.insertInDb;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -70,15 +69,15 @@ public class OidcTokenAuthenticationMechanismTest {

private AuthResult authResult;
private OidcTokenAuthenticationMechanism authenticationMechanism =
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, e -> rawToken) {};
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, null, e -> rawToken) {};

@RegisterExtension
public final JpaTestExtensions.JpaUnitTestExtension jpaExtension =
new JpaTestExtensions.Builder().withEntityClass(User.class).buildUnitTestExtension();

@BeforeEach
void beforeEach() throws Exception {
when(tokenVerifier.verify(eq(rawToken))).thenReturn(jwt);
when(tokenVerifier.verify(rawToken)).thenReturn(jwt);
payload.setEmail(email);
payload.setSubject(gaiaId);
insertInDb(user);
Expand All @@ -98,18 +97,30 @@ void testAuthResultBypass() {
@Test
void testAuthenticate_noTokenFromRequest() {
authenticationMechanism =
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, e -> null) {};
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, null, e -> null) {};
authResult = authenticationMechanism.authenticate(request);
assertThat(authResult).isEqualTo(AuthResult.NOT_AUTHENTICATED);
}

@Test
void testAuthenticate_invalidToken() throws Exception {
when(tokenVerifier.verify(eq(rawToken))).thenThrow(new VerificationException("Bad token"));
when(tokenVerifier.verify(rawToken)).thenThrow(new VerificationException("Bad token"));
authResult = authenticationMechanism.authenticate(request);
assertThat(authResult).isEqualTo(AuthResult.NOT_AUTHENTICATED);
}

@Test
void testAuthenticate_fallbackVerifier() throws Exception {
TokenVerifier fallbackVerifier = mock(TokenVerifier.class);
when(tokenVerifier.verify(rawToken)).thenThrow(new VerificationException("Bad token"));
when(fallbackVerifier.verify(rawToken)).thenReturn(jwt);
authenticationMechanism =
new OidcTokenAuthenticationMechanism(
serviceAccounts, tokenVerifier, fallbackVerifier, e -> rawToken) {};
authResult = authenticationMechanism.authenticate(request);
assertThat(authResult.isAuthenticated()).isEqualTo(true);
}

@Test
void testAuthenticate_noEmailAddress() throws Exception {
payload.setEmail(null);
Expand Down Expand Up @@ -223,5 +234,12 @@ ImmutableSet<String> provideAllowedServiceAccountEmails() {
String provideOauthClientId() {
return "client-id";
}

@Provides
@Singleton
@Config("fallbackOauthClientId")
String provideFallbackOauthClientId() {
return "fallback-client-id";
}
}
}

0 comments on commit 779d0c9

Please sign in to comment.