nagaboinaramgopal commented on code in PR #14205:
URL: https://github.com/apache/cloudstack/pull/14205#discussion_r4207177878


##########
plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/oidc/GenericOIDCOAuth2Provider.java:
##########
@@ -0,0 +1,393 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 org.apache.cloudstack.oauth2.oidc;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.ws.rs.core.HttpHeaders;
+
+import org.apache.cloudstack.auth.UserOAuth2Authenticator;
+import org.apache.cloudstack.oauth2.dao.OauthProviderDao;
+import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm;
+import org.apache.cxf.rs.security.jose.jwk.JsonWebKey;
+import org.apache.cxf.rs.security.jose.jwk.JsonWebKeys;
+import org.apache.cxf.rs.security.jose.jwk.JwkUtils;
+import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer;
+import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
+import org.apache.cxf.rs.security.jose.jwt.JwtUtils;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.cloud.exception.CloudAuthenticationException;
+import com.cloud.utils.component.AdapterBase;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+/**
+ * A single provider implementation for any OIDC compliant identity provider. 
Unlike the per vendor
+ * providers it is not bound to one registration: the registration is selected 
by name on every call,
+ * so one bean serves any number of registrations of type "oidc".
+ */
+public class GenericOIDCOAuth2Provider extends AdapterBase implements 
UserOAuth2Authenticator {
+
+    public static final String OIDC_PROVIDER_TYPE = "oidc";
+
+    private static final String DISCOVERY_PATH = 
"/.well-known/openid-configuration";
+    private static final int CLOCK_SKEW_SECONDS = 60;
+    private static final long METADATA_CACHE_MINUTES = 60;
+    private static final long VERIFIED_EMAIL_CACHE_SECONDS = 60;
+    private static final int HTTP_TIMEOUT_MILLIS = 10000;
+
+    @Inject
+    OauthProviderDao oauthProviderDao;
+
+    private CloseableHttpClient httpClient;
+
+    private final Cache<String, OIDCMetadata> metadataCache =
+            Caffeine.newBuilder()
+                    .expireAfterWrite(METADATA_CACHE_MINUTES, TimeUnit.MINUTES)
+                    .maximumSize(64)
+                    .build();
+
+    private final Cache<String, String> verifiedEmailCache =
+            Caffeine.newBuilder()
+                    .expireAfterWrite(VERIFIED_EMAIL_CACHE_SECONDS, 
TimeUnit.SECONDS)
+                    .maximumSize(1024)
+                    .build();
+
+    private final Cache<String, JsonWebKeys> jwksCache =
+            Caffeine.newBuilder()
+                    .expireAfterWrite(METADATA_CACHE_MINUTES, TimeUnit.MINUTES)
+                    .maximumSize(64)
+                    .build();
+
+    protected Logger logger = LogManager.getLogger(getClass());
+
+    public GenericOIDCOAuth2Provider() {
+        this(HttpClientBuilder.create()
+                .setDefaultRequestConfig(RequestConfig.custom()
+                        .setConnectTimeout(HTTP_TIMEOUT_MILLIS)
+                        .setConnectionRequestTimeout(HTTP_TIMEOUT_MILLIS)
+                        .setSocketTimeout(HTTP_TIMEOUT_MILLIS)
+                        .build())
+                .build());
+    }
+
+    public GenericOIDCOAuth2Provider(CloseableHttpClient httpClient) {
+        this.httpClient = httpClient;
+    }
+
+    @Override
+    public String getName() {
+        return OIDC_PROVIDER_TYPE;
+    }
+
+    @Override
+    public String getDescription() {
+        return "Generic OpenID Connect Provider Plugin";
+    }
+
+    @Override
+    public boolean verifyUser(String email, String secretCode) {
+        throw new CloudRuntimeException("The generic OIDC provider requires 
the registered provider name");
+    }
+
+    @Override
+    public boolean verifyUser(String email, String secretCode, Long domainId) {
+        throw new CloudRuntimeException("The generic OIDC provider requires 
the registered provider name");
+    }
+
+    @Override
+    public String verifySecretCodeAndFetchEmail(String secretCode) {
+        throw new CloudRuntimeException("The generic OIDC provider requires 
the registered provider name");
+    }
+
+    @Override
+    public String verifySecretCodeAndFetchEmail(String secretCode, Long 
domainId) {
+        throw new CloudRuntimeException("The generic OIDC provider requires 
the registered provider name");
+    }
+
+    @Override
+    public boolean verifyUser(String email, String secretCode, Long domainId, 
String providerName) {
+        if (StringUtils.isAnyEmpty(email, secretCode)) {
+            throw new CloudAuthenticationException("Either email or secret 
code should not be null/empty");
+        }
+
+        String verifiedEmail = 
verifiedEmailCache.asMap().remove(verifiedEmailKey(providerName, secretCode, 
domainId));
+        if (verifiedEmail == null) {
+            verifiedEmail = resolveEmail(secretCode, domainId, providerName);
+        }
+        if (StringUtils.isBlank(verifiedEmail) || 
!email.equals(verifiedEmail)) {
+            throw new CloudRuntimeException("Unable to verify the email 
address with the provided secret");
+        }
+
+        return true;
+    }
+
+    @Override
+    public String verifySecretCodeAndFetchEmail(String secretCode, Long 
domainId, String providerName) {
+        String email = resolveEmail(secretCode, domainId, providerName);
+        verifiedEmailCache.put(verifiedEmailKey(providerName, secretCode, 
domainId), email);
+        return email;
+    }
+
+    protected String resolveEmail(String secretCode, Long domainId, String 
providerName) {
+        OauthProviderVO provider = findRegistration(providerName, domainId);
+        OIDCMetadata metadata = getMetadata(provider);
+        String idToken = exchangeAuthorizationCode(secretCode, provider, 
metadata);
+
+        return validateAndExtractEmail(idToken, provider, metadata);
+    }
+
+    @Override
+    public String getUserEmailAddress() throws CloudRuntimeException {
+        return null;
+    }
+
+    private String verifiedEmailKey(String providerName, String secretCode, 
Long domainId) {
+        return DigestUtils.sha256Hex(providerName + ":" + domainId + ":" + 
secretCode);

Review Comment:
   It keyed on the raw request domain, so a verify with no domain and a login 
that resolved to root produced different keys and the code was exchanged a 
second time. It now keys on the resolved registration's uuid, so the null and 
the concrete domain hit the same entry and the code is redeemed once. Added a 
test that verifies with no domain, logs in on domain 1, and asserts a single 
exchange. Fixed in a6b99d9a1d.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to