From f72599670d9afb2ce02122619a16f1921ab708a4 Mon Sep 17 00:00:00 2001 From: Okke Harsta Date: Tue, 31 Oct 2023 11:19:20 +0100 Subject: [PATCH] Option to select AuthnContextClassRef for the SAML response --- mujina-common/pom.xml | 2 +- .../main/java/mujina/saml/SAMLBuilder.java | 8 +- mujina-idp/pom.xml | 2 +- .../mujina/config/AuthnContextClassRefs.java | 17 +++ .../java/mujina/idp/SAMLMessageHandler.java | 6 +- .../main/java/mujina/idp/SsoController.java | 16 ++- .../main/java/mujina/idp/UserController.java | 5 + mujina-idp/src/main/resources/application.yml | 11 ++ mujina-idp/src/main/resources/public/main.css | 21 +++ mujina-idp/src/main/resources/public/main.js | 130 +++++++++--------- .../src/main/resources/templates/login.html | 15 ++ mujina-sp/pom.xml | 2 +- pom.xml | 2 +- 13 files changed, 161 insertions(+), 76 deletions(-) create mode 100644 mujina-idp/src/main/java/mujina/config/AuthnContextClassRefs.java diff --git a/mujina-common/pom.xml b/mujina-common/pom.xml index 11ca3c7e..7fa07aac 100644 --- a/mujina-common/pom.xml +++ b/mujina-common/pom.xml @@ -21,7 +21,7 @@ org.openconext mujina - 8.0.8 + 8.0.9 ../pom.xml diff --git a/mujina-common/src/main/java/mujina/saml/SAMLBuilder.java b/mujina-common/src/main/java/mujina/saml/SAMLBuilder.java index 946cd26e..8418031d 100644 --- a/mujina-common/src/main/java/mujina/saml/SAMLBuilder.java +++ b/mujina-common/src/main/java/mujina/saml/SAMLBuilder.java @@ -83,7 +83,7 @@ public static Status buildStatus(String value, String subStatus, String message) return status; } - public static Assertion buildAssertion(SAMLPrincipal principal, Status status, String entityId) { + public static Assertion buildAssertion(SAMLPrincipal principal, String authnContextClassRefValue, Status status, String entityId) { Assertion assertion = buildSAMLObject(Assertion.class, Assertion.DEFAULT_ELEMENT_NAME); if (status.getStatusCode().getValue().equals(StatusCode.SUCCESS_URI)) { @@ -104,7 +104,7 @@ public static Assertion buildAssertion(SAMLPrincipal principal, Status status, S conditions.getAudienceRestrictions().add(audienceRestriction); assertion.setConditions(conditions); - AuthnStatement authnStatement = buildAuthnStatement(new DateTime(), entityId); + AuthnStatement authnStatement = buildAuthnStatement(new DateTime(), entityId, authnContextClassRefValue); assertion.setIssuer(issuer); assertion.getAuthnStatements().add(authnStatement); @@ -156,9 +156,9 @@ public static String randomSAMLId() { return "_" + UUID.randomUUID().toString(); } - private static AuthnStatement buildAuthnStatement(DateTime authnInstant, String entityID) { + private static AuthnStatement buildAuthnStatement(DateTime authnInstant, String entityID, String authnContextClassRefValue) { AuthnContextClassRef authnContextClassRef = buildSAMLObject(AuthnContextClassRef.class, AuthnContextClassRef.DEFAULT_ELEMENT_NAME); - authnContextClassRef.setAuthnContextClassRef(AuthnContext.PASSWORD_AUTHN_CTX); + authnContextClassRef.setAuthnContextClassRef(StringUtils.hasText(authnContextClassRefValue) ? authnContextClassRefValue : AuthnContext.PASSWORD_AUTHN_CTX); AuthenticatingAuthority authenticatingAuthority = buildSAMLObject(AuthenticatingAuthority.class, AuthenticatingAuthority.DEFAULT_ELEMENT_NAME); authenticatingAuthority.setURI(entityID); diff --git a/mujina-idp/pom.xml b/mujina-idp/pom.xml index 44de8c8d..61f5363d 100644 --- a/mujina-idp/pom.xml +++ b/mujina-idp/pom.xml @@ -21,7 +21,7 @@ org.openconext mujina - 8.0.8 + 8.0.9 ../pom.xml diff --git a/mujina-idp/src/main/java/mujina/config/AuthnContextClassRefs.java b/mujina-idp/src/main/java/mujina/config/AuthnContextClassRefs.java new file mode 100644 index 00000000..d51316b9 --- /dev/null +++ b/mujina-idp/src/main/java/mujina/config/AuthnContextClassRefs.java @@ -0,0 +1,17 @@ +package mujina.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@ConfigurationProperties(prefix = "acr") +@Getter +@Setter +public class AuthnContextClassRefs { + + private List values; +} diff --git a/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java b/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java index 24b405b6..8815b4c3 100644 --- a/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java +++ b/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java @@ -96,7 +96,9 @@ private SAMLMessageDecoder samlMessageDecoder(boolean postRequest) { } @SuppressWarnings("unchecked") - public void sendAuthnResponse(SAMLPrincipal principal, HttpServletResponse response) throws MarshallingException, SignatureException, MessageEncodingException { + public void sendAuthnResponse(SAMLPrincipal principal, + String authnContextClassRefValue, + HttpServletResponse response) throws MarshallingException, SignatureException, MessageEncodingException { Status status = buildStatus(StatusCode.SUCCESS_URI); String entityId = idpConfiguration.getEntityId(); @@ -110,7 +112,7 @@ public void sendAuthnResponse(SAMLPrincipal principal, HttpServletResponse respo authResponse.setIssueInstant(new DateTime()); authResponse.setInResponseTo(principal.getRequestID()); - Assertion assertion = buildAssertion(principal, status, entityId); + Assertion assertion = buildAssertion(principal, authnContextClassRefValue, status, entityId); signAssertion(assertion, signingCredential); authResponse.getAssertions().add(assertion); diff --git a/mujina-idp/src/main/java/mujina/idp/SsoController.java b/mujina-idp/src/main/java/mujina/idp/SsoController.java index ae76bb91..7ecd5267 100644 --- a/mujina-idp/src/main/java/mujina/idp/SsoController.java +++ b/mujina-idp/src/main/java/mujina/idp/SsoController.java @@ -4,6 +4,7 @@ import mujina.saml.SAMLAttribute; import mujina.saml.SAMLPrincipal; import org.opensaml.common.binding.SAMLMessageContext; +import org.opensaml.saml2.core.AuthnContext; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.metadata.provider.MetadataProviderException; @@ -58,8 +59,13 @@ private void doSSO(HttpServletRequest request, HttpServletResponse response, Aut AuthnRequest authnRequest = (AuthnRequest) messageContext.getInboundSAMLMessage(); String assertionConsumerServiceURL = idpConfiguration.getAcsEndpoint() != null ? idpConfiguration.getAcsEndpoint() : authnRequest.getAssertionConsumerServiceURL(); + Map parameterMap = (Map) authentication.getDetails(); + String[] authnContextClassRefs = parameterMap.get("authn-context-class-ref-value"); + List attributes = attributes(authentication); + String authnContextClassRefValue = authnContextClassRefs != null ? authnContextClassRefs[0] : AuthnContext.PASSWORD_AUTHN_CTX; + SAMLPrincipal principal = new SAMLPrincipal( authentication.getName(), attributes.stream() @@ -71,7 +77,7 @@ private void doSSO(HttpServletRequest request, HttpServletResponse response, Aut assertionConsumerServiceURL, messageContext.getRelayState()); - samlMessageHandler.sendAuthnResponse(principal, response); + samlMessageHandler.sendAuthnResponse(principal, authnContextClassRefValue, response); } @SuppressWarnings("unchecked") @@ -79,7 +85,6 @@ private List attributes(Authentication authentication) { String uid = authentication.getName(); Map> result = new HashMap<>(idpConfiguration.getAttributes()); - Optional>> optionalMap = idpConfiguration.getUsers().stream() .filter(user -> user.getPrincipal().equals(uid)) .findAny() @@ -91,6 +96,9 @@ private List attributes(Authentication authentication) { parameterMap.forEach((key, values) -> { result.put(key, Arrays.asList(values)); }); + if (parameterMap.containsKey("authn-context-class-ref-value")) { + result.remove("authn-context-class-ref-value"); + } //Check if the user wants to be persisted if (parameterMap.containsKey("persist-me") && "on".equalsIgnoreCase(parameterMap.get("persist-me")[0])) { @@ -106,8 +114,8 @@ private List attributes(Authentication authentication) { Map standardAttributes = idpConfiguration.getStandardAttributes().getAttributes(); Map> replacements = new HashMap<>(); String mail = String.format("%s@%s", - uid.replaceAll("[^a-zA-Z0-9]", ""), - "example.com") + uid.replaceAll("[^a-zA-Z0-9]", ""), + "example.com") .toLowerCase(); String givenName = uid.substring(0, 1).toUpperCase() + uid.substring(1); result.keySet().forEach(key -> { diff --git a/mujina-idp/src/main/java/mujina/idp/UserController.java b/mujina-idp/src/main/java/mujina/idp/UserController.java index 034d0e2a..71fdbba7 100644 --- a/mujina-idp/src/main/java/mujina/idp/UserController.java +++ b/mujina-idp/src/main/java/mujina/idp/UserController.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import mujina.config.AuthnContextClassRefs; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.DefaultResourceLoader; @@ -22,10 +23,12 @@ public class UserController { private final List> samlAttributes; + private final AuthnContextClassRefs authnContextClassRefs; @Autowired @SuppressWarnings("unchecked") public UserController(ObjectMapper objectMapper, + AuthnContextClassRefs authnContextClassRefs, @Value("${idp.saml_attributes_config_file}") String samlAttributesConfigFile) throws IOException { DefaultResourceLoader loader = new DefaultResourceLoader(); @@ -33,6 +36,7 @@ public UserController(ObjectMapper objectMapper, loader.getResource(samlAttributesConfigFile).getInputStream(), new TypeReference<>() { }); this.samlAttributes.sort(comparing(m -> m.get("id"))); + this.authnContextClassRefs = authnContextClassRefs; } @GetMapping("/") @@ -49,6 +53,7 @@ public String user(Authentication authentication, ModelMap modelMap) { @GetMapping("/login") public String login(ModelMap modelMap) { modelMap.addAttribute("samlAttributes", samlAttributes); + modelMap.addAttribute("authnContextClassRefs", authnContextClassRefs.getValues()); return "login"; } } diff --git a/mujina-idp/src/main/resources/application.yml b/mujina-idp/src/main/resources/application.yml index a4c647d4..cfb33d04 100644 --- a/mujina-idp/src/main/resources/application.yml +++ b/mujina-idp/src/main/resources/application.yml @@ -51,6 +51,17 @@ idp: [urn:mace:dir:attribute-def:eduPersonPrincipalName]: "j.doe@example.com" [urn:oasis:names:tc:SAML:attribute:subject-id]: "j.doe@example.com" +acr: + values: + - "http://test2.surfconext.nl/assurance/loa1" + - "http://test2.surfconext.nl/assurance/loa1.5" + - "http://test2.surfconext.nl/assurance/loa2" + - "http://test2.surfconext.nl/assurance/loa3" + - "https://eduid.nl/trust/linked-institution" + - "https://eduid.nl/trust/validate-names" + - "https://eduid.nl/trust/affiliation-student" + - "https://refeds.org/profile/mfa" + spring: mvc: favicon: diff --git a/mujina-idp/src/main/resources/public/main.css b/mujina-idp/src/main/resources/public/main.css index d769b5da..341ed3bf 100644 --- a/mujina-idp/src/main/resources/public/main.css +++ b/mujina-idp/src/main/resources/public/main.css @@ -130,6 +130,16 @@ label.persist-me { font-size: 14px; } +div.authn-context-class-ref { + display: flex; + flex-direction: column; + margin: 15px 0 5px 0; +} + +label.authn-context-class-ref { + font-size: 16px; +} + div.add-attribute { display: flex; margin-bottom: 10px; @@ -147,6 +157,17 @@ select.attribute-select { height: 40px; } +select.acr-select { + flex-grow: 2; + border-radius: 3px; + margin: 2px 0 8px 0; + font-size: 16px; + background-color: white; + border: 1px solid #e4e4e4; + box-shadow: inset 0 1px 3px #e6e6e6; + height: 40px; +} + li.attribute-value { display: flex; position: relative; diff --git a/mujina-idp/src/main/resources/public/main.js b/mujina-idp/src/main/resources/public/main.js index 30948f0b..d9143e63 100644 --- a/mujina-idp/src/main/resources/public/main.js +++ b/mujina-idp/src/main/resources/public/main.js @@ -1,73 +1,79 @@ function guid() { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); } document.addEventListener("DOMContentLoaded", function () { - [].forEach.call(document.querySelectorAll(".help,.close"), function (el) { - el.addEventListener("click", function (e) { - e.stopPropagation(); - e.preventDefault(); - var explanation = document.getElementById("explanation"); - explanation.classList.toggle("hide"); - if (!explanation.classList.contains("hide")) { - setTimeout(function () { - document.getElementById("close").focus(); - }, 25); - } + [].forEach.call(document.querySelectorAll(".help,.close"), function (el) { + el.addEventListener("click", function (e) { + e.stopPropagation(); + e.preventDefault(); + var explanation = document.getElementById("explanation"); + explanation.classList.toggle("hide"); + if (!explanation.classList.contains("hide")) { + setTimeout(function () { + document.getElementById("close").focus(); + }, 25); + } + }); }); - }); - document.getElementById("close").addEventListener("blur", function () { - document.getElementById("explanation").classList.add("hide"); - }); + document.getElementById("close").addEventListener("blur", function () { + document.getElementById("explanation").classList.add("hide"); + }); - document.querySelector(".attribute-select").addEventListener("change", function (e) { - var val = e.target.value; - var selectedOption = document.querySelector('option[value="' + val + '"]'); - var text = selectedOption.text; - var multiplicity = selectedOption.dataset.multiplicity === "true"; - var newElement = document.createElement("li"); - newElement.setAttribute("class", "attribute-value"); - var mainId = guid(); - newElement.setAttribute("id", mainId); - var spanId = guid(); - var inputId = guid(); - newElement.innerHTML = "" + - "" + - "🗑"; - document.getElementById("attribute-list").appendChild(newElement); - document.getElementById(spanId).addEventListener("click", function () { - var element = document.getElementById(mainId); - element.parentNode.removeChild(element); - if (!multiplicity) { - var select = document.getElementById("add-attribute"); - var option = document.createElement("option"); - option.text = text; - option.value = val; - select.add(option); - } + document.querySelector(".acr-select").addEventListener("change", function (e) { + var val = e.target.value; + var acrSelect = document.getElementById("authn-context-class-ref-value"); + acrSelect.value = val; }); - var select = document.getElementById("add-attribute"); - if (!multiplicity) { - select.remove(select.selectedIndex); - } - select.value = "Add attribute..."; - setTimeout(function () { - var inputElement = document.getElementById(inputId); - inputElement.focus(); - inputElement.addEventListener("keypress", function (e) { - if (e.code === "Enter") { - e.stopPropagation(); - e.preventDefault(); - select.focus(); + + document.querySelector(".attribute-select").addEventListener("change", function (e) { + var val = e.target.value; + var selectedOption = document.querySelector('option[value="' + val + '"]'); + var text = selectedOption.text; + var multiplicity = selectedOption.dataset.multiplicity === "true"; + var newElement = document.createElement("li"); + newElement.setAttribute("class", "attribute-value"); + var mainId = guid(); + newElement.setAttribute("id", mainId); + var spanId = guid(); + var inputId = guid(); + newElement.innerHTML = "" + + "" + + "🗑"; + document.getElementById("attribute-list").appendChild(newElement); + document.getElementById(spanId).addEventListener("click", function () { + var element = document.getElementById(mainId); + element.parentNode.removeChild(element); + if (!multiplicity) { + var select = document.getElementById("add-attribute"); + var option = document.createElement("option"); + option.text = text; + option.value = val; + select.add(option); + } + }); + var select = document.getElementById("add-attribute"); + if (!multiplicity) { + select.remove(select.selectedIndex); } - }); - }, 25); - }); + select.value = "Add attribute..."; + setTimeout(function () { + var inputElement = document.getElementById(inputId); + inputElement.focus(); + inputElement.addEventListener("keypress", function (e) { + if (e.code === "Enter") { + e.stopPropagation(); + e.preventDefault(); + select.focus(); + } + }); + }, 25); + }); }); diff --git a/mujina-idp/src/main/resources/templates/login.html b/mujina-idp/src/main/resources/templates/login.html index 1035b4e6..f36f6194 100644 --- a/mujina-idp/src/main/resources/templates/login.html +++ b/mujina-idp/src/main/resources/templates/login.html @@ -55,6 +55,21 @@

Mujina Identity Provider

+
+ + + +
+