[SAP IAS] Validate SAP IAS token during login with token

This change uses the official validation tools provided by SAP IAS
to verify that the token is valid for the requested application.

Change-Id: I56a5044566741539d8b0d236ea18d868568cf09c
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java
index 037281d..dc41e8b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.oauth.sap.SAPIasModule;
 import com.googlesource.gerrit.plugins.oauth.sap.SAPIasOAuthLoginProvider;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
@@ -58,21 +59,22 @@
                   externalIdFactory, OAuthServiceProviderExternalIdScheme.create(provider)));
     }
 
-    boolean loginProviderBound = bindOAuthLoginProvider(SAPIasOAuthLoginProvider.class);
+    boolean oAuthModuleInstalled =
+        installOAuthModule(SAPIasOAuthLoginProvider.class, new SAPIasModule());
 
-    if (!loginProviderBound) {
+    if (!oAuthModuleInstalled) {
       bind(OAuthLoginProvider.class)
           .annotatedWith(Exports.named(pluginName))
           .to(DisabledOAuthLoginProvider.class);
     }
   }
 
-  private boolean bindOAuthLoginProvider(Class<SAPIasOAuthLoginProvider> loginClass) {
+  private boolean installOAuthModule(
+      Class<? extends OAuthLoginProvider> loginClass, AbstractModule oAuthModule) {
     String loginProviderName = loginClass.getAnnotation(OAuthServiceProviderConfig.class).name();
     String cfgSuffix = OAuthPluginConfigFactory.getConfigSuffix(loginProviderName);
-    String extIdScheme = OAuthServiceProviderExternalIdScheme.create(loginProviderName);
     if (cfg.getString("plugin", pluginName + cfgSuffix, InitOAuth.CLIENT_ID) != null) {
-      bind(OAuthLoginProvider.class).annotatedWith(Exports.named(extIdScheme)).to(loginClass);
+      install(oAuthModule);
       return true;
     }
     return false;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasModule.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasModule.java
new file mode 100644
index 0000000..11fefe3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasModule.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// 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 com.googlesource.gerrit.plugins.oauth.sap;
+
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
+import com.google.inject.AbstractModule;
+import com.google.inject.TypeLiteral;
+import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.token.validation.CombiningValidator;
+
+public class SAPIasModule extends AbstractModule {
+  @Override
+  public void configure() {
+    String extIdScheme =
+        OAuthServiceProviderExternalIdScheme.create(SAPIasOAuthService.PROVIDER_NAME);
+    bind(new TypeLiteral<CombiningValidator<Token>>() {})
+        .toProvider(SAPIasTokenValidatorProvider.class)
+        .asEagerSingleton();
+    bind(OAuthLoginProvider.class)
+        .annotatedWith(Exports.named(extIdScheme))
+        .to(SAPIasOAuthLoginProvider.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthLoginProvider.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthLoginProvider.java
index a029cec..138e1e0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthLoginProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthLoginProvider.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
@@ -56,11 +57,10 @@
       OAuthPluginConfigFactory cfgFactory,
       ExternalIds externalIds,
       ExternalIdKeyFactory externalIdKeyFactory) {
+    PluginConfig cfg = cfgFactory.create(SAPIasOAuthService.PROVIDER_NAME);
     this.service = service;
     this.enableResourceOwnerPasswordFlow =
-        cfgFactory
-            .create(SAPIasOAuthService.PROVIDER_NAME)
-            .getBoolean("enable-resource-owner-password-flow", false);
+        cfg.getBoolean("enable-resource-owner-password-flow", false);
     this.externalIds = externalIds;
     this.externalIdKeyFactory = externalIdKeyFactory;
     this.extIdScheme =
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthService.java
index a41c500..4aa2b53 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthService.java
@@ -38,6 +38,9 @@
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
 import com.sap.cloud.security.json.DefaultJsonObject;
 import com.sap.cloud.security.token.SapIdToken;
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.token.validation.CombiningValidator;
+import com.sap.cloud.security.token.validation.ValidationResult;
 import java.io.IOException;
 import java.net.URI;
 import java.util.concurrent.ExecutionException;
@@ -57,10 +60,13 @@
   private final boolean enablePKCE;
   private final AuthorizationUrlBuilder authorizationUrlBuilder;
   private final String extIdScheme;
+  private final CombiningValidator<Token> tokenValidator;
 
   @Inject
   SAPIasOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory,
+      @CanonicalWebUrl Provider<String> urlProvider,
+      CombiningValidator<Token> tokenValidator) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
     String canonicalWebUrl = urlProvider.get();
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
@@ -78,6 +84,7 @@
             .build(new SAPIasApi(rootUrl));
     authorizationUrlBuilder = service.createAuthorizationUrlBuilder();
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
+    this.tokenValidator = tokenValidator;
   }
 
   @Override
@@ -86,8 +93,14 @@
     return getUserInfo(t);
   }
 
-  public OAuthUserInfo getUserInfo(OAuth2AccessToken token) {
+  public OAuthUserInfo getUserInfo(OAuth2AccessToken token) throws IOException {
     SapIdToken sapToken = new SapIdToken(getIdToken(token));
+    ValidationResult res = tokenValidator.validate(sapToken);
+    if (!res.isValid()) {
+      log.warn("Invalid token received for " + sapToken.getClaimAsString("sub"));
+      throw new IOException("Authentication error");
+    }
+
     String username = sapToken.getClaimAsString("sub");
     String externalId = this.extIdScheme + ":" + username;
     String email = sapToken.getClaimAsString("email");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasTokenValidatorProvider.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasTokenValidatorProvider.java
new file mode 100644
index 0000000..2f1c28d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasTokenValidatorProvider.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// 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 com.googlesource.gerrit.plugins.oauth.sap;
+
+import static com.googlesource.gerrit.plugins.oauth.sap.SAPIasOAuthService.PROVIDER_NAME;
+
+import com.google.common.base.Splitter;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
+import com.sap.cloud.security.client.DefaultHttpClientFactory;
+import com.sap.cloud.security.config.ClientCredentials;
+import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
+import com.sap.cloud.security.config.OAuth2ServiceConfigurationBuilder;
+import com.sap.cloud.security.config.Service;
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.token.validation.CombiningValidator;
+import com.sap.cloud.security.token.validation.validators.JwtValidatorBuilder;
+import java.util.List;
+
+@Singleton
+public class SAPIasTokenValidatorProvider implements Provider<CombiningValidator<Token>> {
+  private static final String ONDEMAND_DOMAIN = ".ondemand.com";
+  private static final String CLOUD_DOMAIN = ".cloud.sap";
+
+  private final PluginConfig cfg;
+  private final OAuth2ServiceConfiguration serviceConfiguration;
+
+  @Inject
+  SAPIasTokenValidatorProvider(OAuthPluginConfigFactory cfgFactory) {
+    cfg = cfgFactory.create(PROVIDER_NAME);
+
+    List<String> rootUrlParts = Splitter.on('.').splitToList(cfg.getString(InitOAuth.ROOT_URL));
+    String universeSubdomain = rootUrlParts.get(rootUrlParts.size() - 3);
+    serviceConfiguration =
+        OAuth2ServiceConfigurationBuilder.forService(Service.IAS)
+            .withUrl(cfg.getString(InitOAuth.ROOT_URL))
+            .withClientId(cfg.getString(InitOAuth.CLIENT_ID))
+            .withDomains(universeSubdomain + ONDEMAND_DOMAIN, universeSubdomain + CLOUD_DOMAIN)
+            .build();
+  }
+
+  @Override
+  public CombiningValidator<Token> get() {
+    ClientCredentials clientCredentials =
+        new ClientCredentials(
+            cfg.getString(InitOAuth.CLIENT_ID), cfg.getString(InitOAuth.CLIENT_SECRET));
+    return JwtValidatorBuilder.getInstance(serviceConfiguration)
+        .withHttpClient(new DefaultHttpClientFactory().createClient(clientCredentials))
+        .build();
+  }
+}