202 lines
7.8 KiB
Groovy
202 lines
7.8 KiB
Groovy
import groovy.json.JsonBuilder
|
|
import groovy.json.JsonSlurper
|
|
import java.util.UUID
|
|
|
|
if (inargs.containsKey('cancel_fido2')) {
|
|
response.setResult('cancel')
|
|
LOG.debug("Fido2Auth: authentication cancelled by user")
|
|
return
|
|
}
|
|
|
|
def base64url(uuid) {
|
|
def msb = uuid.getMostSignificantBits()
|
|
def lsb = uuid.getLeastSignificantBits()
|
|
return new byte[] {
|
|
(byte) msb,
|
|
(byte) (msb >> 8),
|
|
(byte) (msb >> 16),
|
|
(byte) (msb >> 24),
|
|
(byte) (msb >> 32),
|
|
(byte) (msb >> 40),
|
|
(byte) (msb >> 48),
|
|
(byte) (msb >> 56),
|
|
(byte) lsb,
|
|
(byte) (lsb >> 8),
|
|
(byte) (lsb >> 16),
|
|
(byte) (lsb >> 24),
|
|
(byte) (lsb >> 32),
|
|
(byte) (lsb >> 40),
|
|
(byte) (lsb >> 48),
|
|
(byte) (lsb >> 56)
|
|
}.encodeBase64Url().toString()
|
|
}
|
|
|
|
def showGui() {
|
|
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.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')) {
|
|
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.debug("Fido2Auth: ==> 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("Fido2Auth: 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 = null
|
|
try {
|
|
def fullPath = "https://${parameters.get('fido')}${path}"
|
|
LOG.debug("Fido2Auth: opening connection to '${fullPath}'")
|
|
connection = new URL(fullPath).openConnection()
|
|
} catch (Exception e) {
|
|
LOG.error("Fido2Auth: opening connection failed", e)
|
|
notes.setProperty('lasterrorinfo', 'FIDO2 authentication failed')
|
|
response.setResult('error')
|
|
return
|
|
}
|
|
|
|
def json = new JsonBuilder()
|
|
|
|
if (path == '/nevisfido/fido2/attestation/options') {
|
|
json {
|
|
"username" userExtId
|
|
"userVerification" "required"
|
|
}
|
|
post(connection, json)
|
|
def responseCode = connection.responseCode
|
|
|
|
// non existing account, or account without FIDO2 key case
|
|
if (responseCode == 404 || responseCode == 400) {
|
|
|
|
LOG.debug("Fido2Auth: <== Response: ${responseCode}")
|
|
|
|
// Accounting
|
|
def requester = session['ch.nevis.auth.saml.request.scoping.requesterId'] ?: 'unknown'
|
|
def requestId = session['ch.nevis.auth.saml.request.id'] ?: 'unknown'
|
|
def requestedAq = session['agov.requestedRoleLevel'] ?: 'unknown'
|
|
def user = session['ch.adnovum.nevisidm.user.extId'] ?: 'unknown'
|
|
def credentialType = session['authenticatedWith'] ?: 'unknown'
|
|
def sourceIp = request.getLoginContext()['connection.HttpHeader.X-Real-IP'] ?: 'unknown'
|
|
def userAgent = request.getLoginContext()['connection.HttpHeader.user-agent'] ?: request.getLoginContext()['connection.HttpHeader.User-Agent'] ?: 'unknown'
|
|
def tAuth = System.currentTimeMillis() - (request.getSession(true).getCreationTime().getEpochSecond() * 1000)
|
|
|
|
LOG.info("Event='NOACCOUNT', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${session['ch.nevis.idm.User.email']}, CredentialType='${credentialType}', tAuth=${tAuth}ms, SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
|
// returning a fake options structure, which shouldn't leak whether the user account exists or not
|
|
// keyId is unique per environment and email, fido2SessionId and challenge are renewed each time
|
|
def keyId = UUID.nameUUIDFromBytes("${parameters['rpId']}.${session['ch.nevis.idm.User.email']}".getBytes())
|
|
def responseText = """{"status": "ok",
|
|
"errorMessage": "",
|
|
"fido2SessionId": "${UUID.randomUUID()}",
|
|
"challenge": "${base64url(UUID.randomUUID())}",
|
|
"timeout": 300000,
|
|
"rpId": "${parameters['rpId']}",
|
|
"allowCredentials": [
|
|
{
|
|
"type": "public-key",
|
|
"id": "${base64url(keyId)}",
|
|
"transports": []
|
|
}
|
|
],
|
|
"userVerification": "required"}"""
|
|
|
|
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.debug("Fido2Auth: <== 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.debug('Fido2Auth: 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.debug("Fido2Auth: 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.debug("Fido2Auth: <== 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() |