From 93eed7e60c7af863954138135f47c09282f412aa Mon Sep 17 00:00:00 2001 From: haburger Date: Wed, 3 Sep 2025 07:02:18 +0000 Subject: [PATCH] new configuration version --- ...8s-nevisauth-7022472ae407577ae604bbb8.yaml | 2 +- .../opt/keys/trust/idp-pem-atb-enc/keypass | 2 + .../keys/trust/idp-pem-atb-enc/truststore.jks | Bin 0 -> 1476 bytes .../keys/trust/idp-pem-atb-enc/truststore.p12 | Bin 0 -> 1798 bytes .../keys/trust/idp-pem-atb-enc/truststore.pem | 32 ++++ .../opt/nevisauth/default/conf/esauth4.xml | 67 +++++-- .../saml_idp_agov_sec_authorization.groovy | 179 ++++++++++++++++++ .../conf/saml_idp_agov_sec_dispatcher.groovy | 174 +++++++++++++++++ .../conf/saml_idp_logout_confirm.groovy | 64 +++++++ ...visproxy-idp-0ceb05c56644a59d648c13b9.yaml | 2 +- .../WEB-INF/web.xml | 19 +- 11 files changed, 522 insertions(+), 19 deletions(-) create mode 100755 DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/keypass create mode 100644 DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/truststore.jks create mode 100644 DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/truststore.p12 create mode 100644 DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/truststore.pem create mode 100644 DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_agov_sec_authorization.groovy create mode 100644 DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_agov_sec_dispatcher.groovy create mode 100644 DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_logout_confirm.groovy diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/etc/nevis/k8s-nevisauth-7022472ae407577ae604bbb8.yaml b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/etc/nevis/k8s-nevisauth-7022472ae407577ae604bbb8.yaml index 649394d..5e66bb1 100644 --- a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/etc/nevis/k8s-nevisauth-7022472ae407577ae604bbb8.yaml +++ b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/etc/nevis/k8s-nevisauth-7022472ae407577ae604bbb8.yaml @@ -46,7 +46,7 @@ spec: podDisruptionBudget: maxUnavailable: "50%" git: - tag: "r-9849dba282e5e9421988bf7092f242ff73d83ce5" + tag: "r-04ad6fd7455702c2a591f4a7b8d6c94222de911e" dir: "DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth" credentials: "git-credentials" database: diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/keypass b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/keypass new file mode 100755 index 0000000..5b0d317 --- /dev/null +++ b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/keypass @@ -0,0 +1,2 @@ +#!/bin/bash +echo 'password' \ No newline at end of file diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/truststore.jks b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..77cb5d89a4e3aeed077d315d8f8b55e4f2c39577 GIT binary patch literal 1476 zcmezO_TO6u1_mY|W(3m$i6u$8<@rU~x|t~jy4k6fK#7@rp@EqUtPy&q29^vAtknih ztmOtx%<&7DnHZUvL`wFqTtCOIXMwFueCpJ&Gp_CP+5eu|t>kL>*U9+frR+1o=EZ!US>JzF(EM#P(=Fao z>+>!P=Q7tl54~6YOguE}r6t!or60X719CPemKVqUNb|IwbX-Z7=X{c3V>AEKztd+X zf8a|HZ(3BXE!TEA&-kv_B^%LMf6hkUnsIdQzk>Aj3YIhH?EUp=^F8I0F<)=a)^MEj zDBgC)nORQnlkc$9q~BSSb$gZE{C8YWD}zI>FUftpDqWAy;pG!t*R80O z;GF(7WtQ`{Sto5HHEL6n#Ep<Q_LWl+8#j*zHn%_R;1S0qv0~mU&C&IBu)FZ~BYB zn*ZhG^wjcI#!PtC9*HFL!iEnJhE;&1tu5cBK*@V-`d+&Tsw891tFq^X+HX;hLbe{Z1b=r|+7( z$a4Qfw{6QO{7IjOwU zTW{6uGB7iBK73i|@ag|IA0`LOf4{Yo{l@x5PhJ0gY>w)Ddwuc4%(=bygV*HlQn}+f zKhKvZ^!G!fXC^jZ|5v6z|2=i-5%C4ym#^n-=Z)Shdg-^u;X^s!AC!G7Tk$u%)so@< zTK~-dfAZ#7%C+D5xo?U8+mi`~Ow5c7jEjQ}0u5w==~b4GMT|vc)yiqimxT}RxhV4d zW9sfbb9$CMyl5Z~l2&GsFc53Nu7Dq;K$wy7KMSh?Gmt_~cECIYOm>V6^JLSr)r#I| zT;}`wxb3d8iBFU53h#eBcj{%U?}RSr-MF3S)M5=;y|>$5MzjZQ`*M8QZwr$b*7EV+ zW6$2%UH$g(;-hyhbyJeEjo2#XFNE0U9jv(g>QqEYoPB249K^8QknHtinI!e6pBGY+qLeOrBcvqk3FsYkjR{_IJYdVlh@ zT2Iuv-S_`Kj;cKH%B}Tlp!`x>2_d8Ftc!cz8Qp5hZM-3R!E)!@;HV=vcDTC>Mn7Nh zrtYzT?Sjbgpw!(*KP3q3kF%{ew}kIHNlhtG%ad+e#*xIWD!dTof_<~d?NZziqP zRPAN>6E$Oo`Q}Tv^*a_{ZLYdD`-sUBS;o1$!c*l|KHm4G+V9dq|GoF~vR-HJJiT7* z1G9?Gzo^>v`6Z9;JYfCdo6Y8^I_Llf-_wEx*YoAyXGVV$BAWO@8(o$)gPVn zF8bE@p6!gqwPk$XLgogOOr%d_F59V?b)j1B;$AP|cO0gzN(~NE3ah&kv(8_X?mwe6 mR-~P`kyU}>GZ??{cJdYyFUoU@HR|5b(kc*E1 literal 0 HcmV?d00001 diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/truststore.p12 b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/truststore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..be0bb022ccbd241d770fef82485e1fdda5cc5cf3 GIT binary patch literal 1798 zcmV+h2l@Cgf(HTu0Ru3C2CN1NDuzgg_YDCD0ic2godkjgnJ|I|l`w(^kp>AWhDe6@ z4FLxRpn?X1FoFht0s#Opf(Ca62`Yw2hW8Bt2LUi<1_>&LNQUL|NAAHuV&y4~CClCSwATSID2r7n1hW8Bu2?YQ! z9R>+thDZTr0|Wso1Q4p)3l1GIy=9nnV0>UcQs{t!1^`X3lJ_6%jpi+s9mePaUIi_~ zfbOe;4D-XC0#gJ-E&(rI-z>7ekc%%LdG`uZd-EOC0cIIvV&>wfg7K4&EPSVo8}JdY zAV)Jgr?QVI|3FysdLlaqr_5%9Ct``WYM~`$+Nh7i&*tIHU3!cJa)~(h)L~&tiELe1 z1blZWNexk{-3{I#cczKAgZPo=nBiRZM)9UuU+iPs84@> z$=J(8b%&<@HmN>LD!4kUr=4v3uZYM#$bEQWBw5>|eWU>rrGJ%BvA&E^egf%}i1k#G z_$aQq{rLT9^zPt)dXBg~&e>u+*$y{t(g6EI**URl&y+||zO)`UIvEemuh(k~L$7;; zj6rt{{$%Q_x`B`sdmx=DO2OUii->(ko`m3|Kh?Lc7Err)f4d3n*E5daeg3>=pcWd( z;2low7}1>#YP>oHM7!Pj{E5BFub(%q7%R$d5x>N%Y9YzVJDgqAgS>uVGM|MMl=R^T zeTR7H?ompD%SP+e3b-+G?ZzJd5N|YPBV!81*)BfXtOC>(6kVOBnz3&6)cyEN)hqno z)7QpecLPbP18lhBHk){6IJ!YZR2e?3^Ed2Hm6ilV%s5dy>z%-~z`tFT3G@yTDcpX* zQn?~StQImjYpLqceaz$N61#hT7_lg))^xN&>*bqw@gEI1iT)XGuuHipzCI_ge|qtq zDmGX39!h8vL1zCG4Y9z~uy+cr9yB%|%n^B0r+C$H2bhsv^9wRe#rbj}}4bDS#;k#WJ~uYR}IeqrS!-Rh9PBr_`R+5muQ~;#tq! zIo%FF4L7v1K$lOsmdW|{c4;c3S#bv1{0sc#(uXI|*k2|3*3eSVbQIYDOG5dL271bLu|2cef0=Zf;J(i(ZTqf!FFYNL``MjH3D_*1suCb&!sf3e4RDWja}Eu@=Q;|4MwEq0`!EAs1y-Ax7^+l6UuB%$%%@vpWHLFQz*;}&dhu7XKUqv(u|-y~;NRmw`fMME zE$#E$wnRtUu?8P$CiGexx|EP@S+lO+2tmREw~2MuYuZ!!{MacAiIO=Wavb|Vg%q2N zD*0S#Q|irf+q{$Zw6G^UeUR{KmY2wFpInBFG2F z3eO42lHLP_fRXUxAcb0P!yO+E2yy)~!mof}t|IqN&TtWpUJo)i)i!eJ3J_;$V+47vWkIl()yCfn6l@*3>7 zm$QNp2h@jfKHuoLT1?FAPm7%hqzz+EQNC9O71OfpC00ba>CsLvaC5s+2?lsB!1#A9?*^XE- o(+3wnCFuMhx#4vL6ce8{+eprb5?vo2vbs4=nH~+LLjnRP5Dh6>i~s-t literal 0 HcmV?d00001 diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/truststore.pem b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/truststore.pem new file mode 100644 index 0000000..d398c57 --- /dev/null +++ b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/keys/trust/idp-pem-atb-enc/truststore.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFdzCCA1+gAwIBAgIUdL2pr5w+jKA9HF9llVbMRTK4MO8wDQYJKoZIhvcNAQEL +BQAwSzELMAkGA1UEBhMCQ0gxDTALBgNVBAcMBEJlcm4xEjAQBgNVBAoMCUFHT1Yg +V29yazEZMBcGA1UEAwwQYXRiLXdvcmstaWRwLWtleTAeFw0yNTA5MDMwNjQ2Mjha +Fw0zNTA5MDEwNjQ2MjhaMEsxCzAJBgNVBAYTAkNIMQ0wCwYDVQQHDARCZXJuMRIw +EAYDVQQKDAlBR09WIFdvcmsxGTAXBgNVBAMMEGF0Yi13b3JrLWlkcC1rZXkwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2s6fPlpWv/1zEnail7TCUphEQ +A/dr/uY+qQqA/okB+Okd5hGDow7zBe/zICn7PJlGXzkq87o4Q3ZFvOFLqvlhwprp +OQquIviN6VBss2F3c174Zkk7ksciLQzPYjGBgw+l/ZeZY/AOYBeConsrHobTbjPd +StI8FZr8zVnamMWd/nBnryA5mZy9+vKz3iPJXPXZmyhBnOJfPZjMmkLvY9wEfGfc +rGrbqh6f7grleVNU16Rt46TtJRIqWEAdqi1I81d3kEWuqHkYCZf1ZJpDtprJPVko +fWViFzMz7zuAK5kdaGVwu0R7zeKz6FCHWWQ5bqScQbZ53zX6D3sP6ZNnZXdo6n0L +i+x17sgZa6VJtWF6s/UUxl8jPteprfRHrgIT3yKK9ewpXEhcc4aNJyCTiXpicOOn +QUBkkxyT7MtG1j51GPFcoFsBn4X9A1BXUmz2+YrDfFKtj0LwKZe6naI5v+FGtqeQ +/GeRpaFISwg/L5ewHe3NTH//8ZyWQsbJ2FEIff3LM+0+ivrORJs45GW12ny6MDY1 +Q8PTEsPL/9nhY1Mf99qpB9ivouVF/vGDWont16PhaZ2N31Osbbok3Emfbk0MVfvh +MuY0PPX/eWfn+5WlxBegS9PXbrcNW7MV0vsow8Js9+B29nao/VeFOQDfrU9p//xu +nDkeh9z5vqRP7clgMQIDAQABo1MwUTAdBgNVHQ4EFgQUqqmWA9MTwbzRFOfxZbu8 +nIyk4dEwHwYDVR0jBBgwFoAUqqmWA9MTwbzRFOfxZbu8nIyk4dEwDwYDVR0TAQH/ +BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAnh1nayZy7CjTDvXjht0jNEyCPahL +/gzcfx173FWnDbG3DMqjKB0u7bbpWIdStvTHpvs4NOg7H1/3Xc3cu3vtw6PF3Tkt +ZGJrMgZ5H9BUPW7BeNPqylh0Xj9vWUhxOdRfthzHcuSg2H6k5GBe+ROVIWLcc5g2 +vIuEEnpL9H5mlt4MofodPJjDrOvbJ5eDOGnNlcSKgPy8ZxrvyesmjFquu9/941p5 +wOpGhfVRH6U9GBIy1wWjjO4y2oRtgdgV0Dm57VNaxNi4R0cRW+eg7H7jED2gWVdS +Zftkrq44/lXFnWZDXWq8JJs0QPPD30i8fbGvZjRbrVQus5wW+dlirSkljQD8WpiY +N7PS2y+Io9WDetabxDSkHQGduldlHqnjvvR7TtLBT73fbmrra7nLrxbwAyQs/lp9 +r2904tzgBfhHb5GCrYE1s3h339eb/HXZlPqG1EcYimsAIyyBQ7WyHOgXq5RqwgbW +9O8aQUWPQrdtWrv8BkYSjjgDSxj9Pu7yBFnSdyI879uvBZDYovm/MmgcguAaJ8UC +PUcchbvgdLJHnbBA5aFm/Fkhb2WKi3Q0vExUHM3sXazJAAjIplbunHkqf8Wc7lva +94y3AXN9dg5LEjcwkjQbyGmmuSFq0Hse0b1KE+4INYUigECUcXuKYWrP0RuPzCKU +4g4p3ZpFGmoq4lM= +-----END CERTIFICATE----- diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/esauth4.xml b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/esauth4.xml index 423b6e5..7e482b0 100644 --- a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/esauth4.xml +++ b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/esauth4.xml @@ -134,6 +134,15 @@ + + + + + + + + + @@ -159,10 +168,13 @@ + + + @@ -1398,7 +1410,7 @@ - + @@ -1546,12 +1558,21 @@ - - - - - + + + + + + + + + + + + + + @@ -1690,6 +1711,22 @@ + + + + + + + + + + + + + + + + @@ -2231,15 +2268,6 @@ - - - - - - - - - @@ -3452,6 +3480,15 @@ + + + + + + + + + diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_agov_sec_authorization.groovy b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_agov_sec_authorization.groovy new file mode 100644 index 0000000..d89725f --- /dev/null +++ b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_agov_sec_authorization.groovy @@ -0,0 +1,179 @@ +boolean isEnabled() { + def paths = parameters.get("paths") + if (paths && !paths.isEmpty()) { + for (path in paths.split(',')) { + String url = request.currentResource + if (url.matches(path)) { + return true + } + } + } + return false +} + +boolean isLevel(String role) { + if (role != null && role.isNumber()) { + def number = Integer.parseInt(role) + if (number > 0 && number <= 9) { + return true + } + } + return false +} + +int getCurrentLevel() { + int level = 1 // level 1 is reached by definition on successful authentication + // levels are stored as roles once the authentication is done + for (String role : response.getActualRoles()) { + if (isLevel(role)) { + Integer number = Integer.parseInt(role) + if (number > level) { + level = number + } + } + } + LOG.debug("current level: $level") + return level +} + +Integer getRequestedLevel() { + // try to determine required level based on SAML request (SP-initiated) + def context = session['ch.nevis.auth.saml.request.authnContextClassRef'] + if (context == null) { + // this is expected for non-Nevis SAML partners + LOG.debug("unable to determine required authentication level: no AuthnContext") + return null + } + String prefix = 'urn:nevis:level:' + Integer level = null + if (context.contains(prefix)) { + def start = context.indexOf(prefix) // the prefix can appear anywhere in the context but only once + def remainder = context.substring(start + prefix.length()) + for (String candidate : remainder.split(',')) { + if (!candidate.isNumber()) { + continue // must be an actual role + } + def number = Integer.parseInt(candidate) + if (level == null || number < level) { + level = number + } + } + } + if (level == null) { + // an AuthnContext has been sent but it does not contain the required authentication level + LOG.debug("unable to determine required authentication level from request: $context") + } + else { + LOG.info("extracted required authentication level from request: $context -> $level") + } + return level +} + +Integer getRequiredLevel(levels, String issuer) { + // try to determine required level based on request + def level = getRequestedLevel() + if (level != null) { + LOG.info("required authentication level from request: $level") + return level + } + // else determine required level based on configuration (IDP-initiated or no authnContextClassRef sent) + if (issuer != null && levels.containsKey(issuer)) { + level = levels[issuer] + LOG.debug("required authentication level for issuer $issuer defined as $level") + return level + } + // else return null + LOG.debug("required authentication level for issuer $issuer is not defined") + return null +} + +void setAuthnContext() { + def parts = [] as Set + def authLevel = response.getAuthLevel() + if (authLevel != null) { + if (isLevel(authLevel)) { + parts.add("urn:nevis:level:$authLevel") + } + else { // might be legacy auth.weak / auth.strong + parts.add(authLevel) + } + } + for (String role : response.getActualRoles()) { + if (isLevel(role)) { // previous authLevels might have been added to the roles already + parts.add("urn:nevis:level:$role") + } + // levels can also be normal roles so we add them always + parts.add(role) + } + def value = parts.sort().join(",") + LOG.debug("calculated AuthnContextClassRef for SAML Response: $value") + session['saml.idp.response.authncontext'] = value +} + +boolean stepupRequired(levels, String issuer) { + + Integer requiredLevel = getRequiredLevel(levels, issuer) + if (requiredLevel == null) { + LOG.info("unable to determine required authentication level for request from issuer $issuer") + setAuthnContext() + return false + } + + Integer currentLevel = getCurrentLevel() + if (currentLevel >= requiredLevel) { + LOG.info("required authentication level $requiredLevel has been reached (current level $currentLevel)") + setAuthnContext() + return false + } + + LOG.info("required authentication level $requiredLevel has not been reached (current level $currentLevel) - session upgrade needed") + request.setRequiredRoles("$requiredLevel") + return true +} + +boolean hasAnyRequiredRole(i2r, issuer) { + if (issuer != null && i2r.containsKey(issuer)) { + def roles = i2r[issuer] + for (role in response.getActualRoles()) { + if (roles.contains(role)) { + return true + } + } + } +} + +if (!isEnabled()) { + LOG.info("skipping SAML authorization checks.") + response.setResult('ok') // skip execution + return +} + +// issuer set by IdentityProviderState (SP-initiated) +def issuer = session['ch.nevis.auth.saml.request.issuer'] + +// issuer to minimum required authentication level +def i2l = [:] + + +if (stepupRequired(i2l, issuer)) { + LOG.info("authentication level stepup required.") + response.setResult("stepup") + return // we are done for now +} + +// issuer to list of required roles +def i2r = [:] + + +// issuer to ResultCond name +def i2e = [:] +i2e.put('https://trustbroker.agov-d.azure.adnovum.net', 'forbidden_0') + + +if (!i2r.isEmpty() && !hasAnyRequiredRole(i2r, issuer)) { + LOG.info("required roles check failed.") + response.setResult(i2e[issuer]) + return // we are done +} + +response.setResult('ok') diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_agov_sec_dispatcher.groovy b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_agov_sec_dispatcher.groovy new file mode 100644 index 0000000..1aebdbb --- /dev/null +++ b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_agov_sec_dispatcher.groovy @@ -0,0 +1,174 @@ +import java.util.zip.Inflater +import java.util.zip.InflaterInputStream + +import groovy.xml.XmlSlurper +import groovy.xml.slurpersupport.GPathResult + +/** + * Gets the value of the Referer header. + * If the header is missing the fallback is returned. + * + * Do NOT remove this method. + * This method is used when SAML IDP / Dispatch Error Redirect is not set. + * A call to this method will be generated into this script (~line 157). + * + * @param fallback - value to return if the Referer header is missing + * @return value of header or fallback + */ +def getReferer(String fallback) { + return request.getHttpHeader('Referer') ?: fallback +} + +def redirect(String url) { + outargs.put('nevis.transfer.type', 'redirect') + outargs.put('nevis.transfer.destination', url) +} + +/** + * Extracts the content of the Issuer element from a parsed SAML message. + * The Issuer is optional according to SAML specification but we need it for dispatching. + * + * @param xml - as parsed by Groovy XmlSlurper + * @return text content of Issuer element converted or null + */ +static String getIssuer(GPathResult xml) { + return xml.depthFirst().find { GPathResult node -> { + node.name().endsWith(":Issuer") || node.name().equalsIgnoreCase("Issuer") + } + }?.text() +} + +String getIssuer(String value) { + if (value == null) { + return + } + String text + byte[] decoded + def parser = new XmlSlurper() + // if value is raw xml then continue otherwise try to parse the base64 encoding + if (value.startsWith("<")) { + text = new String(value) + } + else { + decoded = value.decodeBase64() + text = new String(decoded) + LOG.info("received SAML request $value") + } + + // after decoded, if redirect binding, we need to parse string to xml + if (text.startsWith("<")) { + LOG.debug("assuming POST/SOAP binding") + // plain String (POST/SOAP parameter) + def xml = parser.parseText(text) + return getIssuer(xml) + } + else { + LOG.debug("assuming redirect binding") + // should be deflate encoded (query parameter) + def is = new InflaterInputStream(new ByteArrayInputStream(decoded), new Inflater(true)) + def xml = parser.parse(is) + return getIssuer(xml) + } +} + +def dispatchIssuer(i2s, String issuer) { + def result = i2s.get(issuer) + if (result == null) { + throw new RuntimeException("No SP found for issuer '$issuer'. Hint: check SAML SP Connector patterns.") + } + response.setResult(result) + session.put("saml.inbound.issuer", issuer) + session.put('saml.idp.result', result) // remember decision for sub-sequent requests without a SAML message +} + +def dispatchMessage(i2s, String message) { + def issuer = getIssuer(message) + if (issuer == null) { + throw new RuntimeException("No issuer found in incoming SAML message. Giving up.") + } + session.put("saml.inbound.issuer", issuer) + dispatchIssuer(i2s, issuer) +} + +if (parameters.get('logoutConfirmation') == 'true' && "stepup" == request.getMethod()) { + String url = request.currentResource + def path = new URL(url).getPath() + if (path.endsWith("/logout")) { + // next AuthState will show a logout confirmation GUI + response.setResult('confirm') + return + } +} + +// ensure session exists +if (request.getSession(false) == null) { + session = request.getSession(true).getData() +} + +// issuer (any case) -> ResultCond name +def i2s = new TreeMap(String.CASE_INSENSITIVE_ORDER) + + +i2s.put('https://trustbroker.agov-d.azure.adnovum.net', 'state0') + +def spInitiatedAllowed = parameters.get('spInitiated') == 'true' +def idpInitiatedAllowed = parameters.get('idpInitiated') == 'true' + +try { + if (spInitiatedAllowed && inargs.containsKey('SAMLRequest')) { // SP-initiated authentication + LOG.debug("found SAMLRequest parameter for SP-initiated authentication") + String message = inargs.get('SAMLRequest') + dispatchMessage(i2s, message) + return + } + + if (inargs.containsKey('SAMLResponse')) { // response to IDP-initiated SAML Logout + LOG.debug("found SAMLResponse parameter") + String message = inargs.get('SAMLResponse') + dispatchMessage(i2s, message) + return + } + + if (spInitiatedAllowed && inargs.containsKey('soapheader')) { // SP-initiated SOAP with soapheader + LOG.debug("found soapheader parameter for SP-initiated") + String message = inargs.get('soapheader') + dispatchMessage(i2s, message) + return + } + + if (spInitiatedAllowed && inargs.containsKey('')) { // SP-initiated SOAP with empty + LOG.debug("found empty parameter for SP-initiated SOAP message") + String message = inargs.get('') + dispatchMessage(i2s, message) + return + } + + String issuer = inargs['Issuer'] ?: inargs['issuer'] + if (idpInitiatedAllowed && issuer != null) { // IDP-initiated authentication + LOG.debug("found Issuer parameter for IDP-initiated authentication") + dispatchIssuer(i2s, issuer) + return + } + + // used as fallback in case of ?logout (we need an IdentityProviderState) + if (inargs.containsKey("logout") && session.containsKey('saml.idp.result')) { + def result = session.get('saml.idp.result') + LOG.debug("dispatching to last used ResultCond: $result") + response.setResult(result) + return + } +} +catch (RuntimeException e) { + LOG.error("Error while dispatching SAML message: ${e.message}") +} + +def redirectEnabled = parameters.get('errorHandling') == 'redirect' +if (redirectEnabled) { + def location = getReferer('/') + LOG.info("Unable to dispatch request. Giving up and redirecting (back) to $location") + redirect(location) +} +else { + LOG.info("Unable to dispatch request. Giving up and showing error GUI.") + response.setResult('default') +} diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_logout_confirm.groovy b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_logout_confirm.groovy new file mode 100644 index 0000000..8f7202b --- /dev/null +++ b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/saml_idp_logout_confirm.groovy @@ -0,0 +1,64 @@ +def redirect(location) { + outargs.put('nevis.transfer.type', 'redirect') + outargs.put('nevis.transfer.destination', location) +} + +def getReturnURL() { + if (inargs.containsKey('return')) { + return inargs.get('return') + } + // determine returnURL based on Referer header (if present and not pointing to this page) + def referer = request.getHttpHeader('Referer') + if (referer == null) { + LOG.debug('no Referer header found') + return null + } + // strip query String for comparison + String previous = referer.contains('?') ? referer.substring(0, referer.indexOf("?")) : referer + def current = request.getCurrentResource() + if (current.startsWith(previous)) { + LOG.debug("Referer header $referer cannot be used as return URL - cyclic redirect") + return null + } + return referer +} + +if (inargs.containsKey('logout-confirm')) { + def current = request.getCurrentResource() + // user has confirmed logout -> replace /logout with /?logout + String location + if (current.contains('?')) { + location = current.replace("/logout?", "/?logout&") + } + else { + location = current.replace("/logout", "/?logout") + } + redirect(location) + return +} + +if (inargs.containsKey('logout-abort')) { + // user has aborted logout -> redirect to stored return URL + def location = session.get('logout-abort-url') + redirect(location) + return +} + +// user has not clicked any button -> render GUI +response.setGuiName('saml_logout_confirm') +response.setGuiLabel('title.logout.confirmation') +// not setting a target as the API has been removed +response.addInfoGuiField('info', 'info.logout.confirmation', null) +response.addButtonGuiField('logout-confirm', 'continue.button.label', 'true') + +def returnURL = getReturnURL() + +if (returnURL != null) { + // store return URL in session + session.put('logout-abort-url', returnURL) +} + +if (session.containsKey('logout-abort-url')) { + // add cancel button to go back + response.addButtonGuiField('logout-abort', 'cancel.button.label', 'true') +} \ No newline at end of file diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/proxy-idp/etc/nevis/k8s-nevisproxy-idp-0ceb05c56644a59d648c13b9.yaml b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/proxy-idp/etc/nevis/k8s-nevisproxy-idp-0ceb05c56644a59d648c13b9.yaml index f58ff0c..a896221 100644 --- a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/proxy-idp/etc/nevis/k8s-nevisproxy-idp-0ceb05c56644a59d648c13b9.yaml +++ b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/proxy-idp/etc/nevis/k8s-nevisproxy-idp-0ceb05c56644a59d648c13b9.yaml @@ -47,7 +47,7 @@ spec: podDisruptionBudget: maxUnavailable: "50%" git: - tag: "r-ca58de85fdf7a911b85ea6cd56b4c1a3d7f94fd6" + tag: "r-04ad6fd7455702c2a591f4a7b8d6c94222de911e" dir: "DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/proxy-idp" credentials: "git-credentials" database: diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/proxy-idp/var/opt/nevisproxy/default/host-auth.agov-w.azure.adnovum.net/WEB-INF/web.xml b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/proxy-idp/var/opt/nevisproxy/default/host-auth.agov-w.azure.adnovum.net/WEB-INF/web.xml index d0dae25..7df3f11 100644 --- a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/proxy-idp/var/opt/nevisproxy/default/host-auth.agov-w.azure.adnovum.net/WEB-INF/web.xml +++ b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/proxy-idp/var/opt/nevisproxy/default/host-auth.agov-w.azure.adnovum.net/WEB-INF/web.xml @@ -1112,6 +1112,11 @@ /pwreset/* + + SessionHandler_Auth_Realm_Main_IDP + /SAML2/SECSSO/* + + SessionHandler_Auth_Realm_Main_IDP /SAML2/SSO/* @@ -1203,6 +1208,11 @@ /pwreset/* + + AuthenticationService_Auth_Realm_Main_IDP + /SAML2/SECSSO/* + + AuthenticationService_Auth_Realm_Main_IDP /SAML2/SSO/* @@ -1635,10 +1645,10 @@ true - + Hosting_Default - + ch::nevis::isiweb4::servlet::defaults::DefaultServlet @@ -1754,6 +1764,11 @@ Hosting_Default /AUTH/RECOVERY/* + + + Hosting_Default + /SAML2/SECSSO/* + Hosting_Default