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": {}
+}
+