diff --git a/patterns/4bc453bf68139ee87966b0c7_authStatesFile/Recovery_Mobile_NLess_Auth.xml b/patterns/4bc453bf68139ee87966b0c7_authStatesFile/Recovery_Mobile_NLess_Auth.xml index b8d8451..4f79102 100644 --- a/patterns/4bc453bf68139ee87966b0c7_authStatesFile/Recovery_Mobile_NLess_Auth.xml +++ b/patterns/4bc453bf68139ee87966b0c7_authStatesFile/Recovery_Mobile_NLess_Auth.xml @@ -17,11 +17,21 @@ - + + + + + + + + + + + diff --git a/patterns/54c1b68431bc2e03b61edcaa_authStatesFile/Recovery_fido2Login.xml b/patterns/54c1b68431bc2e03b61edcaa_authStatesFile/Recovery_fido2Login.xml index 107cd8c..fde4bd1 100644 --- a/patterns/54c1b68431bc2e03b61edcaa_authStatesFile/Recovery_fido2Login.xml +++ b/patterns/54c1b68431bc2e03b61edcaa_authStatesFile/Recovery_fido2Login.xml @@ -4,11 +4,10 @@ - - + - + diff --git a/patterns/54c1b68431bc2e03b61edcaa_resources/recovery_fido2_auth.groovy b/patterns/54c1b68431bc2e03b61edcaa_resources/recovery_fido2_auth.groovy index 188ab89..5ed2b89 100644 --- a/patterns/54c1b68431bc2e03b61edcaa_resources/recovery_fido2_auth.groovy +++ b/patterns/54c1b68431bc2e03b61edcaa_resources/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') diff --git a/patterns/584964c837512845d7940809_authStatesFile/recovery-preprocessing.xml b/patterns/584964c837512845d7940809_authStatesFile/recovery-preprocessing.xml index d3f8ef6..4688b0c 100644 --- a/patterns/584964c837512845d7940809_authStatesFile/recovery-preprocessing.xml +++ b/patterns/584964c837512845d7940809_authStatesFile/recovery-preprocessing.xml @@ -197,6 +197,8 @@ + + @@ -232,6 +234,7 @@ + diff --git a/patterns/584964c837512845d7940809_resources/recovery-processing.groovy b/patterns/584964c837512845d7940809_resources/recovery-processing.groovy index c485147..7db532f 100644 --- a/patterns/584964c837512845d7940809_resources/recovery-processing.groovy +++ b/patterns/584964c837512845d7940809_resources/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,42 @@ 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" + + 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) { + 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 true + } + + credInfoArray = new JsonSlurper().parseText(idmRestClient.get("$baseEndPoint/fido2")) + + def fido2Key = credInfoArray['items'].find( it -> it.stateName == "active") + if (fido2Key) { + response.setSessionAttribute('agov.recovery.securityKey', fido2Key.userFriendlyName) + response.setSessionAttribute('agov.recovery.newLoginFactor', 'FIDO2') + return true + } + + } catch(Exception e) { + LOG.error(e.toString()) + } + return false +} + // for autditing def user = session['ch.adnovum.nevisidm.user.extId'] ?: 'unknown' @@ -95,14 +135,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 +151,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 +164,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 +183,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 +192,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 +200,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/patterns/Recovery_Auth_584964c837512845d7940809.yml b/patterns/Recovery_Auth_584964c837512845d7940809.yml index eae4fab..322c2cb 100644 --- a/patterns/Recovery_Auth_584964c837512845d7940809.yml +++ b/patterns/Recovery_Auth_584964c837512845d7940809.yml @@ -17,3 +17,5 @@ pattern: - "pattern://717094cbd4ddbadeab4b2cc1" - "pattern://ae023be7e097522c74e31d17" resources: "res://584964c837512845d7940809#resources" + keyObjects: + - "pattern://b199c1561394770481c01e23" diff --git a/patterns/Recovery_getCredentials_c1c0941f54cc36340578ff5f.yml b/patterns/Recovery_authWithNewCredentials_c1c0941f54cc36340578ff5f.yml similarity index 68% rename from patterns/Recovery_getCredentials_c1c0941f54cc36340578ff5f.yml rename to patterns/Recovery_authWithNewCredentials_c1c0941f54cc36340578ff5f.yml index 7026d27..be931eb 100644 --- a/patterns/Recovery_getCredentials_c1c0941f54cc36340578ff5f.yml +++ b/patterns/Recovery_authWithNewCredentials_c1c0941f54cc36340578ff5f.yml @@ -2,10 +2,9 @@ schemaVersion: "1.0" pattern: id: "c1c0941f54cc36340578ff5f" className: "ch.nevis.admin.v4.plugin.nevisauth.patterns2.GenericAuthenticationStep" - name: "Recovery_getCredentials" + name: "Recovery_authWithNewCredentials" properties: authStatesFile: "res://c1c0941f54cc36340578ff5f#authStatesFile" - parameters: "url: \"https://idm:8989/nevisidm\"" onSuccess: - "pattern://6061abea33a234fad73897b7" onFailure: @@ -13,6 +12,3 @@ pattern: nextSteps: - "pattern://54c1b68431bc2e03b61edcaa" - "pattern://4bc453bf68139ee87966b0c7" - resources: "res://c1c0941f54cc36340578ff5f#resources" - keyObjects: - - "pattern://b199c1561394770481c01e23" diff --git a/patterns/c1c0941f54cc36340578ff5f_authStatesFile/Recovery_authWithNewCredentials.xml b/patterns/c1c0941f54cc36340578ff5f_authStatesFile/Recovery_authWithNewCredentials.xml new file mode 100644 index 0000000..3ade8f5 --- /dev/null +++ b/patterns/c1c0941f54cc36340578ff5f_authStatesFile/Recovery_authWithNewCredentials.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/patterns/c1c0941f54cc36340578ff5f_authStatesFile/Recovery_getCredentials.xml b/patterns/c1c0941f54cc36340578ff5f_authStatesFile/Recovery_getCredentials.xml deleted file mode 100644 index 9bfa0d1..0000000 --- a/patterns/c1c0941f54cc36340578ff5f_authStatesFile/Recovery_getCredentials.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/patterns/c1c0941f54cc36340578ff5f_resources/Recovery_getCredentials.groovy b/patterns/c1c0941f54cc36340578ff5f_resources/Recovery_getCredentials.groovy deleted file mode 100644 index f889eee..0000000 --- a/patterns/c1c0941f54cc36340578ff5f_resources/Recovery_getCredentials.groovy +++ /dev/null @@ -1,62 +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.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/variables.yml b/variables.yml index 48e3de7..557e608 100644 --- a/variables.yml +++ b/variables.yml @@ -192,7 +192,7 @@ variables: parameters: required: false syntax: "YAML" - value: "client.name: agov\nattributes: loginId,extId,firstName,name,email,mobile\n\ + value: "url: \"https://idm:8989/nevisidm\"\nclient.name: agov\nattributes: loginId,extId,firstName,name,email,mobile\n\ properties: eIdNumber,gender,placeOfBirth,svnr\nidm-service: idm\nagov.unitExtId:\ \ 1000\nagov.level100.roleExtid: aee52e9f-7084-4e55-9aea-9383ac7757f7" requireOverloading: true