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')