Secure IdP

This commit is contained in:
haburger 2025-09-09 16:07:28 +00:00
parent 343e92f628
commit 558458b26a
5 changed files with 146 additions and 90 deletions

View File

@ -1,5 +1,5 @@
<WebService class="ch.nevis.esauth.auth.adapter.saml.ArtifactResolutionService" name="IDP_AGOV_SEC_ARS" uri="/nevisauth/services/ars/sec" SSODomain="Auth_Realm_Main_IDP"> <WebService class="ch.nevis.esauth.auth.adapter.saml.ArtifactResolutionService" name="IDP_AGOV_SEC_ARS" uri="/nevisauth/services/ars/sec" SSODomain="Auth_Realm_Main_IDP">
<property name="issuer" value="${var.idp_agov_sec-saml-issuer}"/> <property name="issuer" value="${var.idp_agov-saml-issuer-sec}"/>
<property name="out.keystoreref" value="Store_IDP_AGOV"/> <property name="out.keystoreref" value="Store_IDP_AGOV"/>
<property name="out.keyobjectref" value="Signer_IDP_AGOV"/> <property name="out.keyobjectref" value="Signer_IDP_AGOV"/>
<property name="in.keystoreref" value="Store_IDP_AGOV"/> <property name="in.keystoreref" value="Store_IDP_AGOV"/>

View File

@ -23,13 +23,25 @@ def redirect(String url) {
outargs.put('nevis.transfer.destination', url) outargs.put('nevis.transfer.destination', url)
} }
/** String getNormalisedSamlMessage(String parameter) {
* Extracts the content of the Issuer element from a parsed SAML message. if (parameter == null) {
* The Issuer is optional according to SAML specification but we need it for dispatching. return
* }
* @param xml - as parsed by Groovy XmlSlurper String text
* @return text content of Issuer element converted or null byte[] decoded
*/
// if parameter is raw xml then continue otherwise try to parse the base64 encoding
if (parameter.startsWith("<")) {
text = new String(parameter)
}
else {
decoded = parameter.decodeBase64()
text = new String(decoded)
}
return text
}
String getNodeText(GPathResult xml, String nodeName) { String getNodeText(GPathResult xml, String nodeName) {
return xml.depthFirst().find { GPathResult node -> { return xml.depthFirst().find { GPathResult node -> {
node.name().endsWith(":${nodeName}") || node.name().equalsIgnoreCase(nodeName) node.name().endsWith(":${nodeName}") || node.name().equalsIgnoreCase(nodeName)
@ -37,45 +49,46 @@ String getNodeText(GPathResult xml, String nodeName) {
}?.text()?.trim() }?.text()?.trim()
} }
String getNodeText(String samlMessage, String nodeName) { String getAttribute(GPathResult xml, String attributeName) {
return xml.depthFirst().find { GPathResult node -> {
node.attributes().containsKey(attributeName)
}
}?.attributes()?.get(attributeName)
}
String getNodeText(String parameter, String nodeName) {
String samlMessage = getNormalisedSamlMessage(parameter)
if (samlMessage == null) { if (samlMessage == null) {
return return
} }
String text
byte[] decoded
def parser = new XmlSlurper() def parser = new XmlSlurper()
// if samlMessage is raw xml then continue otherwise try to parse the base64 encoding def xml = parser.parseText(samlMessage)
if (samlMessage.startsWith("<")) { return getNodeText(xml, nodeName)
text = new String(samlMessage)
}
else {
decoded = samlMessage.decodeBase64()
text = new String(decoded)
} }
// after decoded, if redirect binding, we need to parse string to xml String getAttribute(String parameter, String attributeName) {
if (text.startsWith("<")) { String samlMessage = getNormalisedSamlMessage(parameter)
// plain String (POST/SOAP parameter) if (samlMessage == null) {
def xml = parser.parseText(text) return
return getNodeText(xml, nodeName)
}
else {
// should be deflate encoded (query parameter)
def is = new InflaterInputStream(new ByteArrayInputStream(decoded), new Inflater(true))
def xml = parser.parse(is)
return getNodeText(xml, nodeName)
} }
def parser = new XmlSlurper()
def xml = parser.parseText(samlMessage)
return getAttribute(xml, attributeName)
} }
String getIssuer(String value) { String getIssuer(String value) {
return getNodeText(value, 'Issuer') return getNodeText(value, 'Issuer')
} }
String getRequesterID(String value) { String getAttributeConsumingServiceIndex(String value) {
return getNodeText(value, 'RequesterID') return getAttribute(value, 'AttributeConsumingServiceIndex')
} }
def dispatchIssuer(i2s, String issuer, String requester) { String getProtocolBinding(String value) {
return getAttribute(value, 'ProtocolBinding')
}
def dispatchIssuer(i2s, String issuer, boolean secureMode) {
def result = i2s.get(issuer) def result = i2s.get(issuer)
if (result == null) { if (result == null) {
LOG.info("No SP found for issuer '$issuer'. Hint: check SAML SP Connector patterns.") LOG.info("No SP found for issuer '$issuer'. Hint: check SAML SP Connector patterns.")
@ -85,30 +98,33 @@ def dispatchIssuer(i2s, String issuer, String requester) {
if(parameters.get('epdMode') == 'artifact' && result == 'epd'){ if(parameters.get('epdMode') == 'artifact' && result == 'epd'){
LOG.debug("EPD: Artifact mode") LOG.debug("EPD: Artifact mode")
result = result + "_artifact" result = result + "_artifact"
} else if (result == 'main') { } else if (result == 'main' && secureMode) {
if ('https://op.agov-w.azure.adnovum.net/SAML2/ACS/' == requester) { LOG.debug("AGOV: Secure mode requested")
result = result + "_secure" result = result + "_secure"
} }
}
response.setResult(result) response.setResult(result)
session.put("saml.inbound.issuer", issuer) session.put('saml.inbound.issuer', issuer)
session.put('saml.idp.result', result) // remember decision for sub-sequent requests without a SAML message session.put('saml.idp.result', result) // remember decision for sub-sequent requests without a SAML message
} }
def dispatchIssuer(i2s, String issuer) { def dispatchIssuer(i2s, String issuer) {
dispatchIssuer(i2s, issuer, 'unknown') dispatchIssuer(i2s, issuer, false)
} }
def dispatchMessage(i2s, String message) { def dispatchMessage(i2s, String message) {
def issuer = getIssuer(message) def issuer = getIssuer(message)
def requester = getRequesterID(message) def secureMode = (getAttributeConsumingServiceIndex(message) == '10101')
def useArtifact = ('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' == getProtocolBinding(message))
LOG.info("secureMode requested: ${secureMode}")
if (issuer == null) { if (issuer == null) {
LOG.info("No issuer found in incoming SAML message. Giving up.") LOG.info("No issuer found in incoming SAML message. Giving up.")
} }
session.put("saml.inbound.issuer", issuer) session.put('saml.inbound.issuer', issuer)
dispatchIssuer(i2s, issuer, requester) session.put('agov.idp.use.artifact', '' + useArtifact)
dispatchIssuer(i2s, issuer, secureMode)
} }
if (parameters.get('logoutConfirmation') == 'true' && "stepup" == request.getMethod()) { if (parameters.get('logoutConfirmation') == 'true' && "stepup" == request.getMethod()) {

View File

@ -7,9 +7,7 @@ pattern:
notes: "modified script taken from what Nevis generated when using a SAM IDP Pattern" notes: "modified script taken from what Nevis generated when using a SAM IDP Pattern"
properties: properties:
authStatesFile: "res://bb9e7806a04578e0ad468829#authStatesFile" authStatesFile: "res://bb9e7806a04578e0ad468829#authStatesFile"
parameters: "out.binding: http-post\nout.post.relayStateEncoding: HTML\nout.encrypt:\ parameters: "var://idp_sp_sec_settings"
\ Assertion\nout.encrypt.keystoreref: EncryptionKeys\nout.encryption_key_from_expression:\
\ \nout.encrypt.keyobjectref: DefaultEncryptionKey\n"
onSuccess: onSuccess:
- "pattern://2f81f8b878ef787fc5cc284a" - "pattern://2f81f8b878ef787fc5cc284a"
onFailure: onFailure:

View File

@ -1,45 +1,76 @@
<AuthState name="${state.entry}" class="ch.nevis.esauth.auth.states.saml.IdentityProviderState" final="false" resumeState="true"> <AuthState name="${state.entry}"
class="ch.nevis.esauth.auth.states.standard.ConditionalDispatcherState" final="false" resumeState="false">
<ResultCond name="useArtifact" next="${state.entry}_artifact" />
<ResultCond name="default" next="${state.entry}_post"/>
<Response value="AUTH_ERROR">
<Gui name="AuthErrorDialog"/>
</Response>
<property name="condition:useArtifact"
value="${sess:agov.idp.use.artifact:^true$}" />
</AuthState>
<AuthState name="${state.entry}_post" class="ch.nevis.esauth.auth.states.saml.IdentityProviderState" final="false" resumeState="true">
<!-- Auth_Realm_Main_IDP_Concurrent_Logout --> <!-- Auth_Realm_Main_IDP_Concurrent_Logout -->
<ResultCond name="IDP-initiated-ConcurrentLogout" next="${state.exit.1}"/> <ResultCond name="IDP-initiated-ConcurrentLogout" next="${state.exit.1}"/>
<ResultCond name="SP-initiated-ConcurrentLogout" next="${state.exit.1}"/> <ResultCond name="SP-initiated-ConcurrentLogout" next="${state.exit.1}"/>
<!-- Auth_Realm_Main_IDP_Prepare_Done --> <!-- Auth_Realm_Main_IDP_Prepare_Done -->
<ResultCond name="IDP-initiated-SingleLogout" next="${state.done}"/> <ResultCond name="IDP-initiated-SingleLogout" next="${state.done}"/>
<ResultCond name="SP-initiated-SingleLogout" next="${state.done}"/> <ResultCond name="SP-initiated-SingleLogout" next="${state.done}"/>
<ResultCond name="ok" next="${state.done}"/> <ResultCond name="ok" next="${state.done}"/>
<!-- Auth_Realm_Main_IDP_Logout_Done --> <!-- Auth_Realm_Main_IDP_Logout_Done -->
<ResultCond name="LogoutCompleted" next="${state.exit.2}"/> <ResultCond name="LogoutCompleted" next="${state.exit.2}"/>
<!-- Auth_Realm_Main_IDP_Logout_Fail --> <!-- Auth_Realm_Main_IDP_Logout_Fail -->
<ResultCond name="LogoutFailed" next="${state.exit.3}"/> <ResultCond name="LogoutFailed" next="${state.exit.3}"/>
<!-- Auth_Realm_Main_IDP_RequestedRoleLevel --> <!-- Auth_Realm_Main_IDP_RequestedRoleLevel -->
<ResultCond name="authenticate:IDP-initiated-SSO" next="${state.exit.4}"/> <ResultCond name="authenticate:IDP-initiated-SSO" next="${state.exit.4}"/>
<ResultCond name="authenticate:SP-initiated-SSO" next="${state.exit.4}"/> <ResultCond name="authenticate:SP-initiated-SSO" next="${state.exit.4}"/>
<ResultCond name="invalidAssertionConsumerUrl" next="${state.entry}"/> <ResultCond name="invalidAssertionConsumerUrl" next="${state.entry}"/>
<!-- Auth_Realm_Main_IDP_Selector --> <!-- Auth_Realm_Main_IDP_Selector -->
<ResultCond name="stepup:IDP-initiated-SSO" next="${state.failed}"/> <ResultCond name="stepup:IDP-initiated-SSO" next="${state.failed}"/>
<ResultCond name="stepup:SP-initiated-SSO" next="${state.failed}"/> <ResultCond name="stepup:SP-initiated-SSO" next="${state.failed}"/>
<Response value="AUTH_ERROR"> <Response value="AUTH_ERROR">
<Gui name="saml_idp" label="title.saml.failed"> <Gui name="saml_idp" label="title.saml.failed">
<GuiElem name="lasterror" type="error" label="error.saml.failed"/> <GuiElem name="lasterror" type="error" label="error.saml.failed"/>
</Gui> </Gui>
</Response> </Response>
<!-- same as Custom_AGOV_IDP --> <!-- same as Custom_AGOV_IDP -->
<propertyRef name="Auth_Realm_Main_IDP_Auth_Realm_Main_IDP_Custom_AGOV_IDP"/> <propertyRef name="Auth_Realm_Main_IDP_Auth_Realm_Main_IDP_Custom_AGOV_IDP"/>
<property name="out.issuer" value="${var.idp_agov-saml-issuer-sec}"/>
<property name="out.binding" value="${param.out.binding:http-post}" /> <property name="out.binding" value="http-post"/>
<property name="out.post.relayStateEncoding" value="${param.out.post.relayStateEncoding:HTML}"/>
<property name="out.encrypt" value="${param.out.encrypt:none}"/>
<property name="out.encrypt.keystoreref" value="${param.out.encrypt.keystoreref:DefaultKeyStore}"/>
<property name="out.encrypt.keyobjectref" value="${param.out.encrypt.keyobjectref:DefaultSigner}"/>
</AuthState>
<AuthState name="${state.entry}_artifact" class="ch.nevis.esauth.auth.states.saml.IdentityProviderState" final="false" resumeState="true">
<!-- Auth_Realm_Main_IDP_Concurrent_Logout -->
<ResultCond name="IDP-initiated-ConcurrentLogout" next="${state.exit.1}"/>
<ResultCond name="SP-initiated-ConcurrentLogout" next="${state.exit.1}"/>
<!-- Auth_Realm_Main_IDP_Prepare_Done -->
<ResultCond name="IDP-initiated-SingleLogout" next="${state.done}"/>
<ResultCond name="SP-initiated-SingleLogout" next="${state.done}"/>
<ResultCond name="ok" next="${state.done}"/>
<!-- Auth_Realm_Main_IDP_Logout_Done -->
<ResultCond name="LogoutCompleted" next="${state.exit.2}"/>
<!-- Auth_Realm_Main_IDP_Logout_Fail -->
<ResultCond name="LogoutFailed" next="${state.exit.3}"/>
<!-- Auth_Realm_Main_IDP_RequestedRoleLevel -->
<ResultCond name="authenticate:IDP-initiated-SSO" next="${state.exit.4}"/>
<ResultCond name="authenticate:SP-initiated-SSO" next="${state.exit.4}"/>
<ResultCond name="invalidAssertionConsumerUrl" next="${state.entry}"/>
<!-- Auth_Realm_Main_IDP_Selector -->
<ResultCond name="stepup:IDP-initiated-SSO" next="${state.failed}"/>
<ResultCond name="stepup:SP-initiated-SSO" next="${state.failed}"/>
<Response value="AUTH_ERROR">
<Gui name="saml_idp" label="title.saml.failed">
<GuiElem name="lasterror" type="error" label="error.saml.failed"/>
</Gui>
</Response>
<!-- same as Custom_AGOV_IDP -->
<propertyRef name="Auth_Realm_Main_IDP_Auth_Realm_Main_IDP_Custom_AGOV_IDP"/>
<property name="out.issuer" value="${var.idp_agov-saml-issuer-sec}"/>
<property name="out.binding" value="http-artifact"/>
<property name="out.post.relayStateEncoding" value="${param.out.post.relayStateEncoding:HTML}"/> <property name="out.post.relayStateEncoding" value="${param.out.post.relayStateEncoding:HTML}"/>
<property name="out.encrypt" value="${param.out.encrypt:none}"/> <property name="out.encrypt" value="${param.out.encrypt:none}"/>
<property name="out.encrypt.keystoreref" value="${param.out.encrypt.keystoreref:DefaultKeyStore}"/> <property name="out.encrypt.keystoreref" value="${param.out.encrypt.keystoreref:DefaultKeyStore}"/>
<property name="out.encrypt.keyobjectref" value="${param.out.encrypt.keyobjectref:DefaultSigner}"/> <property name="out.encrypt.keyobjectref" value="${param.out.encrypt.keyobjectref:DefaultSigner}"/>
<!-- property name="out.encryption_key_from_expression" value="${param.out.encryption_key_from_expression:DefaultSigner}" / -->
</AuthState> </AuthState>

View File

@ -550,6 +550,17 @@ variables:
format: "^[^\\s,]*$" format: "^[^\\s,]*$"
value: "atb-sec" value: "atb-sec"
requireOverloading: true requireOverloading: true
idp_sp_sec_settings:
className: "ch.nevis.admin.v4.plugin.base.generation.property.TextProperty"
parameters:
required: false
syntax: "YAML"
value: |
out.post.relayStateEncoding: HTML
out.encrypt: Assertion
out.encrypt.keystoreref: EncryptionKeys
out.encrypt.keyobjectref: DefaultEncryptionKey
requireOverloading: false
internal-idp-auth-signer-trust-additional-trusted-certificates: internal-idp-auth-signer-trust-additional-trusted-certificates:
className: "ch.nevis.admin.v4.plugin.base.generation.property.AttachmentProperty" className: "ch.nevis.admin.v4.plugin.base.generation.property.AttachmentProperty"
parameters: parameters: