|
20 | 20 | */ |
21 | 21 | package eu.openanalytics.containerproxy.auth.impl; |
22 | 22 |
|
| 23 | +import java.util.ArrayList; |
23 | 24 | import java.util.Collections; |
24 | 25 | import java.util.HashSet; |
25 | 26 | import java.util.List; |
|
41 | 42 | import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; |
42 | 43 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; |
43 | 44 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; |
| 45 | +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; |
| 46 | +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; |
44 | 47 | import org.springframework.security.oauth2.client.registration.ClientRegistration; |
45 | 48 | import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; |
46 | 49 | import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; |
47 | 50 | import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; |
48 | 51 | import org.springframework.security.oauth2.core.AuthorizationGrantType; |
| 52 | +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; |
49 | 53 | import org.springframework.security.oauth2.core.oidc.OidcIdToken; |
| 54 | +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; |
| 55 | +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; |
50 | 56 | import org.springframework.security.oauth2.core.oidc.user.OidcUser; |
51 | 57 | import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; |
52 | 58 |
|
53 | 59 | import eu.openanalytics.containerproxy.auth.IAuthenticationBackend; |
54 | 60 | import eu.openanalytics.containerproxy.util.SessionHelper; |
| 61 | +import net.minidev.json.JSONArray; |
| 62 | +import net.minidev.json.parser.JSONParser; |
| 63 | +import net.minidev.json.parser.ParseException; |
55 | 64 |
|
56 | 65 | public class OpenIDAuthenticationBackend implements IAuthenticationBackend { |
57 | 66 |
|
@@ -89,7 +98,9 @@ public void configureHttpSecurity(HttpSecurity http) throws Exception { |
89 | 98 | .loginPage("/login") |
90 | 99 | .clientRegistrationRepository(clientRegistrationRepo) |
91 | 100 | .authorizedClientService(authorizedClientService) |
92 | | - .userInfoEndpoint().userAuthoritiesMapper(createAuthoritiesMapper()); |
| 101 | + .userInfoEndpoint() |
| 102 | + .userAuthoritiesMapper(createAuthoritiesMapper()) |
| 103 | + .oidcUserService(createOidcUserService()); |
93 | 104 | } |
94 | 105 |
|
95 | 106 | @Override |
@@ -165,21 +176,82 @@ protected GrantedAuthoritiesMapper createAuthoritiesMapper() { |
165 | 176 | String claims = idToken.getClaims().entrySet().stream() |
166 | 177 | .map(e -> String.format("%s -> %s", e.getKey(), e.getValue())) |
167 | 178 | .collect(Collectors.joining(lineSep)); |
168 | | - log.debug(String.format("Checking for roles in claim '%s'. Available claims in ID token:%s%s", |
169 | | - rolesClaimName, lineSep, claims)); |
170 | | - |
| 179 | + log.debug(String.format("Checking for roles in claim '%s'. Available claims in ID token (%d):%s%s", |
| 180 | + rolesClaimName, idToken.getClaims().size(), lineSep, claims)); |
171 | 181 | } |
172 | 182 |
|
| 183 | + Object claimValue = idToken.getClaims().get(rolesClaimName); |
| 184 | + if (claimValue == null) { |
| 185 | + log.debug("No matching claim found."); |
| 186 | + } else { |
| 187 | + log.debug(String.format("Matching claim found: %s -> %s (%s)", rolesClaimName, claimValue, claimValue.getClass())); |
| 188 | + } |
| 189 | + |
| 190 | + // Workaround: in some cases, getClaimAsStringList fails to parse?? |
173 | 191 | List<String> roles = idToken.getClaimAsStringList(rolesClaimName); |
174 | | - if (roles == null) continue; |
| 192 | + if (roles == null && claimValue instanceof String) { |
| 193 | + List<String> parsedRoles = new ArrayList<>(); |
| 194 | + try { |
| 195 | + Object value = new JSONParser(JSONParser.MODE_PERMISSIVE).parse((String) claimValue); |
| 196 | + if (value instanceof List) { |
| 197 | + List<?> valueList = (List<?>) value; |
| 198 | + valueList.forEach(o -> parsedRoles.add(o.toString())); |
| 199 | + } |
| 200 | + } catch (ParseException e) { |
| 201 | + // Unable to parse JSON |
| 202 | + } |
| 203 | + roles = parsedRoles; |
| 204 | + } |
| 205 | + if (roles == null) { |
| 206 | + if (log.isDebugEnabled()) log.debug("Failed to parse claim value as an array: " + claimValue); |
| 207 | + continue; |
| 208 | + } |
| 209 | + |
175 | 210 | for (String role: roles) { |
176 | 211 | String mappedRole = role.toUpperCase().startsWith("ROLE_") ? role : "ROLE_" + role; |
177 | 212 | mappedAuthorities.add(new SimpleGrantedAuthority(mappedRole.toUpperCase())); |
178 | 213 | } |
| 214 | + if (log.isDebugEnabled()) log.debug("The following roles were successfully parsed: " + roles); |
179 | 215 | } |
180 | 216 | } |
181 | 217 | return mappedAuthorities; |
182 | 218 | }; |
183 | 219 | } |
184 | 220 | } |
| 221 | + |
| 222 | + protected OidcUserService createOidcUserService() { |
| 223 | + // Use a custom UserService that supports the 'emails' array attribute. |
| 224 | + return new OidcUserService() { |
| 225 | + @Override |
| 226 | + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { |
| 227 | + OidcUser user = super.loadUser(userRequest); |
| 228 | + String nameAttributeKey = environment.getProperty("proxy.openid.username-attribute", "email"); |
| 229 | + return new CustomNameOidcUser(new HashSet<>(user.getAuthorities()), user.getIdToken(), user.getUserInfo(), nameAttributeKey); |
| 230 | + } |
| 231 | + }; |
| 232 | + } |
| 233 | + |
| 234 | + private static class CustomNameOidcUser extends DefaultOidcUser { |
| 235 | + |
| 236 | + private static final long serialVersionUID = 7563253562760236634L; |
| 237 | + private static final String ID_ATTR_EMAILS = "emails"; |
| 238 | + |
| 239 | + private boolean isEmailsAttribute; |
| 240 | + |
| 241 | + public CustomNameOidcUser(Set<GrantedAuthority> authorities, OidcIdToken idToken, OidcUserInfo userInfo, String nameAttributeKey) { |
| 242 | + super(authorities, idToken, userInfo, nameAttributeKey); |
| 243 | + this.isEmailsAttribute = nameAttributeKey.equals(ID_ATTR_EMAILS); |
| 244 | + } |
| 245 | + |
| 246 | + @Override |
| 247 | + public String getName() { |
| 248 | + if (isEmailsAttribute) { |
| 249 | + Object emails = getAttributes().get(ID_ATTR_EMAILS); |
| 250 | + if (emails instanceof String[]) return ((String[]) emails)[0]; |
| 251 | + else if (emails instanceof JSONArray) return ((JSONArray) emails).get(0).toString(); |
| 252 | + else return emails.toString(); |
| 253 | + } |
| 254 | + else return super.getName(); |
| 255 | + } |
| 256 | + } |
185 | 257 | } |
0 commit comments