feat: 🎸 Refactored userinfo serv., new SAML-based claim sources

Refactored userinfo to fetch attributes only when needed and requested.
Also added the possibility to extract attributes from the actual SAML
session

BREAKING CHANGE: 🧨 requires database update
pull/1580/head
Dominik Frantisek Bucik 2022-01-12 10:08:30 +01:00
parent 13973560d9
commit 2c413d9916
No known key found for this signature in database
GPG Key ID: 25014C8DB2E7E62D
48 changed files with 1521 additions and 892 deletions

View File

@ -84,6 +84,7 @@ CREATE TABLE IF NOT EXISTS saved_user_auth (
acr VARCHAR(1024), acr VARCHAR(1024),
name VARCHAR(1024), name VARCHAR(1024),
authenticated BOOLEAN, authenticated BOOLEAN,
authentication_attributes VARCHAR(2048)
); );
CREATE TABLE IF NOT EXISTS saved_user_auth_authority ( CREATE TABLE IF NOT EXISTS saved_user_auth_authority (

View File

@ -82,7 +82,8 @@ CREATE TABLE IF NOT EXISTS saved_user_auth (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY,
acr VARCHAR(1024), acr VARCHAR(1024),
name VARCHAR(1024), name VARCHAR(1024),
authenticated BOOLEAN authenticated BOOLEAN,
authentication_attributes TEXT
); );
CREATE TABLE IF NOT EXISTS saved_user_auth_authority ( CREATE TABLE IF NOT EXISTS saved_user_auth_authority (

View File

@ -83,7 +83,8 @@ CREATE TABLE IF NOT EXISTS saved_user_auth (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
acr VARCHAR(1024), acr VARCHAR(1024),
name VARCHAR(1024), name VARCHAR(1024),
authenticated BOOLEAN authenticated BOOLEAN,
authentication_attributes TEXT
); );
CREATE TABLE IF NOT EXISTS saved_user_auth_authority ( CREATE TABLE IF NOT EXISTS saved_user_auth_authority (

View File

@ -17,20 +17,24 @@
limitations under the License. limitations under the License.
--> -->
<beans xmlns="http://www.springframework.org/schema/beans" <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context" xmlns:context="http://www.springframework.org/schema/context"
xmlns:security="http://www.springframework.org/schema/security" xmlns:security="http://www.springframework.org/schema/security"
xmlns:oauth="http://www.springframework.org/schema/security/oauth2" xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2
xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.2.xsd http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/security
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd http://www.springframework.org/schema/security/spring-security-4.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/beans
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd"> http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<!-- Scan for components --> <!-- Scan for components -->
<context:component-scan annotation-config="true" base-package="cz.muni.ics" /> <context:component-scan annotation-config="true" base-package="cz.muni.ics" />
@ -57,10 +61,10 @@
<mvc:interceptor> <mvc:interceptor>
<!-- Exclude APIs and other machine-facing endpoints from these interceptors --> <!-- Exclude APIs and other machine-facing endpoints from these interceptors -->
<mvc:mapping path="/**" /> <mvc:mapping path="/**" />
<mvc:exclude-mapping path="/token**"/>
<mvc:exclude-mapping path="/resources/**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.JWKSetPublishingEndpoint).URL}**" /> <mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.JWKSetPublishingEndpoint).URL}**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.discovery.web.DiscoveryEndpoint).WELL_KNOWN_URL}/**" /> <mvc:exclude-mapping path="/#{T(cz.muni.ics.discovery.web.DiscoveryEndpoint).WELL_KNOWN_URL}/**" />
<mvc:exclude-mapping path="/resources/**" />
<mvc:exclude-mapping path="/token**"/>
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.DynamicClientRegistrationEndpoint).URL}/**" /> <mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.DynamicClientRegistrationEndpoint).URL}/**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.ProtectedResourceRegistrationEndpoint).URL}/**" /> <mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.ProtectedResourceRegistrationEndpoint).URL}/**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.UserInfoEndpoint).URL}**" /> <mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.UserInfoEndpoint).URL}**" />
@ -70,9 +74,26 @@
<mvc:exclude-mapping path="#{T(cz.muni.ics.oauth2.web.DeviceEndpoint).DEVICE_APPROVED_URL}**" /> <mvc:exclude-mapping path="#{T(cz.muni.ics.oauth2.web.DeviceEndpoint).DEVICE_APPROVED_URL}**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.oauth2.web.IntrospectionEndpoint).URL}**" /> <mvc:exclude-mapping path="/#{T(cz.muni.ics.oauth2.web.IntrospectionEndpoint).URL}**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.oauth2.web.RevocationEndpoint).URL}**" /> <mvc:exclude-mapping path="/#{T(cz.muni.ics.oauth2.web.RevocationEndpoint).URL}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.IsTestSpController).MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.AupController).URL}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_AUTHORIZATION}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_ENSURE_VO_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_IS_CESNET_ELIGIBLE_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_MANDATORY_VOS_GROUPS}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_PROD_VOS_GROUPS}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_TEST_VOS_GROUPS}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_LOGGED_IN}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_SPECIFIC_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedRegistrationController).REGISTRATION_CONTINUE_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedRegistrationController).REGISTRATION_FORM_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedRegistrationController).REGISTRATION_FORM_SUBMIT_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.RegistrationController).CONTINUE_DIRECT_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.LogoutController).MAPPING_SUCCESS}" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.LoginController).MAPPING_FAILURE}" />
<mvc:exclude-mapping path="/saml**" />
<!-- Inject the UserInfo into the response --> <!-- Inject the UserInfo into the response -->
<ref bean="userInfoInterceptor"/> <ref bean="userInfoInterceptor" />
</mvc:interceptor> </mvc:interceptor>
<mvc:interceptor> <mvc:interceptor>
<!-- Exclude APIs and other machine-facing endpoints from these interceptors --> <!-- Exclude APIs and other machine-facing endpoints from these interceptors -->

View File

@ -13,8 +13,3 @@
<li class="nav-header"><spring:message code="sidebar.personal.title"/></li> <li class="nav-header"><spring:message code="sidebar.personal.title"/></li>
<li><a href="manage/#user/approved" data-toggle="collapse" data-target=".nav-collapse"><spring:message code="sidebar.personal.approved_sites"/></a></li> <li><a href="manage/#user/approved" data-toggle="collapse" data-target=".nav-collapse"><spring:message code="sidebar.personal.approved_sites"/></a></li>
<li><a href="manage/#user/tokens" data-toggle="collapse" data-target=".nav-collapse"><spring:message code="sidebar.personal.active_tokens"/></a></li> <li><a href="manage/#user/tokens" data-toggle="collapse" data-target=".nav-collapse"><spring:message code="sidebar.personal.active_tokens"/></a></li>
<li><a href="manage/#user/profile" data-toggle="collapse" data-target=".nav-collapse"><spring:message code="sidebar.personal.profile_information"/></a></li>
<li class="divider"></li>
<li class="nav-header"><spring:message code="sidebar.developer.title"/></li>
<li><a href="manage/#dev/dynreg" data-toggle="collapse" data-target=".nav-collapse"><spring:message code="sidebar.developer.client_registration"/></a><li>
<li><a href="manage/#dev/resource" data-toggle="collapse" data-target=".nav-collapse"><spring:message code="sidebar.developer.resource_registration"/></a><li>

View File

@ -11,8 +11,3 @@
</div><!--/.well --> </div><!--/.well -->
</div><!--/span--> </div><!--/span-->
</security:authorize> </security:authorize>
<security:authorize access="!hasRole('ROLE_USER')">
<div class="span1">
<!-- placeholder for non-logged-in users -->
</div><!--/span-->
</security:authorize>

View File

@ -3,29 +3,6 @@
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%> <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> <%@ taglib prefix="o" tagdir="/WEB-INF/tags"%>
<c:choose>
<c:when test="${ not empty userInfo.preferredUsername }">
<c:set var="shortName" value="${ userInfo.preferredUsername }" />
</c:when>
<c:otherwise>
<c:set var="shortName" value="${ userInfo.sub }" />
</c:otherwise>
</c:choose>
<c:choose>
<c:when test="${ not empty userInfo.name }">
<c:set var="longName" value="${ userInfo.name }" />
</c:when>
<c:otherwise>
<c:choose>
<c:when test="${ not empty userInfo.givenName || not empty userInfo.familyName }">
<c:set var="longName" value="${ userInfo.givenName } ${ userInfo.familyName }" />
</c:when>
<c:otherwise>
<c:set var="longName" value="${ shortName }" />
</c:otherwise>
</c:choose>
</c:otherwise>
</c:choose>
<div class="navbar navbar-fixed-top"> <div class="navbar navbar-fixed-top">
<div class="navbar-inner"> <div class="navbar-inner">
<div class="container"> <div class="container">
@ -48,44 +25,27 @@
</ul> </ul>
<security:authorize access="hasRole('ROLE_USER')"> <security:authorize access="hasRole('ROLE_USER')">
<ul class="nav hidden-desktop"> <ul class="nav hidden-desktop">
<o:actionmenu /> <o:actionmenu />
</ul> </ul>
</security:authorize> </security:authorize>
<!-- use a full user menu and button when not collapsed --> <!-- use a full user menu and button when not collapsed -->
<ul class="nav pull-right visible-desktop"> <ul class="nav pull-right visible-desktop">
<security:authorize access="hasRole('ROLE_USER')"> <security:authorize access="hasRole('ROLE_USER')">
<li class="dropdown"> <li class="dropdown">
<a id="userButton" class="dropdown-toggle" data-toggle="dropdown" href=""><i class="icon-user icon-white"></i> ${ shortName } <span class="caret"></span></a> <a id="userButton" class="dropdown-toggle" data-toggle="dropdown" href=""><i class="icon-user icon-white"></i> <security:authentication property="principal.username" /> <span class="caret"></span></a>
<ul class="dropdown-menu pull-right"> <ul class="dropdown-menu pull-right">
<li><a href="manage/#user/profile" data-toggle="collapse" data-target=".nav-collapse">${ longName }</a></li>
<li class="divider"></li>
<li><a href="" data-toggle="collapse" data-target=".nav-collapse" class="logoutLink"><i class="icon-remove"></i> <spring:message code="topbar.logout"/></a></li> <li><a href="" data-toggle="collapse" data-target=".nav-collapse" class="logoutLink"><i class="icon-remove"></i> <spring:message code="topbar.logout"/></a></li>
</ul> </ul>
</li> </li>
</security:authorize> </security:authorize>
<security:authorize access="!hasRole('ROLE_USER')">
<li>
<a id="loginButton" href="login" data-toggle="collapse" data-target=".nav-collapse"><i class="icon-lock icon-white"></i> <spring:message code="topbar.login"/></a>
</li>
</security:authorize>
</ul> </ul>
<!-- use a simplified user button system when collapsed --> <!-- use a simplified user button system when collapsed -->
<ul class="nav hidden-desktop"> <ul class="nav hidden-desktop">
<security:authorize access="hasRole('ROLE_USER')"> <security:authorize access="hasRole('ROLE_USER')">
<li><a href="manage/#user/profile">${ longName }</a></li>
<li class="divider"></li>
<li><a href="" class="logoutLink"><i class="icon-remove"></i> <spring:message code="topbar.logout"/></a></li> <li><a href="" class="logoutLink"><i class="icon-remove"></i> <spring:message code="topbar.logout"/></a></li>
</security:authorize> </security:authorize>
<security:authorize access="!hasRole('ROLE_USER')">
<li>
<a href="login" data-toggle="collapse" data-target=".nav-collapse"><i class="icon-lock"></i> <spring:message code="topbar.login"/></a>
</li>
</security:authorize>
</ul> </ul>
<form action="${ config.issuer }${ config.issuer.endsWith('/') ? '' : '/' }logout" method="POST" class="hidden" id="logoutForm"> <form action="${ config.issuer }${ config.issuer.endsWith('/') ? '' : '/' }logout" method="POST" class="hidden" id="logoutForm">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />

View File

@ -7,7 +7,6 @@
xmlns:security="http://www.springframework.org/schema/security" xmlns:security="http://www.springframework.org/schema/security"
xmlns:context="http://www.springframework.org/schema/context" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/security xsi:schemaLocation="http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans
@ -15,9 +14,7 @@
http://www.springframework.org/schema/context http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/aop/spring-aop.xsd">
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd">
<context:property-placeholder properties-ref="nonOverwrittenAttributeProperties" ignore-unresolvable="true" order="0"/> <context:property-placeholder properties-ref="nonOverwrittenAttributeProperties" ignore-unresolvable="true" order="0"/>
<context:property-placeholder properties-ref="userAttrMappingsProperties" ignore-unresolvable="true" order="1"/> <context:property-placeholder properties-ref="userAttrMappingsProperties" ignore-unresolvable="true" order="1"/>
@ -31,67 +28,6 @@
<aop:aspectj-autoproxy proxy-target-class="true"/> <aop:aspectj-autoproxy proxy-target-class="true"/>
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<ref bean="localeChangeInterceptor"/>
</mvc:interceptor>
<mvc:interceptor>
<!-- Exclude APIs and other machine-facing endpoints from these interceptors -->
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/token**"/>
<mvc:exclude-mapping path="/resources/**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.JWKSetPublishingEndpoint).URL}**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.discovery.web.DiscoveryEndpoint).WELL_KNOWN_URL}/**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.DynamicClientRegistrationEndpoint).URL}/**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.ProtectedResourceRegistrationEndpoint).URL}/**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.UserInfoEndpoint).URL}**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.RootController).API_URL}/**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oauth2.web.DeviceEndpoint).ENDPOINT_URL}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oauth2.web.DeviceEndpoint).REQUEST_USER_CODE_URL}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oauth2.web.DeviceEndpoint).DEVICE_APPROVED_URL}**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.oauth2.web.IntrospectionEndpoint).URL}**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.oauth2.web.RevocationEndpoint).URL}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.IsTestSpController).MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.AupController).URL}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_AUTHORIZATION}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_ENSURE_VO_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_IS_CESNET_ELIGIBLE_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_MANDATORY_VOS_GROUPS}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_PROD_VOS_GROUPS}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_TEST_VOS_GROUPS}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_LOGGED_IN}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_SPECIFIC_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedRegistrationController).REGISTRATION_CONTINUE_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedRegistrationController).REGISTRATION_FORM_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedRegistrationController).REGISTRATION_FORM_SUBMIT_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.RegistrationController).CONTINUE_DIRECT_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.LogoutController).MAPPING_SUCCESS}" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.LoginController).MAPPING_FAILURE}" />
<mvc:exclude-mapping path="/saml**" />
<!-- Inject the UserInfo into the response -->
<ref bean="userInfoInterceptor" />
</mvc:interceptor>
<mvc:interceptor>
<!-- Exclude APIs and other machine-facing endpoints from these interceptors -->
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/token**"/>
<mvc:exclude-mapping path="/resources/**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.JWKSetPublishingEndpoint).URL}**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.discovery.web.DiscoveryEndpoint).WELL_KNOWN_URL}/**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.DynamicClientRegistrationEndpoint).URL}/**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.ProtectedResourceRegistrationEndpoint).URL}/**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.UserInfoEndpoint).URL}**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.openid.connect.web.RootController).API_URL}/**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oauth2.web.DeviceEndpoint).ENDPOINT_URL}**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.oauth2.web.IntrospectionEndpoint).URL}**" />
<mvc:exclude-mapping path="/#{T(cz.muni.ics.oauth2.web.RevocationEndpoint).URL}**" />
<!-- Inject the server configuration into the response -->
<ref bean="serverConfigInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
<!-- default config values, by default override in file /etc/perun/perun-mitreid.properties --> <!-- default config values, by default override in file /etc/perun/perun-mitreid.properties -->
<bean id="defaultCoreProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean"> <bean id="defaultCoreProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="properties"> <property name="properties">
@ -217,7 +153,7 @@
<bean id="nonOverwrittenAttributeProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean"> <bean id="nonOverwrittenAttributeProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="properties"> <property name="properties">
<props> <props>
<prop key="user.attribute_names.fixedList">openid_sub,profile_preferred_username,profile_given_name,profile_middle_name,profile_family_name,profile_name,profile_zoneinfo,profile_locale,email_email,address_address_formatted,phone_phone,aups</prop> <prop key="user.attribute_names.fixedList">openid_sub,profile_name,profile_preferred_username,profile_nickname,profile_given_name,profile_family_name,profile_middle_name,profile_zoneinfo,profile_locale,profile_birthdate,profile_gender,profile_picture,profile_profile,profile_website,email_email,email_email_verified,phone_phone,phone_phone_verified,address_address_formatted,address_country,address_locality,address_postal_code,address_region,address_street_address,aups</prop>
<prop key="facility.attribute_names.fixedList">checkGroupMembership,allowRegistration,registrationUrl,dynamicRegistration,clientId,voShortNames,wayfFilter,wayfEFilter,requestedAups,capabilities,testSp</prop> <prop key="facility.attribute_names.fixedList">checkGroupMembership,allowRegistration,registrationUrl,dynamicRegistration,clientId,voShortNames,wayfFilter,wayfEFilter,requestedAups,capabilities,testSp</prop>
<prop key="group.attribute_names.fixedList"/> <prop key="group.attribute_names.fixedList"/>
<prop key="vo.attribute_names.fixedList">aup</prop> <prop key="vo.attribute_names.fixedList">aup</prop>
@ -231,9 +167,11 @@
<props> <props>
<prop key="user.attribute_names.customList"/> <prop key="user.attribute_names.customList"/>
<!-- ATTRIBUTES MAPPINGS --> <!-- ATTRIBUTES MAPPINGS -->
<!-- Scope Openid -->
<prop key="openid_sub.mapping.ldap">login;x-ns-einfraid-persistent-shadow</prop> <prop key="openid_sub.mapping.ldap">login;x-ns-einfraid-persistent-shadow</prop>
<prop key="openid_sub.mapping.rpc">urn:perun:user:attribute-def:core:id</prop> <prop key="openid_sub.mapping.rpc">urn:perun:user:attribute-def:core:id</prop>
<prop key="openid_sub.type">STRING</prop> <prop key="openid_sub.type">STRING</prop>
<!-- Scope Profile -->
<prop key="profile_preferred_username.mapping.ldap">login;x-ns-einfra</prop> <prop key="profile_preferred_username.mapping.ldap">login;x-ns-einfra</prop>
<prop key="profile_preferred_username.mapping.rpc">urn:perun:user:attribute-def:def:login-namespace:einfra</prop> <prop key="profile_preferred_username.mapping.rpc">urn:perun:user:attribute-def:def:login-namespace:einfra</prop>
<prop key="profile_preferred_username.type">STRING</prop> <prop key="profile_preferred_username.type">STRING</prop>
@ -255,15 +193,58 @@
<prop key="profile_locale.mapping.ldap">preferredLanguage</prop> <prop key="profile_locale.mapping.ldap">preferredLanguage</prop>
<prop key="profile_locale.mapping.rpc">urn:perun:user:attribute-def:def:preferredLanguage</prop> <prop key="profile_locale.mapping.rpc">urn:perun:user:attribute-def:def:preferredLanguage</prop>
<prop key="profile_locale.type">STRING</prop> <prop key="profile_locale.type">STRING</prop>
<prop key="profile_nickname.mapping.ldap"/>
<prop key="profile_nickname.mapping.rpc"/>
<prop key="profile_nickname.type"/>
<prop key="profile_birthdate.mapping.ldap"/>
<prop key="profile_birthdate.mapping.rpc"/>
<prop key="profile_birthdate.type"/>
<prop key="profile_gender.mapping.ldap"/>
<prop key="profile_gender.mapping.rpc"/>
<prop key="profile_gender.type"/>
<prop key="profile_picture.mapping.ldap"/>
<prop key="profile_picture.mapping.rpc"/>
<prop key="profile_picture.type"/>
<prop key="profile_profile.mapping.ldap"/>
<prop key="profile_profile.mapping.rpc"/>
<prop key="profile_profile.type"/>
<prop key="profile_website.mapping.ldap"/>
<prop key="profile_website.mapping.rpc"/>
<prop key="profile_website.type"/>
<!-- Scope Email -->
<prop key="email_email.mapping.ldap">preferredMail</prop> <prop key="email_email.mapping.ldap">preferredMail</prop>
<prop key="email_email.mapping.rpc">urn:perun:user:attribute-def:def:preferredMail</prop> <prop key="email_email.mapping.rpc">urn:perun:user:attribute-def:def:preferredMail</prop>
<prop key="email_email.type">STRING</prop> <prop key="email_email.type">STRING</prop>
<prop key="email_email_verified.mapping.ldap"/>
<prop key="email_email_verified.mapping.rpc"/>
<prop key="email_email_verified.type"/>
<!-- Scope Phone -->
<prop key="phone_phone.mapping.ldap">telephoneNumber</prop> <prop key="phone_phone.mapping.ldap">telephoneNumber</prop>
<prop key="phone_phone.mapping.rpc">urn:perun:user:attribute-def:def:phone</prop> <prop key="phone_phone.mapping.rpc">urn:perun:user:attribute-def:def:phone</prop>
<prop key="phone_phone.type">STRING</prop> <prop key="phone_phone.type">STRING</prop>
<prop key="phone_phone_verified.mapping.ldap"/>
<prop key="phone_phone_verified.mapping.rpc"/>
<prop key="phone_phone_verified.type"/>
<!-- Scope Address -->
<prop key="address_address_formatted.mapping.ldap">postalAddress</prop> <prop key="address_address_formatted.mapping.ldap">postalAddress</prop>
<prop key="address_address_formatted.mapping.rpc">urn:perun:user:attribute-def:def:address</prop> <prop key="address_address_formatted.mapping.rpc">urn:perun:user:attribute-def:def:address</prop>
<prop key="address_address_formatted.type">STRING</prop> <prop key="address_address_formatted.type">STRING</prop>
<prop key="address_country.mapping.ldap"/>
<prop key="address_country.mapping.rpc"/>
<prop key="address_country.type"/>
<prop key="address_locality.mapping.ldap"/>
<prop key="address_locality.mapping.rpc"/>
<prop key="address_locality.type"/>
<prop key="address_postal_code.mapping.ldap"/>
<prop key="address_postal_code.mapping.rpc"/>
<prop key="address_postal_code.type"/>
<prop key="address_region.mapping.ldap"/>
<prop key="address_region.mapping.rpc"/>
<prop key="address_region.type"/>
<prop key="address_street_address.mapping.ldap"/>
<prop key="address_street_address.mapping.rpc"/>
<prop key="address_street_address.type"/>
<!-- Attributes -->
<prop key="aups.mapping.ldap">aups</prop> <prop key="aups.mapping.ldap">aups</prop>
<prop key="aups.mapping.rpc">urn:perun:user:attribute-def:def:aups</prop> <prop key="aups.mapping.rpc">urn:perun:user:attribute-def:def:aups</prop>
<prop key="aups.type">MAP_KEY_VALUE</prop> <prop key="aups.type">MAP_KEY_VALUE</prop>
@ -401,20 +382,48 @@
<property name="ignoreResourceNotFound" value="true"/> <property name="ignoreResourceNotFound" value="true"/>
</bean> </bean>
<bean id="openidMappings" class="cz.muni.ics.oidc.server.userInfo.mappings.OpenidMappings">
<property name="sub" value="openid_sub"/>
</bean>
<bean id="profileMappings" class="cz.muni.ics.oidc.server.userInfo.mappings.ProfileMappings">
<property name="name" value="profile_name"/>
<property name="preferredUsername" value="profile_preferred_username"/>
<property name="nickname" value="profile_nickname"/>
<property name="givenName" value="profile_given_name"/>
<property name="familyName" value="profile_family_name"/>
<property name="middleName" value="profile_middle_name"/>
<property name="zoneinfo" value="profile_zoneinfo"/>
<property name="locale" value="profile_locale"/>
<property name="birthdate" value="profile_birthdate"/>
<property name="gender" value="profile_gender"/>
<property name="picture" value="profile_picture"/>
<property name="profile" value="profile_profile"/>
<property name="website" value="profile_website"/>
</bean>
<bean id="emailMappings" class="cz.muni.ics.oidc.server.userInfo.mappings.EmailMappings">
<property name="email" value="email_email"/>
<property name="emailVerified" value="email_email_verified"/>
</bean>
<bean id="phoneMappings" class="cz.muni.ics.oidc.server.userInfo.mappings.PhoneMappings">
<property name="phoneNumber" value="phone_phone"/>
<property name="phoneNumberVerified" value="phone_phone_verified"/>
</bean>
<bean id="addressMappings" class="cz.muni.ics.oidc.server.userInfo.mappings.AddressMappings">
<property name="formatted" value="address_address_formatted"/>
<property name="country" value="address_country"/>
<property name="locality" value="address_locality"/>
<property name="postalCode" value="address_postal_code"/>
<property name="region" value="address_region"/>
<property name="streetAddress" value="address_street_address"/>
</bean>
<!-- defines our own user info service --> <!-- defines our own user info service -->
<bean id="userInfoService" primary="true" class="cz.muni.ics.oidc.server.userInfo.PerunUserInfoService"> <bean id="userInfoService" primary="true" class="cz.muni.ics.oidc.server.userInfo.PerunUserInfoService">
<property name="perunAdapter" ref="perunAdapter"/> <property name="perunAdapter" ref="perunAdapter"/>
<property name="subAttribute" value="openid_sub"/>
<property name="preferredUsernameAttribute" value="profile_preferred_username"/>
<property name="givenNameAttribute" value="profile_given_name"/>
<property name="familyNameAttribute" value="profile_family_name"/>
<property name="middleNameAttribute" value="profile_middle_name"/>
<property name="fullNameAttribute" value="profile_name"/>
<property name="emailAttribute" value="email_email"/>
<property name="addressAttribute" value="address_address_formatted"/>
<property name="phoneAttribute" value="phone_phone"/>
<property name="zoneinfoAttribute" value="profile_zoneinfo"/>
<property name="localeAttribute" value="profile_locale"/>
<property name="properties" ref="coreProperties"/> <property name="properties" ref="coreProperties"/>
<property name="customClaimNames" value="#{'${custom.claims}'.split('\s*,\s*')}"/> <property name="customClaimNames" value="#{'${custom.claims}'.split('\s*,\s*')}"/>
<property name="forceRegenerateUserinfoCustomClaims" value="#{'${force.regenerate.userinfo.custom.claims}'.split('\s*,\s*')}"/> <property name="forceRegenerateUserinfoCustomClaims" value="#{'${force.regenerate.userinfo.custom.claims}'.split('\s*,\s*')}"/>

View File

@ -133,11 +133,9 @@ public class DiscoveryEndpoint {
} }
private UserInfo extractUser(UriComponents resourceUri) { private UserInfo extractUser(UriComponents resourceUri) {
UserInfo user = userService.getByEmailAddress(resourceUri.getUserInfo() + "@" + resourceUri.getHost()); String username = resourceUri.getUserInfo() + "@" + resourceUri.getHost();
if (user == null) { //TODO: lookup username in Perun
user = userService.getByUsername(resourceUri.getUserInfo()); // first part is the username return null;
}
return user;
} }
@RequestMapping("/" + OPENID_CONFIGURATION_URL) @RequestMapping("/" + OPENID_CONFIGURATION_URL)

View File

@ -0,0 +1,20 @@
package cz.muni.ics.oauth2.model;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
@EqualsAndHashCode
@AllArgsConstructor
public class AuthenticationStatement {
private List<String> authenticatingAuthorities;
private String authnContextClassRef;
}

View File

@ -0,0 +1,194 @@
package cz.muni.ics.oauth2.model;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.opensaml.saml2.core.Attribute;
import org.opensaml.saml2.core.AuthenticatingAuthority;
import org.opensaml.saml2.core.AuthnStatement;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.util.StringUtils;
@Getter
@Setter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class SamlAuthenticationDetails {
public static final String RELAY_STATE = "relayState";
public static final String LOCAL_ENTITY_ID = "localEntityId";
public static final String REMOTE_ENTITY_ID = "remoteEntityId";
public static final String ATTRIBUTES = "attributes";
public static final String AUTHN_STATEMENTS = "authnStatements";
public static final String AUTHN_CONTEXT_CLASS_REF = "authnContextClassRef";
public static final String AUTHENTICATING_AUTHORITIES = "authenticatingAuthorities";
private String relayState;
private String remoteEntityID;
private String localEntityID;
private Map<String, String[]> attributes;
private List<AuthenticationStatement> authnStatements;
public SamlAuthenticationDetails(SAMLCredential samlCredential) {
this.relayState = samlCredential.getRelayState();
this.remoteEntityID = samlCredential.getRemoteEntityID();
this.localEntityID = samlCredential.getLocalEntityID();
this.attributes = processAttributes(samlCredential);
this.authnStatements = processAuthnStatement(samlCredential);
}
private List<AuthenticationStatement> processAuthnStatement(SAMLCredential samlCredential) {
List<AuthenticationStatement> authenticationStatements = new ArrayList<>();
List<AuthnStatement> samlAuthnStatements = samlCredential.getAuthenticationAssertion().getAuthnStatements();
if (samlAuthnStatements != null) {
for (AuthnStatement as : samlAuthnStatements) {
if (as == null || as.getAuthnContext() == null) {
continue;
}
List<String> authenticatingAuthorities = new ArrayList<>();
List<AuthenticatingAuthority> authnAuthorities = as.getAuthnContext()
.getAuthenticatingAuthorities();
if (authnAuthorities != null) {
for (AuthenticatingAuthority aa : authnAuthorities) {
if (aa != null && StringUtils.hasText(aa.getURI())) {
authenticatingAuthorities.add(aa.getURI());
}
}
}
String authnContextClassRef = null;
if (as.getAuthnContext().getAuthnContextClassRef() != null &&
StringUtils.hasText(as.getAuthnContext().getAuthnContextClassRef().getAuthnContextClassRef()))
{
authnContextClassRef = as.getAuthnContext().getAuthnContextClassRef().getAuthnContextClassRef();
}
authenticationStatements.add(new AuthenticationStatement(authenticatingAuthorities, authnContextClassRef));
}
}
return authenticationStatements;
}
private Map<String, String[]> processAttributes(SAMLCredential samlCredential) {
Map<String, String[]> attributes = new HashMap<>();
List<Attribute> samlAttributes = samlCredential.getAttributes();
if (samlAttributes != null) {
for (Attribute a: samlAttributes) {
if (a == null) {
continue;
}
String name = a.getName();
String[] val = samlCredential.getAttributeAsStringArray(name);
attributes.put(name, val);
}
}
return attributes;
}
public static SamlAuthenticationDetails deserialize(String strJson) {
if (!StringUtils.hasText(strJson)) {
return null;
}
JsonObject json = (JsonObject) JsonParser.parseString(strJson);
SamlAuthenticationDetails details = new SamlAuthenticationDetails();
details.setRelayState(getStringOrNull(json.get(RELAY_STATE)));
details.setRemoteEntityID(getStringOrNull(json.get(REMOTE_ENTITY_ID)));
details.setLocalEntityID(getStringOrNull(json.get(LOCAL_ENTITY_ID)));
Map<String, String[]> attributes = new HashMap<>();
JsonObject attrs = json.getAsJsonObject(ATTRIBUTES);
for (Map.Entry<String, JsonElement> e: attrs.entrySet()) {
JsonArray elements = e.getValue().getAsJsonArray();
String[] val = new String[elements.size()];
int i = 0;
for (JsonElement element: elements) {
val[i++] = getStringOrNull(element);
}
attributes.put(e.getKey(), val);
}
details.setAttributes(attributes);
List<AuthenticationStatement> authnStatements = new ArrayList<>();
JsonArray authStmts = json.getAsJsonArray(AUTHN_STATEMENTS);
for (JsonElement e: authStmts) {
JsonObject obj = e.getAsJsonObject();
JsonArray authoritiesArr = obj.getAsJsonArray(AUTHENTICATING_AUTHORITIES);
List<String> authorities = new ArrayList<>();
for (JsonElement authority: authoritiesArr) {
authorities.add(authority.getAsString());
}
String authnContextClassRef = getStringOrNull(obj.get(AUTHN_CONTEXT_CLASS_REF));
authnStatements.add(new AuthenticationStatement(authorities, authnContextClassRef));
}
details.setAuthnStatements(authnStatements);
return details;
}
public static String serialize(SamlAuthenticationDetails o) {
if (o == null) {
return null;
}
JsonObject object = new JsonObject();
addStringOrNull(object, RELAY_STATE, o.getRelayState());
addStringOrNull(object, LOCAL_ENTITY_ID, o.getLocalEntityID());
addStringOrNull(object, REMOTE_ENTITY_ID, o.getRemoteEntityID());
JsonObject attrs = new JsonObject();
for (Map.Entry<String, String[]> e: o.getAttributes().entrySet()) {
JsonArray val = new JsonArray();
for (String v: e.getValue()) {
if (v == null) {
continue;
}
val.add(v);
}
attrs.add(e.getKey(), val);
}
object.add(ATTRIBUTES, attrs);
JsonArray authnStatements = new JsonArray();
for (AuthenticationStatement as: o.getAuthnStatements()) {
JsonObject asJson = new JsonObject();
addStringOrNull(asJson, AUTHN_CONTEXT_CLASS_REF, as.getAuthnContextClassRef());
JsonArray authorities = new JsonArray();
for (String authAuthority: as.getAuthenticatingAuthorities()) {
if (authAuthority == null) {
continue;
}
authorities.add(authAuthority);
}
asJson.add(AUTHENTICATING_AUTHORITIES, asJson);
}
object.add(AUTHN_STATEMENTS, authnStatements);
return object.toString();
}
private static void addStringOrNull(JsonObject target, String key, String value) {
if (value == null) {
target.add(key, new JsonNull());
} else {
target.addProperty(key, value);
}
}
private static String getStringOrNull(JsonElement jsonElement) {
if (jsonElement.isJsonPrimitive()) {
return jsonElement.getAsString();
} else {
return null;
}
}
}

View File

@ -16,6 +16,8 @@
package cz.muni.ics.oauth2.model; package cz.muni.ics.oauth2.model;
import cz.muni.ics.oauth2.model.convert.JsonElementStringConverter;
import cz.muni.ics.oauth2.model.convert.SamlAuthenticationDetailsStringConverter;
import cz.muni.ics.oauth2.model.convert.SimpleGrantedAuthorityStringConverter; import cz.muni.ics.oauth2.model.convert.SimpleGrantedAuthorityStringConverter;
import cz.muni.ics.oidc.saml.SamlPrincipal; import cz.muni.ics.oidc.saml.SamlPrincipal;
import java.util.Collection; import java.util.Collection;
@ -40,7 +42,6 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.persistence.annotations.CascadeOnDelete; import org.eclipse.persistence.annotations.CascadeOnDelete;
import org.opensaml.saml2.core.AuthnContext; import org.opensaml.saml2.core.AuthnContext;
import org.opensaml.saml2.core.AuthnContextClassRef; import org.opensaml.saml2.core.AuthnContextClassRef;
@ -48,6 +49,7 @@ import org.opensaml.saml2.core.AuthnStatement;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.providers.ExpiringUsernameAuthenticationToken; import org.springframework.security.providers.ExpiringUsernameAuthenticationToken;
import org.springframework.security.saml.SAMLCredential;
/** /**
* This class stands in for an original Authentication object. * This class stands in for an original Authentication object.
@ -89,6 +91,10 @@ public class SavedUserAuthentication implements Authentication {
@Column(name = "acr") @Column(name = "acr")
private String acr; private String acr;
@Column(name = "authentication_attributes")
@Convert(converter = SamlAuthenticationDetailsStringConverter.class)
private SamlAuthenticationDetails authenticationDetails;
public SavedUserAuthentication(Authentication src) { public SavedUserAuthentication(Authentication src) {
setName(src.getName()); setName(src.getName());
setAuthorities(new HashSet<>(src.getAuthorities())); setAuthorities(new HashSet<>(src.getAuthorities()));
@ -104,6 +110,7 @@ public class SavedUserAuthentication implements Authentication {
.map(AuthnContext::getAuthnContextClassRef) .map(AuthnContext::getAuthnContextClassRef)
.map(AuthnContextClassRef::getAuthnContextClassRef) .map(AuthnContextClassRef::getAuthnContextClassRef)
.collect(Collectors.joining()); .collect(Collectors.joining());
this.authenticationDetails = new SamlAuthenticationDetails((SAMLCredential) src.getCredentials());
} }
} }

View File

@ -0,0 +1,48 @@
/*******************************************************************************
* Copyright 2018 The MIT 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 cz.muni.ics.oauth2.model.convert;
import cz.muni.ics.oauth2.model.SamlAuthenticationDetails;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import lombok.extern.slf4j.Slf4j;
/**
* Translates a Serializable object of certain primitive types
* into a String for storage in the database, for use with the
* OAuth2Request extensions map.
*
* This class does allow some extension data to be lost.
*
* @author jricher
*/
@Converter
@Slf4j
public class SamlAuthenticationDetailsStringConverter implements AttributeConverter<SamlAuthenticationDetails, String> {
@Override
public String convertToDatabaseColumn(SamlAuthenticationDetails attribute) {
return SamlAuthenticationDetails.serialize(attribute);
}
@Override
public SamlAuthenticationDetails convertToEntityAttribute(String dbData) {
return SamlAuthenticationDetails.deserialize(dbData);
}
}

View File

@ -24,6 +24,7 @@ import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
import cz.muni.ics.oauth2.service.DeviceCodeService; import cz.muni.ics.oauth2.service.DeviceCodeService;
import cz.muni.ics.oauth2.service.SystemScopeService; import cz.muni.ics.oauth2.service.SystemScopeService;
import cz.muni.ics.oauth2.token.DeviceTokenGranter; import cz.muni.ics.oauth2.token.DeviceTokenGranter;
import cz.muni.ics.oidc.saml.SamlPrincipal;
import cz.muni.ics.oidc.server.configurations.PerunOidcConfig; import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
import cz.muni.ics.oidc.server.userInfo.PerunUserInfo; import cz.muni.ics.oidc.server.userInfo.PerunUserInfo;
import cz.muni.ics.oidc.web.WebHtmlClasses; import cz.muni.ics.oidc.web.WebHtmlClasses;
@ -56,13 +57,13 @@ import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request; import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory; import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/** /**
* Implements https://tools.ietf.org/html/draft-ietf-oauth-device-flow * Implements https://tools.ietf.org/html/draft-ietf-oauth-device-flow
@ -247,7 +248,7 @@ public class DeviceEndpoint {
@GetMapping(value = CHECK_USER_CODE_URL) @GetMapping(value = CHECK_USER_CODE_URL)
public String startApproveDevice(@RequestParam(USER_CODE) String userCode, public String startApproveDevice(@RequestParam(USER_CODE) String userCode,
ModelMap model, ModelMap model,
Principal p, Authentication auth,
HttpServletRequest req) HttpServletRequest req)
{ {
DeviceCode dc = deviceCodeService.lookUpByUserCode(userCode); DeviceCode dc = deviceCodeService.lookUpByUserCode(userCode);
@ -267,6 +268,8 @@ public class DeviceEndpoint {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(dc.getRequestParameters()); AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(dc.getRequestParameters());
session.setAttribute(AUTHORIZATION_REQUEST, authorizationRequest); session.setAttribute(AUTHORIZATION_REQUEST, authorizationRequest);
SamlPrincipal p = (SamlPrincipal) auth.getPrincipal();
return getApproveDeviceViewName(model, p, req, dc); return getApproveDeviceViewName(model, p, req, dc);
} }
@ -274,7 +277,6 @@ public class DeviceEndpoint {
@PostMapping(value = DEVICE_APPROVED_URL) @PostMapping(value = DEVICE_APPROVED_URL)
public String processApproveDevice(@RequestParam(USER_CODE) String userCode, public String processApproveDevice(@RequestParam(USER_CODE) String userCode,
@RequestParam(value = USER_OAUTH_APPROVAL) Boolean approve, @RequestParam(value = USER_OAUTH_APPROVAL) Boolean approve,
Principal p,
HttpServletRequest req, HttpServletRequest req,
ModelMap model, ModelMap model,
Authentication auth) Authentication auth)
@ -308,6 +310,7 @@ public class DeviceEndpoint {
// user did not approve // user did not approve
if (!approve) { if (!approve) {
model.addAttribute(APPROVED, false); model.addAttribute(APPROVED, false);
SamlPrincipal p = (SamlPrincipal) auth.getPrincipal();
return getApproveDeviceViewName(model, p, req, dc); return getApproveDeviceViewName(model, p, req, dc);
} }
@ -402,7 +405,7 @@ public class DeviceEndpoint {
return THEMED_DEVICE_APPROVED; return THEMED_DEVICE_APPROVED;
} }
private String getApproveDeviceViewName(ModelMap model, Principal p, HttpServletRequest req, DeviceCode dc) { private String getApproveDeviceViewName(ModelMap model, SamlPrincipal p, HttpServletRequest req, DeviceCode dc) {
if (perunOidcConfig.getTheme().equalsIgnoreCase(DEFAULT)) { if (perunOidcConfig.getTheme().equalsIgnoreCase(DEFAULT)) {
model.put(SCOPES, ControllerUtils.getSortedScopes(dc.getScope(), scopeService)); model.put(SCOPES, ControllerUtils.getSortedScopes(dc.getScope(), scopeService));
return APPROVE_DEVICE; return APPROVE_DEVICE;
@ -410,9 +413,11 @@ public class DeviceEndpoint {
ClientDetailsEntity client = (ClientDetailsEntity) model.get(CLIENT); ClientDetailsEntity client = (ClientDetailsEntity) model.get(CLIENT);
PerunUserInfo user = (PerunUserInfo) userInfoService.getByUsernameAndClientId( PerunUserInfo user = (PerunUserInfo) userInfoService.get(
p.getName(), p.getUsername(),
client.getClientId() client.getClientId(),
dc.getScope(),
p.getSamlCredential()
); );
ControllerUtils.setScopesAndClaims(scopeService, scopeClaimTranslationService, model, dc.getScope(), user); ControllerUtils.setScopesAndClaims(scopeService, scopeClaimTranslationService, model, dc.getScope(), user);

View File

@ -141,7 +141,8 @@ public class IntrospectionEndpoint {
// get the user information of the user that authorized this token in the first place // get the user information of the user that authorized this token in the first place
String userName = accessToken.getAuthenticationHolder().getAuthentication().getName(); String userName = accessToken.getAuthenticationHolder().getAuthentication().getName();
user = userInfoService.getByUsernameAndClientId(userName, tokenClient.getClientId()); user = userInfoService.get(userName, tokenClient.getClientId(),
authScopes, accessToken.getAuthenticationHolder().getUserAuth());
} catch (InvalidTokenException e) { } catch (InvalidTokenException e) {
log.info("Invalid access token. Checking refresh token."); log.info("Invalid access token. Checking refresh token.");
@ -154,7 +155,8 @@ public class IntrospectionEndpoint {
// get the user information of the user that authorized this token in the first place // get the user information of the user that authorized this token in the first place
String userName = refreshToken.getAuthenticationHolder().getAuthentication().getName(); String userName = refreshToken.getAuthenticationHolder().getAuthentication().getName();
user = userInfoService.getByUsernameAndClientId(userName, tokenClient.getClientId()); user = userInfoService.get(userName, tokenClient.getClientId(), authScopes,
refreshToken.getAuthenticationHolder().getUserAuth());
} catch (InvalidTokenException e2) { } catch (InvalidTokenException e2) {
log.error("Invalid refresh token"); log.error("Invalid refresh token");

View File

@ -23,14 +23,13 @@ package cz.muni.ics.oauth2.web;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import cz.muni.ics.oauth2.model.ClientDetailsEntity; import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oauth2.model.SystemScope; import cz.muni.ics.oauth2.model.SystemScope;
import cz.muni.ics.oauth2.service.ClientDetailsEntityService; import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
import cz.muni.ics.oauth2.service.SystemScopeService; import cz.muni.ics.oauth2.service.SystemScopeService;
import cz.muni.ics.oidc.saml.SamlPrincipal;
import cz.muni.ics.oidc.server.configurations.PerunOidcConfig; import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
import cz.muni.ics.oidc.server.userInfo.PerunUserInfo;
import cz.muni.ics.oidc.web.WebHtmlClasses; import cz.muni.ics.oidc.web.WebHtmlClasses;
import cz.muni.ics.oidc.web.controllers.ControllerUtils; import cz.muni.ics.oidc.web.controllers.ControllerUtils;
import cz.muni.ics.openid.connect.model.UserInfo; import cz.muni.ics.openid.connect.model.UserInfo;
@ -39,13 +38,11 @@ import cz.muni.ics.openid.connect.service.ScopeClaimTranslationService;
import cz.muni.ics.openid.connect.service.UserInfoService; import cz.muni.ics.openid.connect.service.UserInfoService;
import cz.muni.ics.openid.connect.view.HttpCodeView; import cz.muni.ics.openid.connect.view.HttpCodeView;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.security.Principal;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -53,6 +50,7 @@ import org.apache.http.client.utils.URIBuilder;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.endpoint.RedirectResolver; import org.springframework.security.oauth2.provider.endpoint.RedirectResolver;
@ -60,8 +58,6 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.annotation.SessionAttributes;
import javax.servlet.http.HttpServletRequest;
/** /**
* @author jricher * @author jricher
* *
@ -121,24 +117,19 @@ public class OAuthConfirmationController {
this.htmlClasses = htmlClasses; this.htmlClasses = htmlClasses;
} }
public OAuthConfirmationController() {
}
public OAuthConfirmationController(ClientDetailsEntityService clientService) { public OAuthConfirmationController(ClientDetailsEntityService clientService) {
this.clientService = clientService; this.clientService = clientService;
} }
@PreAuthorize("hasRole('ROLE_USER')") @PreAuthorize("hasRole('ROLE_USER')")
@RequestMapping("/oauth/confirm_access") @RequestMapping("/oauth/confirm_access")
public String confirmAccess(Map<String, Object> model, HttpServletRequest req, Principal p) { public String confirmAccess(Map<String, Object> model, HttpServletRequest req, Authentication auth) {
AuthorizationRequest authRequest = (AuthorizationRequest) model.get(AUTHORIZATION_REQUEST); AuthorizationRequest authRequest = (AuthorizationRequest) model.get(AUTHORIZATION_REQUEST);
// Check the "prompt" parameter to see if we need to do special processing // Check the "prompt" parameter to see if we need to do special processing
String prompt = (String)authRequest.getExtensions().get(ConnectRequestParameters.PROMPT); String prompt = (String)authRequest.getExtensions().get(ConnectRequestParameters.PROMPT);
List<String> prompts = Splitter.on(ConnectRequestParameters.PROMPT_SEPARATOR).splitToList(Strings.nullToEmpty(prompt)); List<String> prompts = Splitter.on(ConnectRequestParameters.PROMPT_SEPARATOR).splitToList(Strings.nullToEmpty(prompt));
ClientDetailsEntity client = null; ClientDetailsEntity client;
try { try {
client = clientService.loadClientByClientId(authRequest.getClientId()); client = clientService.loadClientByClientId(authRequest.getClientId());
@ -167,27 +158,33 @@ public class OAuthConfirmationController {
model.put(CLIENT, client); model.put(CLIENT, client);
model.put(REDIRECT_URI, authRequest.getRedirectUri()); model.put(REDIRECT_URI, authRequest.getRedirectUri());
model.put(REMEMBER_ENABLED, !prompts.contains(CONSENT)); model.put(REMEMBER_ENABLED, !prompts.contains(CONSENT));
model.put(GRAS, true);
Set<SystemScope> sortedScopes = ControllerUtils.getSortedScopes(authRequest.getScope(), scopeService);
model.put(SCOPES, sortedScopes);
// get the userinfo claims for each scope // get the userinfo claims for each scope
model.put(CLAIMS, getClaimsForScopes(p, sortedScopes));
// contacts
if (client.getContacts() != null) {
String contacts = Joiner.on(", ").join(client.getContacts());
model.put(CONTACTS, contacts);
}
SamlPrincipal p = (SamlPrincipal) auth.getPrincipal();
UserInfo user = userInfoService.get(p.getUsername(), client.getClientId(), authRequest.getScope(), p.getSamlCredential());
// contacts // contacts
if (client.getContacts() != null) { if (client.getContacts() != null) {
model.put(CONTACTS, Joiner.on(", ").join(client.getContacts())); model.put(CONTACTS, Joiner.on(", ").join(client.getContacts()));
} }
model.put(GRAS, true);
if (perunOidcConfig.getTheme().equalsIgnoreCase(DEFAULT)) { if (perunOidcConfig.getTheme().equalsIgnoreCase(DEFAULT)) {
Set<SystemScope> sortedScopes = ControllerUtils.getSortedScopes(authRequest.getScope(), scopeService);
model.put(SCOPES, sortedScopes);
model.put(CLAIMS, getClaimsForScopes(user, sortedScopes));
return APPROVE; return APPROVE;
} }
PerunUserInfo perunUser = (PerunUserInfo) userInfoService.getByUsernameAndClientId(
p.getName(), client.getClientId());
ControllerUtils.setScopesAndClaims(scopeService, scopeClaimTranslationService, model, authRequest.getScope(), ControllerUtils.setScopesAndClaims(scopeService, scopeClaimTranslationService, model, authRequest.getScope(),
perunUser); user);
ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig); ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
model.put(PAGE, CONSENT); model.put(PAGE, CONSENT);
@ -214,8 +211,7 @@ public class OAuthConfirmationController {
} }
} }
private Map<String, Map<String, String>> getClaimsForScopes(Principal p, Set<SystemScope> sortedScopes) { private Map<String, Map<String, String>> getClaimsForScopes(UserInfo user, Set<SystemScope> sortedScopes) {
UserInfo user = userInfoService.getByUsername(p.getName());
Map<String, Map<String, String>> claimsForScopes = new HashMap<>(); Map<String, Map<String, String>> claimsForScopes = new HashMap<>();
if (user != null) { if (user != null) {

View File

@ -12,6 +12,7 @@ import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
/** /**
* Service providing methods to use AttributeMapping objects when fetching attributes. * Service providing methods to use AttributeMapping objects when fetching attributes.
@ -93,8 +94,10 @@ public class AttributeMappingsService {
continue; continue;
} }
AttributeMapping am = initAttrMapping(identifier, attrProperties); AttributeMapping am = initAttrMapping(identifier, attrProperties);
log.debug("Initialized attributeMapping: {}", am); if (am != null) {
attributeMap.put(am.getIdentifier(), am); log.debug("Initialized attributeMapping: {}", am);
attributeMap.put(am.getIdentifier(), am);
}
} }
} }
@ -111,6 +114,12 @@ public class AttributeMappingsService {
separator = attrProperties.getProperty(attrIdentifier + SEPARATOR); separator = attrProperties.getProperty(attrIdentifier + SEPARATOR);
} }
if (!StringUtils.hasText(rpcIdentifier) && !StringUtils.hasText(ldapIdentifier)) {
log.warn("Attribute mapping for {} has no RPC nor LDAP mapping. It won't be used - check your configuration",
attrIdentifier);
return null;
}
return new AttributeMapping(attrIdentifier, rpcIdentifier, ldapIdentifier, type, separator); return new AttributeMapping(attrIdentifier, rpcIdentifier, ldapIdentifier, type, separator);
} }

View File

@ -80,7 +80,9 @@ public class PerunAccessTokenEnhancer implements TokenEnhancer {
UserInfo userInfo = null; UserInfo userInfo = null;
if (originalAuthRequest.getScope().contains(SystemScopeService.OPENID_SCOPE) if (originalAuthRequest.getScope().contains(SystemScopeService.OPENID_SCOPE)
&& !authentication.isClientOnly()) { && !authentication.isClientOnly()) {
userInfo = userInfoService.getByUsernameAndClientId(authentication.getName(), clientId); userInfo = userInfoService.get(authentication.getName(), clientId,
accessToken.getScope(),
((OAuth2AccessTokenEntity) accessToken).getAuthenticationHolder().getUserAuth());
} }
// create signed access token // create signed access token

View File

@ -2,6 +2,7 @@ package cz.muni.ics.oidc.server;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import com.nimbusds.jose.shaded.json.JSONArray; import com.nimbusds.jose.shaded.json.JSONArray;
import com.nimbusds.jose.util.JSONObjectUtils; import com.nimbusds.jose.util.JSONObjectUtils;
@ -61,8 +62,9 @@ public class PerunOIDCTokenService extends DefaultOIDCTokenService {
Set<String> authorizedClaims = translator.getClaimsForScopeSet(scopes); Set<String> authorizedClaims = translator.getClaimsForScopeSet(scopes);
Set<String> idTokenClaims = translator.getClaimsForScopeSet(perunOidcConfig.getIdTokenScopes()); Set<String> idTokenClaims = translator.getClaimsForScopeSet(perunOidcConfig.getIdTokenScopes());
for (Map.Entry<String, JsonElement> claim : userInfoService.getByUsernameAndClientId(userId, JsonObject userInfoJson = userInfoService.get(userId, clientId, accessToken.getScope(), accessToken.getAuthenticationHolder().getUserAuth())
clientId).toJson().entrySet()) { .toJson();
for (Map.Entry<String, JsonElement> claim : userInfoJson.entrySet()) {
String claimKey = claim.getKey(); String claimKey = claim.getKey();
JsonElement claimValue = claim.getValue(); JsonElement claimValue = claim.getValue();
if (claimValue != null && !claimValue.isJsonNull() && authorizedClaims.contains(claimKey) if (claimValue != null && !claimValue.isJsonNull() && authorizedClaims.contains(claimKey)

View File

@ -1,21 +0,0 @@
package cz.muni.ics.oidc.server.claims;
import cz.muni.ics.oidc.models.Facility;
public class ClaimContextCommonParameters {
private Facility client;
public ClaimContextCommonParameters(Facility client) {
this.client = client;
}
public Facility getClient() {
return client;
}
public void setClient(Facility client) {
this.client = client;
}
}

View File

@ -1,68 +1,41 @@
package cz.muni.ics.oidc.server.claims; package cz.muni.ics.oidc.server.claims;
import cz.muni.ics.oauth2.model.ClientDetailsEntity; import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oauth2.model.SamlAuthenticationDetails;
import cz.muni.ics.oidc.models.Facility;
import cz.muni.ics.oidc.models.PerunAttributeValue; import cz.muni.ics.oidc.models.PerunAttributeValue;
import cz.muni.ics.oidc.server.adapters.PerunAdapter; import cz.muni.ics.oidc.server.adapters.PerunAdapter;
import java.util.Map; import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/** /**
* Context in which the value of the claim is produced. * Context in which the value of the claim is produced.
* *
* @author Martin Kuba <makub@ics.muni.cz> * @author Martin Kuba <makub@ics.muni.cz>
*/ */
@Getter
@Setter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ClaimSourceProduceContext { public class ClaimSourceProduceContext {
private final long perunUserId; private long perunUserId;
private final String sub; private String sub;
private final Map<String, PerunAttributeValue> attrValues; private Map<String, PerunAttributeValue> attrValues;
private final PerunAdapter perunAdapter; private PerunAdapter perunAdapter;
private final ClientDetailsEntity client; private ClientDetailsEntity client;
private final ClaimContextCommonParameters contextCommonParameters; private Facility facility;
private SamlAuthenticationDetails samlAuthenticationDetails;
private Set<String> scopes;
public ClaimSourceProduceContext(long perunUserId,
String sub,
Map<String, PerunAttributeValue> attrValues,
PerunAdapter perunAdapter,
ClientDetailsEntity client,
ClaimContextCommonParameters contextCommonParameters)
{
this.perunUserId = perunUserId;
this.sub = sub;
this.attrValues = attrValues;
this.perunAdapter = perunAdapter;
this.client = client;
this.contextCommonParameters = contextCommonParameters;
}
public Map<String, PerunAttributeValue> getAttrValues() {
return attrValues;
}
public long getPerunUserId() {
return perunUserId;
}
public String getSub() {
return sub;
}
public PerunAdapter getPerunAdapter() {
return perunAdapter;
}
public ClientDetailsEntity getClient() {
return client;
}
public ClaimContextCommonParameters getContextCommonParameters() {
return contextCommonParameters;
}
@Override
public String toString() {
return "ClaimSourceProduceContext{" +
"perunUserId=" + perunUserId +
", sub='" + sub + '\'' +
'}';
}
} }

View File

@ -1,5 +1,8 @@
package cz.muni.ics.oidc.server.claims; package cz.muni.ics.oidc.server.claims;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import java.util.List;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
public class ClaimUtils { public class ClaimUtils {
@ -41,4 +44,27 @@ public class ClaimUtils {
} }
} }
public static boolean fillBooleanPropertyOrDefaultVal(String suffix, ClaimSourceInitContext ctx, boolean defaultVal) {
return fillBooleanPropertyOrDefaultVal(ctx.getProperty(suffix, NO_VALUE), defaultVal);
}
private static boolean fillBooleanPropertyOrDefaultVal(String prop, boolean defaultVal) {
if (StringUtils.hasText(prop)) {
return Boolean.parseBoolean(prop);
} else {
return defaultVal;
}
}
public static ArrayNode listToArrayNode(List<String> list) {
ArrayNode res = JsonNodeFactory.instance.arrayNode();
if (list != null && !list.isEmpty()) {
for (String s : list) {
if (StringUtils.hasText(s)) {
res.add(s);
}
}
}
return res;
}
} }

View File

@ -35,7 +35,7 @@ public class EntitlementExtendedClaimSource extends EntitlementSource {
@Override @Override
public JsonNode produceValue(ClaimSourceProduceContext pctx) { public JsonNode produceValue(ClaimSourceProduceContext pctx) {
Long userId = pctx.getPerunUserId(); Long userId = pctx.getPerunUserId();
Set<String> entitlements = produceEntitlementsExtended(pctx.getContextCommonParameters().getClient(), Set<String> entitlements = produceEntitlementsExtended(pctx.getFacility(),
userId, pctx.getPerunAdapter()); userId, pctx.getPerunAdapter());
JsonNode result = convertResultStringsToJsonArray(entitlements); JsonNode result = convertResultStringsToJsonArray(entitlements);
log.debug("{} - produced value for user({}): '{}'", getClaimName(), userId, result); log.debug("{} - produced value for user({}): '{}'", getClaimName(), userId, result);

View File

@ -83,7 +83,7 @@ public class EntitlementSource extends GroupNamesSource {
public JsonNode produceValue(ClaimSourceProduceContext pctx) { public JsonNode produceValue(ClaimSourceProduceContext pctx) {
PerunAdapter perunAdapter = pctx.getPerunAdapter(); PerunAdapter perunAdapter = pctx.getPerunAdapter();
Long userId = pctx.getPerunUserId(); Long userId = pctx.getPerunUserId();
Facility facility = pctx.getContextCommonParameters().getClient(); Facility facility = pctx.getFacility();
Set<Group> userGroups = getUserGroupsOnFacility(facility, userId, perunAdapter); Set<Group> userGroups = getUserGroupsOnFacility(facility, userId, perunAdapter);
Set<String> entitlements = produceEntitlements(facility, userGroups, userId, perunAdapter); Set<String> entitlements = produceEntitlements(facility, userGroups, userId, perunAdapter);

View File

@ -49,7 +49,7 @@ public class GroupNamesSource extends ClaimSource {
protected Map<Long, String> produceGroupNames(ClaimSourceProduceContext pctx) { protected Map<Long, String> produceGroupNames(ClaimSourceProduceContext pctx) {
log.trace("{} - produce group names with trimming 'members' part of the group names", getClaimName()); log.trace("{} - produce group names with trimming 'members' part of the group names", getClaimName());
Facility facility = pctx.getContextCommonParameters().getClient(); Facility facility = pctx.getFacility();
Set<Group> userGroups = getUserGroupsOnFacility(facility, pctx.getPerunUserId(), pctx.getPerunAdapter()); Set<Group> userGroups = getUserGroupsOnFacility(facility, pctx.getPerunUserId(), pctx.getPerunAdapter());
return getGroupIdToNameMap(userGroups, true); return getGroupIdToNameMap(userGroups, true);
} }

View File

@ -0,0 +1,68 @@
package cz.muni.ics.oidc.server.claims.sources;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import cz.muni.ics.oauth2.model.SamlAuthenticationDetails;
import cz.muni.ics.oidc.server.claims.ClaimSource;
import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext;
import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext;
import cz.muni.ics.oidc.server.claims.ClaimUtils;
import java.util.Collections;
import java.util.Set;
public class SamlAttributeClaimSource extends ClaimSource {
private static final String ATTRIBUTE = "attribute";
private static final String MULTI_VALUE = "isMultiValue";
private static final String NO_VALUE_AS_NULL = "noValueAsNull";
private static final String SEPARATOR = "separator";
private final String attributeName;
private final String separator;
private final boolean multiValue;
private final boolean noValueAsNull;
public SamlAttributeClaimSource(ClaimSourceInitContext ctx) {
super(ctx);
this.attributeName = ClaimUtils.fillStringMandatoryProperty(ATTRIBUTE, ctx, getClaimName());
this.multiValue = ClaimUtils.fillBooleanPropertyOrDefaultVal(MULTI_VALUE, ctx, true);
this.noValueAsNull = ClaimUtils.fillBooleanPropertyOrDefaultVal(NO_VALUE_AS_NULL, ctx, false);
this.separator = ClaimUtils.fillStringPropertyOrDefaultVal(SEPARATOR, ctx, ";");
}
@Override
public Set<String> getAttrIdentifiers() {
return Collections.emptySet();
}
@Override
public JsonNode produceValue(ClaimSourceProduceContext pctx) {
SamlAuthenticationDetails details = pctx.getSamlAuthenticationDetails();
if (details == null || details.getAttributes() == null || details.getAttributes().isEmpty()) {
return JsonNodeFactory.instance.nullNode();
}
String[] attrValue = details.getAttributes().getOrDefault(attributeName, null);
if (multiValue) {
if (attrValue == null || attrValue.length == 0) {
return !noValueAsNull ? JsonNodeFactory.instance.arrayNode() : JsonNodeFactory.instance.nullNode();
} else {
ArrayNode arrayNode = JsonNodeFactory.instance.arrayNode();
for (String val: attrValue) {
arrayNode.add(val);
}
return arrayNode;
}
} else {
if (attrValue == null || attrValue.length == 0) {
return JsonNodeFactory.instance.nullNode();
} else {
StringBuilder finalStr = new StringBuilder(separator);
for (String s: attrValue) {
finalStr.append(s);
}
return JsonNodeFactory.instance.textNode(finalStr.toString());
}
}
}
}

View File

@ -0,0 +1,68 @@
package cz.muni.ics.oidc.server.claims.sources;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import cz.muni.ics.oauth2.model.AuthenticationStatement;
import cz.muni.ics.oauth2.model.SamlAuthenticationDetails;
import cz.muni.ics.oidc.server.claims.ClaimSource;
import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext;
import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext;
import cz.muni.ics.oidc.server.claims.ClaimUtils;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.springframework.util.StringUtils;
public class SamlAuthnStatementClaimSource extends ClaimSource {
public SamlAuthnStatementClaimSource(ClaimSourceInitContext ctx) {
super(ctx);
}
@Override
public Set<String> getAttrIdentifiers() {
return Collections.emptySet();
}
@Override
public JsonNode produceValue(ClaimSourceProduceContext pctx) {
SamlAuthenticationDetails details = pctx.getSamlAuthenticationDetails();
if (details == null || details.getAttributes() == null || details.getAttributes().isEmpty()) {
return JsonNodeFactory.instance.nullNode();
}
List<AuthenticationStatement> statements = details.getAuthnStatements();
if (statements == null || statements.isEmpty()) {
return JsonNodeFactory.instance.arrayNode();
} else {
ArrayNode res = JsonNodeFactory.instance.arrayNode();
for (AuthenticationStatement s: statements) {
if (s == null) {
continue;
}
ObjectNode subNode = JsonNodeFactory.instance.objectNode();
subNode.put("authenticatingAuthorities", transformAuthenticatingAuthorities(s));
subNode.put("authnContextClassRef", transformAuthnContextClassRef(s));
res.add(subNode);
}
return res;
}
}
private JsonNode transformAuthenticatingAuthorities(AuthenticationStatement s) {
if (s == null) {
return JsonNodeFactory.instance.arrayNode();
}
return ClaimUtils.listToArrayNode(s.getAuthenticatingAuthorities());
}
private JsonNode transformAuthnContextClassRef(AuthenticationStatement s) {
if (s == null) {
return JsonNodeFactory.instance.nullNode();
}
return StringUtils.hasText(s.getAuthnContextClassRef()) ?
JsonNodeFactory.instance.textNode(s.getAuthnContextClassRef()) : JsonNodeFactory.instance.nullNode();
}
}

View File

@ -9,6 +9,8 @@ import com.google.gson.JsonSyntaxException;
import cz.muni.ics.openid.connect.model.DefaultUserInfo; import cz.muni.ics.openid.connect.model.DefaultUserInfo;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
@ -16,39 +18,41 @@ import lombok.extern.slf4j.Slf4j;
* *
* @author Martin Kuba <makub@ics.muni.cz> * @author Martin Kuba <makub@ics.muni.cz>
*/ */
@Getter
@Setter
@Slf4j @Slf4j
public class PerunUserInfo extends DefaultUserInfo { public class PerunUserInfo extends DefaultUserInfo {
private final Map<String, JsonNode> customClaims = new LinkedHashMap<>(); private final Map<String, JsonNode> customClaims = new LinkedHashMap<>();
private JsonObject obj;
public Map<String, JsonNode> getCustomClaims() { private long updatedAt;
return customClaims;
} private JsonObject renderedObject;
@Override @Override
public JsonObject toJson() { public JsonObject toJson() {
if (obj == null) { //TODO: include updatedAd in the object
if (renderedObject == null) {
//delegate standard claims to DefaultUserInfo //delegate standard claims to DefaultUserInfo
obj = super.toJson(); renderedObject = super.toJson();
//add custom claims //add custom claims
for (Map.Entry<String, JsonNode> entry : customClaims.entrySet()) { for (Map.Entry<String, JsonNode> entry : customClaims.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
JsonNode value = entry.getValue(); JsonNode value = entry.getValue();
if (value == null || value.isNull()) { if (value == null || value.isNull()) {
obj.addProperty(key, (String) null); renderedObject.addProperty(key, (String) null);
log.debug("adding null claim {}=null", key); log.debug("adding null claim {}=null", key);
} else if (value.isTextual() || value.isBoolean()) { } else if (value.isTextual() || value.isBoolean()) {
obj.addProperty(key, value.asText()); renderedObject.addProperty(key, value.asText());
log.debug("adding string claim {}={}", key, value.asText()); log.debug("adding string claim {}={}", key, value.asText());
} else if (value.isNumber()) { } else if (value.isNumber()) {
obj.addProperty(key, value.asLong()); renderedObject.addProperty(key, value.asLong());
log.debug("adding long claim {}={}", key, value.asText()); log.debug("adding long claim {}={}", key, value.asText());
} else if (value.isContainerNode()) { } else if (value.isContainerNode()) {
try { try {
//convert from Jackson to GSon //convert from Jackson to GSon
String rawJson = new ObjectMapper().writeValueAsString(value); String rawJson = new ObjectMapper().writeValueAsString(value);
obj.add(key, new JsonParser().parse(rawJson)); renderedObject.add(key, new JsonParser().parse(rawJson));
log.debug("adding JSON claim {}={}", key, rawJson); log.debug("adding JSON claim {}={}", key, rawJson);
} catch (JsonProcessingException | JsonSyntaxException e) { } catch (JsonProcessingException | JsonSyntaxException e) {
log.error("cannot convert Jackson/Gson value " + value, e); log.error("cannot convert Jackson/Gson value " + value, e);
@ -60,7 +64,7 @@ public class PerunUserInfo extends DefaultUserInfo {
} else { } else {
log.debug("already rendered to JSON"); log.debug("already rendered to JSON");
} }
return obj; return renderedObject;
} }
} }

View File

@ -0,0 +1,323 @@
package cz.muni.ics.oidc.server.userInfo;
import static cz.muni.ics.oidc.server.PerunScopeClaimTranslationService.ADDRESS;
import static cz.muni.ics.oidc.server.PerunScopeClaimTranslationService.EMAIL;
import static cz.muni.ics.oidc.server.PerunScopeClaimTranslationService.OPENID;
import static cz.muni.ics.oidc.server.PerunScopeClaimTranslationService.PHONE;
import static cz.muni.ics.oidc.server.PerunScopeClaimTranslationService.PROFILE;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.cache.CacheLoader;
import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oidc.models.PerunAttributeValue;
import cz.muni.ics.oidc.models.PerunAttributeValueAwareModel;
import cz.muni.ics.oidc.server.adapters.PerunAdapter;
import cz.muni.ics.oidc.server.claims.ClaimModifier;
import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext;
import cz.muni.ics.oidc.server.claims.PerunCustomClaimDefinition;
import cz.muni.ics.oidc.server.userInfo.mappings.AddressMappings;
import cz.muni.ics.oidc.server.userInfo.mappings.EmailMappings;
import cz.muni.ics.oidc.server.userInfo.mappings.OpenidMappings;
import cz.muni.ics.oidc.server.userInfo.mappings.PhoneMappings;
import cz.muni.ics.oidc.server.userInfo.mappings.ProfileMappings;
import cz.muni.ics.openid.connect.model.Address;
import cz.muni.ics.openid.connect.model.DefaultAddress;
import cz.muni.ics.openid.connect.model.UserInfo;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.Builder;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
@Setter
@Slf4j
@Builder
public class PerunUserInfoCacheLoader extends CacheLoader<UserInfoCacheKey, UserInfo> {
private OpenidMappings openidMappings;
private ProfileMappings profileMappings;
private EmailMappings emailMappings;
private AddressMappings addressMappings;
private PhoneMappings phoneMappings;
private PerunAdapter perunAdapter;
private List<PerunCustomClaimDefinition> customClaims;
private boolean fillAttributes;
private List<ClaimModifier> subModifiers;
@Override
public UserInfo load(UserInfoCacheKey key) {
log.debug("load({}) ... populating cache for the key", key);
PerunUserInfo ui = new PerunUserInfo();
long perunUserId = key.getUserId();
Set<String> attributes = constructAttributes(key.getScopes());
Map<String, PerunAttributeValue> userAttributeValues = fetchUserAttributes(perunUserId, attributes);
ClaimSourceProduceContext.ClaimSourceProduceContextBuilder builder = ClaimSourceProduceContext.builder()
.perunUserId(perunUserId)
.sub(ui.getSub())
.attrValues(userAttributeValues)
.scopes(key.getScopes())
.client(key.getClient())
.perunAdapter(perunAdapter)
.samlAuthenticationDetails(key.getAuthenticationDetails());
if (key.getClient() != null) {
builder = builder.facility(perunAdapter.getFacilityByClientId(key.getClient().getClientId()));
}
ClaimSourceProduceContext pctx = builder.build();
processStandardScopes(pctx, ui);
processCustomScopes(pctx, ui);
return ui;
}
private Map<String, PerunAttributeValue> fetchUserAttributes(long perunUserId, Set<String> attributes) {
Map<String, PerunAttributeValue> userAttributeValues =
perunAdapter.getUserAttributeValues(perunUserId, attributes);
if (shouldFillAttrs(userAttributeValues)) {
List<String> attrNames = userAttributeValues.entrySet()
.stream()
.filter(entry -> (null == entry.getValue() || entry.getValue().isNullValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
Map<String, PerunAttributeValue> missingAttrs = perunAdapter.getAdapterFallback()
.getUserAttributeValues(perunUserId, attrNames);
userAttributeValues.putAll(missingAttrs);
}
return userAttributeValues;
}
private Set<String> constructAttributes(Set<String> requestedScopes) {
Set<String> attributes = new HashSet<>();
if (requestedScopes != null && !requestedScopes.isEmpty()) {
if (requestedScopes.contains(OPENID)) {
attributes.addAll(openidMappings.getAttrNames());
}
if (requestedScopes.contains(PROFILE)) {
attributes.addAll(profileMappings.getAttrNames());
}
if (requestedScopes.contains(EMAIL)) {
attributes.addAll(emailMappings.getAttrNames());
}
if (requestedScopes.contains(ADDRESS)) {
attributes.addAll(addressMappings.getAttrNames());
}
if (requestedScopes.contains(PHONE)) {
attributes.addAll(phoneMappings.getAttrNames());
}
for (PerunCustomClaimDefinition pccd : customClaims) {
if (requestedScopes.contains(pccd.getScope())) {
attributes.addAll(pccd.getClaimSource().getAttrIdentifiers());
}
}
}
return attributes;
}
private void processCustomScopes(ClaimSourceProduceContext pctx, PerunUserInfo ui) {
log.debug("processing custom claims");
for (PerunCustomClaimDefinition claimDef : customClaims) {
if (isScopeRequested(claimDef.getScope(), pctx.getScopes())) {
processCustomScope(claimDef, pctx, ui);
}
}
log.debug("UserInfo created");
}
private void processCustomScope(PerunCustomClaimDefinition claimDef, ClaimSourceProduceContext pctx, PerunUserInfo ui) {
log.debug("producing value for custom claim {}", claimDef.getClaim());
JsonNode claimInJson = claimDef.getClaimSource().produceValue(pctx);
log.debug("produced value {}={}", claimDef.getClaim(), claimInJson);
if (claimInJson == null || claimInJson.isNull()) {
log.debug("claim {} is null", claimDef.getClaim());
return;
} else if (claimInJson.isTextual() && !StringUtils.hasText(claimInJson.asText())) {
log.debug("claim {} is a string and it is empty or null", claimDef.getClaim());
return;
} else if ((claimInJson.isArray() || claimInJson.isObject()) && claimInJson.size() == 0) {
log.debug("claim {} is an object or array and it is empty or null", claimDef.getClaim());
return;
}
List<ClaimModifier> claimModifiers = claimDef.getClaimModifiers();
if (claimModifiers != null && !claimModifiers.isEmpty()) {
claimInJson = modifyClaims(claimModifiers, claimInJson);
}
ui.getCustomClaims().put(claimDef.getClaim(), claimInJson);
}
private boolean isScopeRequested(String scope, Set<String> scopes) {
return scopes != null && scopes.contains(scope);
}
private void processStandardScopes(ClaimSourceProduceContext ctx, PerunUserInfo ui) {
Set<String> scopes = ctx.getScopes();
if (scopes != null && !scopes.isEmpty()) {
if (scopes.contains(OPENID)) {
processOpenid(ctx.getAttrValues(), ctx.getPerunUserId(), ui);
}
if (scopes.contains(PROFILE)) {
processProfile(ctx.getAttrValues(), ui);
}
if (scopes.contains(EMAIL)) {
processEmail(ctx.getAttrValues(), ui);
}
if (scopes.contains(ADDRESS)) {
processAddress(ctx.getAttrValues(), ui);
}
if (scopes.contains(PHONE)) {
processPhone(ctx.getAttrValues(), ui);
}
}
}
private void processOpenid(Map<String, PerunAttributeValue> userAttributeValues, long perunUserId,
PerunUserInfo ui) {
JsonNode subJson = extractJsonValue(openidMappings.getSub(), userAttributeValues);
if (subJson != null && !subJson.isNull() && StringUtils.hasText(subJson.asText())) {
if (subModifiers != null) {
subJson = modifyClaims(subModifiers, subJson);
if (subJson.asText() == null || !StringUtils.hasText(subJson.asText())) {
throw new RuntimeException("Sub has no value after modification for username " + perunUserId);
}
}
ui.setSub(subJson.asText());
}
ui.setId(perunUserId);
}
private void processProfile(Map<String, PerunAttributeValue> userAttributeValues, PerunUserInfo ui) {
ui.setPreferredUsername(extractStringValue(profileMappings.getPreferredUsername(), userAttributeValues));
ui.setGivenName(extractStringValue(profileMappings.getGivenName(), userAttributeValues));
ui.setFamilyName(extractStringValue(profileMappings.getFamilyName(), userAttributeValues));
ui.setMiddleName(extractStringValue(profileMappings.getMiddleName(), userAttributeValues));
ui.setName(extractStringValue(profileMappings.getName(), userAttributeValues));
ui.setNickname(extractStringValue(profileMappings.getNickname(), userAttributeValues));
ui.setProfile(extractStringValue(profileMappings.getProfile(), userAttributeValues));
ui.setPicture(extractStringValue(profileMappings.getPicture(), userAttributeValues));
ui.setWebsite(extractStringValue(profileMappings.getWebsite(), userAttributeValues));
ui.setZoneinfo(extractStringValue(profileMappings.getZoneinfo(), userAttributeValues));
ui.setGender(extractStringValue(profileMappings.getGender(), userAttributeValues));
ui.setBirthdate(extractStringValue(profileMappings.getBirthdate(), userAttributeValues));
ui.setLocale(extractStringValue(profileMappings.getLocale(), userAttributeValues));
ui.setUpdatedAt(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC));
}
private void processEmail(Map<String, PerunAttributeValue> userAttributeValues, PerunUserInfo ui) {
ui.setEmail(extractStringValue(emailMappings.getEmail(), userAttributeValues));
ui.setEmailVerified(Boolean.parseBoolean(
extractStringValue(emailMappings.getEmailVerified(), userAttributeValues)));
}
private void processAddress(Map<String, PerunAttributeValue> userAttributeValues, PerunUserInfo ui) {
Address address = null;
if (isAddressAvailable(userAttributeValues)) {
address = new DefaultAddress();
address.setFormatted(extractStringValue(addressMappings.getFormatted(), userAttributeValues));
address.setStreetAddress(extractStringValue(addressMappings.getStreetAddress(), userAttributeValues));
address.setLocality(extractStringValue(addressMappings.getLocality(), userAttributeValues));
address.setPostalCode(extractStringValue(addressMappings.getPostalCode(), userAttributeValues));
address.setCountry(extractStringValue(addressMappings.getCountry(), userAttributeValues));
}
ui.setAddress(address);
}
private void processPhone(Map<String, PerunAttributeValue> userAttributeValues, PerunUserInfo ui) {
ui.setPhoneNumber(extractStringValue(phoneMappings.getPhoneNumber(), userAttributeValues));
ui.setPhoneNumberVerified(Boolean.parseBoolean(
extractStringValue(phoneMappings.getPhoneNumber(), userAttributeValues)));
}
private boolean isAddressAvailable(Map<String, PerunAttributeValue> userAttributeValues) {
return hasNonNullValue(addressMappings.getFormatted(), userAttributeValues)
|| hasNonNullValue(addressMappings.getStreetAddress(), userAttributeValues)
|| hasNonNullValue(addressMappings.getLocality(), userAttributeValues)
|| hasNonNullValue(addressMappings.getPostalCode(), userAttributeValues)
|| hasNonNullValue(addressMappings.getCountry(), userAttributeValues);
}
private boolean hasNonNullValue(String mapping, Map<String, PerunAttributeValue> valueMap) {
if (mapping == null) {
return false;
}
PerunAttributeValue v = valueMap.getOrDefault(mapping, null);
return v != null && !v.isNullValue();
}
private JsonNode extractJsonValue(String mapping, Map<String, PerunAttributeValue> valueMap) {
PerunAttributeValue v = extractValue(mapping, valueMap);
if (v != null) {
return v.valueAsJson();
}
return JsonNodeFactory.instance.nullNode();
}
private String extractStringValue(String mapping, Map<String, PerunAttributeValue> valueMap) {
PerunAttributeValue v = extractValue(mapping, valueMap);
if (v != null) {
return v.valueAsString();
}
return null;
}
private PerunAttributeValue extractValue(String mapping, Map<String, PerunAttributeValue> valueMap) {
if (!StringUtils.hasText(mapping)) {
return null;
}
return valueMap.getOrDefault(mapping, null);
}
private JsonNode modifyClaims(List<ClaimModifier> claimModifiers, JsonNode value) {
for (ClaimModifier modifier: claimModifiers) {
value = modifyClaim(modifier, value);
}
return value;
}
private JsonNode modifyClaim(ClaimModifier modifier, JsonNode orig) {
JsonNode claimInJson = orig.deepCopy();
if (claimInJson.isTextual()) {
return TextNode.valueOf(modifier.modify(claimInJson.asText()));
} else if (claimInJson.isArray()) {
ArrayNode arrayNode = (ArrayNode) claimInJson;
for (int i = 0; i < arrayNode.size(); i++) {
JsonNode item = arrayNode.get(i);
if (item.isTextual()) {
String original = item.asText();
String modified = modifier.modify(original);
arrayNode.set(i, TextNode.valueOf(modified));
}
}
return arrayNode;
} else {
log.warn("Original value is neither string nor array of strings - cannot modify values");
return orig;
}
}
private boolean shouldFillAttrs(Map<String, PerunAttributeValue> userAttributeValues) {
if (fillAttributes) {
if (userAttributeValues.isEmpty()) {
return true;
} else if (userAttributeValues.containsValue(null)) {
return true;
} else {
return !userAttributeValues.values().stream()
.filter(PerunAttributeValueAwareModel::isNullValue)
.collect(Collectors.toSet())
.isEmpty();
}
}
return false;
}
}

View File

@ -1,49 +1,35 @@
package cz.muni.ics.oidc.server.userInfo; package cz.muni.ics.oidc.server.userInfo;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache; import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;
import cz.muni.ics.jwt.signer.service.JWTSigningAndValidationService; import cz.muni.ics.jwt.signer.service.JWTSigningAndValidationService;
import cz.muni.ics.oauth2.model.ClientDetailsEntity; import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oauth2.model.SamlAuthenticationDetails;
import cz.muni.ics.oauth2.model.SavedUserAuthentication;
import cz.muni.ics.oauth2.service.ClientDetailsEntityService; import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
import cz.muni.ics.oidc.exceptions.ConfigurationException; import cz.muni.ics.oidc.exceptions.ConfigurationException;
import cz.muni.ics.oidc.models.Facility;
import cz.muni.ics.oidc.models.PerunAttributeValue;
import cz.muni.ics.oidc.models.PerunAttributeValueAwareModel;
import cz.muni.ics.oidc.server.adapters.PerunAdapter; import cz.muni.ics.oidc.server.adapters.PerunAdapter;
import cz.muni.ics.oidc.server.claims.ClaimContextCommonParameters;
import cz.muni.ics.oidc.server.claims.ClaimModifier; import cz.muni.ics.oidc.server.claims.ClaimModifier;
import cz.muni.ics.oidc.server.claims.ClaimModifierInitContext;
import cz.muni.ics.oidc.server.claims.ClaimSource;
import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext;
import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext;
import cz.muni.ics.oidc.server.claims.PerunCustomClaimDefinition; import cz.muni.ics.oidc.server.claims.PerunCustomClaimDefinition;
import cz.muni.ics.oidc.server.claims.modifiers.NoOperationModifier;
import cz.muni.ics.oidc.server.configurations.PerunOidcConfig; import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
import cz.muni.ics.openid.connect.model.Address; import cz.muni.ics.oidc.server.userInfo.mappings.AddressMappings;
import cz.muni.ics.openid.connect.model.DefaultAddress; import cz.muni.ics.oidc.server.userInfo.mappings.EmailMappings;
import cz.muni.ics.oidc.server.userInfo.mappings.OpenidMappings;
import cz.muni.ics.oidc.server.userInfo.mappings.PhoneMappings;
import cz.muni.ics.oidc.server.userInfo.mappings.ProfileMappings;
import cz.muni.ics.oidc.server.userInfo.modifiers.UserInfoModifierContext;
import cz.muni.ics.openid.connect.model.UserInfo; import cz.muni.ics.openid.connect.model.UserInfo;
import cz.muni.ics.openid.connect.service.UserInfoService; import cz.muni.ics.openid.connect.service.UserInfoService;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@ -54,12 +40,6 @@ import org.springframework.util.StringUtils;
@Slf4j @Slf4j
public class PerunUserInfoService implements UserInfoService { public class PerunUserInfoService implements UserInfoService {
private static final String CUSTOM_CLAIM = "custom.claim.";
private static final String SOURCE = ".source";
private static final String CLASS = ".class";
private static final String NAMES = ".names";
private static final String MODIFIER = ".modifier";
@Autowired @Autowired
private ClientDetailsEntityService clientService; private ClientDetailsEntityService clientService;
@ -69,128 +49,44 @@ public class PerunUserInfoService implements UserInfoService {
@Autowired @Autowired
private PerunOidcConfig perunOidcConfig; private PerunOidcConfig perunOidcConfig;
private List<String> forceRegenerateUserinfoCustomClaims = new ArrayList<>(); @Autowired
private List<String> forceRegenerateUserinfoStandardClaims = new ArrayList<>(); private OpenidMappings openidMappings;
private final LoadingCache<UserClientPair, UserInfo> cache; @Autowired
private ProfileMappings profileMappings;
@Autowired
private EmailMappings emailMappings;
@Autowired
private AddressMappings addressMappings;
@Autowired
private PhoneMappings phoneMappings;
@Autowired
private PerunAdapter perunAdapter; private PerunAdapter perunAdapter;
private LoadingCache<UserInfoCacheKey, UserInfo> cache;
private Properties properties; private Properties properties;
private String subAttribute;
private List<ClaimModifier> subModifiers;
private String preferredUsernameAttribute;
private String givenNameAttribute;
private String familyNameAttribute;
private String middleNameAttribute;
private String fullNameAttribute;
private String emailAttribute;
private String addressAttribute;
private String phoneAttribute;
private String zoneinfoAttribute;
private String localeAttribute;
private Set<String> customClaimNames; private Set<String> customClaimNames;
private List<PerunCustomClaimDefinition> customClaims = new ArrayList<>(); private List<PerunCustomClaimDefinition> customClaims = new ArrayList<>();
private UserInfoModifierContext userInfoModifierContext; private UserInfoModifierContext userInfoModifierContext;
private final Set<String> userAttrNames = new HashSet<>(); private List<String> forceRegenerateUserinfoCustomClaims = new ArrayList<>();
private List<String> forceRegenerateUserinfoStandardClaims = new ArrayList<>();
// == setters and getters ==
public void setProperties(Properties properties) { public void setProperties(Properties properties) {
this.properties = properties; this.properties = properties;
} }
public void setPerunAdapter(PerunAdapter perunAdapter) {
this.perunAdapter = perunAdapter;
}
public void setSubAttribute(String subAttribute) {
if (this.subAttribute != null) {
userAttrNames.remove(this.subAttribute);
}
userAttrNames.add(subAttribute);
this.subAttribute = subAttribute;
}
public void setPreferredUsernameAttribute(String preferredUsernameAttribute) {
if (this.preferredUsernameAttribute != null) {
userAttrNames.remove(this.preferredUsernameAttribute);
}
userAttrNames.add(preferredUsernameAttribute);
this.preferredUsernameAttribute = preferredUsernameAttribute;
}
public void setGivenNameAttribute(String givenNameAttribute) {
if (this.givenNameAttribute != null) {
userAttrNames.remove(this.givenNameAttribute);
}
userAttrNames.add(givenNameAttribute);
this.givenNameAttribute = givenNameAttribute;
}
public void setFamilyNameAttribute(String familyNameAttribute) {
if (this.familyNameAttribute != null) {
userAttrNames.remove(this.familyNameAttribute);
}
userAttrNames.add(familyNameAttribute);
this.familyNameAttribute = familyNameAttribute;
}
public void setMiddleNameAttribute(String middleNameAttribute) {
if (this.middleNameAttribute != null) {
userAttrNames.remove(this.middleNameAttribute);
}
userAttrNames.add(middleNameAttribute);
this.middleNameAttribute = middleNameAttribute;
}
public void setFullNameAttribute(String fullNameAttribute) {
if (this.fullNameAttribute != null) {
userAttrNames.remove(this.fullNameAttribute);
}
userAttrNames.add(fullNameAttribute);
this.fullNameAttribute = fullNameAttribute;
}
public void setEmailAttribute(String emailAttribute) {
if (this.emailAttribute != null) {
userAttrNames.remove(this.emailAttribute);
}
userAttrNames.add(emailAttribute);
this.emailAttribute = emailAttribute;
}
public void setAddressAttribute(String addressAttribute) {
if (this.addressAttribute != null) {
userAttrNames.remove(this.addressAttribute);
}
userAttrNames.add(addressAttribute);
this.addressAttribute = addressAttribute;
}
public void setPhoneAttribute(String phoneAttribute) {
if (this.phoneAttribute != null) {
userAttrNames.remove(this.phoneAttribute);
}
userAttrNames.add(phoneAttribute);
this.phoneAttribute = phoneAttribute;
}
public void setZoneinfoAttribute(String zoneinfoAttribute) {
if (this.zoneinfoAttribute != null) {
userAttrNames.remove(this.zoneinfoAttribute);
}
userAttrNames.add(zoneinfoAttribute);
this.zoneinfoAttribute = zoneinfoAttribute;
}
public void setLocaleAttribute(String localeAttribute) {
if (this.localeAttribute != null) {
userAttrNames.remove(this.localeAttribute);
}
userAttrNames.add(localeAttribute);
this.localeAttribute = localeAttribute;
}
public void setCustomClaimNames(Set<String> customClaimNames) { public void setCustomClaimNames(Set<String> customClaimNames) {
this.customClaimNames = customClaimNames; this.customClaimNames = customClaimNames;
} }
@ -207,52 +103,84 @@ public class PerunUserInfoService implements UserInfoService {
return customClaims; return customClaims;
} }
public void setPerunAdapter(PerunAdapter perunAdapter) {
this.perunAdapter = perunAdapter;
}
// == init ==
@PostConstruct @PostConstruct
public void postInit() throws ConfigurationException { public void postInit() throws ConfigurationException {
log.debug("trying to load modifier for attribute.openid.sub");
subModifiers = loadClaimValueModifiers("sub", "attribute.openid.sub" + MODIFIER);
//custom claims //custom claims
this.customClaims = new ArrayList<>(customClaimNames.size()); this.customClaims = UserInfoUtils.loadCustomClaims(customClaimNames, properties, perunOidcConfig, jwtService);
for (String claimName : customClaimNames) {
String propertyBase = CUSTOM_CLAIM + claimName;
//get scope
String scopeProperty = propertyBase + ".scope";
String scope = properties.getProperty(scopeProperty);
if (scope == null) {
log.error("property {} not found, skipping custom claim {}", scopeProperty, claimName);
continue;
}
//get ClaimSource
ClaimSource claimSource = loadClaimSource(claimName, propertyBase + SOURCE);
userAttrNames.addAll(claimSource.getAttrIdentifiers());
//optional claim value modifier
List<ClaimModifier> claimModifiers = loadClaimValueModifiers(claimName, propertyBase + MODIFIER);
//add claim definition
customClaims.add(new PerunCustomClaimDefinition(scope, claimName, claimSource, claimModifiers));
}
this.userInfoModifierContext = new UserInfoModifierContext(properties, perunAdapter); this.userInfoModifierContext = new UserInfoModifierContext(properties, perunAdapter);
log.debug("trying to load modifier for attribute.openid.sub");
List<ClaimModifier> subModifiers = UserInfoUtils.loadClaimValueModifiers(
properties, "sub", "attribute.openid.sub");
PerunUserInfoCacheLoader cacheLoader = PerunUserInfoCacheLoader.builder()
.openidMappings(openidMappings)
.profileMappings(profileMappings)
.emailMappings(emailMappings)
.phoneMappings(phoneMappings)
.addressMappings(addressMappings)
.customClaims(customClaims)
.fillAttributes(perunOidcConfig.isFillMissingUserAttrs())
.perunAdapter(perunAdapter)
.subModifiers(subModifiers)
.build();
this.cache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterAccess(java.time.Duration.ofSeconds(60))
.expireAfterWrite(java.time.Duration.ofSeconds(300))
.build(cacheLoader);
}
// == public methods ==
@Override
public UserInfo get(String username, String clientId, Set<String> scope, SavedUserAuthentication userAuthentication) {
return get(username, clientId, scope, userAuthentication.getAuthenticationDetails());
} }
@Override @Override
public UserInfo getByUsernameAndClientId(String username, String clientId) { public UserInfo get(String username, String clientId, Set<String> scope, SAMLCredential samlCredential) {
ClientDetailsEntity client = clientService.loadClientByClientId(clientId); return get(username, clientId, scope, new SamlAuthenticationDetails(samlCredential));
if (client == null) { }
log.warn("did not found client with id {}", clientId);
@Override
public UserInfo get(String username, String clientId, Set<String> scope) {
return get(username, clientId, scope, new SamlAuthenticationDetails());
}
// == private methods ==
private UserInfo get(String username, String clientId, Set<String> scope, SamlAuthenticationDetails details) {
if (!StringUtils.hasText(clientId)) {
log.warn("No client_id provided, cannot get userinfo");
return null; return null;
} }
ClientDetailsEntity client = null;
if (StringUtils.hasText(clientId)) {
client = clientService.loadClientByClientId(clientId);
if (client == null) {
log.warn("Did not find client with id '{}', cannot get userinfo", clientId);
return null;
}
}
PerunUserInfo userInfo; PerunUserInfo userInfo;
try { try {
UserClientPair cacheKey = new UserClientPair(username, clientId, client); UserInfoCacheKey cacheKey = new UserInfoCacheKey(username, client, details, scope);
userInfo = (PerunUserInfo) cache.get(cacheKey); userInfo = (PerunUserInfo) cache.get(cacheKey);
if (!checkStandardClaims(userInfo) || !checkCustomClaims(userInfo)) { if (!checkStandardClaims(userInfo) || !checkCustomClaims(userInfo)) {
log.info("Some required claim is null, regenerate userInfo"); log.info("Some required claim is null, regenerate userInfo");
cache.invalidate(cacheKey); cache.invalidate(cacheKey);
userInfo = (PerunUserInfo) cache.get(cacheKey); userInfo = (PerunUserInfo) cache.get(cacheKey);
} }
log.debug("loaded UserInfo from cache for '{}'/'{}'", userInfo.getName(), client.getClientName());
userInfo = userInfoModifierContext.modify(userInfo, clientId); userInfo = userInfoModifierContext.modify(userInfo, clientId);
} catch (ExecutionException e) { } catch (ExecutionException e) {
log.error("cannot get user from cache", e); log.error("cannot get user from cache", e);
@ -262,330 +190,6 @@ public class PerunUserInfoService implements UserInfoService {
return userInfo; return userInfo;
} }
@Override
public UserInfo getByUsername(String username) {
PerunUserInfo userInfo;
try {
UserClientPair cacheKey = new UserClientPair(username);
userInfo = (PerunUserInfo) cache.get(cacheKey);
if (!checkStandardClaims(userInfo) || !checkCustomClaims(userInfo)) {
log.info("Some required claim is null, regenerate userInfo");
cache.invalidate(cacheKey);
userInfo = (PerunUserInfo) cache.get(cacheKey);
}
log.debug("loaded UserInfo from cache for '{}'", userInfo.getName());
userInfo = userInfoModifierContext.modify(userInfo, null);
} catch (UncheckedExecutionException | ExecutionException e) {
log.error("cannot get user from cache", e);
return null;
}
return userInfo;
}
@Override
public UserInfo getByEmailAddress(String email) {
throw new RuntimeException("PerunUserInfoService.getByEmailAddress() not implemented");
}
public PerunUserInfoService() {
this.cache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterAccess(60, TimeUnit.SECONDS)
.build(cacheLoader);
}
private List<ClaimModifier> loadClaimValueModifiers(String claimName, String propertyPrefix)
throws ConfigurationException
{
String names = properties.getProperty(propertyPrefix + NAMES, "");
String[] nameArr = names.split(",");
List<ClaimModifier> modifiers = new ArrayList<>();
if (nameArr.length > 0) {
for (String name : nameArr) {
modifiers.add(loadClaimValueModifier(claimName, propertyPrefix + '.' + name, name));
}
}
return modifiers;
}
private ClaimModifier loadClaimValueModifier(String claimName, String propertyPrefix, String modifierName)
throws ConfigurationException
{
String modifierClass = properties.getProperty(propertyPrefix + CLASS, NoOperationModifier.class.getName());
if (!StringUtils.hasText(modifierClass)) {
log.debug("{}:{} - no class has ben configured for claim value modifier, use noop modifier",
claimName, modifierName);
modifierClass = NoOperationModifier.class.getName();
}
log.trace("{}:{} - loading ClaimModifier class '{}'", claimName, modifierName, modifierClass);
try {
Class<?> rawClazz = Class.forName(modifierClass);
if (!ClaimModifier.class.isAssignableFrom(rawClazz)) {
log.error("{}:{} - failed to initialized claim modifier: class '{}' does not extend ClaimModifier",
claimName, modifierName, modifierClass);
throw new ConfigurationException("No instantiable class modifier configured for claim " + claimName);
}
@SuppressWarnings("unchecked") Class<ClaimModifier> clazz = (Class<ClaimModifier>) rawClazz;
Constructor<ClaimModifier> constructor = clazz.getConstructor(ClaimModifierInitContext.class);
ClaimModifierInitContext ctx = new ClaimModifierInitContext(
propertyPrefix, properties, claimName, modifierName);
return constructor.newInstance(ctx);
} catch (ClassNotFoundException e) {
log.error("{}:{} - failed to initialize claim modifier: class '{}' was not found",
claimName, modifierName, modifierClass);
log.trace("{}:{} - details:", claimName, modifierName, e);
throw new ConfigurationException("Error has occurred when instantiating claim modifier '"
+ modifierName + "' of claim '" + claimName + '\'');
} catch (NoSuchMethodException e) {
log.error("{}:{} - failed to initialize claim modifier: class '{}' does not have proper constructor",
claimName, modifierName, modifierClass);
log.trace("{}:{} - details:", claimName, e);
throw new ConfigurationException("Error has occurred when instantiating claim modifier '"
+ modifierName + "' of claim '" + claimName + '\'');
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
log.error("{}:{} - failed to initialize claim modifier: class '{}' cannot be instantiated",
claimName, modifierName, modifierClass);
log.trace("{}:{} - details:", claimName, e);
throw new ConfigurationException("Error has occurred when instantiating claim modifier '"
+ modifierName + "' of claim '" + claimName + '\'');
}
}
private ClaimSource loadClaimSource(String claimName, String propertyPrefix) throws ConfigurationException {
String sourceClass = properties.getProperty(propertyPrefix + CLASS);
if (!StringUtils.hasText(sourceClass)) {
log.error("{} - failed to initialized claim source: no class has ben configured", claimName);
throw new ConfigurationException("No class configured for claim source");
}
log.trace("{} - loading ClaimSource class '{}'", claimName, sourceClass);
try {
Class<?> rawClazz = Class.forName(sourceClass);
if (!ClaimSource.class.isAssignableFrom(rawClazz)) {
log.error("{} - failed to initialized claim source: class '{}' does not extend ClaimSource",
claimName, sourceClass);
throw new ConfigurationException("No instantiable class source configured for claim " + claimName);
}
@SuppressWarnings("unchecked") Class<ClaimSource> clazz = (Class<ClaimSource>) rawClazz;
Constructor<ClaimSource> constructor = clazz.getConstructor(ClaimSourceInitContext.class);
ClaimSourceInitContext ctx = new ClaimSourceInitContext(perunOidcConfig, jwtService, propertyPrefix,
properties, claimName);
return constructor.newInstance(ctx);
} catch (ClassNotFoundException e) {
log.error("{} - failed to initialize claim source: class '{}' was not found", claimName, sourceClass);
log.trace("{} - details:", claimName, e);
throw new ConfigurationException("Error has occurred when instantiating claim source for claim " + claimName);
} catch (NoSuchMethodException e) {
log.error("{} - failed to initialize claim source: class '{}' does not have proper constructor",
claimName, sourceClass);
log.trace("{} - details:", claimName, e);
throw new ConfigurationException("Error has occurred when instantiating claim source for claim " + claimName);
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
log.error("{} - failed to initialize claim source: class '{}' cannot be instantiated", claimName, sourceClass);
log.trace("{} - details:", claimName, e);
throw new ConfigurationException("Error has occurred when instantiating claim source for claim " + claimName);
}
}
private static class UserClientPair {
private final long userId;
private String clientId;
private ClientDetailsEntity client;
UserClientPair(String userId) {
this.userId = Long.parseLong(userId);
}
UserClientPair(String userId, String clientId, ClientDetailsEntity client) {
this.userId = Long.parseLong(userId);
this.clientId = clientId;
this.client = client;
}
public long getUserId() {
return userId;
}
public String getClientId() {
return clientId;
}
public ClientDetailsEntity getClient() {
return client;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserClientPair that = (UserClientPair) o;
return userId == that.userId &&
Objects.equals(clientId, that.clientId);
}
@Override
public int hashCode() {
return Objects.hash(userId, clientId);
}
@Override
public String toString() {
return "(" + "userId=" + userId + "," + (client == null ? "null" : "client=" + clientId + " '" + client.getClientName()) + "')";
}
}
@SuppressWarnings("FieldCanBeLocal")
private final CacheLoader<UserClientPair, UserInfo> cacheLoader = new CacheLoader<UserClientPair, UserInfo>() {
@Override
public UserInfo load(UserClientPair pair) {
log.debug("load({}) ... populating cache for the key", pair);
PerunUserInfo ui = new PerunUserInfo();
long perunUserId = pair.getUserId();
Map<String, PerunAttributeValue> userAttributeValues = perunAdapter.getUserAttributeValues(perunUserId, userAttrNames);
if (shouldFillAttrs(userAttributeValues)) {
List<String> attrNames = userAttributeValues.entrySet()
.stream()
.filter(entry -> (null == entry.getValue() || entry.getValue().isNullValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
Map<String, PerunAttributeValue> missingAttrs = perunAdapter.getAdapterFallback()
.getUserAttributeValues(perunUserId, attrNames);
for (Map.Entry<String, PerunAttributeValue> entry : missingAttrs.entrySet()) {
userAttributeValues.put(entry.getKey(), entry.getValue());
}
}
JsonNode subJson = userAttributeValues.get(subAttribute).valueAsJson();
if (subJson == null || subJson.isNull() || !StringUtils.hasText(subJson.asText())) {
throw new RuntimeException("cannot get sub from attribute " + subAttribute + " for username " + perunUserId);
}
String sub = subJson.asText();
if (subModifiers != null) {
subJson = modifyClaims(subModifiers, subJson);
sub = subJson.asText();
if (sub == null || !StringUtils.hasText(sub)) {
throw new RuntimeException("Sub has no value after modification for username " + perunUserId);
}
}
ui.setId(perunUserId);
ui.setSub(sub); // Subject - Identifier for the End-User at the Issuer.
ui.setPreferredUsername(userAttributeValues.get(preferredUsernameAttribute).valueAsString()); // Shorthand name by which the End-User wishes to be referred to at the RP
ui.setGivenName(userAttributeValues.get(givenNameAttribute).valueAsString()); // Given name(s) or first name(s) of the End-User
ui.setFamilyName(userAttributeValues.get(familyNameAttribute).valueAsString()); // Surname(s) or last name(s) of the End-User
ui.setMiddleName(userAttributeValues.get(middleNameAttribute).valueAsString()); // Middle name(s) of the End-User
ui.setName(userAttributeValues.get(fullNameAttribute).valueAsString()); // End-User's full name
//ui.setNickname(); // Casual name of the End-User
//ui.setProfile(); // URL of the End-User's profile page.
//ui.setPicture(); // URL of the End-User's profile picture.
//ui.setWebsite(); // URL of the End-User's Web page or blog.
ui.setEmail(userAttributeValues.get(emailAttribute).valueAsString()); // End-User's preferred e-mail address.
//ui.setEmailVerified(true); // True if the End-User's e-mail address has been verified
//ui.setGender("male"); // End-User's gender. Values defined by this specification are female and male.
//ui.setBirthdate("1975-01-01");//End-User's birthday, represented as an ISO 8601:2004 [ISO86012004] YYYY-MM-DD format.
ui.setZoneinfo(userAttributeValues.get(zoneinfoAttribute).valueAsString());//String from zoneinfo [zoneinfo] time zone database, For example, Europe/Paris
ui.setLocale(userAttributeValues.get(localeAttribute).valueAsString()); // For example, en-US or fr-CA.
ui.setPhoneNumber(userAttributeValues.get(phoneAttribute).valueAsString()); //[E.164] is RECOMMENDED as the format, for example, +1 (425) 555-121
//ui.setPhoneNumberVerified(true); // True if the End-User's phone number has been verified
//ui.setUpdatedTime(Long.toString(System.currentTimeMillis()/1000L));// value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time
Address address = null;
if (StringUtils.hasText(userAttributeValues.get(addressAttribute).valueAsString())) {
address = new DefaultAddress();
address.setFormatted(userAttributeValues.get(addressAttribute).valueAsString());
//address.setStreetAddress("Šumavská 15");
//address.setLocality("Brno");
//address.setPostalCode("61200");
//address.setCountry("Czech Republic");
}
ui.setAddress(address);
//custom claims
ClaimContextCommonParameters contextCommonParameters = getClaimContextCommonParameters(perunUserId,
pair.getClientId(), perunAdapter);
ClaimSourceProduceContext pctx = new ClaimSourceProduceContext(perunUserId, sub, userAttributeValues,
perunAdapter, pair.getClient(), contextCommonParameters);
log.debug("processing custom claims");
for (PerunCustomClaimDefinition pccd : customClaims) {
log.debug("producing value for custom claim {}", pccd.getClaim());
JsonNode claimInJson = pccd.getClaimSource().produceValue(pctx);
log.debug("produced value {}={}", pccd.getClaim(), claimInJson);
if (claimInJson == null || claimInJson.isNull()) {
log.debug("claim {} is null", pccd.getClaim());
continue;
} else if (claimInJson.isTextual() && !StringUtils.hasText(claimInJson.asText())) {
log.debug("claim {} is a string and it is empty or null", pccd.getClaim());
continue;
} else if ((claimInJson.isArray() || claimInJson.isObject()) && claimInJson.size() == 0) {
log.debug("claim {} is an object or array and it is empty or null", pccd.getClaim());
continue;
}
List<ClaimModifier> claimModifiers = pccd.getClaimModifiers();
if (claimModifiers != null && !claimModifiers.isEmpty()) {
claimInJson = modifyClaims(claimModifiers, claimInJson);
}
ui.getCustomClaims().put(pccd.getClaim(), claimInJson);
}
log.debug("UserInfo created");
return ui;
}
private ClaimContextCommonParameters getClaimContextCommonParameters(long perunUserId, String clientId,
PerunAdapter perunAdapter)
{
Facility facility = perunAdapter.getFacilityByClientId(clientId);
return new ClaimContextCommonParameters(facility);
}
};
private JsonNode modifyClaims(List<ClaimModifier> claimModifiers, JsonNode value) {
for (ClaimModifier modifier: claimModifiers) {
value = modifyClaim(modifier, value);
}
return value;
}
private JsonNode modifyClaim(ClaimModifier modifier, JsonNode orig) {
JsonNode claimInJson = orig.deepCopy();
if (claimInJson.isTextual()) {
return TextNode.valueOf(modifier.modify(claimInJson.asText()));
} else if (claimInJson.isArray()) {
ArrayNode arrayNode = (ArrayNode) claimInJson;
for (int i = 0; i < arrayNode.size(); i++) {
JsonNode item = arrayNode.get(i);
if (item.isTextual()) {
String original = item.asText();
String modified = modifier.modify(original);
arrayNode.set(i, TextNode.valueOf(modified));
}
}
return arrayNode;
} else {
log.warn("Original value is neither string nor array of strings - cannot modify values");
return orig;
}
}
private boolean shouldFillAttrs(Map<String, PerunAttributeValue> userAttributeValues) {
if (perunOidcConfig.isFillMissingUserAttrs()) {
if (userAttributeValues.isEmpty()) {
return true;
} else if (userAttributeValues.containsValue(null)) {
return true;
} else {
return !userAttributeValues.values().stream()
.filter(PerunAttributeValueAwareModel::isNullValue)
.collect(Collectors.toSet())
.isEmpty();
}
}
return false;
}
private boolean checkStandardClaims(PerunUserInfo userInfo) { private boolean checkStandardClaims(PerunUserInfo userInfo) {
for (String claim: forceRegenerateUserinfoStandardClaims) { for (String claim: forceRegenerateUserinfoStandardClaims) {
switch (claim.toLowerCase()) { switch (claim.toLowerCase()) {

View File

@ -0,0 +1,55 @@
package cz.muni.ics.oidc.server.userInfo;
import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oauth2.model.SamlAuthenticationDetails;
import java.util.Objects;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class UserInfoCacheKey {
private final long userId;
private final ClientDetailsEntity client;
private final SamlAuthenticationDetails authenticationDetails;
private final Set<String> scopes;
public UserInfoCacheKey(String userId,
ClientDetailsEntity client,
SamlAuthenticationDetails authenticationDetails,
Set<String> scopes)
{
this.userId = Long.parseLong(userId);
this.client = client;
this.authenticationDetails = authenticationDetails;
this.scopes = scopes;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserInfoCacheKey that = (UserInfoCacheKey) o;
String ourClientId = client != null ? client.getClientId() : null;
String theirClientId = that.client != null ? that.client.getClientId() : null;
return userId == that.userId
&& Objects.equals(ourClientId, theirClientId)
&& Objects.equals(authenticationDetails, that.authenticationDetails)
&& Objects.equals(scopes, that.scopes);
}
@Override
public int hashCode() {
String clientId = client != null ? client.getClientId() : null;
if (clientId != null) {
return Objects.hash(userId, clientId, authenticationDetails, scopes);
} else {
return Objects.hash(userId, authenticationDetails, scopes);
}
}
}

View File

@ -0,0 +1,170 @@
package cz.muni.ics.oidc.server.userInfo;
import cz.muni.ics.jwt.signer.service.JWTSigningAndValidationService;
import cz.muni.ics.oidc.exceptions.ConfigurationException;
import cz.muni.ics.oidc.server.claims.ClaimModifier;
import cz.muni.ics.oidc.server.claims.ClaimModifierInitContext;
import cz.muni.ics.oidc.server.claims.ClaimSource;
import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext;
import cz.muni.ics.oidc.server.claims.PerunCustomClaimDefinition;
import cz.muni.ics.oidc.server.claims.modifiers.NoOperationModifier;
import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
@Slf4j
public class UserInfoUtils {
public static final String CUSTOM_CLAIM = "custom.claim.";
public static final String SOURCE = ".source";
public static final String CLASS = ".class";
public static final String NAMES = ".names";
public static final String MODIFIER = ".modifier";
public static final String SCOPE = ".scope";
public static List<PerunCustomClaimDefinition> loadCustomClaims(Collection<String> customClaimNames,
Properties properties,
PerunOidcConfig oidcConfig,
JWTSigningAndValidationService jwtService)
throws ConfigurationException
{
List<PerunCustomClaimDefinition> customClaims = new ArrayList<>();
if (customClaimNames == null || customClaimNames.isEmpty()) {
return customClaims;
}
for (String claimName : customClaimNames) {
String propertyBase = UserInfoUtils.CUSTOM_CLAIM + claimName;
//get scope
String scopeProperty = propertyBase + UserInfoUtils.SCOPE;
String scope = properties.getProperty(scopeProperty);
if (scope == null) {
log.error("property {} not found, skipping custom claim {}", scopeProperty, claimName);
continue;
}
//get ClaimSource
ClaimSource claimSource = UserInfoUtils.loadClaimSource(
properties, oidcConfig, jwtService, claimName, propertyBase);
//optional claim value modifier
List<ClaimModifier> claimModifiers = UserInfoUtils.loadClaimValueModifiers(
properties, claimName, propertyBase);
//add claim definition
customClaims.add(new PerunCustomClaimDefinition(scope, claimName, claimSource, claimModifiers));
}
return customClaims;
}
public static ClaimSource loadClaimSource(Properties properties,
PerunOidcConfig perunOidcConfig,
JWTSigningAndValidationService jwtService,
String claimName,
String propertyBase)
throws ConfigurationException
{
String propertyPrefix = propertyBase + SOURCE;
String sourceClass = properties.getProperty(propertyPrefix + CLASS);
if (!StringUtils.hasText(sourceClass)) {
log.error("{} - failed to initialized claim source: no class has ben configured", claimName);
throw new ConfigurationException("No class configured for claim source");
}
log.trace("{} - loading ClaimSource class '{}'", claimName, sourceClass);
try {
Class<?> rawClazz = Class.forName(sourceClass);
if (!ClaimSource.class.isAssignableFrom(rawClazz)) {
log.error("{} - failed to initialized claim source: class '{}' does not extend ClaimSource",
claimName, sourceClass);
throw new ConfigurationException("No instantiable class source configured for claim " + claimName);
}
@SuppressWarnings("unchecked") Class<ClaimSource> clazz = (Class<ClaimSource>) rawClazz;
Constructor<ClaimSource> constructor = clazz.getConstructor(ClaimSourceInitContext.class);
ClaimSourceInitContext ctx = new ClaimSourceInitContext(perunOidcConfig, jwtService, propertyPrefix,
properties, claimName);
return constructor.newInstance(ctx);
} catch (ClassNotFoundException e) {
log.error("{} - failed to initialize claim source: class '{}' was not found", claimName, sourceClass);
log.trace("{} - details:", claimName, e);
throw new ConfigurationException("Error has occurred when instantiating claim source for claim " + claimName);
} catch (NoSuchMethodException e) {
log.error("{} - failed to initialize claim source: class '{}' does not have proper constructor",
claimName, sourceClass);
log.trace("{} - details:", claimName, e);
throw new ConfigurationException("Error has occurred when instantiating claim source for claim " + claimName);
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
log.error("{} - failed to initialize claim source: class '{}' cannot be instantiated", claimName, sourceClass);
log.trace("{} - details:", claimName, e);
throw new ConfigurationException("Error has occurred when instantiating claim source for claim " + claimName);
}
}
public static List<ClaimModifier> loadClaimValueModifiers(Properties properties,
String claimName,
String propertyBase)
throws ConfigurationException
{
String propertyPrefix = propertyBase + MODIFIER;
String names = properties.getProperty(propertyPrefix + NAMES, "");
String[] nameArr = names.split(",");
List<ClaimModifier> modifiers = new ArrayList<>();
if (nameArr.length > 0) {
for (String name : nameArr) {
modifiers.add(loadClaimValueModifier(properties, claimName, propertyPrefix + '.' + name, name));
}
}
return modifiers;
}
private static ClaimModifier loadClaimValueModifier(Properties properties,
String claimName,
String propertyPrefix,
String modifierName)
throws ConfigurationException
{
String modifierClass = properties.getProperty(propertyPrefix + CLASS, NoOperationModifier.class.getName());
if (!StringUtils.hasText(modifierClass)) {
log.debug("{}:{} - no class has ben configured for claim value modifier, use noop modifier",
claimName, modifierName);
modifierClass = NoOperationModifier.class.getName();
}
log.trace("{}:{} - loading ClaimModifier class '{}'", claimName, modifierName, modifierClass);
try {
Class<?> rawClazz = Class.forName(modifierClass);
if (!ClaimModifier.class.isAssignableFrom(rawClazz)) {
log.error("{}:{} - failed to initialized claim modifier: class '{}' does not extend ClaimModifier",
claimName, modifierName, modifierClass);
throw new ConfigurationException("No instantiable class modifier configured for claim " + claimName);
}
@SuppressWarnings("unchecked") Class<ClaimModifier> clazz = (Class<ClaimModifier>) rawClazz;
Constructor<ClaimModifier> constructor = clazz.getConstructor(ClaimModifierInitContext.class);
ClaimModifierInitContext ctx = new ClaimModifierInitContext(
propertyPrefix, properties, claimName, modifierName);
return constructor.newInstance(ctx);
} catch (ClassNotFoundException e) {
log.error("{}:{} - failed to initialize claim modifier: class '{}' was not found",
claimName, modifierName, modifierClass);
log.trace("{}:{} - details:", claimName, modifierName, e);
throw new ConfigurationException("Error has occurred when instantiating claim modifier '"
+ modifierName + "' of claim '" + claimName + '\'');
} catch (NoSuchMethodException e) {
log.error("{}:{} - failed to initialize claim modifier: class '{}' does not have proper constructor",
claimName, modifierName, modifierClass);
log.trace("{}:{} - details:", claimName, e);
throw new ConfigurationException("Error has occurred when instantiating claim modifier '"
+ modifierName + "' of claim '" + claimName + '\'');
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
log.error("{}:{} - failed to initialize claim modifier: class '{}' cannot be instantiated",
claimName, modifierName, modifierClass);
log.trace("{}:{} - details:", claimName, e);
throw new ConfigurationException("Error has occurred when instantiating claim modifier '"
+ modifierName + "' of claim '" + claimName + '\'');
}
}
}

View File

@ -0,0 +1,26 @@
package cz.muni.ics.oidc.server.userInfo.mappings;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class AddressMappings {
private String formatted = null;
private String streetAddress = null;
private String locality = null;
private String region = null;
private String postalCode = null;
private String country = null;
public Set<String> getAttrNames() {
return new HashSet<>(Arrays.asList(formatted, streetAddress, locality, region, postalCode, country));
}
}

View File

@ -0,0 +1,22 @@
package cz.muni.ics.oidc.server.userInfo.mappings;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class EmailMappings {
private String email = null;
private String emailVerified = null;
public Set<String> getAttrNames() {
return new HashSet<>(Arrays.asList(email, emailVerified));
}
}

View File

@ -0,0 +1,21 @@
package cz.muni.ics.oidc.server.userInfo.mappings;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class OpenidMappings {
private String sub = null;
public Set<String> getAttrNames() {
return new HashSet<>(Arrays.asList(sub));
}
}

View File

@ -0,0 +1,22 @@
package cz.muni.ics.oidc.server.userInfo.mappings;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class PhoneMappings {
private String phoneNumber = null;
private String phoneNumberVerified = null;
public Set<String> getAttrNames() {
return new HashSet<>(Arrays.asList(phoneNumber, phoneNumberVerified));
}
}

View File

@ -0,0 +1,34 @@
package cz.muni.ics.oidc.server.userInfo.mappings;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class ProfileMappings {
private String name = null;
private String familyName = null;
private String givenName = null;
private String middleName = null;
private String nickname = null;
private String preferredUsername = null;
private String profile = null;
private String picture = null;
private String website = null;
private String gender = null;
private String birthdate = null;
private String zoneinfo = null;
private String locale = null;
public Set<String> getAttrNames() {
return new HashSet<>(Arrays.asList(name, familyName, givenName, middleName, nickname, preferredUsername,
profile, picture, website, gender, birthdate, zoneinfo, locale));
}
}

View File

@ -1,4 +1,6 @@
package cz.muni.ics.oidc.server.userInfo; package cz.muni.ics.oidc.server.userInfo.modifiers;
import cz.muni.ics.oidc.server.userInfo.PerunUserInfo;
/** /**
* Interface for all code that needs to modify user info. * Interface for all code that needs to modify user info.

View File

@ -1,6 +1,7 @@
package cz.muni.ics.oidc.server.userInfo; package cz.muni.ics.oidc.server.userInfo.modifiers;
import cz.muni.ics.oidc.server.adapters.PerunAdapter; import cz.muni.ics.oidc.server.adapters.PerunAdapter;
import cz.muni.ics.oidc.server.userInfo.PerunUserInfo;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.LinkedList; import java.util.LinkedList;

View File

@ -1,4 +1,4 @@
package cz.muni.ics.oidc.server.userInfo; package cz.muni.ics.oidc.server.userInfo.modifiers;
import cz.muni.ics.oidc.server.adapters.PerunAdapter; import cz.muni.ics.oidc.server.adapters.PerunAdapter;

View File

@ -1,5 +1,8 @@
package cz.muni.ics.oidc.web.controllers; package cz.muni.ics.oidc.web.controllers;
import static cz.muni.ics.oauth2.web.OAuthConfirmationController.CLAIMS;
import static cz.muni.ics.oauth2.web.OAuthConfirmationController.SCOPES;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
@ -10,6 +13,7 @@ import cz.muni.ics.oauth2.service.SystemScopeService;
import cz.muni.ics.oidc.server.configurations.PerunOidcConfig; import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
import cz.muni.ics.oidc.web.WebHtmlClasses; import cz.muni.ics.oidc.web.WebHtmlClasses;
import cz.muni.ics.openid.connect.model.UserInfo; import cz.muni.ics.openid.connect.model.UserInfo;
import cz.muni.ics.openid.connect.service.ScopeClaimTranslationService;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URLEncoder; import java.net.URLEncoder;
@ -153,20 +157,9 @@ public class ControllerUtils {
ScopeClaimTranslationService translationService, ScopeClaimTranslationService translationService,
Map<String, Object> model, Map<String, Object> model,
Set<String> scope, Set<String> scope,
UserInfo user) { UserInfo user)
Set<SystemScope> scopes = scopeService.fromStrings(scope); {
Set<SystemScope> sortedScopes = new LinkedHashSet<>(scopes.size()); Set<SystemScope> sortedScopes = ControllerUtils.getSortedScopes(scope, scopeService);
Set<SystemScope> systemScopes = scopeService.getAll();
// sort scopes for display based on the inherent order of system scopes
for (SystemScope s : systemScopes) {
if (scopes.contains(s)) {
sortedScopes.add(s);
}
}
// add in any scopes that aren't system scopes to the end of the list
sortedScopes.addAll(Sets.difference(scopes, systemScopes));
Map<String, Map<String, Object>> claimsForScopes = new LinkedHashMap<>(); Map<String, Map<String, Object>> claimsForScopes = new LinkedHashMap<>();
if (user != null) { if (user != null) {
@ -208,8 +201,8 @@ public class ControllerUtils {
}) })
.sorted((o1, o2) -> compareByClaimsAmount(o1, o2, claimsForScopes)) .sorted((o1, o2) -> compareByClaimsAmount(o1, o2, claimsForScopes))
.collect(Collectors.toCollection(LinkedHashSet::new)); .collect(Collectors.toCollection(LinkedHashSet::new));
model.put("claims", claimsForScopes); model.put(CLAIMS, claimsForScopes);
model.put("scopes", sortedScopes); model.put(SCOPES, sortedScopes);
} }
/** /**

View File

@ -17,7 +17,10 @@
*******************************************************************************/ *******************************************************************************/
package cz.muni.ics.openid.connect.service; package cz.muni.ics.openid.connect.service;
import cz.muni.ics.oauth2.model.SavedUserAuthentication;
import cz.muni.ics.openid.connect.model.UserInfo; import cz.muni.ics.openid.connect.model.UserInfo;
import java.util.Set;
import org.springframework.security.saml.SAMLCredential;
/** /**
* Interface for UserInfo service * Interface for UserInfo service
@ -27,30 +30,10 @@ import cz.muni.ics.openid.connect.model.UserInfo;
*/ */
public interface UserInfoService { public interface UserInfoService {
/** UserInfo get(String username, String clientId, Set<String> scope, SavedUserAuthentication userAuthentication);
* Get the UserInfo for the given username (usually maps to the
* preferredUsername field).
* @param username
* @return
*/
UserInfo getByUsername(String username);
/** UserInfo get(String username, String clientId, Set<String> scope, SAMLCredential samlCredential);
* Get the UserInfo for the given username (usually maps to the
* preferredUsername field) and clientId. This allows pairwise
* client identifiers where appropriate.
* @param username
* @param clientId
* @return
*/
UserInfo getByUsernameAndClientId(String username, String clientId);
/** UserInfo get(String username, String clientId, Set<String> scope);
* Get the user registered at this server with the given email address.
*
* @param email
* @return
*/
UserInfo getByEmailAddress(String email);
} }

View File

@ -18,13 +18,17 @@
package cz.muni.ics.openid.connect.service.impl; package cz.muni.ics.openid.connect.service.impl;
import cz.muni.ics.oauth2.model.ClientDetailsEntity; import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oauth2.model.SavedUserAuthentication;
import cz.muni.ics.oauth2.model.SystemScope;
import cz.muni.ics.oauth2.model.enums.SubjectType; import cz.muni.ics.oauth2.model.enums.SubjectType;
import cz.muni.ics.oauth2.service.ClientDetailsEntityService; import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
import cz.muni.ics.openid.connect.model.UserInfo; import cz.muni.ics.openid.connect.model.UserInfo;
import cz.muni.ics.openid.connect.repository.UserInfoRepository; import cz.muni.ics.openid.connect.repository.UserInfoRepository;
import cz.muni.ics.openid.connect.service.PairwiseIdentiferService; import cz.muni.ics.openid.connect.service.PairwiseIdentiferService;
import cz.muni.ics.openid.connect.service.UserInfoService; import cz.muni.ics.openid.connect.service.UserInfoService;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
@ -33,7 +37,6 @@ import org.springframework.stereotype.Service;
* @author Michael Joseph Walsh, jricher * @author Michael Joseph Walsh, jricher
* *
*/ */
@Service
public class DefaultUserInfoService implements UserInfoService { public class DefaultUserInfoService implements UserInfoService {
@Autowired @Autowired
@ -46,20 +49,31 @@ public class DefaultUserInfoService implements UserInfoService {
private PairwiseIdentiferService pairwiseIdentifierService; private PairwiseIdentiferService pairwiseIdentifierService;
@Override @Override
public UserInfo getByUsername(String username) { public UserInfo get(String username, String clientId, Set<String> scope, SavedUserAuthentication userAuthentication) {
return userInfoRepository.getByUsername(username); return getByUsernameAndClientId(username, clientId);
} }
@Override @Override
public UserInfo getByUsernameAndClientId(String username, String clientId) { public UserInfo get(String username, String clientId, Set<String> scope, SAMLCredential samlCredential) {
return getByUsernameAndClientId(username, clientId);
}
@Override
public UserInfo get(String username, String clientId, Set<String> scope) {
return getByUsernameAndClientId(username, clientId);
}
private UserInfo getByUsernameAndClientId(String username, String clientId) {
ClientDetailsEntity client = clientService.loadClientByClientId(clientId); ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
if (client == null) {
UserInfo userInfo = getByUsername(username);
if (client == null || userInfo == null) {
return null; return null;
} }
UserInfo userInfo = userInfoRepository.getByUsername(username);
if (userInfo == null) {
return null;
}
if (SubjectType.PAIRWISE.equals(client.getSubjectType())) { if (SubjectType.PAIRWISE.equals(client.getSubjectType())) {
String pairwiseSub = pairwiseIdentifierService.getIdentifier(userInfo, client); String pairwiseSub = pairwiseIdentifierService.getIdentifier(userInfo, client);
@ -70,9 +84,4 @@ public class DefaultUserInfoService implements UserInfoService {
} }
@Override
public UserInfo getByEmailAddress(String email) {
return userInfoRepository.getByEmailAddress(email);
}
} }

View File

@ -17,10 +17,7 @@
package cz.muni.ics.openid.connect.service.impl; package cz.muni.ics.openid.connect.service.impl;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import cz.muni.ics.openid.connect.model.UserInfo;
import cz.muni.ics.openid.connect.service.LoginHintExtracter; import cz.muni.ics.openid.connect.service.LoginHintExtracter;
import cz.muni.ics.openid.connect.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
/** /**
* Checks the login hint against the User Info collection, only populates it if a user is found. * Checks the login hint against the User Info collection, only populates it if a user is found.
@ -29,9 +26,6 @@ import org.springframework.beans.factory.annotation.Autowired;
*/ */
public class MatchLoginHintsAgainstUsers implements LoginHintExtracter { public class MatchLoginHintsAgainstUsers implements LoginHintExtracter {
@Autowired
private UserInfoService userInfoService;
/* (non-Javadoc) /* (non-Javadoc)
* @see cz.muni.ics.openid.connect.service.LoginHintTester#useHint(java.lang.String) * @see cz.muni.ics.openid.connect.service.LoginHintTester#useHint(java.lang.String)
*/ */
@ -39,19 +33,8 @@ public class MatchLoginHintsAgainstUsers implements LoginHintExtracter {
public String extractHint(String loginHint) { public String extractHint(String loginHint) {
if (Strings.isNullOrEmpty(loginHint)) { if (Strings.isNullOrEmpty(loginHint)) {
return null; return null;
} else {
UserInfo user = userInfoService.getByEmailAddress(loginHint);
if (user == null) {
user = userInfoService.getByUsername(loginHint);
if (user == null) {
return null;
} else {
return user.getPreferredUsername();
}
} else {
return user.getPreferredUsername();
}
} }
return loginHint;
} }
} }

View File

@ -111,7 +111,7 @@ public class ConnectTokenEnhancer implements TokenEnhancer {
&& !authentication.isClientOnly()) { && !authentication.isClientOnly()) {
String username = authentication.getName(); String username = authentication.getName();
UserInfo userInfo = userInfoService.getByUsernameAndClientId(username, clientId); UserInfo userInfo = userInfoService.get(username, clientId, originalAuthRequest.getScope(), token.getAuthenticationHolder().getUserAuth());
if (userInfo != null) { if (userInfo != null) {

View File

@ -19,6 +19,8 @@ package cz.muni.ics.openid.connect.web;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import cz.muni.ics.oauth2.model.ClientDetailsEntity; import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oauth2.model.OAuth2AccessTokenEntity;
import cz.muni.ics.oauth2.model.SavedUserAuthentication;
import cz.muni.ics.oauth2.service.ClientDetailsEntityService; import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
import cz.muni.ics.oauth2.service.SystemScopeService; import cz.muni.ics.oauth2.service.SystemScopeService;
import cz.muni.ics.openid.connect.model.UserInfo; import cz.muni.ics.openid.connect.model.UserInfo;
@ -76,7 +78,9 @@ public class UserInfoEndpoint {
} }
String username = auth.getName(); String username = auth.getName();
UserInfo userInfo = userInfoService.getByUsernameAndClientId(username, auth.getOAuth2Request().getClientId()); UserInfo userInfo = userInfoService.get(username, auth.getOAuth2Request().getClientId(),
auth.getOAuth2Request().getScope(),
(SavedUserAuthentication)auth.getUserAuthentication());
if (userInfo == null) { if (userInfo == null) {
log.error("getInfo failed; user not found: " + username); log.error("getInfo failed; user not found: " + username);

View File

@ -20,21 +20,35 @@
*/ */
package cz.muni.ics.openid.connect.web; package cz.muni.ics.openid.connect.web;
import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.CLIENT_ID;
import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.SCOPE;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializer; import com.google.gson.JsonSerializer;
import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oauth2.model.SamlAuthenticationDetails;
import cz.muni.ics.oauth2.model.SystemScope;
import cz.muni.ics.oidc.models.PerunUser;
import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
import cz.muni.ics.openid.connect.model.OIDCAuthenticationToken; import cz.muni.ics.openid.connect.model.OIDCAuthenticationToken;
import cz.muni.ics.openid.connect.model.UserInfo; import cz.muni.ics.openid.connect.model.UserInfo;
import cz.muni.ics.openid.connect.request.ConnectRequestParameters;
import cz.muni.ics.openid.connect.service.UserInfoService; import cz.muni.ics.openid.connect.service.UserInfoService;
import java.util.Set;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
/** /**
@ -43,6 +57,7 @@ import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
* @author jricher * @author jricher
* *
*/ */
@Slf4j
public class UserInfoInterceptor extends HandlerInterceptorAdapter { public class UserInfoInterceptor extends HandlerInterceptorAdapter {
private final Gson gson = new GsonBuilder() private final Gson gson = new GsonBuilder()
@ -50,13 +65,13 @@ public class UserInfoInterceptor extends HandlerInterceptorAdapter {
(JsonSerializer<GrantedAuthority>) (src, typeOfSrc, context) -> new JsonPrimitive(src.getAuthority())) (JsonSerializer<GrantedAuthority>) (src, typeOfSrc, context) -> new JsonPrimitive(src.getAuthority()))
.create(); .create();
@Autowired(required = false) @Autowired
private UserInfoService userInfoService; private UserInfoService userInfoService;
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){ if (auth != null){
@ -75,12 +90,25 @@ public class UserInfoInterceptor extends HandlerInterceptorAdapter {
request.setAttribute("userInfoJson", "null"); request.setAttribute("userInfoJson", "null");
} }
} else { } else {
// don't bother checking if we don't have a principal or a userInfoService to work with if (auth == null || auth.getName() == null || userInfoService == null) {
if (auth != null && auth.getName() != null && userInfoService != null) { log.debug("No point to handle, skip");
UserInfo user = userInfoService.getByUsername(auth.getName()); } else {
if (user != null) { if (request.getAttribute("userInfo") == null && request.getAttribute("userInfoJson") == null) {
request.setAttribute("userInfo", user); String clientId = request.getParameter(CLIENT_ID);
request.setAttribute("userInfoJson", user.toJson()); if (clientId == null) {
log.debug("No client provided, no reason to continue processing");
return true;
}
Set<String> scopes = OAuth2Utils.parseParameterList(request.getParameter(ConnectRequestParameters.SCOPE));
UserInfo user = userInfoService.get(auth.getName(), clientId, scopes, (SAMLCredential) auth.getCredentials());
if (user != null) {
request.setAttribute("userInfo", user);
request.setAttribute("userInfoJson", user.toJson());
}
} else {
log.debug("Already has userInfo or userInfoJson");
log.trace("userInfo: {}", request.getAttribute("userInfo"));
log.trace("userInfoJson: {}", request.getAttribute("userInfoJson"));
} }
} }
} }

View File

@ -31,6 +31,7 @@ import cz.muni.ics.openid.connect.model.DefaultUserInfo;
import cz.muni.ics.openid.connect.model.UserInfo; import cz.muni.ics.openid.connect.model.UserInfo;
import cz.muni.ics.openid.connect.repository.UserInfoRepository; import cz.muni.ics.openid.connect.repository.UserInfoRepository;
import cz.muni.ics.openid.connect.service.PairwiseIdentiferService; import cz.muni.ics.openid.connect.service.PairwiseIdentiferService;
import java.util.HashSet;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -140,42 +141,6 @@ public class TestDefaultUserInfoService {
}
/**
* Test loading an admin user, ensuring that the UserDetails object returned
* has both the ROLE_USER and ROLE_ADMIN authorities.
*/
@Test
public void loadByUsername_admin_success() {
Mockito.when(userInfoRepository.getByUsername(adminUsername)).thenReturn(userInfoAdmin);
UserInfo user = service.getByUsername(adminUsername);
assertEquals(user.getSub(), adminSub);
}
/**
* Test loading a regular, non-admin user, ensuring that the returned UserDetails
* object has ROLE_USER but *not* ROLE_ADMIN.
*/
@Test
public void loadByUsername_regular_success() {
Mockito.when(userInfoRepository.getByUsername(regularUsername)).thenReturn(userInfoRegular);
UserInfo user = service.getByUsername(regularUsername);
assertEquals(user.getSub(), regularSub);
}
/**
* If a user is not found, the loadByUsername method should throw an exception.
*/
@Test()
public void loadByUsername_nullUser() {
Mockito.when(userInfoRepository.getByUsername(adminUsername)).thenReturn(null);
UserInfo user = service.getByUsername(adminUsername);
assertNull(user);
} }
/** /**
@ -191,8 +156,8 @@ public class TestDefaultUserInfoService {
Mockito.verify(pairwiseIdentiferService, Mockito.never()).getIdentifier(any(UserInfo.class), any(ClientDetailsEntity.class)); Mockito.verify(pairwiseIdentiferService, Mockito.never()).getIdentifier(any(UserInfo.class), any(ClientDetailsEntity.class));
UserInfo user1 = service.getByUsernameAndClientId(regularUsername, publicClientId1); UserInfo user1 = service.get(regularUsername, publicClientId1, new HashSet<>());
UserInfo user2 = service.getByUsernameAndClientId(regularUsername, publicClientId2); UserInfo user2 = service.get(regularUsername, publicClientId2, new HashSet<>());
assertEquals(regularSub, user1.getSub()); assertEquals(regularSub, user1.getSub());
assertEquals(regularSub, user2.getSub()); assertEquals(regularSub, user2.getSub());
@ -225,10 +190,10 @@ public class TestDefaultUserInfoService {
Mockito.when(pairwiseIdentiferService.getIdentifier(userInfoRegular, pairwiseClient3)).thenReturn(pairwiseSub3); Mockito.when(pairwiseIdentiferService.getIdentifier(userInfoRegular, pairwiseClient3)).thenReturn(pairwiseSub3);
Mockito.when(pairwiseIdentiferService.getIdentifier(userInfoRegular, pairwiseClient4)).thenReturn(pairwiseSub4); Mockito.when(pairwiseIdentiferService.getIdentifier(userInfoRegular, pairwiseClient4)).thenReturn(pairwiseSub4);
UserInfo user1 = service.getByUsernameAndClientId(regularUsername, pairwiseClientId1); UserInfo user1 = service.get(regularUsername, pairwiseClientId1, new HashSet<>());
UserInfo user2 = service.getByUsernameAndClientId(regularUsername, pairwiseClientId2); UserInfo user2 = service.get(regularUsername, pairwiseClientId2, new HashSet<>());
UserInfo user3 = service.getByUsernameAndClientId(regularUsername, pairwiseClientId3); UserInfo user3 = service.get(regularUsername, pairwiseClientId3, new HashSet<>());
UserInfo user4 = service.getByUsernameAndClientId(regularUsername, pairwiseClientId4); UserInfo user4 = service.get(regularUsername, pairwiseClientId4, new HashSet<>());
assertEquals(pairwiseSub12, user1.getSub()); assertEquals(pairwiseSub12, user1.getSub());
assertEquals(pairwiseSub12, user2.getSub()); assertEquals(pairwiseSub12, user2.getSub());