diff --git a/CHANGELOG.md b/CHANGELOG.md index c6e6358c..a983ad9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. +20.3.0 (2020-11-23) +=================== + +- Supports the department attribute for institution SSO +- Added postman tests for testing SAML institutions + 20.2.5 (2020-09-18) =================== diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java index f17c5bb5..e4fe7b06 100644 --- a/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java +++ b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java @@ -75,6 +75,8 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; import javax.security.auth.login.AccountException; import javax.security.auth.login.FailedLoginException; import javax.servlet.http.Cookie; @@ -170,6 +172,8 @@ public String getInstitutionId() { private static final String SHIBBOLETH_COOKIE_PREFIX = "_shibsession_"; + private static final String LDAP_DN_OU_PREFIX = "ou="; + private static final int SIXTY_SECONDS = 60 * 1000; /** The Logger Instance. */ @@ -581,6 +585,22 @@ protected PrincipalAuthenticationResult notifyRemotePrincipalAuthenticated( isMemberOf ); } + + final String departmentRaw = user.optString("departmentRaw").trim(); + // Unlike all the above attributes that are released to us from the institutions, the `eduPerson` is a boolean + // per-institution flag set by CAS in the "institutions-auth.xsl" file. It determines whether the department + // attribute uses the the eduPerson schema (https://wiki.refeds.org/display/STAN/eduPerson). + final boolean eduPerson = user.optBoolean("eduPerson"); + + String department = ""; + if (departmentRaw.isEmpty()) { + logger.warn("[CAS XSLT] Missing department: fullname={}, username={}, institution={}", fullname, username, institutionId); + } else { + department = this.retrieveDepartment(departmentRaw, eduPerson); + } + // Insert the `department` attribute into the payload, which does not overwrite `departmentRaw`. + normalizedPayload.getJSONObject("provider").getJSONObject("user").put("department", department); + final String payload = normalizedPayload.toString(); logger.info( "[CAS XSLT] All attributes checked: username={}, institution={}, member={}", @@ -699,6 +719,42 @@ protected JSONObject normalizeRemotePrincipal(final OpenScienceFrameworkCredenti return XML.toJSONObject(writer.getBuffer().toString()); } + /** + * Retrieve the department value from the raw department string. + * + * @param departmentRaw the raw department string + * @param eduPerson whether the department attribute uses eduPerson schema + * @return the department value + */ + private String retrieveDepartment(final String departmentRaw, final boolean eduPerson) { + + // Return the raw value as it is if institutions do not use eduPerson schema for the department attribute + if (!eduPerson) { + return departmentRaw; + } + + // For institutions that use the eduPerson schema, the department must be retrieved from the raw value. Here is + // an example: "ou=Music Department, o=Notre Dame, dc=nd, dc=edu", whose syntax is LDAP Distinguished Names. + try { + final LdapName dn = new LdapName(departmentRaw); + for (int i = dn.size() - 1; i >=0; i--) { + final String rdn = dn.get(i); + if (rdn.startsWith(LDAP_DN_OU_PREFIX)) { + return rdn.substring(LDAP_DN_OU_PREFIX.length()); + } + } + } catch (final InvalidNameException | IndexOutOfBoundsException e) { + logger.error( + "[CAS XSLT] Invalid syntax for LDAP Distinguished Names: departmentRaw={}, error={}", + departmentRaw, + e.getMessage() + ); + // Return an empty string if the syntax is wrong + return ""; + } + return ""; + } + public void setInstitutionsAuthUrl(final String institutionsAuthUrl) { this.institutionsAuthUrl = institutionsAuthUrl; } diff --git a/etc/institutions-auth.xsl b/etc/institutions-auth.xsl index 9dd41d3a..4784365d 100644 --- a/etc/institutions-auth.xsl +++ b/etc/institutions-auth.xsl @@ -36,10 +36,12 @@ esu - + + + true @@ -83,6 +85,10 @@ + pu @@ -90,6 +96,25 @@ + + true + + + + + + + ua + + + + + + + false @@ -150,6 +175,8 @@ + + false diff --git a/postman/osf-cas-shib-saml-instn-sso-test.json b/postman/osf-cas-shib-saml-instn-sso-test.json new file mode 100644 index 00000000..b750a5c9 --- /dev/null +++ b/postman/osf-cas-shib-saml-instn-sso-test.json @@ -0,0 +1,270 @@ +{ + "info": { + "_postman_id": "390768cf-591d-455f-a623-d0734d07adfe", + "name": "OSF Institution Shib-SAML SSO", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Brown University User", + "request": { + "method": "GET", + "header": [ + { + "key": "AUTH-mail", + "value": "chen@brown.edu", + "type": "text" + }, + { + "key": "AUTH-displayName", + "value": "Longzebrown Chenbrown", + "type": "text" + }, + { + "key": "AUTH-givenName", + "value": "Longzebrown", + "type": "text" + }, + { + "key": "AUTH-sn", + "value": "Chenbrown", + "type": "text" + }, + { + "key": "REMOTE_USER", + "value": "chenbrown123", + "type": "text" + }, + { + "key": "AUTH-Shib-Session-ID", + "value": "1234567812345678", + "type": "text" + }, + { + "key": "AUTH-Shib-Identity-Provider", + "value": "https://sso.brown.edu/idp/shibboleth", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:8080/login", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "login" + ] + }, + "description": "Standard SAML SSO without shared SSO and without the department attribute." + }, + "response": [] + }, + { + "name": "The Policy Lab User", + "request": { + "method": "GET", + "header": [ + { + "key": "AUTH-mail", + "value": "chen@policylab.io", + "type": "text" + }, + { + "key": "AUTH-displayName", + "value": "Longzepolicylab Chenpolicylab", + "type": "text" + }, + { + "key": "AUTH-givenName", + "value": "Longzepolicylab", + "type": "text" + }, + { + "key": "AUTH-sn", + "value": "Chenpolicylab", + "type": "text" + }, + { + "key": "REMOTE_USER", + "value": "chenpolicylab123", + "type": "text" + }, + { + "key": "AUTH-Shib-Session-ID", + "value": "1234567812345678", + "type": "text" + }, + { + "key": "AUTH-Shib-Identity-Provider", + "value": "https://sso.brown.edu/idp/shibboleth", + "type": "text" + }, + { + "key": "AUTH-isMemberOf", + "value": "thepolicylab", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:8080/login", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "login" + ] + }, + "description": "A SAML SSO institution user using another (the primary) institution's SSO server with an extra attribute indicate which secondary institution the user belongs to." + }, + "response": [] + }, + { + "name": "University of Arizona User", + "request": { + "method": "GET", + "header": [ + { + "key": "AUTH-mail", + "type": "text", + "value": "chen@ua.edu" + }, + { + "key": "AUTH-displayName", + "type": "text", + "value": "Longzeua Chenua" + }, + { + "key": "AUTH-givenName", + "type": "text", + "value": "Longzeua" + }, + { + "key": "AUTH-sn", + "type": "text", + "value": "Chenua" + }, + { + "key": "REMOTE_USER", + "type": "text", + "value": "chenua12" + }, + { + "key": "AUTH-Shib-Session-ID", + "type": "text", + "value": "1234567812345678" + }, + { + "key": "AUTH-Shib-Identity-Provider", + "type": "text", + "value": "urn:mace:incommon:arizona.edu" + }, + { + "key": "AUTH-department", + "value": "Department of Computer Science", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:8080/login", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "login" + ] + }, + "description": "A SAML SSO institution user with the department information released using a customized attribute. OSF CAS will take it as it is." + }, + "response": [] + }, + { + "name": "Clear CAS Session", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/logout", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "logout" + ] + }, + "description": "Clear successful SSO sessions created by each test." + }, + "response": [] + }, + { + "name": "Princeton User", + "request": { + "method": "GET", + "header": [ + { + "key": "AUTH-mail", + "value": "chen@princeton.edu", + "type": "text" + }, + { + "key": "AUTH-displayName", + "value": "Longzepu Chen pu", + "type": "text" + }, + { + "key": "AUTH-givenName", + "value": "Longzepu", + "type": "text" + }, + { + "key": "AUTH-sn", + "value": "Chenpu", + "type": "text" + }, + { + "key": "REMOTE_USER", + "value": "chenpu12", + "type": "text" + }, + { + "key": "AUTH-Shib-Identity-Provider", + "value": "https://idp.princeton.edu/idp/shibboleth", + "type": "text" + }, + { + "key": "AUTH-department", + "value": "ou=Music Department, o=Princton Uiniversity, dc=princeton, dc=edu", + "type": "text" + }, + { + "key": "AUTH-Shib-Session-Id", + "value": "1234567812345678", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:8080/login", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "login" + ] + }, + "description": "A SAML SSO institution user with the department attribute released using eduPerson of which the syntax is LDAP Distinguished Names." + }, + "response": [] + } + ], + "protocolProfileBehavior": {} +} +