Skip to content

Commit 4408f2c

Browse files
committed
feat: forgerock oauth provider
Abstracted the shared OIDC token exchange into a new AbstractOIDCAuth2PRovider base class.
1 parent 4816e05 commit 4408f2c

9 files changed

Lines changed: 384 additions & 280 deletions

File tree

plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.apache.cloudstack.context.CallContext;
3232
import org.apache.cloudstack.oauth2.OAuth2AuthManager;
3333
import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse;
34+
import org.apache.cloudstack.oauth2.forgerock.ForgeRockOAuth2Provider;
3435
import org.apache.cloudstack.oauth2.keycloak.KeycloakOAuth2Provider;
3536
import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
3637
import org.apache.commons.collections.MapUtils;
@@ -70,10 +71,10 @@ public class RegisterOAuthProviderCmd extends BaseCmd {
7071
description = "Domain path for domain-specific OAuth provider. Ignored when Domain ID is passed.", since = "4.23.0")
7172
private String domainPath;
7273

73-
@Parameter(name = ApiConstants.AUTHORIZE_URL, type = CommandType.STRING, description = "Authorize URL for OAuth initialization (only required for keycloak provider)")
74+
@Parameter(name = ApiConstants.AUTHORIZE_URL, type = CommandType.STRING, description = "Authorize URL for OAuth initialization (only required for OIDC providers)")
7475
private String authorizeUrl;
7576

76-
@Parameter(name = ApiConstants.TOKEN_URL, type = CommandType.STRING, description = "Token URL for OAuth finalization (only required for keycloak provider)")
77+
@Parameter(name = ApiConstants.TOKEN_URL, type = CommandType.STRING, description = "Token URL for OAuth finalization (only required for OIDC providers)")
7778
private String tokenUrl;
7879

7980
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP,
@@ -134,12 +135,12 @@ public Map getDetails() {
134135

135136
@Override
136137
public void execute() throws ServerApiException, ConcurrentOperationException, EntityExistsException {
137-
if (StringUtils.equals(KeycloakOAuth2Provider.KEYCLOAK_PROVIDER, getProvider())) {
138+
if (StringUtils.equalsAny(getProvider(), KeycloakOAuth2Provider.KEYCLOAK_PROVIDER, ForgeRockOAuth2Provider.FORGEROCK_PROVIDER)) {
138139
if (StringUtils.isBlank(getAuthorizeUrl())) {
139-
throw new ServerApiException(ApiErrorCode.BAD_REQUEST, "Parameter authorizeurl is mandatory for keycloak OAuth Provider");
140+
throw new ServerApiException(ApiErrorCode.BAD_REQUEST, String.format("Parameter authorizeurl is mandatory for %s OAuth Provider", getProvider()));
140141
}
141142
if (StringUtils.isBlank(getTokenUrl())) {
142-
throw new ServerApiException(ApiErrorCode.BAD_REQUEST, "Parameter tokenurl is mandatory for keycloak OAuth Provider");
143+
throw new ServerApiException(ApiErrorCode.BAD_REQUEST, String.format("Parameter tokenurl is mandatory for %s OAuth Provider", getProvider()));
143144
}
144145
}
145146

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Licensed to the Apache Software Foundation (ASF) under one
3+
// or more contributor license agreements. See the NOTICE file
4+
// distributed with this work for additional information
5+
// regarding copyright ownership. The ASF licenses this file
6+
// to you under the Apache License, Version 2.0 (the
7+
// "License"); you may not use this file except in compliance
8+
// with the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing,
13+
// software distributed under the License is distributed on an
14+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
// KIND, either express or implied. See the License for the
16+
// specific language governing permissions and limitations
17+
// under the License.
18+
//
19+
package org.apache.cloudstack.oauth2.forgerock;
20+
21+
import org.apache.cloudstack.oauth2.oidc.AbstractOIDCOAuth2Provider;
22+
23+
public class ForgeRockOAuth2Provider extends AbstractOIDCOAuth2Provider {
24+
25+
public static final String FORGEROCK_PROVIDER = "forgerock";
26+
27+
@Override
28+
public String getName() {
29+
return FORGEROCK_PROVIDER;
30+
}
31+
32+
@Override
33+
public String getDescription() {
34+
return "ForgeRock OAuth2 Provider Plugin";
35+
}
36+
}

plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java

Lines changed: 2 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -18,57 +18,12 @@
1818
//
1919
package org.apache.cloudstack.oauth2.keycloak;
2020

21-
import java.io.IOException;
22-
import java.io.UnsupportedEncodingException;
23-
import java.nio.charset.StandardCharsets;
24-
import java.util.ArrayList;
25-
import java.util.Base64;
26-
import java.util.List;
21+
import org.apache.cloudstack.oauth2.oidc.AbstractOIDCOAuth2Provider;
2722

28-
import javax.inject.Inject;
29-
import javax.ws.rs.core.HttpHeaders;
30-
31-
import org.apache.cloudstack.auth.UserOAuth2Authenticator;
32-
import org.apache.cloudstack.oauth2.dao.OauthProviderDao;
33-
import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
34-
import org.apache.commons.lang3.StringUtils;
35-
import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer;
36-
import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
37-
import org.apache.http.NameValuePair;
38-
import org.apache.http.client.entity.UrlEncodedFormEntity;
39-
import org.apache.http.client.methods.CloseableHttpResponse;
40-
import org.apache.http.client.methods.HttpPost;
41-
import org.apache.http.impl.client.CloseableHttpClient;
42-
import org.apache.http.impl.client.HttpClientBuilder;
43-
import org.apache.http.message.BasicNameValuePair;
44-
import org.apache.http.util.EntityUtils;
45-
46-
import com.cloud.exception.CloudAuthenticationException;
47-
import com.cloud.utils.component.AdapterBase;
48-
import com.cloud.utils.exception.CloudRuntimeException;
49-
import com.google.gson.JsonElement;
50-
import com.google.gson.JsonObject;
51-
import com.google.gson.JsonParser;
52-
53-
public class KeycloakOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator {
23+
public class KeycloakOAuth2Provider extends AbstractOIDCOAuth2Provider {
5424

5525
public static final String KEYCLOAK_PROVIDER = "keycloak";
5626

57-
protected String idToken = null;
58-
59-
@Inject
60-
OauthProviderDao oauthProviderDao;
61-
62-
private CloseableHttpClient httpClient;
63-
64-
public KeycloakOAuth2Provider() {
65-
this(HttpClientBuilder.create().build());
66-
}
67-
68-
public KeycloakOAuth2Provider(CloseableHttpClient httpClient) {
69-
this.httpClient = httpClient;
70-
}
71-
7227
@Override
7328
public String getName() {
7429
return KEYCLOAK_PROVIDER;
@@ -78,117 +33,4 @@ public String getName() {
7833
public String getDescription() {
7934
return "Keycloak OAuth2 Provider Plugin";
8035
}
81-
82-
@Override
83-
public boolean verifyUser(String email, String secretCode) {
84-
return verifyUser(email, secretCode, null);
85-
}
86-
87-
@Override
88-
public boolean verifyUser(String email, String secretCode, Long domainId) {
89-
if (StringUtils.isAnyEmpty(email, secretCode)) {
90-
throw new CloudAuthenticationException("Either email or secret code should not be null/empty");
91-
}
92-
93-
OauthProviderVO providerVO = oauthProviderDao.findByProviderAndDomainWithGlobalFallback(getName(), domainId);
94-
if (providerVO == null) {
95-
throw new CloudAuthenticationException("Keycloak provider is not registered, so user cannot be verified");
96-
}
97-
98-
String verifiedEmail = verifySecretCodeAndFetchEmail(secretCode, domainId);
99-
if (StringUtils.isBlank(verifiedEmail) || !email.equals(verifiedEmail)) {
100-
throw new CloudRuntimeException("Unable to verify the email address with the provided secret");
101-
}
102-
clearIdToken();
103-
104-
return true;
105-
}
106-
107-
@Override
108-
public String verifySecretCodeAndFetchEmail(String secretCode) {
109-
return verifySecretCodeAndFetchEmail(secretCode, null);
110-
}
111-
112-
@Override
113-
public String verifySecretCodeAndFetchEmail(String secretCode, Long domainId) {
114-
OauthProviderVO provider = oauthProviderDao.findByProviderAndDomainWithGlobalFallback(getName(), domainId);
115-
if (provider == null) {
116-
throw new CloudAuthenticationException("Keycloak provider is not registered, so user cannot be verified");
117-
}
118-
119-
if (StringUtils.isBlank(idToken)) {
120-
String auth = provider.getClientId() + ":" + provider.getSecretKey();
121-
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
122-
123-
List<NameValuePair> params = new ArrayList<>();
124-
params.add(new BasicNameValuePair("grant_type", "authorization_code"));
125-
params.add(new BasicNameValuePair("code", secretCode));
126-
params.add(new BasicNameValuePair("redirect_uri", provider.getRedirectUri()));
127-
128-
HttpPost post = new HttpPost(provider.getTokenUrl());
129-
post.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + encodedAuth);
130-
131-
try {
132-
post.setEntity(new UrlEncodedFormEntity(params));
133-
} catch (UnsupportedEncodingException e) {
134-
throw new CloudRuntimeException("Unable to generate URL parameters: " + e.getMessage());
135-
}
136-
137-
try (CloseableHttpResponse response = httpClient.execute(post)) {
138-
String body = EntityUtils.toString(response.getEntity());
139-
140-
if (response.getStatusLine().getStatusCode() != 200) {
141-
throw new CloudRuntimeException("Keycloak error during token generation: " + body);
142-
}
143-
144-
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
145-
JsonElement fetchedIdToken = json.get("id_token");
146-
if (fetchedIdToken == null) {
147-
throw new CloudRuntimeException("No id_token found in token");
148-
}
149-
String idTokenAsString = fetchedIdToken.getAsString();
150-
validateIdToken(idTokenAsString , provider);
151-
152-
this.idToken = idTokenAsString ;
153-
} catch (IOException e) {
154-
throw new CloudRuntimeException("Unable to connect to Keycloak server", e);
155-
}
156-
}
157-
158-
return obtainEmail(idToken, provider);
159-
}
160-
161-
@Override
162-
public String getUserEmailAddress() throws CloudRuntimeException {
163-
return null;
164-
}
165-
166-
private void validateIdToken(String idTokenStr, OauthProviderVO provider) {
167-
JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(idTokenStr);
168-
JwtClaims claims = jwtConsumer.getJwtToken().getClaims();
169-
170-
if (!claims.getAudiences().contains(provider.getClientId())) {
171-
throw new CloudAuthenticationException("Audience mismatch");
172-
}
173-
}
174-
175-
private String obtainEmail(String idTokenStr, OauthProviderVO provider) {
176-
JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(idTokenStr);
177-
JwtClaims claims = jwtConsumer.getJwtToken().getClaims();
178-
179-
if (!claims.getAudiences().contains(provider.getClientId())) {
180-
throw new CloudAuthenticationException("Audience mismatch");
181-
}
182-
183-
return (String) claims.getClaim("email");
184-
}
185-
186-
protected void clearIdToken() {
187-
idToken = null;
188-
}
189-
190-
public void setHttpClient(CloseableHttpClient httpClient) {
191-
this.httpClient = httpClient;
192-
}
193-
19436
}

0 commit comments

Comments
 (0)