mirror of
https://github.com/google/nomulus.git
synced 2025-07-23 19:20:44 +02:00
Add a fallback token verifier (#2216)
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.
This commit is contained in:
parent
2855944214
commit
779d0c9d37
6 changed files with 72 additions and 9 deletions
|
@ -1192,6 +1192,12 @@ public final class RegistryConfig {
|
||||||
return config.auth.oauthClientId;
|
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.
|
* Provides the OAuth scopes required for accessing Google APIs using the default credential.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -61,6 +61,7 @@ public class RegistryConfigSettings {
|
||||||
public static class Auth {
|
public static class Auth {
|
||||||
public List<String> allowedServiceAccountEmails;
|
public List<String> allowedServiceAccountEmails;
|
||||||
public String oauthClientId;
|
public String oauthClientId;
|
||||||
|
public String fallbackOauthClientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Configuration options for accessing Google APIs. */
|
/** Configuration options for accessing Google APIs. */
|
||||||
|
|
|
@ -321,6 +321,10 @@ auth:
|
||||||
# the same as this one.
|
# the same as this one.
|
||||||
oauthClientId: iap-oauth-clientid
|
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:
|
credentialOAuth:
|
||||||
# OAuth scopes required for accessing Google APIs using the default
|
# OAuth scopes required for accessing Google APIs using the default
|
||||||
# credential.
|
# credential.
|
||||||
|
|
|
@ -55,6 +55,9 @@ public class AuthModule {
|
||||||
@Qualifier
|
@Qualifier
|
||||||
@interface RegularOidc {}
|
@interface RegularOidc {}
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@interface RegularOidcFallback {}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@IapOidc
|
@IapOidc
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@ -71,6 +74,14 @@ public class AuthModule {
|
||||||
return TokenVerifier.newBuilder().setAudience(clientId).setIssuer(REGULAR_ISSUER_URL).build();
|
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
|
@Provides
|
||||||
@IapOidc
|
@IapOidc
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
|
@ -25,6 +25,7 @@ import google.registry.model.console.User;
|
||||||
import google.registry.model.console.UserDao;
|
import google.registry.model.console.UserDao;
|
||||||
import google.registry.request.auth.AuthModule.IapOidc;
|
import google.registry.request.auth.AuthModule.IapOidc;
|
||||||
import google.registry.request.auth.AuthModule.RegularOidc;
|
import google.registry.request.auth.AuthModule.RegularOidc;
|
||||||
|
import google.registry.request.auth.AuthModule.RegularOidcFallback;
|
||||||
import google.registry.request.auth.AuthSettings.AuthLevel;
|
import google.registry.request.auth.AuthSettings.AuthLevel;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
@ -53,6 +54,8 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
|
||||||
|
|
||||||
protected final TokenVerifier tokenVerifier;
|
protected final TokenVerifier tokenVerifier;
|
||||||
|
|
||||||
|
protected final Optional<TokenVerifier> fallbackTokenVerifier;
|
||||||
|
|
||||||
protected final TokenExtractor tokenExtractor;
|
protected final TokenExtractor tokenExtractor;
|
||||||
|
|
||||||
private final ImmutableSet<String> serviceAccountEmails;
|
private final ImmutableSet<String> serviceAccountEmails;
|
||||||
|
@ -60,9 +63,11 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
|
||||||
protected OidcTokenAuthenticationMechanism(
|
protected OidcTokenAuthenticationMechanism(
|
||||||
ImmutableSet<String> serviceAccountEmails,
|
ImmutableSet<String> serviceAccountEmails,
|
||||||
TokenVerifier tokenVerifier,
|
TokenVerifier tokenVerifier,
|
||||||
|
@Nullable TokenVerifier fallbackTokenVerifier,
|
||||||
TokenExtractor tokenExtractor) {
|
TokenExtractor tokenExtractor) {
|
||||||
this.serviceAccountEmails = serviceAccountEmails;
|
this.serviceAccountEmails = serviceAccountEmails;
|
||||||
this.tokenVerifier = tokenVerifier;
|
this.tokenVerifier = tokenVerifier;
|
||||||
|
this.fallbackTokenVerifier = Optional.ofNullable(fallbackTokenVerifier);
|
||||||
this.tokenExtractor = tokenExtractor;
|
this.tokenExtractor = tokenExtractor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +82,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
|
||||||
if (rawIdToken == null) {
|
if (rawIdToken == null) {
|
||||||
return AuthResult.NOT_AUTHENTICATED;
|
return AuthResult.NOT_AUTHENTICATED;
|
||||||
}
|
}
|
||||||
JsonWebSignature token;
|
JsonWebSignature token = null;
|
||||||
try {
|
try {
|
||||||
token = tokenVerifier.verify(rawIdToken);
|
token = tokenVerifier.verify(rawIdToken);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -86,8 +91,25 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
|
||||||
RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)
|
RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)
|
||||||
? "Raw token redacted in prod"
|
? "Raw token redacted in prod"
|
||||||
: rawIdToken);
|
: 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");
|
String email = (String) token.getPayload().get("email");
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
logger.atWarning().log("No email address from the OIDC token:\n%s", token.getPayload());
|
logger.atWarning().log("No email address from the OIDC token:\n%s", token.getPayload());
|
||||||
|
@ -141,7 +163,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
|
||||||
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
|
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
|
||||||
@IapOidc TokenVerifier tokenVerifier,
|
@IapOidc TokenVerifier tokenVerifier,
|
||||||
@IapOidc TokenExtractor tokenExtractor) {
|
@IapOidc TokenExtractor tokenExtractor) {
|
||||||
super(serviceAccountEmails, tokenVerifier, tokenExtractor);
|
super(serviceAccountEmails, tokenVerifier, null, tokenExtractor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,8 +183,9 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
|
||||||
protected RegularOidcAuthenticationMechanism(
|
protected RegularOidcAuthenticationMechanism(
|
||||||
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
|
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
|
||||||
@RegularOidc TokenVerifier tokenVerifier,
|
@RegularOidc TokenVerifier tokenVerifier,
|
||||||
|
@RegularOidcFallback TokenVerifier fallbackTokenVerifier,
|
||||||
@RegularOidc TokenExtractor tokenExtractor) {
|
@RegularOidc TokenExtractor tokenExtractor) {
|
||||||
super(serviceAccountEmails, tokenVerifier, tokenExtractor);
|
super(serviceAccountEmails, tokenVerifier, fallbackTokenVerifier, tokenExtractor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat;
|
||||||
import static google.registry.request.auth.AuthModule.BEARER_PREFIX;
|
import static google.registry.request.auth.AuthModule.BEARER_PREFIX;
|
||||||
import static google.registry.request.auth.AuthModule.IAP_HEADER_NAME;
|
import static google.registry.request.auth.AuthModule.IAP_HEADER_NAME;
|
||||||
import static google.registry.testing.DatabaseHelper.insertInDb;
|
import static google.registry.testing.DatabaseHelper.insertInDb;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ -70,7 +69,7 @@ public class OidcTokenAuthenticationMechanismTest {
|
||||||
|
|
||||||
private AuthResult authResult;
|
private AuthResult authResult;
|
||||||
private OidcTokenAuthenticationMechanism authenticationMechanism =
|
private OidcTokenAuthenticationMechanism authenticationMechanism =
|
||||||
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, e -> rawToken) {};
|
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, null, e -> rawToken) {};
|
||||||
|
|
||||||
@RegisterExtension
|
@RegisterExtension
|
||||||
public final JpaTestExtensions.JpaUnitTestExtension jpaExtension =
|
public final JpaTestExtensions.JpaUnitTestExtension jpaExtension =
|
||||||
|
@ -78,7 +77,7 @@ public class OidcTokenAuthenticationMechanismTest {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void beforeEach() throws Exception {
|
void beforeEach() throws Exception {
|
||||||
when(tokenVerifier.verify(eq(rawToken))).thenReturn(jwt);
|
when(tokenVerifier.verify(rawToken)).thenReturn(jwt);
|
||||||
payload.setEmail(email);
|
payload.setEmail(email);
|
||||||
payload.setSubject(gaiaId);
|
payload.setSubject(gaiaId);
|
||||||
insertInDb(user);
|
insertInDb(user);
|
||||||
|
@ -98,18 +97,30 @@ public class OidcTokenAuthenticationMechanismTest {
|
||||||
@Test
|
@Test
|
||||||
void testAuthenticate_noTokenFromRequest() {
|
void testAuthenticate_noTokenFromRequest() {
|
||||||
authenticationMechanism =
|
authenticationMechanism =
|
||||||
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, e -> null) {};
|
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, null, e -> null) {};
|
||||||
authResult = authenticationMechanism.authenticate(request);
|
authResult = authenticationMechanism.authenticate(request);
|
||||||
assertThat(authResult).isEqualTo(AuthResult.NOT_AUTHENTICATED);
|
assertThat(authResult).isEqualTo(AuthResult.NOT_AUTHENTICATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAuthenticate_invalidToken() throws Exception {
|
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);
|
authResult = authenticationMechanism.authenticate(request);
|
||||||
assertThat(authResult).isEqualTo(AuthResult.NOT_AUTHENTICATED);
|
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
|
@Test
|
||||||
void testAuthenticate_noEmailAddress() throws Exception {
|
void testAuthenticate_noEmailAddress() throws Exception {
|
||||||
payload.setEmail(null);
|
payload.setEmail(null);
|
||||||
|
@ -223,5 +234,12 @@ public class OidcTokenAuthenticationMechanismTest {
|
||||||
String provideOauthClientId() {
|
String provideOauthClientId() {
|
||||||
return "client-id";
|
return "client-id";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@Config("fallbackOauthClientId")
|
||||||
|
String provideFallbackOauthClientId() {
|
||||||
|
return "fallback-client-id";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue