adn-agov-iam-project/patterns/e335f57d4c64dfc97223697a_re.../eid_verification_auth.groovy

417 lines
14 KiB
Groovy

import ch.nevis.esauth.auth.engine.AuthResponse
import ch.nevis.esauth.sess.Session
import ch.nevis.esauth.util.httpclient.api.HttpClient
import groovy.json.JsonSlurper
import io.opentelemetry.api.trace.Span
import com.fasterxml.uuid.Generators
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]
}
// returns true on success and false on failure
def getNewVerification(Session sess, HttpClient httpClient, String verification_request_template, String traceparent){
// 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}")
return false
}
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)
// TODO/aca/2025-04-04:This could probably also be INITIATED, once the verifier supports this status
if (json.state != 'PENDING') {
return false
}
}
catch (Exception e) {
LOG.error("Eid verification failed: $e")
return false
}
return true
}
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
// or if the frontend requested a timeout
if ( (inargs.containsKey('authRequestId') && (inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'])) || inargs['oid4vp'] == 'TIMEOUT') {
// wrong request, "force" a timeout
LOG.debug('authentication timeout enforced, due to concurrent requests (authRequestId missmatch) -> 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
}
def sess = request.getAuthSession(true)
if (inargs['oid4vp'] == 'ERROR') {
LOG.debug("oid4vp error")
response.setResult('error')
return
}
if (inargs['oid4vp'] == 'SUCCEEDED') {
LOG.debug("oid4vp succeeded")
response.setResult('ok')
return
}
/*
// Temporary for CANCELED
if (inargs['oid4vp'] == 'CANCELED') {
LOG.debug("oid4vp canceled")
response.setResult('error')
return
}
*/
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']) {
LOG.debug("Initializing verification")
if(!getNewVerification(sess, httpClient, verification_request_template, traceparent)){
response.setResult('error')
return
}
}
if (getHeader('Content-Type') == 'application/json' && inargs.containsKey('o.id.v')) {
LOG.debug("Request Status Update")
// 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']
// check, whether we are still processing the correct verification request
//
if(inargs.containsKey('authRequestId') && (inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'])){
//if(inargs['o.id.v'] && inargs['o.id.v'] != 'NEW' && inargs['o.id.v'] != session['agov.eid.verification.id']){
// wrong request, tell fe to stop polling and request a timeout
LOG.debug('authentication timeout enforced, due to concurrent requests (verificationRequest missmatch) -> Notify FE & then return a 408')
result = """{
"oid4vp": {
"status": "TIMEOUT",
"verification_url": "${session['agov.eid.verification.link']}",
"id": "${idvalue}",
"error_code": "REQUEST-MISMATCH",
"error_message": "Request Mismatch Detected: Forcing Timeout"
}}"""
response.setContent(result.toString())
response.setContentType('application/json')
response.setHttpStatusCode(200)
response.setIsDirectResponse(true)
response.setStatus(AuthResponse.AUTH_CONTINUE)
return
}
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)
// 404 -> request a new verification
if(httpResponse.code() == 404){
// Frontend should know that we are starting a new request and not recieve an error
def status = "FAILED"
// Delete session variable to start a new verification
sess.removeAttribute('agov.eid.verification')
result = """{
"oid4vp": {
"status": "${status}",
"verification_url": "",
"id": "",
"error_code": "HTTP-ERROR",
"error_message": "Faild to verify status of verification, http status: ${httpResponse.code()}"
}}"""
LOG.warn("<== Response: ${responseCode}")
}
else if (httpResponse.code() != 200) {
LOG.debug("Result: ${httpResponse}")
def status = "ERROR"
result = """{
"oid4vp": {
"status": "${status}",
"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())
LOG.debug(httpResponse.bodyAsString())
if (json.state == 'SUCCESS') {
def claims = json.wallet_response.credential_subject_data
LOG.debug("Store user data in session")
// TODO/haburger/2025-03-25: format changes to align with IDM read data => No changes needed(?)
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.replace('.',''))
sess.setAttribute('ch.nevis.idm.User.prop.placeOfBirth', claims.birth_place)
sess.setAttribute('ch.nevis.idm.User.prop.eIdNumber', claims.document_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")
// subjectUUID v5
def namespace = UUID.fromString(parameters.get('eidUUIDNamespace'))
def uuid = Generators.nameBasedGenerator(namespace).generate(claims.personal_administrative_number)
LOG.debug("UUID: ${uuid}")
String uuidString = uuid.toString()
sess.setAttribute('agov.subjectUUID', '' + uuidString)
response.setUserId(uuidString)
sess.setAttribute('ch.adnovum.nevisidm.user.extId', uuidString)
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})")
def status = ERROR_CODE_TO_STATUS_MAPPER[json.wallet_response.error_code] ?: 'ERROR'
// Send new request & return variables with new id and url
if(status == 'FAILED' || status == 'CANCELED'){
// Delete session variable to start a new verification
sess.removeAttribute('agov.eid.verification')
// Clear variables for for a cleaner result
sess.removeAttribute('agov.eid.verification.link')
}
result = """{
"oid4vp": {
"status": "${status}",
"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)
LOG.debug("Recieved json: End")
return
}
// if we reach this place, display GUI
LOG.debug("Show GUI")
response.setStatus(AuthResponse.AUTH_CONTINUE)
return