E-ID Passthrough: First draft
This commit is contained in:
parent
eed508938b
commit
4605d7367b
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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()}"
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<AuthState name="${state.entry}" class="ch.nevis.esauth.auth.states.scripting.ScriptState" final="false" resumeState="true">
|
||||
<ResultCond name="error" next="${state.failed}"/>
|
||||
<ResultCond name="ok" next="${state.done}"/>
|
||||
<ResultCond name="default" next="${state.entry}"/>
|
||||
<Response value="AUTH_CONTINUE">
|
||||
<Gui name="eid_verification">
|
||||
<GuiElem name="agov.appDisplayNameDE" type="hidden" value="${sess:agov.appDisplayNameDE}" optional="true"/>
|
||||
<GuiElem name="agov.appDisplayNameFR" type="hidden" value="${sess:agov.appDisplayNameFR}" optional="true"/>
|
||||
<GuiElem name="agov.appDisplayNameIT" type="hidden" value="${sess:agov.appDisplayNameIT}" optional="true"/>
|
||||
<GuiElem name="agov.appDisplayNameEN" type="hidden" value="${sess:agov.appDisplayNameEN}" optional="true"/>
|
||||
<GuiElem name="agov.appSamlRpEntityId" type="hidden" value="${var.appIconUrl}${sess:ch.nevis.auth.saml.request.scoping.requesterId}" optional="true"/>
|
||||
<GuiElem name="authRequestId" type="hidden" value="${sess:ch.nevis.auth.saml.request.id}" optional="true"/>
|
||||
<GuiElem name="oid4vp" type="hidden" value="UNKNOWN" optional="true"/>
|
||||
</Gui>
|
||||
</Response>
|
||||
<property name="scriptTraceGroup" value="AGOV-ACCT"/>
|
||||
<property name="script" value="file:///var/opt/nevisauth/default/conf/eid_verification_auth.groovy"/>
|
||||
<property name="parameter.eidVerifierBaseUrl" value="${var.eidVerifierBaseUrl}"/>
|
||||
</AuthState>
|
|
@ -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
|
||||
|
|
@ -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"
|
|
@ -14,4 +14,6 @@ pattern:
|
|||
- "pattern://f63c475c35b616b7c6c1901c"
|
||||
onFailure:
|
||||
- "pattern://4c65de021d362462324a3a5f"
|
||||
customSteps:
|
||||
- "pattern://7441fca76f479e4beb5ca796"
|
||||
scriptTraceGroup: "AGOV-ACCT"
|
||||
|
|
Loading…
Reference in New Issue