From d1e8529a7b23bb07e8d1d17613b0d2350a72e185 Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Mon, 1 Jun 2015 21:11:19 -0400 Subject: [PATCH] expose ID Token and UserInfo to the AuthoritiesProvider and AuthoritiesMapper, both extensible closes #699 closes #761 --- .../client/NamedAdminAuthoritiesMapper.java | 50 ++++++------- .../client/OIDCAuthenticationProvider.java | 72 +++++++++++++------ .../connect/client/OIDCAuthoritiesMapper.java | 40 +++++++++++ .../openid/connect/model/DefaultUserInfo.java | 6 ++ .../convert/JsonObjectStringConverter.java | 57 +++++++++++++++ .../db/tables/hsql_database_tables.sql | 3 +- .../filter/AuthorizationRequestFilter.java | 4 +- 7 files changed, 181 insertions(+), 51 deletions(-) create mode 100644 openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthoritiesMapper.java create mode 100644 openid-connect-common/src/main/java/org/mitre/openid/connect/model/convert/JsonObjectStringConverter.java diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/NamedAdminAuthoritiesMapper.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/NamedAdminAuthoritiesMapper.java index f0eb55728..26257aa10 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/NamedAdminAuthoritiesMapper.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/NamedAdminAuthoritiesMapper.java @@ -19,14 +19,19 @@ */ package org.mitre.openid.connect.client; +import java.text.ParseException; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import org.mitre.openid.connect.model.UserInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; -import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; + +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; /** * @@ -37,31 +42,36 @@ import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper * @author jricher * */ -public class NamedAdminAuthoritiesMapper implements GrantedAuthoritiesMapper { +public class NamedAdminAuthoritiesMapper implements OIDCAuthoritiesMapper { + private static Logger logger = LoggerFactory.getLogger(NamedAdminAuthoritiesMapper.class); + private static final SimpleGrantedAuthority ROLE_ADMIN = new SimpleGrantedAuthority("ROLE_ADMIN"); private static final SimpleGrantedAuthority ROLE_USER = new SimpleGrantedAuthority("ROLE_USER"); private Set admins = new HashSet(); - private GrantedAuthoritiesMapper chain = new NullAuthoritiesMapper(); - @Override - public Collection mapAuthorities(Collection authorities) { + public Collection mapAuthorities(JWT idToken, UserInfo userInfo) { Set out = new HashSet(); - out.addAll(authorities); + try { + ReadOnlyJWTClaimsSet claims = idToken.getJWTClaimsSet(); + + SubjectIssuerGrantedAuthority authority = new SubjectIssuerGrantedAuthority(claims.getSubject(), claims.getIssuer()); + out.add(authority); - for (GrantedAuthority authority : authorities) { if (admins.contains(authority)) { out.add(ROLE_ADMIN); } + + // everybody's a user by default + out.add(ROLE_USER); + + } catch (ParseException e) { + logger.error("Unable to parse ID Token inside of authorities mapper (huh?)"); } - - // everybody's a user by default - out.add(ROLE_USER); - - return chain.mapAuthorities(out); + return out; } /** @@ -78,18 +88,4 @@ public class NamedAdminAuthoritiesMapper implements GrantedAuthoritiesMapper { this.admins = admins; } - /** - * @return the chain - */ - public GrantedAuthoritiesMapper getChain() { - return chain; - } - - /** - * @param chain the chain to set - */ - public void setChain(GrantedAuthoritiesMapper chain) { - this.chain = chain; - } - } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationProvider.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationProvider.java index 0fa7b4e0b..04987c4f0 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationProvider.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationProvider.java @@ -16,18 +16,22 @@ *******************************************************************************/ package org.mitre.openid.connect.client; +import java.text.ParseException; import java.util.Collection; import org.mitre.openid.connect.model.OIDCAuthenticationToken; import org.mitre.openid.connect.model.UserInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UsernameNotFoundException; import com.google.common.base.Strings; -import com.google.common.collect.Lists; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; /** * @author nemonik @@ -35,9 +39,11 @@ import com.google.common.collect.Lists; */ public class OIDCAuthenticationProvider implements AuthenticationProvider { + private static Logger logger = LoggerFactory.getLogger(OIDCAuthenticationProvider.class); + private UserInfoFetcher userInfoFetcher = new UserInfoFetcher(); - private GrantedAuthoritiesMapper authoritiesMapper = new NamedAdminAuthoritiesMapper(); + private OIDCAuthoritiesMapper authoritiesMapper = new NamedAdminAuthoritiesMapper(); /* * (non-Javadoc) @@ -46,8 +52,7 @@ public class OIDCAuthenticationProvider implements AuthenticationProvider { * authenticate(org.springframework.security.core.Authentication) */ @Override - public Authentication authenticate(final Authentication authentication) - throws AuthenticationException { + public Authentication authenticate(final Authentication authentication) throws AuthenticationException { if (!supports(authentication.getClass())) { return null; @@ -56,33 +61,56 @@ public class OIDCAuthenticationProvider implements AuthenticationProvider { if (authentication instanceof OIDCAuthenticationToken) { OIDCAuthenticationToken token = (OIDCAuthenticationToken) authentication; - - Collection authorities = Lists.newArrayList(new SubjectIssuerGrantedAuthority(token.getSub(), token.getIssuer())); - - UserInfo userInfo = userInfoFetcher.loadUserInfo(token); - - if (userInfo == null) { - // TODO: user Info not found -- error? - } else { - if (!Strings.isNullOrEmpty(userInfo.getSub()) && !userInfo.getSub().equals(token.getSub())) { - // the userinfo came back and the user_id fields don't match what was in the id_token - throw new UsernameNotFoundException("user_id mismatch between id_token and user_info call: " + token.getSub() + " / " + userInfo.getSub()); + + try { + + // get the ID Token value out + String idTokenString = token.getIdTokenValue(); + JWT idToken = JWTParser.parse(idTokenString); + + // load the user info if we can + UserInfo userInfo = userInfoFetcher.loadUserInfo(token); + + if (userInfo == null) { + // user info not found -- could be an error, could be fine + } else { + // if we found userinfo, double check it + if (!Strings.isNullOrEmpty(userInfo.getSub()) && !userInfo.getSub().equals(token.getSub())) { + // the userinfo came back and the user_id fields don't match what was in the id_token + throw new UsernameNotFoundException("user_id mismatch between id_token and user_info call: " + token.getSub() + " / " + userInfo.getSub()); + } } + + return createAuthenticationToken(token, authoritiesMapper.mapAuthorities(idToken, userInfo), userInfo); + } catch (ParseException e) { + logger.error("Unable to parse ID token in the token"); + return null; } - - return new OIDCAuthenticationToken(token.getSub(), - token.getIssuer(), - userInfo, authoritiesMapper.mapAuthorities(authorities), - token.getIdTokenValue(), token.getAccessTokenValue(), token.getRefreshTokenValue()); } return null; } + /** + * Override this function to return a different kind of Authentication, processes the authorities differently, + * or do post-processing based on the UserInfo object. + * + * @param token + * @param authorities + * @param userInfo + * @return + */ + protected Authentication createAuthenticationToken(OIDCAuthenticationToken token, Collection authorities, UserInfo userInfo) { + return new OIDCAuthenticationToken(token.getSub(), + token.getIssuer(), + userInfo, authorities, + token.getIdTokenValue(), token.getAccessTokenValue(), token.getRefreshTokenValue()); + } + /** * @param authoritiesMapper */ - public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { + public void setAuthoritiesMapper(OIDCAuthoritiesMapper authoritiesMapper) { this.authoritiesMapper = authoritiesMapper; } diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthoritiesMapper.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthoritiesMapper.java new file mode 100644 index 000000000..4300d26a9 --- /dev/null +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthoritiesMapper.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright 2015 The MITRE Corporation + * and the MIT Kerberos and Internet Trust Consortium + * + * Licensed 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.mitre.openid.connect.client; + +import java.util.Collection; + +import org.mitre.openid.connect.model.UserInfo; +import org.springframework.security.core.GrantedAuthority; + +import com.nimbusds.jwt.JWT; + +/** + * @author jricher + * + */ +public interface OIDCAuthoritiesMapper { + + /** + * @param idToken the ID Token (parsed as a JWT, cannot be @null) + * @param userInfo userInfo of the current user (could be @null) + * @return the set of authorities to map to this user + */ + Collection mapAuthorities(JWT idToken, UserInfo userInfo); + +} diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java index c6d8d9692..9743677c1 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/DefaultUserInfo.java @@ -18,6 +18,7 @@ package org.mitre.openid.connect.model; import javax.persistence.Basic; import javax.persistence.Column; +import javax.persistence.Convert; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -28,6 +29,8 @@ import javax.persistence.NamedQuery; import javax.persistence.OneToOne; import javax.persistence.Table; +import org.mitre.openid.connect.model.convert.JsonObjectStringConverter; + import com.google.gson.JsonObject; @Entity @@ -510,6 +513,9 @@ public class DefaultUserInfo implements UserInfo { * @return the jsonString */ @Override + @Basic + @Column(name = "src") + @Convert(converter = JsonObjectStringConverter.class) public JsonObject getSource() { return src; } diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/model/convert/JsonObjectStringConverter.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/convert/JsonObjectStringConverter.java new file mode 100644 index 000000000..7f6016232 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/model/convert/JsonObjectStringConverter.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright 2015 The MITRE Corporation + * and the MIT Kerberos and Internet Trust Consortium + * + * Licensed 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.mitre.openid.connect.model.convert; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +import com.google.common.base.Strings; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +/** + * @author jricher + * + */ +@Converter +public class JsonObjectStringConverter implements AttributeConverter { + + private JsonParser parser = new JsonParser(); + + @Override + public String convertToDatabaseColumn(JsonObject attribute) { + if (attribute != null) { + return attribute.toString(); + } else { + return null; + } + } + + /* (non-Javadoc) + * @see javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.Object) + */ + @Override + public JsonObject convertToEntityAttribute(String dbData) { + if (!Strings.isNullOrEmpty(dbData)) { + return parser.parse(dbData).getAsJsonObject(); + } else { + return null; + } + } + +} diff --git a/openid-connect-server-webapp/src/main/resources/db/tables/hsql_database_tables.sql b/openid-connect-server-webapp/src/main/resources/db/tables/hsql_database_tables.sql index 220e0eaa5..d550f7e06 100644 --- a/openid-connect-server-webapp/src/main/resources/db/tables/hsql_database_tables.sql +++ b/openid-connect-server-webapp/src/main/resources/db/tables/hsql_database_tables.sql @@ -253,7 +253,8 @@ CREATE TABLE IF NOT EXISTS user_info ( phone_number_verified BOOLEAN, address_id VARCHAR(256), updated_time VARCHAR(256), - birthdate VARCHAR(256) + birthdate VARCHAR(256), + src VARCHAR(4096) ); CREATE TABLE IF NOT EXISTS whitelisted_site ( diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/AuthorizationRequestFilter.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/AuthorizationRequestFilter.java index 56bb980ab..e5b55918f 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/AuthorizationRequestFilter.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/filter/AuthorizationRequestFilter.java @@ -110,7 +110,9 @@ public class AuthorizationRequestFilter extends GenericFilterBean { ClientDetailsEntity client = null; authRequest = authRequestFactory.createAuthorizationRequest(createRequestMap(request.getParameterMap())); - client = clientService.loadClientByClientId(authRequest.getClientId()); + if (!Strings.isNullOrEmpty(authRequest.getClientId())) { + client = clientService.loadClientByClientId(authRequest.getClientId()); + } // save the login hint to the session if (authRequest.getExtensions().get(LOGIN_HINT) != null) {