adn-agov-iam-project/patterns/4f15bae09cbda04a7a515158_re.../eid_fetch_linked_accounts.g...

236 lines
10 KiB
Groovy

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
}