import groovy.json.JsonBuilder import groovy.json.JsonSlurper if (inargs.containsKey('cancel_fido2')) { response.setResult('cancel') return } 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']) response.addHiddenGuiField('securityKey', 'not used', session['agov.recovery.securityKey']) response.addTextGuiField('email', 'email', session['ch.nevis.idm.User.email']) if (notes.containsKey('lasterrorinfo') || notes.containsKey('lasterror')) { 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') } } def getPath() { if (inargs.containsKey('path')) { // form POST return inargs['path'] } if (inargs.containsKey('o.path.v')) { // AJAX POST return inargs['o.path.v'] } return null } def post(connection, json) { connection.setRequestMethod("POST") connection.setRequestProperty("Content-Type", "application/json") connection.setDoOutput(true) // required to write body String body = json.toString() LOG.info("==> Request: ${body}") connection.getOutputStream().write(body.getBytes()) } String userExtId = session['ch.adnovum.nevisidm.user.extId'] ?: session['ch.nevis.idm.User.extId'] ?: request.getUserId() ?: notes['userid'] if (userExtId == null) { LOG.error("missing extId of nevisIDM user. check your authentication flow.") } // 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) { showGui() // POST from JavaScript not received return } def connection = new URL("https://${parameters.get('fido')}${path}").openConnection() def json = new JsonBuilder() if (path == '/nevisfido/fido2/attestation/options') { json { "username" userExtId "userVerification" "required" } 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) return } def responseText = connection.inputStream.text LOG.info("<== Response: ${responseCode} : ${responseText}") response.setContent(responseText) // return response from nevisFIDO "as-is" response.setContentType('application/json') response.setHttpStatusCode(200) response.setIsDirectResponse(true) return } 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 { "id" inargs['id'] "type" inargs['type'] response { "clientDataJSON" inargs['response.clientDataJSON'] "authenticatorData" inargs['response.authenticatorData'] "signature" inargs['response.signature'] "userHandle" userHandleValue } } post(connection, json) def responseCode = connection.responseCode // test if credentials exist if (responseCode != 400) { def responseText = connection.inputStream.text LOG.info("<== Response: ${responseCode} : ${responseText}") if (responseCode == 200 && new JsonSlurper().parseText(responseText).status == 'ok') { 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') return } response.setError(1, "FIDO2 authentication failed") showGui()