236 lines
10 KiB
Groovy
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
|
|
}
|
|
|