new configuration version

This commit is contained in:
haburger 2025-01-10 15:16:34 +00:00
parent d17d6873ba
commit 19e3dc9a2d
5 changed files with 105 additions and 138 deletions

View File

@ -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:

View File

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

View File

@ -119,7 +119,7 @@
<KeyObject name="FIDO_UAF_Truststore" certificate="/var/opt/keys/trust/env-ca/truststore.jks"/>
</KeyStore>
<!-- source: pattern://b199c1561394770481c01e23 -->
<KeyStore name="Recovery_getCredentials">
<KeyStore name="Recovery_Auth">
<!-- source: pattern://b199c1561394770481c01e23 -->
<KeyObject name="internal_tls_Truststore" certificate="/var/opt/keys/trust/env-ca/truststore.jks"/>
</KeyStore>
@ -2103,7 +2103,7 @@
<!-- source: pattern://584964c837512845d7940809 -->
<ResultCond name="notFullyRegistered" next="Auth_Realm_Recovery_Recovery_sendEmail031b"/>
<!-- source: pattern://584964c837512845d7940809 -->
<ResultCond name="ok" next="Auth_Realm_Recovery_Recovery_getCredentials"/>
<ResultCond name="ok" next="Auth_Realm_Recovery_Recovery_authWithNewCredentials"/>
<!-- source: pattern://584964c837512845d7940809 -->
<Response value="AUTH_CONTINUE">
<!-- source: pattern://584964c837512845d7940809 -->
@ -2117,6 +2117,10 @@
</Gui>
</Response>
<!-- source: pattern://584964c837512845d7940809 -->
<property name="parameter.baseUrl" value="https://idm:8989/nevisidm"/>
<!-- source: pattern://584964c837512845d7940809 -->
<property name="parameter.idm.httpclient.tls.trustStoreRef" value="Recovery_Auth"/>
<!-- source: pattern://584964c837512845d7940809 -->
<property name="scriptTraceGroup" value="Recovery"/>
<!-- source: pattern://584964c837512845d7940809 -->
<property name="script" value="file:///var/opt/nevisauth/default/conf/recovery-processing.groovy"/>
@ -2230,28 +2234,26 @@
<!-- source: pattern://584964c837512845d7940809 -->
<property name="client.name" value="agov"/>
</AuthState>
<AuthState name="Auth_Realm_Recovery_Recovery_getCredentials" class="ch.nevis.esauth.auth.states.scripting.ScriptState" final="false" resumeState="true">
<AuthState name="Auth_Realm_Recovery_Recovery_authWithNewCredentials" class="ch.nevis.esauth.auth.states.standard.ConditionalDispatcherState" final="false" resumeState="false">
<!-- source: pattern://c1c0941f54cc36340578ff5f -->
<ResultCond name="error" next="Auth_Realm_Recovery_Auth_Failed"/>
<ResultCond name="default" next="Auth_Realm_Recovery_Auth_Failed"/>
<!-- source: pattern://c1c0941f54cc36340578ff5f -->
<ResultCond name="loginWithFido2" next="Auth_Realm_Recovery_Recovery_fido2Login"/>
<!-- source: pattern://c1c0941f54cc36340578ff5f -->
<ResultCond name="loginWithFidoUAF" next="Auth_Realm_Recovery_Recovery_mobile_nless_auth"/>
<!-- source: pattern://c1c0941f54cc36340578ff5f -->
<ResultCond name="ok" next="Auth_Realm_Recovery_Recovery_redirectAgovMe"/>
<ResultCond name="notNeeded" next="Auth_Realm_Recovery_Recovery_redirectAgovMe"/>
<!-- source: pattern://c1c0941f54cc36340578ff5f -->
<Response value="AUTH_CONTINUE">
<Response value="AUTH_ERROR">
<!-- source: pattern://c1c0941f54cc36340578ff5f -->
<Gui name="noGui"/>
<Gui name="AuthErrorDialog"/>
</Response>
<!-- source: pattern://c1c0941f54cc36340578ff5f -->
<property name="parameter.baseUrl" value="https://idm:8989/nevisidm"/>
<property name="condition:loginWithFido2" value="${sess:agov.recovery.newLoginFactor}==FIDO2"/>
<!-- source: pattern://c1c0941f54cc36340578ff5f -->
<property name="parameter.idm.httpclient.tls.trustStoreRef" value="Recovery_getCredentials"/>
<property name="condition:loginWithFidoUAF" value="${sess:agov.recovery.newLoginFactor}==ACCESS_APP"/>
<!-- source: pattern://c1c0941f54cc36340578ff5f -->
<property name="scriptTraceGroup" value="Recovery"/>
<!-- source: pattern://c1c0941f54cc36340578ff5f -->
<property name="script" value="file:///var/opt/nevisauth/default/conf/Recovery_getCredentials.groovy"/>
<property name="condition:notNeeded" value="${sess:agov.recovery.newLoginFactor}==NONE"/>
</AuthState>
<AuthState name="Auth_Realm_Recovery_Recovery_createURLTicket_logReason" class="ch.nevis.esauth.auth.states.scripting.ScriptState" final="false" resumeState="false">
<!-- source: pattern://9a1d3c6052019748d3510261 -->
@ -2330,7 +2332,7 @@
</AuthState>
<AuthState name="Auth_Realm_Recovery_Recovery_Auth_codeSkipped" class="ch.nevis.esauth.auth.states.standard.TransformAttributes" final="false" resumeState="false">
<!-- source: pattern://584964c837512845d7940809 -->
<ResultCond name="default" next="Auth_Realm_Recovery_Recovery_getCredentials"/>
<ResultCond name="default" next="Auth_Realm_Recovery_Recovery_authWithNewCredentials"/>
<!-- source: pattern://584964c837512845d7940809 -->
<Response value="AUTH_CONTINUE"/>
<!-- source: pattern://584964c837512845d7940809 -->
@ -2340,7 +2342,7 @@
</AuthState>
<AuthState name="Auth_Realm_Recovery_Recovery_Auth_codeVerified" class="ch.nevis.esauth.auth.states.standard.TransformAttributes" final="false" resumeState="false">
<!-- source: pattern://584964c837512845d7940809 -->
<ResultCond name="default" next="Auth_Realm_Recovery_Recovery_getCredentials"/>
<ResultCond name="default" next="Auth_Realm_Recovery_Recovery_authWithNewCredentials"/>
<!-- source: pattern://584964c837512845d7940809 -->
<Response value="AUTH_CONTINUE"/>
<!-- source: pattern://584964c837512845d7940809 -->
@ -2419,7 +2421,7 @@
<!-- source: pattern://4bc453bf68139ee87966b0c7 -->
<ResultCond name="failed" next="Auth_Realm_Recovery_Recovery_mobile_nless_auth"/>
<!-- source: pattern://4bc453bf68139ee87966b0c7 -->
<ResultCond name="ok" next="Auth_Realm_Recovery_Recovery_redirectAgovMe" authLevel="2"/>
<ResultCond name="ok" next="Auth_Realm_Recovery_Recovery_mobile_nless_auth_PostProcessing"/>
<!-- source: pattern://4bc453bf68139ee87966b0c7 -->
<Response value="AUTH_ERROR">
<!-- source: pattern://4bc453bf68139ee87966b0c7 -->
@ -2447,6 +2449,14 @@
<!-- source: pattern://6061abea33a234fad73897b7, pattern://204c22beaccdfd22727af378 -->
<property name="script" value="file:///var/opt/nevisauth/default/conf/prepare_done.groovy"/>
</AuthState>
<AuthState name="Auth_Realm_Recovery_Recovery_mobile_nless_auth_PostProcessing" class="ch.nevis.esauth.auth.states.standard.TransformAttributes" final="false" resumeState="false">
<!-- source: pattern://4bc453bf68139ee87966b0c7 -->
<ResultCond name="default" next="Auth_Realm_Recovery_Recovery_redirectAgovMe"/>
<!-- source: pattern://4bc453bf68139ee87966b0c7 -->
<Response value="AUTH_CONTINUE"/>
<!-- source: pattern://4bc453bf68139ee87966b0c7 -->
<property name="sess:agov.recovery.authenticatedWith" value="urn:qa.agov.ch:names:tc:authfactor:accessapp"/>
</AuthState>
<AuthState name="Auth_Realm_Recovery_Auth_Done" class="ch.nevis.esauth.auth.states.standard.AuthDone" final="false">
<!-- source: pattern://6061abea33a234fad73897b7, pattern://204c22beaccdfd22727af378 -->
<Response value="AUTH_DONE">

View File

@ -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())) {
@ -159,12 +204,19 @@ if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null
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'})")
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')

View File

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