From 19e3dc9a2daa977731be056ca1b35e30ef024c6d Mon Sep 17 00:00:00 2001 From: haburger Date: Fri, 10 Jan 2025 15:16:34 +0000 Subject: [PATCH] new configuration version --- ...8s-nevisauth-7022472ae407577ae604bbb8.yaml | 2 +- .../conf/Recovery_getCredentials.groovy | 63 --------------- .../opt/nevisauth/default/conf/esauth4.xml | 40 ++++++---- .../default/conf/recovery-processing.groovy | 78 +++++++++++++++---- .../default/conf/recovery_fido2_auth.groovy | 60 ++++---------- 5 files changed, 105 insertions(+), 138 deletions(-) delete mode 100644 DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/Recovery_getCredentials.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 ba491a8..5e23020 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 @@ -45,7 +45,7 @@ spec: podDisruptionBudget: maxUnavailable: "50%" git: - tag: "r-5fa3629fafa6609b3d40f948feeec493651fe174" + tag: "r-9af4078cd1befd57d74704e77388710ec33873d4" dir: "DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth" credentials: "git-credentials" keystores: diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/Recovery_getCredentials.groovy b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/Recovery_getCredentials.groovy deleted file mode 100644 index 1e916ec..0000000 --- a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/Recovery_getCredentials.groovy +++ /dev/null @@ -1,63 +0,0 @@ -import ch.nevis.idm.client.IdmRestClient -import ch.nevis.idm.client.IdmRestClientFactory -import groovy.json.JsonSlurper -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.time.ZoneId -import ch.nevis.esauth.auth.engine.AuthResponse -import groovy.xml.XmlSlurper - -IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters) - -String baseUrl = parameters.get('baseUrl') -String clientExtId = session.get('ch.adnovum.nevisidm.user.clientExtId') -String userExtId = session.get('ch.adnovum.nevisidm.user.extId') -String endPoint = "$baseUrl/api/core/v1/$clientExtId/users/$userExtId/fido2" -String endPointFidoUAF = "$baseUrl/api/core/v1/$clientExtId/users/$userExtId/generic-credentials" - -def userDto = new XmlSlurper().parseText(session['ch.adnovum.nevisidm.userDto']) -def hasRecoveryRole = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'recovery' } -if (hasRecoveryRole != null) { - String result - try { - result = idmRestClient.get(endPoint) - resultFidoUAF = idmRestClient.get(endPointFidoUAF) - - def json = new JsonSlurper().parseText(result) -LOG.info('Result fido2: ' + json) - - def login=false - json['items'].each { - if ("active".equals(it.stateName)) { - response.setSessionAttribute('agov.recovery.securityKey', it.userFriendlyName) - response.setResult('loginWithFido2') - login=true - return - } - - } - if (login) { - return - } - def jsonFidoUAF = new JsonSlurper().parseText(resultFidoUAF) - LOG.info('Result fidoUAF: ' + jsonFidoUAF) - jsonFidoUAF['items'].each { - if ("active".equals(it.stateName)) { - response.setSessionAttribute('agov.recovery.accessapp', it.properties.fidouaf_name) - response.setSessionAttribute('agov.recovery.accessapp.dispatchTargetId', it.identification.replaceAll('dispatch_target_', '')) - response.setResult('loginWithFidoUAF') - login=true - return - } - } - if (login) { - return - } - } catch(Exception e) { - LOG.error(e.toString()) - response.setResult('failed') - return - } - -} -response.setResult('ok') \ No newline at end of file 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 c69dd44..cfd458a 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 @@ -119,7 +119,7 @@ - + @@ -2103,7 +2103,7 @@ - + @@ -2117,6 +2117,10 @@ + + + + @@ -2230,28 +2234,26 @@ - + - + - + - + - + - + - + - - - + @@ -2330,7 +2332,7 @@ - + @@ -2340,7 +2342,7 @@ - + @@ -2419,7 +2421,7 @@ - + @@ -2447,6 +2449,14 @@ + + + + + + + + diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/recovery-processing.groovy b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/recovery-processing.groovy index 3076e78..6dce281 100644 --- a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/recovery-processing.groovy +++ b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/recovery-processing.groovy @@ -1,5 +1,9 @@ -import org.codehaus.groovy.runtime.StackTraceUtils +import groovy.json.JsonSlurper import groovy.xml.XmlSlurper +import org.codehaus.groovy.runtime.StackTraceUtils + +import ch.nevis.idm.client.IdmRestClient +import ch.nevis.idm.client.IdmRestClientFactory // AGOVaq conversion @@ -77,6 +81,45 @@ def getUserMustRecoverValidFrom() { return (authzNode) ? ((authzNode.validFrom && !authzNode.validFrom.text().isEmpty()) ? authzNode.validFrom?.text() : authzNode.ctlCreDat?.text()) : '' } +def userHasNewLoginFactor() { + IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters) + + String baseUrl = parameters.get('baseUrl') + String clientExtId = session.get('ch.adnovum.nevisidm.user.clientExtId') + String userExtId = session.get('ch.adnovum.nevisidm.user.extId') + String baseEndPoint = "$baseUrl/api/core/v1/$clientExtId/users/$userExtId" + + def result = false + response.setSessionAttribute('agov.recovery.newLoginFactor', 'NONE') + + try { + def credInfoArray = new JsonSlurper().parseText(idmRestClient.get("$baseEndPoint/generic-credentials")) + + def accessApp = credInfoArray['items'].find( it -> it.stateName == "active") + if (accessApp) { + result = true; + response.setSessionAttribute('agov.recovery.accessapp', accessApp.properties.fidouaf_name) + response.setSessionAttribute('agov.recovery.accessapp.dispatchTargetId', accessApp.identification.replaceAll('dispatch_target_', '')) + response.setSessionAttribute('agov.recovery.newLoginFactor', 'ACCESS_APP') + return + } + + credInfoArray = new JsonSlurper().parseText(idmRestClient.get("$baseEndPoint/fido2")) + + def fido2Key = credInfoArray['items'].find( it -> it.stateName == "active") + if (fido2Key) { + result = true; + response.setSessionAttribute('agov.recovery.securityKey', fido2Key.userFriendlyName) + response.setSessionAttribute('agov.recovery.newLoginFactor', 'FIDO2') + return + } + + } catch(Exception e) { + LOG.error(e.toString()) + } + return result +} + // for autditing def user = session['ch.adnovum.nevisidm.user.extId'] ?: 'unknown' @@ -95,14 +138,14 @@ if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null LOG.debug("Recovery: Dto is '${userDto}") LOG.debug("Recovery: state is '${userState}") LOG.debug("Recovery: RecoveryCode is '${recoveryCode ? recoveryCode : 'none'}'") - def session = request.getAuthSession(true) if (userState == 'ACTIVE') { - session.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recovery') - session.setAttribute('agov.recovery.authenticatedWith', 'urn:qa.agov.ch:names:tc:authfactor:email') - session.setAttribute('agov.recovery.codeStatus', 'notNeeded') - session.setAttribute('agov.recovery.codeDetailStatus', 'n/a') + response.setSessionAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recovery') + response.setSessionAttribute('agov.recovery.authenticatedWith', 'urn:qa.agov.ch:names:tc:authfactor:email') + response.setSessionAttribute('agov.recovery.codeStatus', 'notNeeded') + response.setSessionAttribute('agov.recovery.codeDetailStatus', 'n/a') + response.setSessionAttribute('agov.recovery.newLoginFactor', 'NONE') def maxLoiList = userDto.'**'.findAll { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-Loi' }.collect({ node -> node.name.text() }) maxLoi = (maxLoiList == null || maxLoiList.isEmpty()) ? null : maxLoiList.sort().last() @@ -111,7 +154,7 @@ if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null def agovAqValidFrom = null if (maxLoi) { if (maxLoi != 'level100') { - session.setAttribute('agov.recovery.codeDetailStatus', '' + maxLoi) + response.setSessionAttribute('agov.recovery.codeDetailStatus', '' + maxLoi) } idVerification = userDto.'**'.find { node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + maxLoi}?.value?.text() @@ -124,11 +167,12 @@ if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null def hasRecoveryRole = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'recovery' } + def hasNewLoginFactor = hasRecoveryRole && userHasNewLoginFactor() if (mustRecover) { // attributes are defined over the mustRecover authorization - session.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:mustRecover') - session.setAttribute('agov.recovery.codeDetailStatus', 'mustRecover') + response.setSessionAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:mustRecover') + response.setSessionAttribute('agov.recovery.codeDetailStatus', 'mustRecover') idVerification = getUserIdVerificationForRecovery(maxLoi ?: 'level100') ?: idVerification @@ -142,6 +186,7 @@ if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null LOG.debug("Recovery: agovAqValidFrom is ${agovAqValidFrom}") LOG.debug("Recovery: mustRecover is '${mustRecover}'") LOG.debug("Recovery: hasRecoveryRole is '${hasRecoveryRole}'") + LOG.debug("Recovery: hasNewLoginFactor is '${hasNewLoginFactor}'") if (maxLoi != null) { if (maxLoiRoleToCtxClssConvertorMap.containsKey(maxLoi)) { @@ -150,7 +195,7 @@ if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null response.setSessionAttribute('agov.recovery.currentIdVerification', '' + idVerification) response.setSessionAttribute('agov.recovery.currentAgovAqRoleValidFrom', '' + agovAqValidFrom) - if ((maxLoi == 'level100') && (mustRecover == null)) { + if ((maxLoi == 'level100') && (mustRecover == null) && !hasNewLoginFactor) { // AQ100 accounts need to use the recovery code, if they can // check the status of recoveryCode credential if (recoveryCode && !blockingCredentialStates.contains(recoveryCode.state.text())) { @@ -158,13 +203,20 @@ if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null response.setResult('needCode') return } else { - LOG.warn("AGOVaq100 recovery: skipped Recovery-Code check '${recoveryCode ? recoveryCode.state.text() : 'MISSING'}'") - session.setAttribute('agov.recovery.codeStatus', 'skipped') - session.setAttribute('agov.recovery.codeDetailStatus', "unusable (state: ${recoveryCode ? recoveryCode.state.text() : 'MISSING'})") + LOG.warn("AGOVaq100 recovery: skipped Recovery-Code check '${recoveryCode ? recoveryCode.state.text() : 'MISSING'}'") + response.setSessionAttribute('agov.recovery.codeStatus', 'skipped') + response.setSessionAttribute('agov.recovery.codeDetailStatus', "unusable (state: ${recoveryCode ? recoveryCode.state.text() : 'MISSING'})") response.setResult('ok') return } + } else if ((maxLoi == 'level100') && hasNewLoginFactor) { + LOG.debug("Recovery: new Login Factor") + response.setSessionAttribute('agov.recovery.codeStatus', 'skipped') + response.setSessionAttribute('agov.recovery.codeDetailStatus', "new login factor already registered (${session['agov.recovery.newLoginFactor']})") + response.setResult('ok') + return + } else { LOG.debug("Recovery: email") response.setResult('ok') diff --git a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/recovery_fido2_auth.groovy b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/recovery_fido2_auth.groovy index 188ab89..5ed2b89 100644 --- a/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/recovery_fido2_auth.groovy +++ b/DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth/var/opt/nevisauth/default/conf/recovery_fido2_auth.groovy @@ -8,7 +8,6 @@ if (inargs.containsKey('cancel_fido2')) { def showGui() { response.setGuiName('recovery_fidokey_auth') // name is the trigger for including the JS - //response.setGuiName('fido2_auth') // name is the trigger for including the JS response.setGuiLabel('title.login.fido2') response.addInfoGuiField('info', 'info.login.fido2', null) response.addHiddenGuiField('authRequestId', 'not used', session['ch.nevis.auth.saml.request.id']) @@ -18,7 +17,6 @@ def showGui() { response.addErrorGuiField('lasterror', notes['lasterrorinfo'], notes['lasterror']) } if (parameters.containsKey('cancel')) { - // TODO koenig 20221021: replace with specific label response.addButtonGuiField('cancel_fido2', 'cancel.login.fido2.button.label', 'true') } } @@ -42,12 +40,14 @@ def post(connection, json) { connection.getOutputStream().write(body.getBytes()) } -String userExtId = session['ch.adnovum.nevisidm.user.extId'] ?: session['ch.nevis.idm.User.extId'] ?: request.getUserId() ?: notes['userid'] +String userExtId = session['ch.adnovum.nevisidm.user.extId'] ?: session['ch.nevis.idm.User.extId'] if (userExtId == null) { LOG.error("missing extId of nevisIDM user. check your authentication flow.") + notes.setProperty('lasterror', '1') + notes.setProperty('lasterrorinfo', 'missing extId of nevisIDM user') + response.setResult('error') + return } -// without the user extId this script won't work and we can fail with a System Error -Objects.requireNonNull(userExtId) def path = getPath() if (path == null) { @@ -65,32 +65,17 @@ if (path == '/nevisfido/fido2/attestation/options') { } post(connection, json) def responseCode = connection.responseCode -// account without FIDO2 case + if (responseCode == 400) { - def responseText = '''{"status": "ok", - "errorMessage": "", - "fido2SessionId": "270312ae-8d74-4ded-ad89-5310da2d2e6f", - "challenge": "tKCqUM6URnykri1ZFz-3ww", - "timeout": 300000, - "rpId": "agov-d.azure.adnovum.net", - "allowCredentials": [ - { - "type": "public-key", - "id": "WVzzUwxOf-1doTGkrdRHWPDbETTawkULLPsEiwiQwA2AFC4_YgL5OVmJJOT2OulAZSq_tvOfNlMSRKRXyXH2kw", - "transports": [] - } - ], - "userVerification": "preferred"}''' - LOG.info("<== Response: ${responseCode}") - response.setContent(responseText) // return response from nevisFIDO "as-is" - response.setContentType('application/json') - response.setHttpStatusCode(200) - response.setIsDirectResponse(true) - return + LOG.error("FIDO2 options call failed for '${userExtId}'") + notes.setProperty('lasterror', '1') + notes.setProperty('lasterrorinfo', 'missing extId of nevisIDM user') + response.setResult('error') + return } def responseText = connection.inputStream.text - LOG.info("<== Response: ${responseCode} : ${responseText}") + LOG.debug("<== Response: ${responseCode} : ${responseText}") response.setContent(responseText) // return response from nevisFIDO "as-is" response.setContentType('application/json') response.setHttpStatusCode(200) @@ -100,21 +85,6 @@ if (path == '/nevisfido/fido2/attestation/options') { if (path == '/nevisfido/fido2/assertion/result') { - if (inargs.containsKey('authRequestId') && (inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'])) { - // wrong request, "force" a timeout - LOG.info('authentication timeout enforced, due to concurrent requests') - - response.setIsDirectResponse(true) - response.setContentType('text/html; charset=UTF-8') - response.setContent('Timeout') - response.setHttpStatusCode(205) - response.setHeader('IDP-AUTH', 'Timeout') - - // CONTINUE to keep the other request beeing processed - response.setStatus(AuthResponse.AUTH_CONTINUE) - return - } - def userHandleValue = userExtId.getBytes().encodeBase64Url().toString() LOG.info("encoded userHandle: ${userHandleValue}") json { @@ -132,15 +102,13 @@ if (path == '/nevisfido/fido2/assertion/result') { // test if credentials exist if (responseCode != 400) { def responseText = connection.inputStream.text - LOG.info("<== Response: ${responseCode} : ${responseText}") + LOG.debug("<== Response: ${responseCode} : ${responseText}") if (responseCode == 200 && new JsonSlurper().parseText(responseText).status == 'ok') { + response.setSessionAttribute('agov.recovery.authenticatedWith', 'urn:qa.agov.ch:names:tc:authfactor:fido') response.setResult('ok') return } } - //response.setHttpStatusCode(400) - //response.setIsDirectResponse(true) - // DEFINE how to handel error notes.setProperty('lasterror', '1') notes.setProperty('lasterrorinfo', 'FIDO2 authentication failed') response.setResult('error')