1
1
package com .github .bcgov .keycloak .protocol .oidc .mappers ;
2
2
3
3
import com .fasterxml .jackson .databind .JsonNode ;
4
+
5
+ import jakarta .ws .rs .ProcessingException ;
4
6
import jakarta .ws .rs .client .Client ;
5
7
import jakarta .ws .rs .client .ClientBuilder ;
8
+
6
9
import org .jboss .logging .Logger ;
10
+ import org .keycloak .broker .provider .IdentityBrokerException ;
11
+ import org .keycloak .crypto .KeyWrapper ;
12
+ import org .keycloak .crypto .SignatureProvider ;
13
+ import org .keycloak .jose .JOSE ;
14
+ import org .keycloak .jose .JOSEParser ;
15
+ import org .keycloak .jose .jws .JWSInput ;
16
+ import org .keycloak .keys .loader .PublicKeyStorageManager ;
7
17
import org .keycloak .models .*;
8
18
import org .keycloak .protocol .oidc .OIDCLoginProtocol ;
9
19
import org .keycloak .protocol .oidc .mappers .*;
12
22
import org .keycloak .representations .IDToken ;
13
23
import org .keycloak .util .JsonSerialization ;
14
24
25
+ import java .io .IOException ;
26
+ import java .nio .charset .StandardCharsets ;
15
27
import java .util .ArrayList ;
16
28
import java .util .HashMap ;
17
29
import java .util .List ;
18
30
import java .util .Map ;
19
- import java .util .Base64 ;
20
31
21
32
/** @author <a href="mailto:[email protected] ">Junmin Ahn</a> */
22
33
public class IDPUserinfoMapper extends AbstractOIDCProtocolMapper
23
34
implements OIDCAccessTokenMapper , OIDCIDTokenMapper , UserInfoTokenMapper {
24
35
25
36
private static final Logger logger = Logger .getLogger (IDPUserinfoMapper .class );
26
37
27
- private static final String BEARER = "Bearer" ;
28
-
29
38
private static final List <ProviderConfigProperty > configProperties = new ArrayList <ProviderConfigProperty >();
30
39
31
40
public static final String CLAIM_VALUE = "claim.value" ;
32
41
33
- public static final String USER_ATTRIBUTE = "userAttribute " ;
42
+ public static final String USER_ATTRIBUTES = "userAttributes " ;
34
43
35
- public static final String DECODE_USERINFO_RESPONSE = "decodeUserInfoResponse" ;
44
+ public static final String SIGNATURE_EXPECTED = "signatureExpected" ;
45
+
46
+ public static final String ENCRYPTION_EXPECTED = "encryptionExpected" ;
36
47
37
48
static {
38
- configProperties .add (new ProviderConfigProperty (DECODE_USERINFO_RESPONSE , "Decode UserInfo Response" ,
39
- "Decode response returned from IDP userinfo endpoint" , ProviderConfigProperty .BOOLEAN_TYPE , false ));
40
- configProperties .add (new ProviderConfigProperty (USER_ATTRIBUTE , "User Attribute" ,
41
- "User Attribute returned from IDP userinfo endpoint" , ProviderConfigProperty .STRING_TYPE , null ));
49
+ configProperties .add (new ProviderConfigProperty (SIGNATURE_EXPECTED , "Signature Expected" ,
50
+ "Whether the signature should be verified" , ProviderConfigProperty .BOOLEAN_TYPE , false ));
51
+
52
+ configProperties .add (new ProviderConfigProperty (ENCRYPTION_EXPECTED , "Encryption Expected" ,
53
+ "Whether the userinfo response requires decryption" , ProviderConfigProperty .BOOLEAN_TYPE ,
54
+ false ));
55
+
56
+ configProperties .add (new ProviderConfigProperty (USER_ATTRIBUTES , "User Attributes" ,
57
+ "List of user attributes returned from IDP userinfo endpoint" , ProviderConfigProperty .STRING_TYPE , null ));
42
58
43
59
OIDCAttributeMapperHelper .addTokenClaimNameConfig (configProperties );
44
60
OIDCAttributeMapperHelper .addIncludeInTokensConfig (configProperties , IDPUserinfoMapper .class );
@@ -86,17 +102,6 @@ private static JsonNode parseJson(String json) {
86
102
}
87
103
}
88
104
89
- private static String decodeUserInfoResponse (String token ) {
90
- try {
91
- String [] tokenParts = token .split ("\\ ." );
92
- Base64 .Decoder decoder = Base64 .getUrlDecoder ();
93
- String payload = new String (decoder .decode (tokenParts [1 ]));
94
- return payload ;
95
- } catch (Exception e ) {
96
- return null ;
97
- }
98
- }
99
-
100
105
@ Override
101
106
protected void setClaim (
102
107
IDToken token ,
@@ -108,6 +113,8 @@ protected void setClaim(
108
113
String idp = userSession .getNotes ().get ("identity_provider" );
109
114
RealmModel realm = userSession .getRealm ();
110
115
IdentityProviderModel identityProviderConfig = realm .getIdentityProviderByAlias (idp );
116
+ JsonNode userInfo ;
117
+ JWSInput jws ;
111
118
112
119
if (identityProviderConfig .isStoreToken ()) {
113
120
IdentityProviderModel identityProviderModel = realm .getIdentityProviderByAlias (idp );
@@ -119,28 +126,56 @@ protected void setClaim(
119
126
String brokerToken = identity .getToken ();
120
127
AccessTokenResponse brokerAccessToken = parseTokenString (brokerToken );
121
128
Client httpClient = ClientBuilder .newClient ();
122
- String userinfoString = httpClient
123
- .target (userInfoUrl )
124
- .request ()
125
- .header ("Authorization" , "Bearer " + brokerAccessToken .getToken ())
126
- .get (String .class );
127
- boolean decode = Boolean .parseBoolean (mappingModel .getConfig ().get (DECODE_USERINFO_RESPONSE ));
128
- if (decode ) {
129
- userinfoString = decodeUserInfoResponse (userinfoString );
130
- }
129
+ String userinfoResponse ;
130
+
131
131
try {
132
- JsonNode jsonNode = parseJson (userinfoString );
133
- if (jsonNode == null ) {
134
- logger .error ("null response returned from [" + idp + "] userinfo URL" );
132
+ userinfoResponse = httpClient
133
+ .target (userInfoUrl )
134
+ .request ()
135
+ .header ("Authorization" , "Bearer " + brokerAccessToken .getToken ())
136
+ .get (String .class );
137
+ } catch (Exception e ) {
138
+ throw new ProcessingException ("Failed to call userinfo endpoint" , e );
139
+ }
140
+
141
+ Boolean signatureExpected = Boolean .parseBoolean (mappingModel .getConfig ().get (SIGNATURE_EXPECTED ));
142
+
143
+ if (signatureExpected ) {
144
+
145
+ try {
146
+ JOSE joseToken = JOSEParser .parse (userinfoResponse );
147
+
148
+ // common signed JWS token
149
+ jws = (JWSInput ) joseToken ;
150
+
151
+ } catch (Exception e ) {
152
+ throw new IdentityBrokerException ("Error parsing userinfo response" , e );
153
+ }
154
+
155
+ // verify signature of the JWS
156
+ if (!verify (keycloakSession , jws )) {
157
+ throw new IdentityBrokerException ("token signature validation failed" );
158
+ }
159
+
160
+ try {
161
+ userInfo = JsonSerialization .readValue (new String (jws .getContent (), StandardCharsets .UTF_8 ),
162
+ JsonNode .class );
163
+ } catch (IOException e ) {
164
+ throw new IdentityBrokerException ("Error parsing userinfo content" , e );
135
165
}
166
+ } else {
167
+ userInfo = parseJson (userinfoResponse );
168
+ }
169
+
170
+ // process string value of user attributes
171
+ String userAttributes = mappingModel .getConfig ().get (USER_ATTRIBUTES );
172
+ String [] userAttributesArr = userAttributes == null ? new String [0 ] : userAttributes .split ("," );
173
+
174
+ if (userAttributesArr .length > 0 ) {
136
175
Map <String , Object > otherClaims = token .getOtherClaims ();
137
- otherClaims .put (
138
- mappingModel .getConfig ().get (OIDCAttributeMapperHelper .TOKEN_CLAIM_NAME ),
139
- jsonNode .get (mappingModel .getConfig ().get (OIDCAttributeMapperHelper .TOKEN_CLAIM_NAME )));
140
- } catch (NullPointerException e ) {
141
- logger .errorf ("'%s' returned invalid response" , idp );
142
- } catch (Exception e ) {
143
- logger .errorf ("unable to fetch attributes from userinfo endpoint '%s'" , userInfoUrl );
176
+ for (String userAttribute : userAttributesArr ) {
177
+ otherClaims .put (userAttribute , getJsonProperty (userInfo , userAttribute ));
178
+ }
144
179
}
145
180
} else {
146
181
logger .error ("Identity Provider [" + idp + "] does not have userinfo URL." );
@@ -165,4 +200,45 @@ public static ProtocolMapperModel create(
165
200
mapper .setConfig (config );
166
201
return mapper ;
167
202
}
203
+
204
+ protected boolean verify (KeycloakSession session , JWSInput jws ) {
205
+
206
+ try {
207
+ KeyWrapper key = PublicKeyStorageManager .getIdentityProviderKeyWrapper (session , session .getContext ().getRealm (),
208
+ getConfig (),
209
+ jws );
210
+ if (key == null ) {
211
+ logger .debugf ("[IDP Userinfo] Failed to verify userinfo JWT signature, public key not found for algorithm %s" ,
212
+ jws .getHeader ().getRawAlgorithm ());
213
+ return false ;
214
+ }
215
+ String algorithm = jws .getHeader ().getRawAlgorithm ();
216
+ if (key .getAlgorithm () == null ) {
217
+ key .setAlgorithm (algorithm );
218
+ }
219
+ SignatureProvider signatureProvider = session .getProvider (SignatureProvider .class , algorithm );
220
+ if (signatureProvider == null ) {
221
+ logger .debugf ("Failed to verify userinfo JWT, signature provider not found for algorithm %s" , algorithm );
222
+ return false ;
223
+ }
224
+
225
+ return signatureProvider .verifier (key ).verify (jws .getEncodedSignatureInput ().getBytes (StandardCharsets .UTF_8 ),
226
+ jws .getSignature ());
227
+ } catch (Exception e ) {
228
+ logger .debug ("Failed to verify signature of userinfo JWT" , e );
229
+ return false ;
230
+ }
231
+ }
232
+
233
+ public String getJsonProperty (JsonNode jsonNode , String name ) {
234
+ if (jsonNode .has (name ) && !jsonNode .get (name ).isNull ()) {
235
+ String s = jsonNode .get (name ).asText ();
236
+ if (s != null && !s .isEmpty ())
237
+ return s ;
238
+ else
239
+ return null ;
240
+ }
241
+
242
+ return null ;
243
+ }
168
244
}
0 commit comments