Skip to content

Commit

Permalink
Option to select AuthnContextClassRef for the SAML response
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Oct 31, 2023
1 parent b0d4c9b commit f725996
Show file tree
Hide file tree
Showing 13 changed files with 161 additions and 76 deletions.
2 changes: 1 addition & 1 deletion mujina-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>org.openconext</groupId>
<artifactId>mujina</artifactId>
<version>8.0.8</version>
<version>8.0.9</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
8 changes: 4 additions & 4 deletions mujina-common/src/main/java/mujina/saml/SAMLBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion mujina-idp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>org.openconext</groupId>
<artifactId>mujina</artifactId>
<version>8.0.8</version>
<version>8.0.9</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
17 changes: 17 additions & 0 deletions mujina-idp/src/main/java/mujina/config/AuthnContextClassRefs.java
Original file line number Diff line number Diff line change
@@ -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<String> values;
}
6 changes: 4 additions & 2 deletions mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down
16 changes: 12 additions & 4 deletions mujina-idp/src/main/java/mujina/idp/SsoController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String[]> parameterMap = (Map<String, String[]>) authentication.getDetails();
String[] authnContextClassRefs = parameterMap.get("authn-context-class-ref-value");

List<SAMLAttribute> attributes = attributes(authentication);

String authnContextClassRefValue = authnContextClassRefs != null ? authnContextClassRefs[0] : AuthnContext.PASSWORD_AUTHN_CTX;

SAMLPrincipal principal = new SAMLPrincipal(
authentication.getName(),
attributes.stream()
Expand All @@ -71,15 +77,14 @@ private void doSSO(HttpServletRequest request, HttpServletResponse response, Aut
assertionConsumerServiceURL,
messageContext.getRelayState());

samlMessageHandler.sendAuthnResponse(principal, response);
samlMessageHandler.sendAuthnResponse(principal, authnContextClassRefValue, response);
}

@SuppressWarnings("unchecked")
private List<SAMLAttribute> attributes(Authentication authentication) {
String uid = authentication.getName();
Map<String, List<String>> result = new HashMap<>(idpConfiguration.getAttributes());


Optional<Map<String, List<String>>> optionalMap = idpConfiguration.getUsers().stream()
.filter(user -> user.getPrincipal().equals(uid))
.findAny()
Expand All @@ -91,6 +96,9 @@ private List<SAMLAttribute> 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])) {
Expand All @@ -106,8 +114,8 @@ private List<SAMLAttribute> attributes(Authentication authentication) {
Map<String, String> standardAttributes = idpConfiguration.getStandardAttributes().getAttributes();
Map<String, List<String>> 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 -> {
Expand Down
5 changes: 5 additions & 0 deletions mujina-idp/src/main/java/mujina/idp/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,17 +23,20 @@
public class UserController {

private final List<Map<String, String>> 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();
this.samlAttributes = objectMapper.readValue(
loader.getResource(samlAttributesConfigFile).getInputStream(), new TypeReference<>() {
});
this.samlAttributes.sort(comparing(m -> m.get("id")));
this.authnContextClassRefs = authnContextClassRefs;
}

@GetMapping("/")
Expand All @@ -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";
}
}
11 changes: 11 additions & 0 deletions mujina-idp/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 21 additions & 0 deletions mujina-idp/src/main/resources/public/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
130 changes: 68 additions & 62 deletions mujina-idp/src/main/resources/public/main.js
Original file line number Diff line number Diff line change
@@ -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 = "<label>" + val + "</label>" +
"<input class='input-attribute-value' type='text' id='" + inputId + "' name='" + val + "'>" +
"<span id='" + spanId + "' class='remove-attribute-value'>🗑</span>";
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 = "<label>" + val + "</label>" +
"<input class='input-attribute-value' type='text' id='" + inputId + "' name='" + val + "'>" +
"<span id='" + spanId + "' class='remove-attribute-value'>🗑</span>";
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);
});
});
15 changes: 15 additions & 0 deletions mujina-idp/src/main/resources/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ <h1>Mujina Identity Provider</h1>
<label class="persist-me" for="persist-me">Persist me?</label>
</div>

<div class="authn-context-class-ref">
<label class="authn-context-class-ref" for="authn-context-class-ref">Select authnContextClassRef</label>
<input id="authn-context-class-ref-value"
type="hidden"
name="authn-context-class-ref-value"
value="urn:oasis:names:tc:SAML:2.0:ac:classes:Password">
<select class="acr-select" id="authn-context-class-ref">
<option selected="selected">urn:oasis:names:tc:SAML:2.0:ac:classes:Password</option>
<option th:each="ref : ${authnContextClassRefs}"
th:value="${ref}"
th:text="${ref}">
</option>
</select>
</div>

<div class="add-attribute">
<label for="add-attribute" class="sr-only">Select attributes</label>
<select class="attribute-select" id="add-attribute">
Expand Down
Loading

0 comments on commit f725996

Please sign in to comment.