import groovy.json.JsonBuilder import java.security.MessageDigest import java.util.HashSet import ch.nevis.esauth.auth.engine.AuthResponse 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 sha256(String input) { // we do not catch NoSuchAlgorithmException, as every implementation of the Java platform is required to support SHA-256 def digestBytes = MessageDigest.getInstance('SHA-256').digest(input.getBytes()) return digestBytes.encodeBase64().toString() } def clearCurrentAuthenticationSession() { // clean up session attributes def s = request.getAuthSession(true) def requestId = session['ch.nevis.auth.saml.request.id'] ?: 'unknown' // we backup the replaced requestId if (requestId != 'unknown') { s.setAttribute('agov.replacedRequestId', '' + requestId) } // fido s.removeAttribute('ch.nevis.auth.fido.uaf.fidouafsessionid') // SAML s.removeAttribute('finisherState-DeferredResponse') s.removeAttribute('saml.idp.result') s.removeAttribute('saml.inbound.issuer') def sessionKeySet = new HashSet(session.keySet()) sessionKeySet.each { key -> if ( key ==~ /ch.nevis.auth.saml.request.*/ ) { s.removeAttribute(key) } } // agov s.removeAttribute('agov.requestedRoleLevel') } // context: script is executed, thus we are in the initial dispatching of the state engine // due to the resetAuthenticationCondition it will be called for sure after each SAMLRequest received if (inargs['SAMLRequest'] != null) { if (session['ch.nevis.auth.saml.request.id'] != null) { // 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' // check if we receive a repost of the ongoing request if (session['agov.currentSamlRequestHash'] != null && session['agov.currentSamlRequestHash'] == sha256(inargs['SAMLRequest'])) { LOG.info("Event='AUTHCONTINUE', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', SourceIp=${sourceIp}, UserAgent='${userAgent}'") request.getInArgs().remove('SAMLRequest') request.getInArgs().remove('RelayState') // restore the finisher again (was removed by resetAuthenticationCondition) def s = request.getAuthSession(true) s.setAttribute('ch.nevis.session.finishers', '' + session['agov.backup.finishers']) // process it the same way, as if frontend triggered a reload request.getInArgs().setProperty('onReload', 'now') response.setResult('continueAfterRepost') return } // else, the new replaces the on-going one LOG.info("Event='AUTHREPL', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', SourceIp=${sourceIp}, UserAgent='${userAgent}'") clearCurrentAuthenticationSession() } // we track the SAML Request we received def s = request.getAuthSession(true) s.setAttribute('agov.currentSamlRequestHash', '' + sha256(inargs['SAMLRequest'])) // we set/update a login Cookie def agovLoginCookie = "agovLogin=${System.currentTimeMillis()}; Domain=${parameters.get('cookie.domain')}; Path=/; SameSite=Strict; Secure; HttpOnly" response.setHeader('Set-Cookie', agovLoginCookie) response.setResult('ok') return } // from here on, corner cases // // ============================= def json = new JsonBuilder() if (inargs.containsKey('o.fidoUafSessionId.v')) { // timeout, and script in login page is still polling -> send fake response LOG.debug('authentication timeout reached, login script is still polling access app status') json { "status" "unknown" "timestamp" org.joda.time.DateTime.now().toString() } String body = json.toString() response.setContent(body) response.setContentType('application/json') response.setHttpStatusCode(200) response.setIsDirectResponse(true) response.setStatus(AuthResponse.AUTH_CONTINUE) return } else { // authentication timeout reached, or SSO-Endpoint bookmarked -> return a 404 def agovLoginCookie = 'missing' if (getHeader('cookie') != null) { def cookies = getHeader('cookie') if (cookies.matches('^.*agovLogin=([^;]+).*$')) { agovLoginCookie = cookies.replaceAll('^.*agovLogin=([^;]+).*$', '$1') } } LOG.debug("agovLoginCookie: ${agovLoginCookie}") if (agovLoginCookie == 'missing' || agovLoginCookie == 'deleted') { LOG.debug('SSO-Endpoint bookmarked -> return a 404') response.setHttpStatusCode(404) response.setIsDirectResponse(true) response.setStatus(AuthResponse.AUTH_ERROR) } else { LOG.debug('authentication timeout reached -> return a 408') response.setHttpStatusCode(408) response.setIsDirectResponse(true) response.setStatus(AuthResponse.AUTH_ERROR) } return }