1818//
1919package 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