diff --git a/src/main/java/com/trifork/unsealed/BootstrapToken.java b/src/main/java/com/trifork/unsealed/BootstrapToken.java index 3e165b0..4bd2295 100755 --- a/src/main/java/com/trifork/unsealed/BootstrapToken.java +++ b/src/main/java/com/trifork/unsealed/BootstrapToken.java @@ -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/" @@ -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 @@ -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 @@ -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, @@ -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); @@ -235,11 +282,17 @@ private Element createBootstrapExchangeRequest(String audience, List clai /** * Exchange thie bootstrap token to a IDCard of type user - * @param audience The AppliesTo 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 AppliesTo 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 diff --git a/src/main/java/com/trifork/unsealed/NsPrefixes.java b/src/main/java/com/trifork/unsealed/NsPrefixes.java index b5be9a8..c69c9c6 100755 --- a/src/main/java/com/trifork/unsealed/NsPrefixes.java +++ b/src/main/java/com/trifork/unsealed/NsPrefixes.java @@ -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; diff --git a/src/main/java/com/trifork/unsealed/SamlUtil.java b/src/main/java/com/trifork/unsealed/SamlUtil.java index fb1c4e9..e96ffbd 100755 --- a/src/main/java/com/trifork/unsealed/SamlUtil.java +++ b/src/main/java/com/trifork/unsealed/SamlUtil.java @@ -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"); } diff --git a/src/main/java/com/trifork/unsealed/XmlUtil.java b/src/main/java/com/trifork/unsealed/XmlUtil.java index 14ab66c..2083f37 100755 --- a/src/main/java/com/trifork/unsealed/XmlUtil.java +++ b/src/main/java/com/trifork/unsealed/XmlUtil.java @@ -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"; diff --git a/src/test/java/com/trifork/unsealed/BootstrapTokenTest.java b/src/test/java/com/trifork/unsealed/BootstrapTokenTest.java index 9abdf75..1ca5d9f 100755 --- a/src/test/java/com/trifork/unsealed/BootstrapTokenTest.java +++ b/src/test/java/com/trifork/unsealed/BootstrapTokenTest.java @@ -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, @@ -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 {