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