import ch.nevis.esauth.auth.engine.AuthResponse import ch.nevis.idm.client.IdmRestClient import ch.nevis.idm.client.IdmRestClientFactory import ch.nevis.idm.client.HTTPRequestWrapper import groovy.json.JsonSlurper import groovy.json.JsonBuilder 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 clearEidSession(){ def s = request.getAuthSession(true) s.removeAttribute('agov.eid.verification') s.removeAttribute('agov.eid.verification.id') s.removeAttribute('agov.eid.verification.link') s.removeAttribute('agov.eid.linkedAccountsDto') s.removeAttribute('agov.eid.User.birthDate') s.removeAttribute('agov.eid.User.eIdNumber') s.removeAttribute('agov.eid.User.firstName') s.removeAttribute('agov.eid.User.lastName') s.removeAttribute('agov.eid.User.gender') s.removeAttribute('agov.eid.User.nationality') s.removeAttribute('agov.eid.User.placeOfBirth') s.removeAttribute('agov.eid.User.svnr') s.removeAttribute('agov.eid.User.origin') } def updateLoginHistory(idmRestClient, userExtId, credentialExtId) { try { def baseUrl = parameters.get("baseUrl") def clientExtId = parameters.get("clientExtId") def endpoint = "$baseUrl/api/core/v1/$clientExtId/users/$userExtId/login-info" def dto = "{\"success\": true,\"credentialExtId\": \"${credentialExtId}\"}" def postRequest = new HTTPRequestWrapper() postRequest.addToHeaders('Content-Type', ['application/json']) postRequest.setPayLoad(dto.getBytes('UTF-8')) postRequest.setPayLoad(dto.getBytes('UTF-8')) def result = idmRestClient.postWithResponse(endpoint, postRequest) if (result.getStatusCode() != 200) { // best effort, we log only // TODO/haburger/2025-06-24: context parameters are missing here (also in getAccounts) LOG.warn("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${userExtId}, CredentialType='E-ID Link', SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='failed to update login history for credential ${credentialExtId} (http status: ${result.getStatusCode()})'") } } catch (Exception e) { // best effort, we log only // TODO/haburger/2025-06-24: context parameters are missing here (also in getAccounts) LOG.warn("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${userExtId}, CredentialType='E-ID Link', SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='failed to update login history for credential ${credentialExtId} (${e})'") } } def getAccounts(json, String svnr) { def idm_users_dto = json["Resources"] def accounts = [:] def frontend_dto = [] for(user in idm_users_dto){ def credentials_dto = user["urn:nevis:idm:scim:schemas:v1:extension:User"]["credentials"] if(!credentials_dto){ LOG.warn("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${extId}, CredentialType='${credentialType}', SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='AGOV account has no credentials'") } for(cred in credentials_dto){ def foundCredential = false def extId = user["externalId"] //TODO/aca/2025/06/11: Can we have multiple email adresses? -> if yes search for primary String email = user["emails"][0]["value"] if(cred["type"] == "SAMLFEDERATION" && cred["issuerNameId"] == svnr){ // we found a second federation credential in one AGOV account -> Throw data error if(foundCredential){ LOG.error("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${extId}, CredentialType='${credentialType}', SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='Multiple EId linking credentials found in one AGOV account'") return [null,null] } // extract login info def firstLogin = true if(cred["credentialLoginInfo"]){ if(cred["credentialLoginInfo"]["lastLogin"] && cred["credentialLoginInfo"]["lastLogin"] != ""){ firstLogin = false } } //NOTE/aca/2025/06/11: Assume that this is sanitized when registered. def accountName = cred['subjectNameId'] def credentialExtId = cred['extId'] accounts.put(email, [ "extId": extId, "credentialExtId": cred['extId'], "firstLogin": firstLogin ] ) frontend_dto.add(["email": email, "description": accountName]) foundCredential=true } } } return [ accounts, [ "accounts": frontend_dto ] ] } def sess = request.getAuthSession(true) IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters) // 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' if(inargs['submit'] && inargs['login'] && inargs['login'] != ''){ LOG.debug("Account with email: ${inargs['login']} was selceted -> Continuing") def accounts = new JsonSlurper().parseText(session['agov.eid.linkedAccountsDto']) def account = accounts.get( inargs['login'].trim() ) sess.setAttribute('agov.eid.linkingCredentialExtId', account["credentialExtId"]) sess.setAttribute('agov.eid.linkedAccountExtId', account["extId"]) // update login history updateLoginHistory(idmRestClient, account["extId"], account["credentialExtId"]) if(account["firstLogin"]){ response.setResult('firstLogin') return } response.setResult('ok') return } if(inargs['cancelEid'] && inargs['cancelEid'] == 'cancel'){ LOG.debug("Account selection was canceled: back to initial login screen") clearEidSession() response.setResult('backToVerification') return } if(getHeader('Content-Type') == 'application/json'){ String account_selection_dto = session['agov.eid.linkedAccountsFrontendDto'] response.setContent(account_selection_dto.toString()) response.setContentType('application/json') response.setHttpStatusCode(200) response.setIsDirectResponse(true) response.setStatus(AuthResponse.AUTH_CONTINUE) return } String baseUrl = parameters.get("baseUrl") String clientExtId = parameters.get("clientExtId") String endPoint = "$baseUrl/api/scim/v1/$clientExtId/Users" // Fetch account identifier String svnr = sess.getAttribute("agov.eid.User.svnr") LOG.debug("search for accounts with SVNR: $svnr") // Pepare GET request String attributes = "externalId,emails,urn:nevis:idm:scim:schemas:v1:extension:User.credentials.type,urn:nevis:idm:scim:schemas:v1:extension:User.credentials.issuerNameId,urn:nevis:idm:scim:schemas:v1:extension:User.credentials.subjectNameId,urn:nevis:idm:scim:schemas:v1:extension:User.credentials.extId,urn:nevis:idm:scim:schemas:v1:extension:User.credentials.credentialLoginInfo.lastLogin" String filter = "urn:nevis:idm:scim:schemas:v1:extension:User.credentials.type=='SAMLFEDERATION'%20AND%20urn:nevis:idm:scim:schemas:v1:extension:User.credentials.issuerNameId=='$svnr'" String requestUrl = "$endPoint?count=20&attributes=$attributes&filter=$filter" String scimResponse try { scimResponse = idmRestClient.get(requestUrl) //TODO/aca/2025/06/11: Fetch more pages if more than 20 entries have been found LOG.debug("SCIM Response: $scimResponse") def json = new JsonSlurper().parseText(scimResponse) def (accounts, frontend_dto) = getAccounts(json, svnr) // unrecoverable DATA ERROR happend if(!accounts){ response.setResult('error') return } def numAccounts = accounts.size() LOG.debug("Linked accounts found: " + frontend_dto.toString()) if(numAccounts == 0){ //TODO/aca/2025-06-10: Implement next step // Redirect to an error page or linking page when that's ready and decided sess.setAttribute("eid.placeholder.text", "EId: No AGOV Account found case not implemented yet") response.setResult('noAccount') return }else if(numAccounts == 1){ // One account found -> continue with loading attributes from idm (+ notification if it is the first login) def account = accounts.values().first() sess.setAttribute('agov.eid.linkingCredentialExtId', account["credentialExtId"]) sess.setAttribute('agov.eid.linkedAccountExtId', account["extId"]) // update login history updateLoginHistory(idmRestClient, account["extId"], account["credentialExtId"]) if(account["firstLogin"]){ response.setResult('firstLogin') return } response.setResult('ok') return }else{ // Multiple accounts found -> Dispatch the account selection screen sess.setAttribute('agov.eid.linkedAccountsDto', new JsonBuilder(accounts).toString()) sess.setAttribute('agov.eid.linkedAccountsFrontendDto', new JsonBuilder(frontend_dto).toString()) LOG.debug("Show GUI") response.setStatus(AuthResponse.AUTH_CONTINUE) return } } catch(Exception e) { LOG.error("Fetching Agov Accounts Failed: ${e}") sess.setAttribute("eid.placeholder.text", "EId: An exception occured while fetching the AGOV accounts\n: ${e}") response.setResult('error') return }