Skip to content

Commit

Permalink
Add support for bootstrap->idws exchange with parenthood claim
Browse files Browse the repository at this point in the history
  • Loading branch information
jsotrifork committed Oct 9, 2024
1 parent 3892a6e commit 37fc364
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 19 deletions.
85 changes: 69 additions & 16 deletions src/main/java/com/trifork/unsealed/BootstrapToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ public class BootstrapToken {
static final String DEFAULT_BST_TO_SOSI_ENDPOINT = "/sts/services/BST2SOSI";
static final String DEFAULT_JWT_TO_ID_ENDPOINT = "/sts/services/JWT2Idws";

static final String ON_BEHALF_OF_CLAIM_URI = "dk:healthcare:saml:attribute:OnBehalfOf";

public static enum OnBehalfOfClaimType {
PROCURATION("urn:dk:healthcare:saml:actThroughProcurationBy:cprNumberIdentifier:"),
WARD("urn:dk:healthcare:saml:actThrough:WardCustody:cprNumberIdentifier:"),
PARTLY_WARD("urn:dk:healthcare:saml:actThrough:PartlyWardCustody:cprNumberIdentifier:"),
PARENTHOOD("urn:dk:healthcare:saml:actThrough:ParentalCustody:cprNumberIdentifier:");

public final String prefix;

OnBehalfOfClaimType(String prefix) {
this.prefix = prefix;
}
}

private static final String REQUEST_SECURITY_TOKEN_RESPONSE_XPATH = "/" + NsPrefixes.soap.name() + ":Envelope/"
+ NsPrefixes.soap.name() + ":Body/" + NsPrefixes.wst13.name()
+ ":RequestSecurityTokenResponseCollection/"
Expand All @@ -69,8 +84,11 @@ public class BootstrapToken {

/**
* Invoke SOSI STS to exchange this bootstrap token to an IDWS identity token.
* @param audience The audience for the identity token, e.g., "https://minlog"
* @param cpr The CPR of the user
*
* @param audience
* The audience for the identity token, e.g., "https://minlog"
* @param cpr
* The CPR of the user
* @return The identity token
* @throws IOException
* @throws InterruptedException
Expand All @@ -91,12 +109,43 @@ public IdentityToken exchangeToIdentityToken(String audience, String cpr)
return exchangeToIdentityToken(audience, cpr, null);
}

/**
* Invoke SOSI STS to exchange this bootstrap token into an IDWS identity token that includes verified procuration access.
* @deprecated Use {@link BootstrapToken#exchangeToIdentityToken(String, String, String, OnBehalfOfClaimType))}
* @param audience
* @param cpr
* @param procurationCpr
* @return
* @throws IOException
* @throws InterruptedException
* @throws NoSuchAlgorithmException
* @throws InvalidAlgorithmParameterException
* @throws MarshalException
* @throws XMLSignatureException
* @throws XPathExpressionException
* @throws STSInvocationException
* @throws ParserConfigurationException
* @throws SAXException
*/
public IdentityToken exchangeToIdentityToken(String audience, String cpr, String procurationCpr)
throws IOException, InterruptedException,
NoSuchAlgorithmException, InvalidAlgorithmParameterException,
MarshalException, XMLSignatureException, XPathExpressionException, STSInvocationException,
ParserConfigurationException, SAXException {
return exchangeToIdentityToken(audience, cpr, procurationCpr, OnBehalfOfClaimType.PROCURATION);
}

/**
*
* Invoke SOSI STS to exchange this bootstrap token to an IDWS identity token that includes verified procuration access.
* @param audience The audience for the identity token, e.g., "https://minlog"
* @param cpr The CPR of the user
* @param procurationCpr The CPR of the person being the procuration subject
* Invoke SOSI STS to exchange this bootstrap token into an IDWS identity token that includes verified procuration access.
*
* @param audience
* The audience for the identity token, e.g., "https://minlog"
* @param cpr
* The CPR of the user
* @param onBehalfOfCpr
* The CPR of the person being the procuration subject
* @param claimType
* @return The identity token
* @throws IOException
* @throws InterruptedException
Expand All @@ -109,7 +158,7 @@ public IdentityToken exchangeToIdentityToken(String audience, String cpr)
* @throws ParserConfigurationException
* @throws SAXException
*/
public IdentityToken exchangeToIdentityToken(String audience, String cpr, String procurationCpr)
public IdentityToken exchangeToIdentityToken(String audience, String cpr, String onBehalfOfCpr, OnBehalfOfClaimType claimType)
throws IOException, InterruptedException,
NoSuchAlgorithmException, InvalidAlgorithmParameterException,
MarshalException, XMLSignatureException, XPathExpressionException, STSInvocationException,
Expand All @@ -121,10 +170,8 @@ public IdentityToken exchangeToIdentityToken(String audience, String cpr, String
claims.add(new Claim("dk:gov:saml:attribute:CprNumberIdentifier", cpr));
}

if (procurationCpr != null) {
claims.add(new Claim("dk:healthcare:saml:attribute:OnBehalfOf",
"urn:dk:healthcare:saml:actThroughProcurationBy:cprNumberIdentifier:"
+ procurationCpr));
if (onBehalfOfCpr != null && claimType != null) {
claims.add(new Claim(ON_BEHALF_OF_CLAIM_URI, claimType.prefix + onBehalfOfCpr));
}

Element request = createBootstrapExchangeRequest(audience, claims);
Expand Down Expand Up @@ -235,11 +282,17 @@ private Element createBootstrapExchangeRequest(String audience, List<Claim> clai

/**
* Exchange thie bootstrap token to a IDCard of type user
* @param audience The <code>AppliesTo</code> for the security token request. This has no effect on the returned IDCard
* @param role The role of the IDCard
* @param occupation The occupation of the IDCard
* @param authId The auth id of the IDCard
* @param systemName The system name of the IDCard
*
* @param audience
* The <code>AppliesTo</code> for the security token request. This has no effect on the returned IDCard
* @param role
* The role of the IDCard
* @param occupation
* The occupation of the IDCard
* @param authId
* The auth id of the IDCard
* @param systemName
* The system name of the IDCard
* @return
* @throws IOException
* @throws InterruptedException
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/trifork/unsealed/NsPrefixes.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public enum NsPrefixes {
medcom(XmlUtil.MEDCOM_SCHEMA), wsp(XmlUtil.WSP_SCHEMA), wsse(XmlUtil.WSSE_SCHEMA), wst(XmlUtil.WST_SCHEMA),
wst13(XmlUtil.WST_1_3_SCHEMA), wst14(XmlUtil.WST_1_4_SCHEMA), wsu(XmlUtil.WSU_SCHEMA),
sosi(XmlUtil.SOSI_SCHEMA), xsd(XmlUtil.XSD_SCHEMA), soap(XmlUtil.SOAP_ENV), wsa(XmlUtil.WSA_1_0_SCHEMA), wsa_Aug_2004(XmlUtil.WSA_Aug2004_SCHEMA), auth(XmlUtil.WSF_AUTH_SCHEMA),
xenc(XmlUtil.XENC), bpp(XmlUtil.BPP);
xenc(XmlUtil.XENC), bpp(XmlUtil.BPP), srp(XmlUtil.SRP);

public final String namespaceUri;

Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/trifork/unsealed/SamlUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public static Element addUriTypeSamlAttribute(Element parent, String name, Strin
public static String getSamlAttribute(Element parent, String name) {
Element attribute = XmlUtil.getChild(parent, NsPrefixes.saml, "Attribute",
child -> name.equals(child.getAttribute("Name")));
if (attribute == null) {
return null;
}
return XmlUtil.getTextChild(attribute, NsPrefixes.saml, "AttributeValue");
}

Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/trifork/unsealed/XmlUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public class XmlUtil {
public static final String OIO_BASIC_PRIVILEGES_PROFILE = "http://itst.dk/oiosaml/basic_privilege_profile";
public static final String XENC = "http://www.w3.org/2001/04/xmlenc#";
public static final String BPP = "http://itst.dk/oiosaml/basic_privilege_profile";
public static final String SRP = "urn:dk:healthcare:saml:subject_relations_profile:1.1";

public static final String NS_SAML = "saml";
public static final String NS_SAMLP = "samlp";
Expand Down
27 changes: 25 additions & 2 deletions src/test/java/com/trifork/unsealed/BootstrapTokenTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,10 @@ void canExchangeBootstrapTokenToIDWSToken() throws Exception {
}

@Test
@Disabled
void canExchangeBootstrapTokenToIDWSTokenWithProcuration() throws Exception {
BootstrapToken bst = issuer.cpr("0501792275").issueForCitizen();

IdentityToken idwsToken = bst.exchangeToIdentityToken("https://fmk", "0501792275", "1111111118");
IdentityToken idwsToken = bst.exchangeToIdentityToken("https://fmk", "0501792275", "1111111118", BootstrapToken.OnBehalfOfClaimType.PROCURATION);

// Extract priviledge attribibute, base64 decode it, and verify that our procuration cpr is there
Element attributeStatement = XmlUtil.getChild(idwsToken.assertion, NsPrefixes.saml,
Expand All @@ -121,6 +120,30 @@ void canExchangeBootstrapTokenToIDWSTokenWithProcuration() throws Exception {
privileges.getAttribute("Scope"));
}

@Test
void canExchangeBootstrapTokenToIDWSTokenWithParenthood() throws Exception {
BootstrapToken bst = issuer.cpr("2811686517").issueForCitizen(); // Karl Bonde

IdentityToken idwsToken = bst.exchangeToIdentityToken("https://fmk", "2811686517", "0904128090", BootstrapToken.OnBehalfOfClaimType.PARENTHOOD);

// Extract priviledge attribibute, base64 decode it, and verify that our procuration cpr is there
Element attributeStatement = XmlUtil.getChild(idwsToken.assertion, NsPrefixes.saml,
"AttributeStatement");
String subjectRelationsBase64 = SamlUtil.getSamlAttribute(attributeStatement,
"urn:dk:healthcare:saml:attribute:SubjectRelations");
String privs = new String(Base64.getDecoder().decode(subjectRelationsBase64), StandardCharsets.UTF_8);

Document doc = XmlUtil.getDocBuilder()
.parse(new ByteArrayInputStream(privs.getBytes(StandardCharsets.UTF_8)));

XPathContext xpath = new XPathContext(doc);
Element verifiedRelation = xpath.findElement(doc.getDocumentElement(),
"/" + NsPrefixes.srp.name() + ":SubjectRelations/"
+ NsPrefixes.srp.name() + ":VerifiedRelation");

assertEquals("0904128090", verifiedRelation.getAttribute("relatedPersonID"));
}

@Test
void cannotExchangeExpiredBootstrapTokenToIDWSToken() throws Exception {

Expand Down

0 comments on commit 37fc364

Please sign in to comment.