diff --git a/patterns/1f0702aaabef60a615abf41f_resources/resources.zip b/patterns/1f0702aaabef60a615abf41f_resources/resources.zip index 493ab27..e15d0fc 100644 Binary files a/patterns/1f0702aaabef60a615abf41f_resources/resources.zip and b/patterns/1f0702aaabef60a615abf41f_resources/resources.zip differ diff --git a/patterns/204c22beaccdfd22727af378_labels/labels.zip b/patterns/204c22beaccdfd22727af378_labels/labels.zip index 94422af..e3328b9 100644 Binary files a/patterns/204c22beaccdfd22727af378_labels/labels.zip and b/patterns/204c22beaccdfd22727af378_labels/labels.zip differ diff --git a/patterns/204c22beaccdfd22727af378_template/webdata.zip b/patterns/204c22beaccdfd22727af378_template/webdata.zip index 75523c0..9c3cd16 100644 Binary files a/patterns/204c22beaccdfd22727af378_template/webdata.zip and b/patterns/204c22beaccdfd22727af378_template/webdata.zip differ diff --git a/patterns/4fcfadb4a5c946ead7e6e995_labels/labels.zip b/patterns/4fcfadb4a5c946ead7e6e995_labels/labels.zip index 94422af..e3328b9 100644 Binary files a/patterns/4fcfadb4a5c946ead7e6e995_labels/labels.zip and b/patterns/4fcfadb4a5c946ead7e6e995_labels/labels.zip differ diff --git a/patterns/4fcfadb4a5c946ead7e6e995_template/webdata.zip b/patterns/4fcfadb4a5c946ead7e6e995_template/webdata.zip index 75523c0..9c3cd16 100644 Binary files a/patterns/4fcfadb4a5c946ead7e6e995_template/webdata.zip and b/patterns/4fcfadb4a5c946ead7e6e995_template/webdata.zip differ diff --git a/patterns/68665057549fd887ea09fb86_scriptFile/requestedRoleLevel.groovy b/patterns/68665057549fd887ea09fb86_scriptFile/requestedRoleLevel.groovy index 56080c5..bc75ffc 100644 --- a/patterns/68665057549fd887ea09fb86_scriptFile/requestedRoleLevel.groovy +++ b/patterns/68665057549fd887ea09fb86_scriptFile/requestedRoleLevel.groovy @@ -26,7 +26,7 @@ int getRequestedLevel(String authnContextClassRef, def roleList){ def session = request.getAuthSession(true) def context = session.get('ch.nevis.auth.saml.request.authnContextClassRef') -def roleLevels = [100,200,300,400] +def roleLevels = [100,200,300,400,500,600] def requestedRoleLevelNumber = getRequestedLevel(context, roleLevels) //set attribute Requested Role Level @@ -58,6 +58,13 @@ if (requestedRoleLevelNumber == 0 || session.get('ch.nevis.auth.saml.request.sco return } +// TODO/haburger/2024-03-21: move this later, now here for a simple start +if (requestedRoleLevelNumber == 600 || session.get('ch.nevis.auth.saml.request.scoping.requesterId') == 'OidcPlaygroundWork') { + session.setAttribute('agov.appSvnrAllowed', 'true') + response.setResult('exit.1'); + return +} + try { def spanCtxt = Span.current().getSpanContext() def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}" diff --git a/patterns/7441fca76f479e4beb5ca796_authStatesFile/EId_Verification_Auth.xml b/patterns/7441fca76f479e4beb5ca796_authStatesFile/EId_Verification_Auth.xml new file mode 100644 index 0000000..c94f25e --- /dev/null +++ b/patterns/7441fca76f479e4beb5ca796_authStatesFile/EId_Verification_Auth.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/patterns/7441fca76f479e4beb5ca796_resources/eid_verification_auth.groovy b/patterns/7441fca76f479e4beb5ca796_resources/eid_verification_auth.groovy new file mode 100644 index 0000000..ed3ec94 --- /dev/null +++ b/patterns/7441fca76f479e4beb5ca796_resources/eid_verification_auth.groovy @@ -0,0 +1,327 @@ +import ch.nevis.esauth.auth.engine.AuthResponse +import ch.nevis.esauth.util.httpclient.api.HttpClient +import groovy.json.JsonSlurper +import io.opentelemetry.api.trace.Span + +def getHeader(String name) { + def inctx = request.getLoginContext() + // case-insensitive lookup of HTTP headers + def map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER) + map.putAll(inctx) + return map['connection.HttpHeader.' + name] +} + +def verification_request_template = ''' +{ "presentation_definition": { + "id": "{{UUID}}", + "name": "AGOV Verification", + "purpose": "AGOV Login", + "format": { + "vc+sd-jwt": { + "sd-jwt_alg_values": [ + "ES256" + ], + "kb-jwt_alg_values": [ + "ES256" + ] + } + }, + "input_descriptors": [ + { + "id": "agov-all-attributes", + "name": "AGOV Identity Verification", + "purpose": "verification and authentication", + "format": { + "vc+sd-jwt": { + "sd-jwt_alg_values": [ + "ES256" + ], + "kb-jwt_alg_values": [ + "ES256" + ] + } + }, + "constraints": { + "fields": [ + { + "path": [ + "$.family_name" + ] + }, + { + "path": [ + "$.given_name" + ] + }, + { + "path": [ + "$.birth_date" + ] + }, + { + "path": [ + "$.sex" + ] + }, + { + "path": [ + "$.place_of_origin" + ] + }, + { + "path": [ + "$.birth_place" + ] + }, + { + "path": [ + "$.nationality" + ] + }, + { + "path": [ + "$.personal_administrative_number" + ] + }, + { + "path": [ + "$.document_number" + ] + }, + { + "path": [ + "$.issuance_date" + ] + }, + { + "path": [ + "$.expiry_date" + ] + }, + { + "path": [ + "$.issuing_authority" + ] + }, + { + "path": [ + "$.issuing_country" + ] + } + ] + } + } + ] + } +} +''' + +def ERROR_CODE_TO_STATUS_MAPPER = [ + 'CREDENTIAL_INVALID' : 'FAILED', + 'JWT_EXPIRED' : 'ERROR', + 'INVALID_FORMAT' : 'ERROR', + 'CREDENTIAL_EXPIRED' : 'FAILED', + 'MISSING_NONCE' : 'ERROR', + 'UNSUPPORTED_FORMAT' : 'ERROR', + 'CREDENTIAL_REVOKED' : 'FAILED', + 'CREDENTIAL_SUSPENDED' : 'FAILED', + 'HOLDER_BINDING_MISMATCH' : 'ERROR', + 'CREDENTIAL_MISSING_DATA' : 'FAILED', + 'UNRESOLVABLE_STATUS_LIST' : 'ERROR', + 'PUBLIC_KEY_OF_ISSUER_UNRESOLVABLE': 'ERROR', + 'CLIENT_REJECTED' : 'CANCELED', + 'ISSUER_NOT_ACCEPTED' : 'ERROR' +] + +// --------------- +// check, whether we are still processing the correct AuthnRequest +if (inargs.containsKey('authRequestId') && (inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'])) { + // wrong request, "force" a timeout + LOG.debug('authentication timeout enforced, due to concurrent requests -> return a 408') + + 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 +} + +if (inargs['oid4vp'] == 'ERROR') { + response.setResult('error') + return +} + +if (inargs['oid4vp'] == 'SUCCEEDED') { + response.setResult('ok') + return +} + + +def sess = request.getAuthSession(true) + +HttpClient httpClient = HttpClients.create(parameters) +def spanCtxt = Span.current().getSpanContext() +def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}" + +if (!session['agov.eid.verification']) { + // Initialize the verification session on the verifier + def endPoint = "${parameters.get('eidVerifierBaseUrl')}/api/v1/verifications" + + try { + def httpResponse = Http.post() + .url(endPoint) + .header("Accept", "application/json") + .header("traceparent", traceparent) + .entity(Http.entity() + .content(verification_request_template.replaceAll("\\{\\{UUID}}", UUID.randomUUID().toString())) + .contentType("application/json") + .build()) + .build() + .send(httpClient) + + + if (httpResponse.code() != 200) { + LOG.debug("Result: ${httpResponse}") + response.setResult('error') + return + } + + def json = new JsonSlurper().parseText(httpResponse.bodyAsString()) + LOG.debug("Result: ${json}") + + sess.setAttribute('agov.eid.verification', 'true') + sess.setAttribute('agov.eid.verification.id', json.id) + sess.setAttribute('agov.eid.verification.link', json.verification_url) + + if (json.state != 'PENDING') { + response.setResult('error') + return + } + } + catch (Exception e) { + LOG.error("Eid verification failed: $e") + response.setResult('error') + return + } +} + +if (getHeader('Content-Type') == 'application/json' && inargs.containsKey('o.id.v')) { + // request for a status update from the verifier + def result + + // TODO/haburger/2025-03-24: we should make sure, that we have an actual session on the verifier with id.v + // and that authRequestId is correct + def idvalue = (!inargs['o.id.v'] || inargs['o.id.v'] == 'NEW') ? session['agov.eid.verification.id'] : inargs['o.id.v'] + + try { + def endPoint = "${parameters.get('eidVerifierBaseUrl')}/api/v1/verifications/${idvalue}" + + def httpResponse = Http.get() + .url(endPoint) + .header("Accept", "application/json") + .header("traceparent", traceparent) + .build() + .send(httpClient) + + if (httpResponse.code() != 200) { + // TODO/haburger/2025-03-25: 404 we should create a new verification request + LOG.debug("Result: ${httpResponse}") + result = """{ + "oid4vp": { + "status": "ERROR", + "verification_url": "${session['agov.eid.verification.link']}", + "id": "${idvalue}", + "error_code": "HTTP-ERROR", + "error_message": "failed to verify status of verification ${idvalue}, http status: ${httpResponse.code()}" + }}""" + LOG.warn("<== Response: ${responseCode}") + } + else { + + def json = new JsonSlurper().parseText(httpResponse.bodyAsString()) + + if (json.state == 'SUCCESS') { + def claims = json.wallet_response.credential_subject_data + + // TODO/haburger/2025-03-25: format changes to align with IDM read data + sess.setAttribute('ch.nevis.idm.User.firstName', claims.given_name) + sess.setAttribute('ch.nevis.idm.User.lastName', claims.family_name) + sess.setAttribute('ch.nevis.idm.User.birthDate', claims.birth_date) + sess.setAttribute('ch.nevis.idm.User.gender', claims.sex) + sess.setAttribute('ch.nevis.idm.User.prop.svnr', claims.personal_administrative_number) + sess.setAttribute('ch.nevis.idm.User.prop.placeOfBirth', claims.birth_place) + sess.setAttribute('ch.nevis.idm.User.prop.eIdNumber', claims.personal_administrative_number) + sess.setAttribute('ch.nevis.idm.User.prop.nationality', claims.nationality.toString()) + sess.setAttribute('ValidFrom', claims.issuance_date) + sess.setAttribute('ValidTo', claims.expiry_date) + sess.setAttribute('authenticatedWith', "urn:qa.agov.ch:names:tc:authfactor:eid") + sess.setAttribute('idVerification', "Eid") + sess.setAttribute('contextClassRefToSet', "urn:qa.agov.ch:names:tc:ac:classes:600") + + response.setUserId(claims.personal_administrative_number) + response.setLoginId(claims.document_number) + response.setAuthLevel("EID") + + result = """{ + "oid4vp": { + "status": "SUCCEEDED", + "verification_url": "${session['agov.eid.verification.link']}", + "id": "${idvalue}", + "error_code": "NONE" + }}""" + } + else if (json.state == 'FAILED') { + // TODO/haburger/2025-03-25: ERROR_CODE_TO_STATUS_MAPPER[json.wallet_response.error_code] == 'FAILED' we should + // initiate a new verification and return the new id, url together with the message + + LOG + .error("Eid verification failed: ${json.wallet_response.error_code} (${json.wallet_response.error_description})") + result = """{ + "oid4vp": { + "status": "${ERROR_CODE_TO_STATUS_MAPPER[json.wallet_response.error_code] ?: 'ERROR'}", + "verification_url": "${session['agov.eid.verification.link']}", + "id": "${idvalue}", + "error_code": "${json.wallet_response.error_code}", + "error_message": "${json.wallet_response.error_description}" + }}""" + } + else { + result = """{ + "oid4vp": { + "status": "${inargs['o.id.v'] == 'NEW' ? 'INITIATED' : 'PENDING'}", + "verification_url": "${session['agov.eid.verification.link']}", + "id": "${idvalue}", + "error_code": "NONE" + }}""" + } + } + + } + catch (Exception e) { + LOG.error("Eid verification failed: ${e}") + result = """{ + "oid4vp": { + "status": "ERROR", + "verification_url": "${session['agov.eid.verification.link']}", + "id": "${idvalue}", + "error_code": "HTTP-ERROR", + "error_message": "failed to verify status of verification ${idvalue}, http exception" + }}""" + } + + response.setContent(result.toString()) + response.setContentType('application/json') + response.setHttpStatusCode(200) + response.setIsDirectResponse(true) + response.setStatus(AuthResponse.AUTH_CONTINUE) + return +} + +// if we reach this place, display GUI +response.setStatus(AuthResponse.AUTH_CONTINUE) +return + diff --git a/patterns/EId_Verification_Auth_7441fca76f479e4beb5ca796.yml b/patterns/EId_Verification_Auth_7441fca76f479e4beb5ca796.yml new file mode 100644 index 0000000..37ffc22 --- /dev/null +++ b/patterns/EId_Verification_Auth_7441fca76f479e4beb5ca796.yml @@ -0,0 +1,12 @@ +schemaVersion: "1.0" +pattern: + id: "7441fca76f479e4beb5ca796" + className: "ch.nevis.admin.v4.plugin.nevisauth.patterns2.GenericAuthenticationStep" + name: "EId_Verification_Auth" + properties: + authStatesFile: "res://7441fca76f479e4beb5ca796#authStatesFile" + onSuccess: + - "pattern://b87d0d2b640e8e545ad70234" + onFailure: + - "pattern://4c65de021d362462324a3a5f" + resources: "res://7441fca76f479e4beb5ca796#resources" diff --git a/patterns/RequestedRoleLevel_68665057549fd887ea09fb86.yml b/patterns/RequestedRoleLevel_68665057549fd887ea09fb86.yml index 69382c9..e757d6f 100644 --- a/patterns/RequestedRoleLevel_68665057549fd887ea09fb86.yml +++ b/patterns/RequestedRoleLevel_68665057549fd887ea09fb86.yml @@ -14,4 +14,6 @@ pattern: - "pattern://f63c475c35b616b7c6c1901c" onFailure: - "pattern://4c65de021d362462324a3a5f" + customSteps: + - "pattern://7441fca76f479e4beb5ca796" scriptTraceGroup: "AGOV-ACCT"