new configuration version
This commit is contained in:
parent
c20a2d0346
commit
042d9dded4
|
@ -11,8 +11,8 @@ metadata:
|
|||
spec:
|
||||
type: "NevisAuth"
|
||||
replicas: 1
|
||||
version: "8.2411.3"
|
||||
gitInitVersion: "1.3.0"
|
||||
version: "8.2505.5"
|
||||
gitInitVersion: "1.4.0"
|
||||
runAsNonRoot: true
|
||||
ports:
|
||||
management: 9000
|
||||
|
@ -39,13 +39,14 @@ spec:
|
|||
management:
|
||||
httpGet:
|
||||
path: "/nevisauth/liveness"
|
||||
initialDelaySeconds: 50
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 6
|
||||
failureThreshold: 50
|
||||
failureThreshold: 30
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: "50%"
|
||||
git:
|
||||
tag: "r-317ed268556b37656f27fb58fcffd4797cea27e4"
|
||||
tag: "r-5e17b7ae74eadb8800587a4f4db74406a7e21e95"
|
||||
dir: "DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth-sts"
|
||||
credentials: "git-credentials"
|
||||
keystores:
|
||||
|
|
|
@ -3,6 +3,7 @@ accept.button.label=Accept
|
|||
cancel.button.label=Cancel
|
||||
continue.button.label=Continue
|
||||
deputy.profile.label=(Deputy Profile)
|
||||
error.account.exists=Account already exists. Continue to log in.
|
||||
error.saml.failed=Please close your browser and try again.
|
||||
error_1=Please check your input.
|
||||
error_10=Please select the correct user account.
|
||||
|
@ -70,6 +71,8 @@ policyInfo.regex.numeric=▪ must contain at least {0} numeric characters.
|
|||
policyInfo.regex.upper=▪ must contain at least {0} upper case characters.
|
||||
policyInfo.title=The password has to comply with the following password policy:
|
||||
reject.button.label=Deny
|
||||
signup.button.label=Signup
|
||||
skip.button.label=Skip
|
||||
submit.button.label=Submit
|
||||
tan.sent=Please enter the security code which has been sent to your mobile phone.
|
||||
title.logout=Logout
|
||||
|
@ -77,4 +80,5 @@ title.logout.confirmation=Logout
|
|||
title.logout.reminder=Logout
|
||||
title.oauth.consent=Client Authorization
|
||||
title.saml.failed=Error
|
||||
title.signup=Create account
|
||||
title.timeout.page=Logout
|
||||
|
|
|
@ -3,6 +3,7 @@ accept.button.label=Akzeptieren
|
|||
cancel.button.label=Abbrechen
|
||||
continue.button.label=Weiter
|
||||
deputy.profile.label=(Profil Stellvertreter)
|
||||
error.account.exists=Konto existiert bereits. Melden Sie sich an.
|
||||
error.saml.failed=Bitte schliessen Sie Ihren Browser und versuchen Sie es erneut.
|
||||
error_1=Bitte überprüfen Sie Ihre Eingabe.
|
||||
error_10=Bitte wählen Sie den gewünschten Benutzer.
|
||||
|
@ -70,6 +71,8 @@ policyInfo.regex.numeric=▪ muss mindestens {0} numerische Zeichen enthalte
|
|||
policyInfo.regex.upper=▪ muss mindestens {0} Grossbuchstaben enthalten.
|
||||
policyInfo.title=Das Passwort muss den folgenden Passwort-Richtlinien entsprechen:
|
||||
reject.button.label=Ablehnen
|
||||
signup.button.label=Registrieren
|
||||
skip.button.label=Überspringen
|
||||
submit.button.label=Senden
|
||||
tan.sent=Bitte erfassen Sie den Sicherheitscode, welcher an Ihr Mobiltelefon gesendet wurde.
|
||||
title.logout=Logout
|
||||
|
@ -77,4 +80,5 @@ title.logout.confirmation=Logout
|
|||
title.logout.reminder=Logout
|
||||
title.oauth.consent=Client Authorisierung
|
||||
title.saml.failed=Error
|
||||
title.signup=Konto erstellen
|
||||
title.timeout.page=Logout
|
||||
|
|
|
@ -3,6 +3,7 @@ accept.button.label=Accept
|
|||
cancel.button.label=Cancel
|
||||
continue.button.label=Continue
|
||||
deputy.profile.label=(Deputy Profile)
|
||||
error.account.exists=Account already exists. Continue to log in.
|
||||
error.saml.failed=Please close your browser and try again.
|
||||
error_1=Please check your input.
|
||||
error_10=Please select the correct user account.
|
||||
|
@ -70,6 +71,8 @@ policyInfo.regex.numeric=▪ must contain at least {0} numeric characters.
|
|||
policyInfo.regex.upper=▪ must contain at least {0} upper case characters.
|
||||
policyInfo.title=The password has to comply with the following password policy:
|
||||
reject.button.label=Deny
|
||||
signup.button.label=Signup
|
||||
skip.button.label=Skip
|
||||
submit.button.label=Submit
|
||||
tan.sent=Please enter the security code which has been sent to your mobile phone.
|
||||
title.logout=Logout
|
||||
|
@ -77,4 +80,5 @@ title.logout.confirmation=Logout
|
|||
title.logout.reminder=Logout
|
||||
title.oauth.consent=Client Authorization
|
||||
title.saml.failed=Error
|
||||
title.signup=Create account
|
||||
title.timeout.page=Logout
|
||||
|
|
|
@ -3,6 +3,7 @@ accept.button.label=Accepter
|
|||
cancel.button.label=Abandonner
|
||||
continue.button.label=Continuer
|
||||
deputy.profile.label=(Profil du suppléant)
|
||||
error.account.exists=Le compte existe déjà. Continuez à vous connecter.
|
||||
error.saml.failed=Fermez votre navigateur et r;eacute;essayez.
|
||||
error_1=Veuillez vérifier vos données, s.v.p.
|
||||
error_10=Choisissez votre compte.
|
||||
|
@ -70,6 +71,8 @@ policyInfo.regex.numeric=▪ doit comprendre au minimum {0} caractères
|
|||
policyInfo.regex.upper=▪ doit contenir au moins {0} caractère(s) majuscule(s).
|
||||
policyInfo.title=Le mot de passe doit respecter les règles suivantes:
|
||||
reject.button.label=Refuser
|
||||
signup.button.label=Inscription
|
||||
skip.button.label=Passer
|
||||
submit.button.label=Envoyer
|
||||
tan.sent=Veuillez saisir le code de sécurité que vous avez reçu au votre téléphone mobile.
|
||||
title.logout=Logout
|
||||
|
@ -77,4 +80,5 @@ title.logout.confirmation=Logout
|
|||
title.logout.reminder=Logout
|
||||
title.oauth.consent=Autorisation du client
|
||||
title.saml.failed=Error
|
||||
title.signup=Créer un compte
|
||||
title.timeout.page=Logout
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
|
||||
accept.button.label=Accettare
|
||||
cancel.button.label=Abortire
|
||||
accept.button.label=Accetta
|
||||
cancel.button.label=Annulla
|
||||
continue.button.label=Continua
|
||||
deputy.profile.label=(profilo del delegato)
|
||||
error.account.exists=L'account esiste gi<67>. Prosegui col login.
|
||||
error.saml.failed=Chiudi il browser e riprova.
|
||||
error_1=Verificare i dati immessi.
|
||||
error_10=Per favore selezionare il conto utente corretto.
|
||||
|
@ -69,7 +70,9 @@ policyInfo.regex.nonLetter=▪ non può contenere più di {0} nu
|
|||
policyInfo.regex.numeric=▪ deve contenere un minimo di {0} carattere/i numerico/i.
|
||||
policyInfo.regex.upper=▪ deve conenere almeno {0} carattere/i maiuscolo/i.
|
||||
policyInfo.title=La password deve rispettare le seguenti direttive:
|
||||
reject.button.label=Rifiuti
|
||||
reject.button.label=Rifiuta
|
||||
signup.button.label=Iscriviti
|
||||
skip.button.label=Salta
|
||||
submit.button.label=Continua
|
||||
tan.sent=Inserisci il codice di sicurezza che è stato inviato al tuo telefono cellulare.
|
||||
title.logout=Logout
|
||||
|
@ -77,4 +80,5 @@ title.logout.confirmation=Logout
|
|||
title.logout.reminder=Logout
|
||||
title.oauth.consent=Autorizzazione del client
|
||||
title.saml.failed=Error
|
||||
title.signup=Crea un account
|
||||
title.timeout.page=Logout
|
||||
|
|
|
@ -13,8 +13,9 @@ JAVA_OPTS=(
|
|||
"-javaagent:/opt/agent/opentelemetry-javaagent.jar"
|
||||
"-Dotel.javaagent.logging=application"
|
||||
"-Dotel.javaagent.configuration-file=/var/opt/nevisauth/default/conf/otel.properties"
|
||||
"-Dotel.resource.attributes=service.version=8.2411.3,service.instance.id=$HOSTNAME"
|
||||
"-Dotel.resource.attributes=service.version=8.2505.5,service.instance.id=$HOSTNAME"
|
||||
"-Djavax.net.ssl.trustStore=/var/opt/keys/trust/auth-sts-idp-extended-truststore/truststore.p12"
|
||||
"-Djavax.net.ssl.trustStorePassword=\${exec:/var/opt/keys/trust/auth-sts-idp-extended-truststore/keypass}"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -431,4 +431,6 @@
|
|||
<!-- source: pattern://eaae1a7d4c4e0ce653074f22 -->
|
||||
<property name="secToken.binary" value="true"/>
|
||||
</WebService>
|
||||
<!-- source: pattern://4bad2fe3ccc54716cc87138f -->
|
||||
<RESTService name="ManagementService" class="ch.nevis.esauth.rest.service.session.ManagementService"/>
|
||||
</esauth-server>
|
||||
|
|
|
@ -16,12 +16,6 @@ Configuration:
|
|||
level: "INFO"
|
||||
- name: "EsAuthStart"
|
||||
level: "INFO"
|
||||
- name: "org.apache.catalina.loader.WebappClassLoader"
|
||||
level: "FATAL"
|
||||
- name: "org.apache.catalina.startup.HostConfig"
|
||||
level: "ERROR"
|
||||
- name: "ch.nevis.esauth.events"
|
||||
level: "FATAL"
|
||||
- name: "AGOV-ACCT"
|
||||
level: "DEBUG"
|
||||
- name: "AgovCaptcha"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
otel.service.name = auth-sts
|
||||
otel.traces.sampler = always_on
|
||||
otel.traces.exporter = none
|
||||
otel.metrics.exporter = none
|
||||
otel.logs.exporter = none
|
||||
|
|
|
@ -14,4 +14,4 @@ try {
|
|||
LOG.warn("Exception in Script: ${e}")
|
||||
} finally {
|
||||
response.setResult('ok')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,4 +13,4 @@ try {
|
|||
LOG.warn("Exception in Script: ${e}")
|
||||
} finally {
|
||||
response.setResult('ok')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ metadata:
|
|||
spec:
|
||||
type: "NevisAuth"
|
||||
replicas: 1
|
||||
version: "8.2411.3"
|
||||
gitInitVersion: "1.3.0"
|
||||
version: "8.2505.5"
|
||||
gitInitVersion: "1.4.0"
|
||||
runAsNonRoot: true
|
||||
ports:
|
||||
management: 9000
|
||||
|
@ -39,13 +39,14 @@ spec:
|
|||
management:
|
||||
httpGet:
|
||||
path: "/nevisauth/liveness"
|
||||
initialDelaySeconds: 50
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 6
|
||||
failureThreshold: 50
|
||||
failureThreshold: 30
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: "50%"
|
||||
git:
|
||||
tag: "r-8c160b6ed06647cec021e38b8bc8f4dffaab04c1"
|
||||
tag: "r-5e17b7ae74eadb8800587a4f4db74406a7e21e95"
|
||||
dir: "DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth"
|
||||
credentials: "git-credentials"
|
||||
keystores:
|
||||
|
|
|
@ -10,11 +10,11 @@ agov-ident.invalid-url.message=Link can't be processed
|
|||
agov-ident.invalid-url.title=Invalid Link
|
||||
agov-ident.onboarding=Registration & Verification
|
||||
agov-ident.retry=Try again
|
||||
button.submit=Submit
|
||||
cancel.button.label=Cancel
|
||||
continue.button.label=Continue
|
||||
darkModeSwitch.aria.label=Dark mode toggle
|
||||
deputy.profile.label=(Deputy Profile)
|
||||
error.account.exists=Account already exists. Continue to log in.
|
||||
error.policy.failed=The new password does not comply with the policy.
|
||||
error.saml.failed=Please close your browser and try again.
|
||||
error_1=Please check your input.
|
||||
|
@ -297,6 +297,8 @@ recovery_start_info.banner.warning=You will not be able to use your account unti
|
|||
recovery_start_info.instruction=During the recovery process you will register a new login factor. If your account contains any verified information you might also have to go through a verification process to finish the recovery.
|
||||
recovery_start_info.title=You are about to start the recovery process
|
||||
reject.button.label=Deny
|
||||
signup.button.label=Signup
|
||||
skip.button.label=Skip
|
||||
submit.button.label=Submit
|
||||
tan.sent=Please enter the security code which has been sent to your mobile phone.
|
||||
title.login=Login
|
||||
|
@ -307,6 +309,7 @@ title.oauth.consent=Client Authorization
|
|||
title.pwchange.label=Password Change
|
||||
title.pwreset=Password Forgotten
|
||||
title.saml.failed=Error
|
||||
title.signup=Create account
|
||||
title.timeout.page=Logout
|
||||
user_input.invalid.email=Please enter a valid email address
|
||||
user_input.invalid.email.required=Field required
|
||||
|
|
|
@ -10,11 +10,11 @@ agov-ident.invalid-url.message=Link kann nicht verarbeitet werden
|
|||
agov-ident.invalid-url.title=Ungültiger Link
|
||||
agov-ident.onboarding=Registrierung & Verifikation
|
||||
agov-ident.retry=Versuchen Sie es erneut
|
||||
button.submit=Senden
|
||||
cancel.button.label=Abbrechen
|
||||
continue.button.label=Weiter
|
||||
darkModeSwitch.aria.label=Dark-Mode-Schalter
|
||||
deputy.profile.label=(Profil Stellvertreter)
|
||||
error.account.exists=Konto existiert bereits. Melden Sie sich an.
|
||||
error.policy.failed=Das neue Passwort stimmt nicht mit der Richtlinie überein.
|
||||
error.saml.failed=Bitte schliessen Sie Ihren Browser und versuchen Sie es erneut.
|
||||
error_1=Bitte überprüfen Sie Ihre Eingaben.
|
||||
|
@ -297,6 +297,8 @@ recovery_start_info.banner.warning=Sie können Ihr Konto nicht nutzen, bis d
|
|||
recovery_start_info.instruction=Während des Wiederherstellungsprozesses werden Sie einen neuen Login-Faktor registrieren. Wenn Ihr Konto verifizierte Informationen enthält, müssen Sie zum Abschluss des Wiederherstellungsprozesses möglicherweise auch einen Verifikationsprozess durchlaufen.
|
||||
recovery_start_info.title=Sie sind dabei, den Wiederherstellungsprozess zu starten
|
||||
reject.button.label=Ablehnen
|
||||
signup.button.label=Registrieren
|
||||
skip.button.label=Überspringen
|
||||
submit.button.label=Senden
|
||||
tan.sent=Bitte erfassen Sie den Sicherheitscode, welcher an Ihr Mobiltelefon gesendet wurde.
|
||||
title.login=Login
|
||||
|
@ -307,6 +309,7 @@ title.oauth.consent=Client Authorisierung
|
|||
title.pwchange.label=Passwort ändern
|
||||
title.pwreset=Passwort Vergesssen
|
||||
title.saml.failed=Error
|
||||
title.signup=Konto erstellen
|
||||
title.timeout.page=Logout
|
||||
user_input.invalid.email=Bitte geben Sie eine gültige E-Mail ein
|
||||
user_input.invalid.email.required=Erforderliches Feld
|
||||
|
|
|
@ -10,11 +10,11 @@ agov-ident.invalid-url.message=Link can't be processed
|
|||
agov-ident.invalid-url.title=Invalid Link
|
||||
agov-ident.onboarding=Registration & Verification
|
||||
agov-ident.retry=Try again
|
||||
button.submit=Submit
|
||||
cancel.button.label=Cancel
|
||||
continue.button.label=Continue
|
||||
darkModeSwitch.aria.label=Dark mode toggle
|
||||
deputy.profile.label=(Deputy Profile)
|
||||
error.account.exists=Account already exists. Continue to log in.
|
||||
error.policy.failed=The new password does not comply with the policy.
|
||||
error.saml.failed=Please close your browser and try again.
|
||||
error_1=Please check your input.
|
||||
|
@ -297,6 +297,8 @@ recovery_start_info.banner.warning=You will not be able to use your account unti
|
|||
recovery_start_info.instruction=During the recovery process you will register a new login factor. If your account contains any verified information you might also have to go through a verification process to finish the recovery.
|
||||
recovery_start_info.title=You are about to start the recovery process
|
||||
reject.button.label=Deny
|
||||
signup.button.label=Signup
|
||||
skip.button.label=Skip
|
||||
submit.button.label=Submit
|
||||
tan.sent=Please enter the security code which has been sent to your mobile phone.
|
||||
title.login=Login
|
||||
|
@ -307,6 +309,7 @@ title.oauth.consent=Client Authorization
|
|||
title.pwchange.label=Password Change
|
||||
title.pwreset=Password Forgotten
|
||||
title.saml.failed=Error
|
||||
title.signup=Create account
|
||||
title.timeout.page=Logout
|
||||
user_input.invalid.email=Please enter a valid email address
|
||||
user_input.invalid.email.required=Field required
|
||||
|
|
|
@ -10,11 +10,11 @@ agov-ident.invalid-url.message=Le lien ne peut pas être traité
|
|||
agov-ident.invalid-url.title=Lien non valide
|
||||
agov-ident.onboarding=Enregistrement et vérification
|
||||
agov-ident.retry=Essayez à nouveau
|
||||
button.submit=Envoyer
|
||||
cancel.button.label=Abandonner
|
||||
continue.button.label=Continuer
|
||||
darkModeSwitch.aria.label=Activer l'apparence sombre
|
||||
deputy.profile.label=(Profil du suppléant)
|
||||
error.account.exists=Le compte existe déjà. Continuez à vous connecter.
|
||||
error.policy.failed=Votre nouveau mot de passe ne conforme pas aux mesures de sécurité
|
||||
error.saml.failed=Fermez votre navigateur et r;eacute;essayez.
|
||||
error_1=Veuillez vérifier votre saisie.
|
||||
|
@ -297,6 +297,8 @@ recovery_start_info.banner.warning=Vous ne pourrez pas utiliser votre compte tan
|
|||
recovery_start_info.instruction=Le processus de récupération nécessitera l’enregistrement d’un nouveau facteur d’authentification. Si votre compte contient des informations ayant déjà été vérifiées, il se peut que vous deviez les faire vérifier à nouveau pour terminer la récupération.
|
||||
recovery_start_info.title=Vous êtes sur le point de démarrer le processus de récupération.
|
||||
reject.button.label=Refuser
|
||||
signup.button.label=Inscription
|
||||
skip.button.label=Passer
|
||||
submit.button.label=Envoyer
|
||||
tan.sent=Veuillez saisir le code de sécurité que vous avez reçu au votre téléphone mobile.
|
||||
title.login=Login
|
||||
|
@ -307,6 +309,7 @@ title.oauth.consent=Autorisation du client
|
|||
title.pwchange.label=Changer mot de passe
|
||||
title.pwreset=Mot de Passe Oublié
|
||||
title.saml.failed=Error
|
||||
title.signup=Créer un compte
|
||||
title.timeout.page=Logout
|
||||
user_input.invalid.email=Veuillez saisir un e-mail valable.
|
||||
user_input.invalid.email.required=Champ requis
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
accept.button.label=Accettare
|
||||
accept.button.label=Accetta
|
||||
agov-ident.done.message=Il vostro conto AGOV è ora pronto per l'uso. Può chiudere questa pagina.
|
||||
agov-ident.done.title=Finito
|
||||
agov-ident.failed.instruction=Per completare la registrazione è necessario disporre di un account AGOV e superare la verifica dei dati suggerita. Riprova.
|
||||
|
@ -10,11 +10,11 @@ agov-ident.invalid-url.message=Il link non può essere elaborato
|
|||
agov-ident.invalid-url.title=Link non valido
|
||||
agov-ident.onboarding=Registrazione e verifica
|
||||
agov-ident.retry=Riprova
|
||||
button.submit=Continua
|
||||
cancel.button.label=Abortire
|
||||
cancel.button.label=Annulla
|
||||
continue.button.label=Continua
|
||||
darkModeSwitch.aria.label=Attivare la modalità scura
|
||||
deputy.profile.label=(profilo del delegato)
|
||||
error.account.exists=L'account esiste gi<67>. Prosegui col login.
|
||||
error.policy.failed=La nuova password non è stata accettata. Scegliere una password che sia conforme ai criteri di password.
|
||||
error.saml.failed=Chiudi il browser e riprova.
|
||||
error_1=Verificare i dati inseriti.
|
||||
|
@ -296,7 +296,9 @@ recovery_questionnaire_reason_selection.instruction=Selezioni il motivo per cui
|
|||
recovery_start_info.banner.warning=Non è possibile utilizzare l’account finché il processo di ripristino non sarà concluso.
|
||||
recovery_start_info.instruction=Durante il processo di ripristino registrerà un nuovo fattore di login. Se il suo account contiene informazioni verificate, potrebbe dover effettuare anche un processo di verificazione per completare il ripristino.
|
||||
recovery_start_info.title=Sta per iniziare il processo di ripristino
|
||||
reject.button.label=Rifiuti
|
||||
reject.button.label=Rifiuta
|
||||
signup.button.label=Iscriviti
|
||||
skip.button.label=Salta
|
||||
submit.button.label=Continua
|
||||
tan.sent=Inserisci il codice di sicurezza che è stato inviato al tuo telefono cellulare.
|
||||
title.login=Login
|
||||
|
@ -307,6 +309,7 @@ title.oauth.consent=Autorizzazione del client
|
|||
title.pwchange.label=Cambiare Password
|
||||
title.pwreset=Password Dimenticata
|
||||
title.saml.failed=Error
|
||||
title.signup=Crea un account
|
||||
title.timeout.page=Logout
|
||||
user_input.invalid.email=Inserire un'e-mail valida.
|
||||
user_input.invalid.email.required=Campo obbligatorio
|
||||
|
|
|
@ -50,3 +50,4 @@ if (inargs.containsKey('onReload')) {
|
|||
clearFidoUAFSession()
|
||||
response.setResult('default')
|
||||
}
|
||||
|
||||
|
|
|
@ -21,4 +21,4 @@ def agovLoginCookie = "agovLogin=deleted; Domain=${parameters.get('cookie.domain
|
|||
response.setHeader('Set-Cookie', agovLoginCookie)
|
||||
|
||||
response.setStatus(AuthResponse.AUTH_ERROR)
|
||||
return
|
||||
return
|
||||
|
|
|
@ -1,109 +1,109 @@
|
|||
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.xml.XmlSlurper
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
|
||||
// Accounting
|
||||
def requester = session['ch.nevis.auth.saml.request.scoping.requesterId'] ?: 'unknown'
|
||||
def requestId = session['ch.nevis.auth.saml.request.id'] ?: 'unknown'
|
||||
def user = session['ch.adnovum.nevisidm.user.extId'] ?: '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'
|
||||
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
|
||||
String clientExtId = session.get('ch.adnovum.nevisidm.user.clientExtId')
|
||||
String userExtId = session.get('ch.adnovum.nevisidm.user.extId')
|
||||
String mobile = session.get('ch.nevis.idm.User.mobile')
|
||||
|
||||
String baseUrl = parameters.get('baseUrl')
|
||||
String endPoint = "${baseUrl}/core/v1/${clientExtId}/users/${userExtId}"
|
||||
|
||||
|
||||
if (!(parameters.get('ask_mobile_number_enabled')?.toLowerCase()?.trim() == "true")) {
|
||||
LOG.debug("Feature 'ask mobile number' is disabled")
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
|
||||
if (mobile) {
|
||||
LOG.debug("User '${user}' has already registered a mobile number")
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
|
||||
def agovSkipAskingMobileCookieValue = 'missing'
|
||||
|
||||
if (getHeader('cookie') != null) {
|
||||
def cookies = getHeader('cookie')
|
||||
if (cookies.matches('^.*agovSkipAskingMobile=([^;]+).*$')) {
|
||||
agovSkipAskingMobileCookieValue = cookies.replaceAll('^.*agovSkipAskingMobile=([^;]+).*$', '$1')
|
||||
}
|
||||
}
|
||||
if (agovSkipAskingMobileCookieValue == 'true') {
|
||||
// Don't aske the user again...
|
||||
LOG.info("Event='SKIPPEDMOBILENUMBER', Requester='${requester}', RequestId='${requestId}', User=${user}, SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!inargs['submit'] && (!inargs['mobile'] || !inargs['mobile'].isEmpty()) && inargs['language'] && inargs['language'] != session['ch.nevis.session.user.language']) {
|
||||
// language switch, nothing else to do, just display again the GUI
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['submit'] && (!inargs['mobile'] || inargs['mobile'].isEmpty()) && inargs['skip']) {
|
||||
// no mobile, and user wants to skip it
|
||||
|
||||
LOG.info("Event='NOMOBILENUMBER', Requester='${requester}', RequestId='${requestId}', User=${user}, SourceIp=${sourceIp}, UserAgent='${userAgent}', Persistent='${ inargs['skip'] == 'persistent' ? true : false }'")
|
||||
|
||||
if (inargs['skip'] == 'persistent') {
|
||||
// persistent cookie for 30d;
|
||||
def agovSkipAskingMobileCookie = "agovSkipAskingMobile=true; Domain=${parameters.get('cookie.domain')}; Path=/; Max-Age=2592000; SameSite=Strict; Secure; HttpOnly"
|
||||
// setHeader doesn't support multiple headers with the same name, so we use
|
||||
// a different one, and rewrite it in the proxy with Lua
|
||||
response.setHeader('Set-Cookie2', agovSkipAskingMobileCookie)
|
||||
}
|
||||
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (inargs['submit'] && inargs['mobile'] && !inargs['mobile'].isEmpty()) {
|
||||
// IMPORTANT/haburger/2024-DEC-09: the pattern must be the same as ch.adnovum.agov.common.util.InputPatterns.PHONE_NUMBER_PATTERN
|
||||
if (!inargs['mobile'].replaceAll('\\s', '').matches('^(?:\\+[0-9]+)?$')) {
|
||||
LOG.warn("Event='MOBILEFAILED', Requester='${requester}', RequestId='${requestId}', User=${user}, SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='User provided invalid number (${inargs['mobile']})'")
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
String result
|
||||
// mobile is also stored without spaces
|
||||
def patchBdy = "{\"contacts\":{\"mobile\":\"${inargs['mobile'].replaceAll('\\s', '')}\"},\"modificationComment\":\"added mobile number from user during request ${requestId}\"}"
|
||||
try {
|
||||
result = idmRestClient.patch(endPoint, patchBdy)
|
||||
} catch(Exception e) {
|
||||
LOG.warn("Event='MOBILEFAILED', Requester='${requester}', RequestId='${requestId}', User=${user}, SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='failed to save number (${e})'")
|
||||
}
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// we should ask the user
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
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.xml.XmlSlurper
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
|
||||
// Accounting
|
||||
def requester = session['ch.nevis.auth.saml.request.scoping.requesterId'] ?: 'unknown'
|
||||
def requestId = session['ch.nevis.auth.saml.request.id'] ?: 'unknown'
|
||||
def user = session['ch.adnovum.nevisidm.user.extId'] ?: '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'
|
||||
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
|
||||
String clientExtId = session.get('ch.adnovum.nevisidm.user.clientExtId')
|
||||
String userExtId = session.get('ch.adnovum.nevisidm.user.extId')
|
||||
String mobile = session.get('ch.nevis.idm.User.mobile')
|
||||
|
||||
String baseUrl = parameters.get('baseUrl')
|
||||
String endPoint = "${baseUrl}/core/v1/${clientExtId}/users/${userExtId}"
|
||||
|
||||
|
||||
if (!(parameters.get('ask_mobile_number_enabled')?.toLowerCase()?.trim() == "true")) {
|
||||
LOG.debug("Feature 'ask mobile number' is disabled")
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
|
||||
if (mobile) {
|
||||
LOG.debug("User '${user}' has already registered a mobile number")
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
|
||||
def agovSkipAskingMobileCookieValue = 'missing'
|
||||
|
||||
if (getHeader('cookie') != null) {
|
||||
def cookies = getHeader('cookie')
|
||||
if (cookies.matches('^.*agovSkipAskingMobile=([^;]+).*$')) {
|
||||
agovSkipAskingMobileCookieValue = cookies.replaceAll('^.*agovSkipAskingMobile=([^;]+).*$', '$1')
|
||||
}
|
||||
}
|
||||
if (agovSkipAskingMobileCookieValue == 'true') {
|
||||
// Don't aske the user again...
|
||||
LOG.info("Event='SKIPPEDMOBILENUMBER', Requester='${requester}', RequestId='${requestId}', User=${user}, SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!inargs['submit'] && (!inargs['mobile'] || !inargs['mobile'].isEmpty()) && inargs['language'] && inargs['language'] != session['ch.nevis.session.user.language']) {
|
||||
// language switch, nothing else to do, just display again the GUI
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['submit'] && (!inargs['mobile'] || inargs['mobile'].isEmpty()) && inargs['skip']) {
|
||||
// no mobile, and user wants to skip it
|
||||
|
||||
LOG.info("Event='NOMOBILENUMBER', Requester='${requester}', RequestId='${requestId}', User=${user}, SourceIp=${sourceIp}, UserAgent='${userAgent}', Persistent='${ inargs['skip'] == 'persistent' ? true : false }'")
|
||||
|
||||
if (inargs['skip'] == 'persistent') {
|
||||
// persistent cookie for 30d;
|
||||
def agovSkipAskingMobileCookie = "agovSkipAskingMobile=true; Domain=${parameters.get('cookie.domain')}; Path=/; Max-Age=2592000; SameSite=Strict; Secure; HttpOnly"
|
||||
// setHeader doesn't support multiple headers with the same name, so we use
|
||||
// a different one, and rewrite it in the proxy with Lua
|
||||
response.setHeader('Set-Cookie2', agovSkipAskingMobileCookie)
|
||||
}
|
||||
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (inargs['submit'] && inargs['mobile'] && !inargs['mobile'].isEmpty()) {
|
||||
// IMPORTANT/haburger/2024-DEC-09: the pattern must be the same as ch.adnovum.agov.common.util.InputPatterns.PHONE_NUMBER_PATTERN
|
||||
if (!inargs['mobile'].replaceAll('\\s', '').matches('^(?:\\+[0-9]+)?$')) {
|
||||
LOG.warn("Event='MOBILEFAILED', Requester='${requester}', RequestId='${requestId}', User=${user}, SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='User provided invalid number (${inargs['mobile']})'")
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
String result
|
||||
// mobile is also stored without spaces
|
||||
def patchBdy = "{\"contacts\":{\"mobile\":\"${inargs['mobile'].replaceAll('\\s', '')}\"},\"modificationComment\":\"added mobile number from user during request ${requestId}\"}"
|
||||
try {
|
||||
result = idmRestClient.patch(endPoint, patchBdy)
|
||||
} catch(Exception e) {
|
||||
LOG.warn("Event='MOBILEFAILED', Requester='${requester}', RequestId='${requestId}', User=${user}, SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='failed to save number (${e})'")
|
||||
}
|
||||
response.setResult('done')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// we should ask the user
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
|
|
|
@ -1,180 +1,180 @@
|
|||
boolean isEnabled() {
|
||||
def paths = parameters.get("paths")
|
||||
if (paths && !paths.isEmpty()) {
|
||||
for (path in paths.split(',')) {
|
||||
String url = request.currentResource
|
||||
if (url.matches(path)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
boolean isLevel(String role) {
|
||||
if (role != null && role.isNumber()) {
|
||||
def number = Integer.parseInt(role)
|
||||
if (number > 0 && number <= 9) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
int getCurrentLevel() {
|
||||
int level = 1 // level 1 is reached by definition on successful authentication
|
||||
// levels are stored as roles once the authentication is done
|
||||
for (String role : response.getActualRoles()) {
|
||||
if (isLevel(role)) {
|
||||
Integer number = Integer.parseInt(role)
|
||||
if (number > level) {
|
||||
level = number
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG.debug("current level: $level")
|
||||
return level
|
||||
}
|
||||
|
||||
Integer getRequestedLevel() {
|
||||
// try to determine required level based on SAML request (SP-initiated)
|
||||
def context = session['ch.nevis.auth.saml.request.authnContextClassRef']
|
||||
if (context == null) {
|
||||
// this is expected for non-Nevis SAML partners
|
||||
LOG.debug("unable to determine required authentication level: no AuthnContext")
|
||||
return null
|
||||
}
|
||||
String prefix = 'urn:nevis:level:'
|
||||
Integer level = null
|
||||
if (context.contains(prefix)) {
|
||||
def start = context.indexOf(prefix) // the prefix can appear anywhere in the context but only once
|
||||
def remainder = context.substring(start + prefix.length())
|
||||
for (String candidate : remainder.split(',')) {
|
||||
if (!candidate.isNumber()) {
|
||||
continue // must be an actual role
|
||||
}
|
||||
def number = Integer.parseInt(candidate)
|
||||
if (level == null || number < level) {
|
||||
level = number
|
||||
}
|
||||
}
|
||||
}
|
||||
if (level == null) {
|
||||
// an AuthnContext has been sent but it does not contain the required authentication level
|
||||
LOG.debug("unable to determine required authentication level from request: $context")
|
||||
}
|
||||
else {
|
||||
LOG.info("extracted required authentication level from request: $context -> $level")
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
Integer getRequiredLevel(levels, String issuer) {
|
||||
// try to determine required level based on request
|
||||
def level = getRequestedLevel()
|
||||
if (level != null) {
|
||||
LOG.info("required authentication level from request: $level")
|
||||
return level
|
||||
}
|
||||
// else determine required level based on configuration (IDP-initiated or no authnContextClassRef sent)
|
||||
if (issuer != null && levels.containsKey(issuer)) {
|
||||
level = levels[issuer]
|
||||
LOG.debug("required authentication level for issuer $issuer defined as $level")
|
||||
return level
|
||||
}
|
||||
// else return null
|
||||
LOG.debug("required authentication level for issuer $issuer is not defined")
|
||||
return null
|
||||
}
|
||||
|
||||
void setAuthnContext() {
|
||||
def parts = [] as Set
|
||||
def authLevel = response.getAuthLevel()
|
||||
if (authLevel != null) {
|
||||
if (isLevel(authLevel)) {
|
||||
parts.add("urn:nevis:level:$authLevel")
|
||||
}
|
||||
else { // might be legacy auth.weak / auth.strong
|
||||
parts.add(authLevel)
|
||||
}
|
||||
}
|
||||
for (String role : response.getActualRoles()) {
|
||||
if (isLevel(role)) { // previous authLevels might have been added to the roles already
|
||||
parts.add("urn:nevis:level:$role")
|
||||
}
|
||||
// levels can also be normal roles so we add them always
|
||||
parts.add(role)
|
||||
}
|
||||
def value = parts.sort().join(",")
|
||||
LOG.debug("calculated AuthnContextClassRef for SAML Response: $value")
|
||||
session['saml.idp.response.authncontext'] = value
|
||||
}
|
||||
|
||||
boolean stepupRequired(levels, String issuer) {
|
||||
|
||||
Integer requiredLevel = getRequiredLevel(levels, issuer)
|
||||
if (requiredLevel == null) {
|
||||
LOG.info("unable to determine required authentication level for request from issuer $issuer")
|
||||
setAuthnContext()
|
||||
return false
|
||||
}
|
||||
|
||||
Integer currentLevel = getCurrentLevel()
|
||||
if (currentLevel >= requiredLevel) {
|
||||
LOG.info("required authentication level $requiredLevel has been reached (current level $currentLevel)")
|
||||
setAuthnContext()
|
||||
return false
|
||||
}
|
||||
|
||||
LOG.info("required authentication level $requiredLevel has not been reached (current level $currentLevel) - session upgrade needed")
|
||||
request.setRequiredRoles("$requiredLevel")
|
||||
return true
|
||||
}
|
||||
|
||||
boolean hasAnyRequiredRole(i2r, issuer) {
|
||||
if (issuer != null && i2r.containsKey(issuer)) {
|
||||
def roles = i2r[issuer]
|
||||
for (role in response.getActualRoles()) {
|
||||
if (roles.contains(role)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEnabled()) {
|
||||
LOG.info("skipping SAML authorization checks.")
|
||||
response.setResult('ok') // skip execution
|
||||
return
|
||||
}
|
||||
|
||||
// issuer set by IdentityProviderState (SP-initiated)
|
||||
def issuer = session['ch.nevis.auth.saml.request.issuer']
|
||||
|
||||
// issuer to minimum required authentication level
|
||||
def i2l = [:]
|
||||
|
||||
|
||||
if (stepupRequired(i2l, issuer)) {
|
||||
LOG.info("authentication level stepup required.")
|
||||
response.setResult("stepup")
|
||||
return // we are done for now
|
||||
}
|
||||
|
||||
// issuer to list of required roles
|
||||
def i2r = [:]
|
||||
|
||||
|
||||
// issuer to ResultCond name
|
||||
def i2e = [:]
|
||||
i2e.put('https://trustbroker.agov-epr-lab.azure.adnovum.net', 'forbidden_0')
|
||||
i2e.put('https://trustbroker-idp.agov-epr-lab.azure.adnovum.net', 'forbidden_1')
|
||||
|
||||
|
||||
if (!i2r.isEmpty() && !hasAnyRequiredRole(i2r, issuer)) {
|
||||
LOG.info("required roles check failed.")
|
||||
response.setResult(i2e[issuer])
|
||||
return // we are done
|
||||
}
|
||||
|
||||
boolean isEnabled() {
|
||||
def paths = parameters.get("paths")
|
||||
if (paths && !paths.isEmpty()) {
|
||||
for (path in paths.split(',')) {
|
||||
String url = request.currentResource
|
||||
if (url.matches(path)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
boolean isLevel(String role) {
|
||||
if (role != null && role.isNumber()) {
|
||||
def number = Integer.parseInt(role)
|
||||
if (number > 0 && number <= 9) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
int getCurrentLevel() {
|
||||
int level = 1 // level 1 is reached by definition on successful authentication
|
||||
// levels are stored as roles once the authentication is done
|
||||
for (String role : response.getActualRoles()) {
|
||||
if (isLevel(role)) {
|
||||
Integer number = Integer.parseInt(role)
|
||||
if (number > level) {
|
||||
level = number
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG.debug("current level: $level")
|
||||
return level
|
||||
}
|
||||
|
||||
Integer getRequestedLevel() {
|
||||
// try to determine required level based on SAML request (SP-initiated)
|
||||
def context = session['ch.nevis.auth.saml.request.authnContextClassRef']
|
||||
if (context == null) {
|
||||
// this is expected for non-Nevis SAML partners
|
||||
LOG.debug("unable to determine required authentication level: no AuthnContext")
|
||||
return null
|
||||
}
|
||||
String prefix = 'urn:nevis:level:'
|
||||
Integer level = null
|
||||
if (context.contains(prefix)) {
|
||||
def start = context.indexOf(prefix) // the prefix can appear anywhere in the context but only once
|
||||
def remainder = context.substring(start + prefix.length())
|
||||
for (String candidate : remainder.split(',')) {
|
||||
if (!candidate.isNumber()) {
|
||||
continue // must be an actual role
|
||||
}
|
||||
def number = Integer.parseInt(candidate)
|
||||
if (level == null || number < level) {
|
||||
level = number
|
||||
}
|
||||
}
|
||||
}
|
||||
if (level == null) {
|
||||
// an AuthnContext has been sent but it does not contain the required authentication level
|
||||
LOG.debug("unable to determine required authentication level from request: $context")
|
||||
}
|
||||
else {
|
||||
LOG.info("extracted required authentication level from request: $context -> $level")
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
Integer getRequiredLevel(levels, String issuer) {
|
||||
// try to determine required level based on request
|
||||
def level = getRequestedLevel()
|
||||
if (level != null) {
|
||||
LOG.info("required authentication level from request: $level")
|
||||
return level
|
||||
}
|
||||
// else determine required level based on configuration (IDP-initiated or no authnContextClassRef sent)
|
||||
if (issuer != null && levels.containsKey(issuer)) {
|
||||
level = levels[issuer]
|
||||
LOG.debug("required authentication level for issuer $issuer defined as $level")
|
||||
return level
|
||||
}
|
||||
// else return null
|
||||
LOG.debug("required authentication level for issuer $issuer is not defined")
|
||||
return null
|
||||
}
|
||||
|
||||
void setAuthnContext() {
|
||||
def parts = [] as Set
|
||||
def authLevel = response.getAuthLevel()
|
||||
if (authLevel != null) {
|
||||
if (isLevel(authLevel)) {
|
||||
parts.add("urn:nevis:level:$authLevel")
|
||||
}
|
||||
else { // might be legacy auth.weak / auth.strong
|
||||
parts.add(authLevel)
|
||||
}
|
||||
}
|
||||
for (String role : response.getActualRoles()) {
|
||||
if (isLevel(role)) { // previous authLevels might have been added to the roles already
|
||||
parts.add("urn:nevis:level:$role")
|
||||
}
|
||||
// levels can also be normal roles so we add them always
|
||||
parts.add(role)
|
||||
}
|
||||
def value = parts.sort().join(",")
|
||||
LOG.debug("calculated AuthnContextClassRef for SAML Response: $value")
|
||||
session['saml.idp.response.authncontext'] = value
|
||||
}
|
||||
|
||||
boolean stepupRequired(levels, String issuer) {
|
||||
|
||||
Integer requiredLevel = getRequiredLevel(levels, issuer)
|
||||
if (requiredLevel == null) {
|
||||
LOG.info("unable to determine required authentication level for request from issuer $issuer")
|
||||
setAuthnContext()
|
||||
return false
|
||||
}
|
||||
|
||||
Integer currentLevel = getCurrentLevel()
|
||||
if (currentLevel >= requiredLevel) {
|
||||
LOG.info("required authentication level $requiredLevel has been reached (current level $currentLevel)")
|
||||
setAuthnContext()
|
||||
return false
|
||||
}
|
||||
|
||||
LOG.info("required authentication level $requiredLevel has not been reached (current level $currentLevel) - session upgrade needed")
|
||||
request.setRequiredRoles("$requiredLevel")
|
||||
return true
|
||||
}
|
||||
|
||||
boolean hasAnyRequiredRole(i2r, issuer) {
|
||||
if (issuer != null && i2r.containsKey(issuer)) {
|
||||
def roles = i2r[issuer]
|
||||
for (role in response.getActualRoles()) {
|
||||
if (roles.contains(role)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEnabled()) {
|
||||
LOG.info("skipping SAML authorization checks.")
|
||||
response.setResult('ok') // skip execution
|
||||
return
|
||||
}
|
||||
|
||||
// issuer set by IdentityProviderState (SP-initiated)
|
||||
def issuer = session['ch.nevis.auth.saml.request.issuer']
|
||||
|
||||
// issuer to minimum required authentication level
|
||||
def i2l = [:]
|
||||
|
||||
|
||||
if (stepupRequired(i2l, issuer)) {
|
||||
LOG.info("authentication level stepup required.")
|
||||
response.setResult("stepup")
|
||||
return // we are done for now
|
||||
}
|
||||
|
||||
// issuer to list of required roles
|
||||
def i2r = [:]
|
||||
|
||||
|
||||
// issuer to ResultCond name
|
||||
def i2e = [:]
|
||||
i2e.put('https://trustbroker.agov-epr-lab.azure.adnovum.net', 'forbidden_0')
|
||||
i2e.put('https://trustbroker-idp.agov-epr-lab.azure.adnovum.net', 'forbidden_1')
|
||||
|
||||
|
||||
if (!i2r.isEmpty() && !hasAnyRequiredRole(i2r, issuer)) {
|
||||
LOG.info("required roles check failed.")
|
||||
response.setResult(i2e[issuer])
|
||||
return // we are done
|
||||
}
|
||||
|
||||
response.setResult('ok')
|
|
@ -1,285 +1,285 @@
|
|||
import org.codehaus.groovy.runtime.StackTraceUtils
|
||||
import groovy.xml.XmlSlurper
|
||||
|
||||
def getUserAGOVLoiRoles() {
|
||||
// we take the roles from actualRoles
|
||||
return request.getActualRoles().findAll { role -> role.startsWith('AGOV-Loi.') }.collect({ role -> role.substring(9) })
|
||||
}
|
||||
|
||||
def getUserAGOVRecoveryRoles() {
|
||||
// set attibutes from DTO: -> AGOV
|
||||
def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
return list.'**'.findAll { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' }.collect({ node -> node.name.text() })
|
||||
}
|
||||
|
||||
def getUserAGOVLoiIdVerification() {
|
||||
// set attibutes from DTO: -> idVerification
|
||||
def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
return list.'**'.findAll {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text().contains('AGOV-Loi,')}.collect({ node -> node.value.text()})
|
||||
}
|
||||
|
||||
def getUserAGOVLoiIdVerification(level) {
|
||||
// set attibutes from DTO: -> idVerification
|
||||
def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
return list.'**'.findAll {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,level' + level}.collect({ node -> node.value.text()})
|
||||
}
|
||||
|
||||
def getUserAGOVLoiValidFrom(level) {
|
||||
// set attibutes from DTO: -> validFrom
|
||||
def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
return payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == level}?.validFrom?.text()
|
||||
}
|
||||
|
||||
def getUserAGOVLoiValidTo(level) {
|
||||
// set attibutes from DTO: -> validTo
|
||||
def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
return payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == level}?.validTo?.text()
|
||||
}
|
||||
|
||||
def getUserIdVerificationForRecovery() {
|
||||
// application is AGOV-AccountStatus
|
||||
def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
def result = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-AccountStatus,mustRecover'}?.value?.text()
|
||||
|
||||
if (!result) {
|
||||
// fallback if not explicitly set
|
||||
def currentLoaRole = getUserAGOVLoiRoles()?.sort()?.last() ?: 'level100'
|
||||
def chDomicile = list.country.text() == 'ch'
|
||||
def lastIdVerification = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + currentLoaRole}?.value?.text()
|
||||
switch (currentLoaRole) {
|
||||
case 'level100':
|
||||
result = chDomicile ? 'SimpleLetter' : 'Video'
|
||||
break
|
||||
case 'level200':
|
||||
result = chDomicile ? 'Bmid' : 'Video'
|
||||
break
|
||||
case 'level300':
|
||||
case 'level400':
|
||||
result = chDomicile ? lastIdVerification : 'Video'
|
||||
break
|
||||
default:
|
||||
LOG.warn("unexpected loa on account: ${currentLoaRole}")
|
||||
// safest default, should work in any case
|
||||
result = 'Video'
|
||||
}
|
||||
LOG.warn("Recovery method not set, choosing ${result} (based on currentLoad: ${currentLoaRole}, CH-domicile: ${chDomicile}, last verification method: ${lastIdVerification})")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def getAqLevelBasedOnIdVerificationForRecovery(idVerification, highestRoleLevelNumber) {
|
||||
def result = 'urn:qa.agov.ch:names:tc:ac:classes:'
|
||||
|
||||
switch (idVerification) {
|
||||
case 'None':
|
||||
result = result.concat('100')
|
||||
break
|
||||
case 'SimpleLetter':
|
||||
result = result.concat('200')
|
||||
break
|
||||
case 'Video':
|
||||
case 'VideoSelfPaid':
|
||||
case 'Bmid':
|
||||
case 'BmidSelfPaid':
|
||||
case 'Counter':
|
||||
result = result.concat((highestRoleLevelNumber == 400) ? '400' : '300')
|
||||
break
|
||||
case 'Eid':
|
||||
result = result.concat('400')
|
||||
break
|
||||
default:
|
||||
LOG.warn("unexpected idVerification for recovery on account: ${idVerification}")
|
||||
// safest default, should work in any case
|
||||
result = result.concat('' + highestRoleLevelNumber)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
def getUserMustRecoverValidFrom() {
|
||||
// set attibutes from DTO: -> validFrom
|
||||
def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
def authzNode = payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == 'mustRecover'}
|
||||
return (authzNode) ? ((authzNode.validFrom && !authzNode.validFrom.text().isEmpty()) ? authzNode.validFrom?.text() : authzNode.ctlCreDat?.text()) : ''
|
||||
}
|
||||
|
||||
// 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'
|
||||
|
||||
try {
|
||||
// beef
|
||||
def s = request.getAuthSession(true)
|
||||
def highestRoleLevelNumber = 0
|
||||
|
||||
if (!session.get('agov.requestedRoleLevel')) {
|
||||
LOG.error("IDP: internal error: agov.requestedRoleLevel not set in session")
|
||||
response.setResult('error');
|
||||
return
|
||||
}
|
||||
def requestedRoleLevelNumber = session.get('agov.requestedRoleLevel').toInteger()
|
||||
|
||||
def authenticationMethod = session.get('authenticatedWith')
|
||||
if (!authenticationMethod) {
|
||||
LOG.error("IDP: internal error: authenticationMethod not set in session")
|
||||
response.setResult('error');
|
||||
return
|
||||
}
|
||||
|
||||
// data transformations needed for SAML and OIDC
|
||||
// Transform sex to number
|
||||
if(session.get('ch.nevis.idm.User.gender') == 'MALE'){
|
||||
s.setAttribute('ch.nevis.idm.User.gender', '1')
|
||||
}
|
||||
if(session.get('ch.nevis.idm.User.gender') == 'FEMALE'){
|
||||
s.setAttribute('ch.nevis.idm.User.gender', '2')
|
||||
}
|
||||
if(s.get('ch.nevis.idm.User.gender') == 'OTHER'){
|
||||
s.setAttribute('ch.nevis.idm.User.gender', '3')
|
||||
}
|
||||
|
||||
|
||||
// handle accounts qa attributes, and set them in session
|
||||
// account itself, only needed if not authenticated with e-ID
|
||||
if (!'urn:qa.agov.ch:names:tc:authfactor:eid'.equalsIgnoreCase(authenticationMethod)) {
|
||||
def idVerificationList = getUserAGOVLoiIdVerification()
|
||||
def idVerification = 'None'
|
||||
if (idVerificationList && !idVerificationList.isEmpty()) {
|
||||
idVerification = idVerificationList.last()
|
||||
}
|
||||
s.setAttribute('idVerification', idVerification)
|
||||
|
||||
// contextClassRefToSet based on highest level-role assigned to default profile
|
||||
for (String role : getUserAGOVLoiRoles()) {
|
||||
if (role.startsWith('level')) {
|
||||
def roleLevel = role.substring(5)
|
||||
int roleLevelNumber = Integer.parseInt(roleLevel)
|
||||
|
||||
if (highestRoleLevelNumber< roleLevelNumber) {
|
||||
highestRoleLevelNumber=roleLevelNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG.debug('CheckLoa: Highest role Level ' + highestRoleLevelNumber.toString() +' contextclassref ' + requestedRoleLevelNumber.toString())
|
||||
LOG.debug('CheckLoa: Compare ' + (highestRoleLevelNumber>=requestedRoleLevelNumber))
|
||||
|
||||
//set attribute Actual Role Level
|
||||
s.setAttribute('agov.actualRoleLevel', '' + highestRoleLevelNumber)
|
||||
LOG.debug('CheckLoa: actual role level (agov) '+ highestRoleLevelNumber)
|
||||
|
||||
// set attribute ValidFrom and ValidTo (only for higher than 100)
|
||||
if (highestRoleLevelNumber > 100) {
|
||||
def validFrom = getUserAGOVLoiValidFrom('level'.concat(highestRoleLevelNumber.toString()))
|
||||
def validTo = getUserAGOVLoiValidTo('level'.concat(highestRoleLevelNumber.toString()))
|
||||
|
||||
LOG.debug('CheckLoa: ValidFrom :' + validFrom)
|
||||
LOG.debug('CheckLoa: ValidTo :' + validTo)
|
||||
|
||||
if(validFrom != '') {
|
||||
s.setAttribute('ValidFrom', '' + validFrom)
|
||||
}
|
||||
if(validTo != '') {
|
||||
s.setAttribute('ValidTo', '' + validTo)
|
||||
}
|
||||
}
|
||||
if (highestRoleLevelNumber > 0) {
|
||||
// set attribute contextClassRefToSet
|
||||
s.setAttribute('contextClassRefToSet','urn:qa.agov.ch:names:tc:ac:classes:' .concat(highestRoleLevelNumber.toString()))
|
||||
} else {
|
||||
// by default 100
|
||||
s.setAttribute('contextClassRefToSet','urn:qa.agov.ch:names:tc:ac:classes:100' )
|
||||
}
|
||||
}
|
||||
// address related, needed in any case (also e-ID)
|
||||
def adressVerificationList = getUserAGOVLoiIdVerification('200')
|
||||
def adressVerification = 'None'
|
||||
if (adressVerificationList && !adressVerificationList.isEmpty()) {
|
||||
adressVerification = adressVerificationList[0]
|
||||
}
|
||||
s.setAttribute('agov.adressVerification', '' + adressVerification)
|
||||
|
||||
if (!session.get('ch.adnovum.nevisidm.profileExtId')) {
|
||||
LOG.error("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', errorMessage='Account without Profile', SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
|
||||
// if the account has no profile, we must not return address or svnr
|
||||
s.setAttribute('agov.appAddressRequired', 'false')
|
||||
s.setAttribute('agov.appSvnrAllowed', 'false')
|
||||
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
// no login for users with a recovery role (but onyl when not logging in with e-Id)
|
||||
// TODO/haburger/2025-07-01: automatic recovery if logging in with e-Id
|
||||
if (!'urn:qa.agov.ch:names:tc:authfactor:eid'.equalsIgnoreCase(authenticationMethod)) {
|
||||
// no login for users with a recovery role
|
||||
def recoveryRoleList = getUserAGOVRecoveryRoles()
|
||||
|
||||
if (recoveryRoleList.contains('mustRecover')) {
|
||||
s.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:mustRecover')
|
||||
s.setAttribute('agov.recovery.authenticatedWith', session.get('authenticatedWith') ?: 'unknown' )
|
||||
|
||||
def origIdVerification = getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString()) ?: 'None'
|
||||
def idVerification = getUserIdVerificationForRecovery() ?: origIdVerification
|
||||
s.setAttribute('agov.recovery.currentIdVerification', '' + idVerification )
|
||||
|
||||
// align currentAgovAq with the method selected for idVerification
|
||||
def currentAgovAqForRecovery = getAqLevelBasedOnIdVerificationForRecovery(idVerification, highestRoleLevelNumber)
|
||||
s.setAttribute('agov.recovery.currentAgovAq', '' + currentAgovAqForRecovery)
|
||||
|
||||
def validFrom = getUserMustRecoverValidFrom() ?: ''
|
||||
s.setAttribute('agov.recovery.currentAgovAqRoleValidFrom', '' + validFrom )
|
||||
|
||||
LOG.debug("CheckLoa: mustRecover: origIdVerification=${origIdVerification}, idVerification=${idVerification}, currentAgovAqForRecovery=${currentAgovAqForRecovery}")
|
||||
|
||||
response.setResult('exit.2')
|
||||
return
|
||||
|
||||
} else if (recoveryRoleList.contains('recovery')) {
|
||||
if (recoveryRoleList.contains('recoveryCascade')) {
|
||||
s.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recoveryCascade')
|
||||
} else {
|
||||
s.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recovery')
|
||||
}
|
||||
s.setAttribute('agov.recovery.authenticatedWith', session.get('authenticatedWith') ?: 'unknown')
|
||||
s.setAttribute('agov.recovery.currentAgovAq', session.get('contextClassRefToSet') ?: 'urn:qa.agov.ch:names:tc:ac:classes:100' )
|
||||
LOG.debug('CheckLoa: idVerification2= '+ getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString()))
|
||||
def idVerification = getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString())
|
||||
s.setAttribute('agov.recovery.currentIdVerification', (idVerification.isEmpty() ? 'None' : idVerification.first()))
|
||||
def validFrom = getUserAGOVLoiValidFrom('level'.concat(highestRoleLevelNumber.toString())) ?: ''
|
||||
s.setAttribute('agov.recovery.currentAgovAqRoleValidFrom', validFrom)
|
||||
|
||||
response.setResult('exit.2')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// authenticated with e-ID, we adjust highestRoleLevelNumber to e-ID login
|
||||
highestRoleLevelNumber = 500
|
||||
s.setAttribute('agov.actualRoleLevel', '' + highestRoleLevelNumber)
|
||||
LOG.debug('CheckLoa: actual role level (agov) '+ highestRoleLevelNumber)
|
||||
}
|
||||
|
||||
// verifiy that AQ level is high enough
|
||||
if (highestRoleLevelNumber>=requestedRoleLevelNumber) {
|
||||
response.setResult('ok')
|
||||
return;
|
||||
} else {
|
||||
// Insufficient_LoaInfo
|
||||
response.setResult('exit.1');
|
||||
return;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', errorMessage='exception occured: ${ex}', SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
ex = StackTraceUtils.sanitize(ex)
|
||||
def affectedLines = ex.stackTrace.findAll { it.className.startsWith('Script') }.collect { "${it.methodName}:${it.lineNumber}" }
|
||||
LOG.error("FATAL: Script failure (at lines: ${affectedLines})", ex)
|
||||
// AuthnFailed_Zero_RoleLvl
|
||||
response.setResult('error');
|
||||
return;
|
||||
}
|
||||
import org.codehaus.groovy.runtime.StackTraceUtils
|
||||
import groovy.xml.XmlSlurper
|
||||
|
||||
def getUserAGOVLoiRoles() {
|
||||
// we take the roles from actualRoles
|
||||
return request.getActualRoles().findAll { role -> role.startsWith('AGOV-Loi.') }.collect({ role -> role.substring(9) })
|
||||
}
|
||||
|
||||
def getUserAGOVRecoveryRoles() {
|
||||
// set attibutes from DTO: -> AGOV
|
||||
def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
return list.'**'.findAll { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' }.collect({ node -> node.name.text() })
|
||||
}
|
||||
|
||||
def getUserAGOVLoiIdVerification() {
|
||||
// set attibutes from DTO: -> idVerification
|
||||
def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
return list.'**'.findAll {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text().contains('AGOV-Loi,')}.collect({ node -> node.value.text()})
|
||||
}
|
||||
|
||||
def getUserAGOVLoiIdVerification(level) {
|
||||
// set attibutes from DTO: -> idVerification
|
||||
def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
return list.'**'.findAll {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,level' + level}.collect({ node -> node.value.text()})
|
||||
}
|
||||
|
||||
def getUserAGOVLoiValidFrom(level) {
|
||||
// set attibutes from DTO: -> validFrom
|
||||
def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
return payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == level}?.validFrom?.text()
|
||||
}
|
||||
|
||||
def getUserAGOVLoiValidTo(level) {
|
||||
// set attibutes from DTO: -> validTo
|
||||
def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
return payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == level}?.validTo?.text()
|
||||
}
|
||||
|
||||
def getUserIdVerificationForRecovery() {
|
||||
// application is AGOV-AccountStatus
|
||||
def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
def result = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-AccountStatus,mustRecover'}?.value?.text()
|
||||
|
||||
if (!result) {
|
||||
// fallback if not explicitly set
|
||||
def currentLoaRole = getUserAGOVLoiRoles()?.sort()?.last() ?: 'level100'
|
||||
def chDomicile = list.country.text() == 'ch'
|
||||
def lastIdVerification = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + currentLoaRole}?.value?.text()
|
||||
switch (currentLoaRole) {
|
||||
case 'level100':
|
||||
result = chDomicile ? 'SimpleLetter' : 'Video'
|
||||
break
|
||||
case 'level200':
|
||||
result = chDomicile ? 'Bmid' : 'Video'
|
||||
break
|
||||
case 'level300':
|
||||
case 'level400':
|
||||
result = chDomicile ? lastIdVerification : 'Video'
|
||||
break
|
||||
default:
|
||||
LOG.warn("unexpected loa on account: ${currentLoaRole}")
|
||||
// safest default, should work in any case
|
||||
result = 'Video'
|
||||
}
|
||||
LOG.warn("Recovery method not set, choosing ${result} (based on currentLoad: ${currentLoaRole}, CH-domicile: ${chDomicile}, last verification method: ${lastIdVerification})")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def getAqLevelBasedOnIdVerificationForRecovery(idVerification, highestRoleLevelNumber) {
|
||||
def result = 'urn:qa.agov.ch:names:tc:ac:classes:'
|
||||
|
||||
switch (idVerification) {
|
||||
case 'None':
|
||||
result = result.concat('100')
|
||||
break
|
||||
case 'SimpleLetter':
|
||||
result = result.concat('200')
|
||||
break
|
||||
case 'Video':
|
||||
case 'VideoSelfPaid':
|
||||
case 'Bmid':
|
||||
case 'BmidSelfPaid':
|
||||
case 'Counter':
|
||||
result = result.concat((highestRoleLevelNumber == 400) ? '400' : '300')
|
||||
break
|
||||
case 'Eid':
|
||||
result = result.concat('400')
|
||||
break
|
||||
default:
|
||||
LOG.warn("unexpected idVerification for recovery on account: ${idVerification}")
|
||||
// safest default, should work in any case
|
||||
result = result.concat('' + highestRoleLevelNumber)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
def getUserMustRecoverValidFrom() {
|
||||
// set attibutes from DTO: -> validFrom
|
||||
def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
def authzNode = payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == 'mustRecover'}
|
||||
return (authzNode) ? ((authzNode.validFrom && !authzNode.validFrom.text().isEmpty()) ? authzNode.validFrom?.text() : authzNode.ctlCreDat?.text()) : ''
|
||||
}
|
||||
|
||||
// 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'
|
||||
|
||||
try {
|
||||
// beef
|
||||
def s = request.getAuthSession(true)
|
||||
def highestRoleLevelNumber = 0
|
||||
|
||||
if (!session.get('agov.requestedRoleLevel')) {
|
||||
LOG.error("IDP: internal error: agov.requestedRoleLevel not set in session")
|
||||
response.setResult('error');
|
||||
return
|
||||
}
|
||||
def requestedRoleLevelNumber = session.get('agov.requestedRoleLevel').toInteger()
|
||||
|
||||
def authenticationMethod = session.get('authenticatedWith')
|
||||
if (!authenticationMethod) {
|
||||
LOG.error("IDP: internal error: authenticationMethod not set in session")
|
||||
response.setResult('error');
|
||||
return
|
||||
}
|
||||
|
||||
// data transformations needed for SAML and OIDC
|
||||
// Transform sex to number
|
||||
if(session.get('ch.nevis.idm.User.gender') == 'MALE'){
|
||||
s.setAttribute('ch.nevis.idm.User.gender', '1')
|
||||
}
|
||||
if(session.get('ch.nevis.idm.User.gender') == 'FEMALE'){
|
||||
s.setAttribute('ch.nevis.idm.User.gender', '2')
|
||||
}
|
||||
if(s.get('ch.nevis.idm.User.gender') == 'OTHER'){
|
||||
s.setAttribute('ch.nevis.idm.User.gender', '3')
|
||||
}
|
||||
|
||||
|
||||
// handle accounts qa attributes, and set them in session
|
||||
// account itself, only needed if not authenticated with e-ID
|
||||
if (!'urn:qa.agov.ch:names:tc:authfactor:eid'.equalsIgnoreCase(authenticationMethod)) {
|
||||
def idVerificationList = getUserAGOVLoiIdVerification()
|
||||
def idVerification = 'None'
|
||||
if (idVerificationList && !idVerificationList.isEmpty()) {
|
||||
idVerification = idVerificationList.last()
|
||||
}
|
||||
s.setAttribute('idVerification', idVerification)
|
||||
|
||||
// contextClassRefToSet based on highest level-role assigned to default profile
|
||||
for (String role : getUserAGOVLoiRoles()) {
|
||||
if (role.startsWith('level')) {
|
||||
def roleLevel = role.substring(5)
|
||||
int roleLevelNumber = Integer.parseInt(roleLevel)
|
||||
|
||||
if (highestRoleLevelNumber< roleLevelNumber) {
|
||||
highestRoleLevelNumber=roleLevelNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG.debug('CheckLoa: Highest role Level ' + highestRoleLevelNumber.toString() +' contextclassref ' + requestedRoleLevelNumber.toString())
|
||||
LOG.debug('CheckLoa: Compare ' + (highestRoleLevelNumber>=requestedRoleLevelNumber))
|
||||
|
||||
//set attribute Actual Role Level
|
||||
s.setAttribute('agov.actualRoleLevel', '' + highestRoleLevelNumber)
|
||||
LOG.debug('CheckLoa: actual role level (agov) '+ highestRoleLevelNumber)
|
||||
|
||||
// set attribute ValidFrom and ValidTo (only for higher than 100)
|
||||
if (highestRoleLevelNumber > 100) {
|
||||
def validFrom = getUserAGOVLoiValidFrom('level'.concat(highestRoleLevelNumber.toString()))
|
||||
def validTo = getUserAGOVLoiValidTo('level'.concat(highestRoleLevelNumber.toString()))
|
||||
|
||||
LOG.debug('CheckLoa: ValidFrom :' + validFrom)
|
||||
LOG.debug('CheckLoa: ValidTo :' + validTo)
|
||||
|
||||
if(validFrom != '') {
|
||||
s.setAttribute('ValidFrom', '' + validFrom)
|
||||
}
|
||||
if(validTo != '') {
|
||||
s.setAttribute('ValidTo', '' + validTo)
|
||||
}
|
||||
}
|
||||
if (highestRoleLevelNumber > 0) {
|
||||
// set attribute contextClassRefToSet
|
||||
s.setAttribute('contextClassRefToSet','urn:qa.agov.ch:names:tc:ac:classes:' .concat(highestRoleLevelNumber.toString()))
|
||||
} else {
|
||||
// by default 100
|
||||
s.setAttribute('contextClassRefToSet','urn:qa.agov.ch:names:tc:ac:classes:100' )
|
||||
}
|
||||
}
|
||||
// address related, needed in any case (also e-ID)
|
||||
def adressVerificationList = getUserAGOVLoiIdVerification('200')
|
||||
def adressVerification = 'None'
|
||||
if (adressVerificationList && !adressVerificationList.isEmpty()) {
|
||||
adressVerification = adressVerificationList[0]
|
||||
}
|
||||
s.setAttribute('agov.adressVerification', '' + adressVerification)
|
||||
|
||||
if (!session.get('ch.adnovum.nevisidm.profileExtId')) {
|
||||
LOG.error("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', errorMessage='Account without Profile', SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
|
||||
// if the account has no profile, we must not return address or svnr
|
||||
s.setAttribute('agov.appAddressRequired', 'false')
|
||||
s.setAttribute('agov.appSvnrAllowed', 'false')
|
||||
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
// no login for users with a recovery role (but onyl when not logging in with e-Id)
|
||||
// TODO/haburger/2025-07-01: automatic recovery if logging in with e-Id
|
||||
if (!'urn:qa.agov.ch:names:tc:authfactor:eid'.equalsIgnoreCase(authenticationMethod)) {
|
||||
// no login for users with a recovery role
|
||||
def recoveryRoleList = getUserAGOVRecoveryRoles()
|
||||
|
||||
if (recoveryRoleList.contains('mustRecover')) {
|
||||
s.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:mustRecover')
|
||||
s.setAttribute('agov.recovery.authenticatedWith', session.get('authenticatedWith') ?: 'unknown' )
|
||||
|
||||
def origIdVerification = getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString()) ?: 'None'
|
||||
def idVerification = getUserIdVerificationForRecovery() ?: origIdVerification
|
||||
s.setAttribute('agov.recovery.currentIdVerification', '' + idVerification )
|
||||
|
||||
// align currentAgovAq with the method selected for idVerification
|
||||
def currentAgovAqForRecovery = getAqLevelBasedOnIdVerificationForRecovery(idVerification, highestRoleLevelNumber)
|
||||
s.setAttribute('agov.recovery.currentAgovAq', '' + currentAgovAqForRecovery)
|
||||
|
||||
def validFrom = getUserMustRecoverValidFrom() ?: ''
|
||||
s.setAttribute('agov.recovery.currentAgovAqRoleValidFrom', '' + validFrom )
|
||||
|
||||
LOG.debug("CheckLoa: mustRecover: origIdVerification=${origIdVerification}, idVerification=${idVerification}, currentAgovAqForRecovery=${currentAgovAqForRecovery}")
|
||||
|
||||
response.setResult('exit.2')
|
||||
return
|
||||
|
||||
} else if (recoveryRoleList.contains('recovery')) {
|
||||
if (recoveryRoleList.contains('recoveryCascade')) {
|
||||
s.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recoveryCascade')
|
||||
} else {
|
||||
s.setAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recovery')
|
||||
}
|
||||
s.setAttribute('agov.recovery.authenticatedWith', session.get('authenticatedWith') ?: 'unknown')
|
||||
s.setAttribute('agov.recovery.currentAgovAq', session.get('contextClassRefToSet') ?: 'urn:qa.agov.ch:names:tc:ac:classes:100' )
|
||||
LOG.debug('CheckLoa: idVerification2= '+ getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString()))
|
||||
def idVerification = getUserAGOVLoiIdVerification(highestRoleLevelNumber.toString())
|
||||
s.setAttribute('agov.recovery.currentIdVerification', (idVerification.isEmpty() ? 'None' : idVerification.first()))
|
||||
def validFrom = getUserAGOVLoiValidFrom('level'.concat(highestRoleLevelNumber.toString())) ?: ''
|
||||
s.setAttribute('agov.recovery.currentAgovAqRoleValidFrom', validFrom)
|
||||
|
||||
response.setResult('exit.2')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// authenticated with e-ID, we adjust highestRoleLevelNumber to e-ID login
|
||||
highestRoleLevelNumber = 500
|
||||
s.setAttribute('agov.actualRoleLevel', '' + highestRoleLevelNumber)
|
||||
LOG.debug('CheckLoa: actual role level (agov) '+ highestRoleLevelNumber)
|
||||
}
|
||||
|
||||
// verifiy that AQ level is high enough
|
||||
if (highestRoleLevelNumber>=requestedRoleLevelNumber) {
|
||||
response.setResult('ok')
|
||||
return;
|
||||
} else {
|
||||
// Insufficient_LoaInfo
|
||||
response.setResult('exit.1');
|
||||
return;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', errorMessage='exception occured: ${ex}', SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
ex = StackTraceUtils.sanitize(ex)
|
||||
def affectedLines = ex.stackTrace.findAll { it.className.startsWith('Script') }.collect { "${it.methodName}:${it.lineNumber}" }
|
||||
LOG.error("FATAL: Script failure (at lines: ${affectedLines})", ex)
|
||||
// AuthnFailed_Zero_RoleLvl
|
||||
response.setResult('error');
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,100 +1,100 @@
|
|||
import groovy.json.JsonBuilder
|
||||
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 clearFidoUAFSession() {
|
||||
LOG.debug("start new FIDO UAF session (skipping ${session['ch.nevis.auth.fido.uaf.fidouafsessionid']}")
|
||||
def s = request.getAuthSession(true)
|
||||
s.removeAttribute('ch.nevis.auth.fido.uaf.fidouafsessionid')
|
||||
inargs.remove('fallback')
|
||||
}
|
||||
|
||||
|
||||
def clearIdmSessionAttributes() {
|
||||
def s = request.getAuthSession(true)
|
||||
def sessionKeySet = new HashSet(session.keySet())
|
||||
sessionKeySet.each { key ->
|
||||
if ( key ==~ /ch.nevis.idm.*/ || key ==~ /ch.adnovum.nevisidm.*/ ) {
|
||||
s.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
// dispatch AJAX calls and form POST when operation is done
|
||||
if (inargs['fidoUafDone'] == 'true' ||
|
||||
inargs.containsKey('o.fidoUafSessionId.v') ||
|
||||
getHeader('Content-Type') == 'application/json') {
|
||||
|
||||
if (inargs.containsKey('o.fidoUafSessionId.v') && (inargs['o.fidoUafSessionId.v'] != session['ch.nevis.auth.fido.uaf.fidouafsessionid'])) {
|
||||
// received polling for wrong fido session; make sure, that stops
|
||||
LOG.debug("received polling for wrong fido session ${inargs['o.fidoUafSessionId.v']} (correct: ${session['ch.nevis.auth.fido.uaf.fidouafsessionid']})")
|
||||
def json = new JsonBuilder()
|
||||
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
|
||||
}
|
||||
|
||||
if (inargs['fidoUafDone'] == 'true') {
|
||||
// get clean state, before validating user in IDM
|
||||
LOG.debug("clear IDM session attributes")
|
||||
clearIdmSessionAttributes()
|
||||
}
|
||||
|
||||
// continue with OutOfBandFidoUafAuthState
|
||||
response.setResult('ok')
|
||||
}
|
||||
|
||||
// dispatch form post with fallback input field : transition to FIDO Token authentication
|
||||
if (inargs['fallback'] == 'fallback') {
|
||||
sess.setAttribute("eid.placeholder.text", "Fido2 login not implemented yet")
|
||||
response.setResult('fido2')
|
||||
}
|
||||
|
||||
// dispatch to recovery
|
||||
if (inargs['fallback'] == 'recovery') {
|
||||
response.addOutArg('nevis.transfer.destination', parameters.get('recoveryurl'))
|
||||
response.setStatus(ch.nevis.esauth.auth.engine.AuthResponse.AUTH_CONTINUE)
|
||||
response.setIsRedirectTransfer(true)
|
||||
// Remove existing cookies before redirecting to RECOVERY
|
||||
def agovRecoveryCookie = "agovRecovery=deleted; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Strict; Secure; HttpOnly"
|
||||
response.setHeader('Set-Cookie', agovRecoveryCookie)
|
||||
return
|
||||
}
|
||||
|
||||
// dispatch form post with fallback input field : go to registration with right loa
|
||||
if (inargs['fallback'] == 'register') {
|
||||
sess.setAttribute("eid.placeholder.text", "Registration should not be called here?")
|
||||
response.setResult('registration')
|
||||
}
|
||||
|
||||
// cancel and go back to login
|
||||
if (inargs['fallback'] == 'back') {
|
||||
response.setResult('back')
|
||||
}
|
||||
|
||||
|
||||
// dispatch form post with onReload input field : refresh QR-code FIDO UAF
|
||||
if (inargs.containsKey('onReload')) {
|
||||
clearFidoUAFSession()
|
||||
response.setResult('default')
|
||||
}
|
||||
import groovy.json.JsonBuilder
|
||||
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 clearFidoUAFSession() {
|
||||
LOG.debug("start new FIDO UAF session (skipping ${session['ch.nevis.auth.fido.uaf.fidouafsessionid']}")
|
||||
def s = request.getAuthSession(true)
|
||||
s.removeAttribute('ch.nevis.auth.fido.uaf.fidouafsessionid')
|
||||
inargs.remove('fallback')
|
||||
}
|
||||
|
||||
|
||||
def clearIdmSessionAttributes() {
|
||||
def s = request.getAuthSession(true)
|
||||
def sessionKeySet = new HashSet(session.keySet())
|
||||
sessionKeySet.each { key ->
|
||||
if ( key ==~ /ch.nevis.idm.*/ || key ==~ /ch.adnovum.nevisidm.*/ ) {
|
||||
s.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
// dispatch AJAX calls and form POST when operation is done
|
||||
if (inargs['fidoUafDone'] == 'true' ||
|
||||
inargs.containsKey('o.fidoUafSessionId.v') ||
|
||||
getHeader('Content-Type') == 'application/json') {
|
||||
|
||||
if (inargs.containsKey('o.fidoUafSessionId.v') && (inargs['o.fidoUafSessionId.v'] != session['ch.nevis.auth.fido.uaf.fidouafsessionid'])) {
|
||||
// received polling for wrong fido session; make sure, that stops
|
||||
LOG.debug("received polling for wrong fido session ${inargs['o.fidoUafSessionId.v']} (correct: ${session['ch.nevis.auth.fido.uaf.fidouafsessionid']})")
|
||||
def json = new JsonBuilder()
|
||||
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
|
||||
}
|
||||
|
||||
if (inargs['fidoUafDone'] == 'true') {
|
||||
// get clean state, before validating user in IDM
|
||||
LOG.debug("clear IDM session attributes")
|
||||
clearIdmSessionAttributes()
|
||||
}
|
||||
|
||||
// continue with OutOfBandFidoUafAuthState
|
||||
response.setResult('ok')
|
||||
}
|
||||
|
||||
// dispatch form post with fallback input field : transition to FIDO Token authentication
|
||||
if (inargs['fallback'] == 'fallback') {
|
||||
sess.setAttribute("eid.placeholder.text", "Fido2 login not implemented yet")
|
||||
response.setResult('fido2')
|
||||
}
|
||||
|
||||
// dispatch to recovery
|
||||
if (inargs['fallback'] == 'recovery') {
|
||||
response.addOutArg('nevis.transfer.destination', parameters.get('recoveryurl'))
|
||||
response.setStatus(ch.nevis.esauth.auth.engine.AuthResponse.AUTH_CONTINUE)
|
||||
response.setIsRedirectTransfer(true)
|
||||
// Remove existing cookies before redirecting to RECOVERY
|
||||
def agovRecoveryCookie = "agovRecovery=deleted; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Strict; Secure; HttpOnly"
|
||||
response.setHeader('Set-Cookie', agovRecoveryCookie)
|
||||
return
|
||||
}
|
||||
|
||||
// dispatch form post with fallback input field : go to registration with right loa
|
||||
if (inargs['fallback'] == 'register') {
|
||||
sess.setAttribute("eid.placeholder.text", "Registration should not be called here?")
|
||||
response.setResult('registration')
|
||||
}
|
||||
|
||||
// cancel and go back to login
|
||||
if (inargs['fallback'] == 'back') {
|
||||
response.setResult('back')
|
||||
}
|
||||
|
||||
|
||||
// dispatch form post with onReload input field : refresh QR-code FIDO UAF
|
||||
if (inargs.containsKey('onReload')) {
|
||||
clearFidoUAFSession()
|
||||
response.setResult('default')
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
if(outargs.containsKey('saml.SAMLResponse')) {
|
||||
// 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['agov.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'
|
||||
|
||||
LOG.info("Event='GOTOEIDLINKING', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
|
||||
// Redirect
|
||||
response.addOutArg('nevis.transfer.destination', parameters.get('agovmedirecturl'))
|
||||
response.addOutArg('nevis.transfer.field.SAMLResponse', outargs.getProperty('saml.SAMLResponse').bytes.encodeBase64().toString())
|
||||
response.setStatus(ch.nevis.esauth.auth.engine.AuthResponse.AUTH_CONTINUE)
|
||||
response.setIsRedirectTransfer(false)
|
||||
|
||||
response.removeOutArg('saml.SAMLResponse')
|
||||
}
|
||||
else {
|
||||
response.setResult('ok')
|
||||
if(outargs.containsKey('saml.SAMLResponse')) {
|
||||
// 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['agov.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'
|
||||
|
||||
LOG.info("Event='GOTOEIDLINKING', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
|
||||
// Redirect
|
||||
response.addOutArg('nevis.transfer.destination', parameters.get('agovmedirecturl'))
|
||||
response.addOutArg('nevis.transfer.field.SAMLResponse', outargs.getProperty('saml.SAMLResponse').bytes.encodeBase64().toString())
|
||||
response.setStatus(ch.nevis.esauth.auth.engine.AuthResponse.AUTH_CONTINUE)
|
||||
response.setIsRedirectTransfer(false)
|
||||
|
||||
response.removeOutArg('saml.SAMLResponse')
|
||||
}
|
||||
else {
|
||||
response.setResult('ok')
|
||||
}
|
|
@ -1,158 +1,159 @@
|
|||
import java.text.SimpleDateFormat
|
||||
import groovy.text.SimpleTemplateEngine
|
||||
|
||||
import ch.nevis.idm.client.IdmRestClient
|
||||
import ch.nevis.idm.client.IdmRestClientFactory
|
||||
|
||||
def getDateWithoutTimestamp(String date){
|
||||
def result = date
|
||||
if(date.matches('^[0-9-]+[+]{1}.*')){
|
||||
result = date.replaceAll('[+]{1}.*', "")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// NOTE/aca/2025/06/19: We could also reload the data from idm after the update instead of updating the session variables manualy -> probably better and less error-prone
|
||||
def compareAndUpdateSessionVariables(sess, keys, isProperty){
|
||||
def updatedKeys = []
|
||||
for(key in keys){
|
||||
def idmkey = isProperty ? "ch.nevis.idm.User.prop.$key" : "ch.nevis.idm.User.$key"
|
||||
def eidValue = session["agov.eid.User.$key"] ?: ""
|
||||
def idmValue = session[idmkey] ?: ""
|
||||
if(!idmValue || eidValue != idmValue){
|
||||
sess.setAttribute(idmkey, eidValue)
|
||||
updatedKeys.add(key)
|
||||
}
|
||||
}
|
||||
return updatedKeys
|
||||
}
|
||||
|
||||
// TODO/haburger/2025-07-01: we should also set the verificationMethod, etc. of the level400 role
|
||||
String user_update_dto_template = '''
|
||||
{
|
||||
"name": {
|
||||
"firstName": "$firstName",
|
||||
"familyName": "$familyName"
|
||||
},
|
||||
"properties": {
|
||||
"svnr": "$svnr",
|
||||
"placeOfBirth": "$placeOfBirth",
|
||||
"nationality": "$nationality",
|
||||
"eIdNumber": "$eIdNumber"
|
||||
},
|
||||
"gender": "$gender",
|
||||
"birthDate": "$birthDate",
|
||||
|
||||
"modificationComment": "updated user information with eid attributes during request $request"
|
||||
}
|
||||
'''
|
||||
|
||||
// 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 sess = request.getAuthSession(true)
|
||||
|
||||
// Convert EID gender format to IDM
|
||||
if(sess.get('agov.eid.User.gender') == '1'){
|
||||
sess.setAttribute('agov.eid.User.gender', 'MALE')
|
||||
}
|
||||
if(sess.get('agov.eid.User.gender') == '2'){
|
||||
sess.setAttribute('agov.eid.User.gender', 'FEMALE')
|
||||
}
|
||||
if(sess.get('agov.eid.User.gender') == '3'){
|
||||
sess.setAttribute('agov.eid.User.gender', 'OTHER')
|
||||
}
|
||||
|
||||
// Compare eid and idm attributes + update idm session variables if they differ
|
||||
def attributesToAudit = compareAndUpdateSessionVariables(sess, ["firstName", "lastName", "gender"], false)
|
||||
// NOTE/aca/2025/06/14/: Potentally Throw a DATA ERROR if the properties are different? -> should the svnr number ever change?
|
||||
def propertiesToAudit = compareAndUpdateSessionVariables(sess, ["svnr", "eIdNumber", "nationality", "placeOfBirth"], true)
|
||||
|
||||
|
||||
// Handle birthdate seperately, since it can contain a timestamp -> we probably don't want to update if only the timestamp is wrong
|
||||
String eidBirthdate = getDateWithoutTimestamp(session["agov.eid.User.birthDate"] ?: "")
|
||||
String idmBirthdate = getDateWithoutTimestamp(session["ch.nevis.idm.User.birthDate"] ?: "")
|
||||
LOG.debug("eidBirthdate: $eidBirthdate idmBirthdate: $idmBirthdate")
|
||||
if(eidBirthdate != idmBirthdate){
|
||||
sess.setAttribute("ch.nevis.idm.User.birthDate", eidBirthdate)
|
||||
// For some reson IdmGetPropertyState uses a different date format than IdmSetPropertyState?
|
||||
//def date = new SimpleDateFormat('yyyy-MM-dd').parse(eidBirthdate)
|
||||
//def idmFromatedBirthDate = new SimpleDateFormat('dd.MM.yyyy').format(date)
|
||||
//sess.setAttribute("ch.nevis.idm.User.birthDate.idmFormat", idmFromatedBirthDate)
|
||||
attributesToAudit.add("birthDate")
|
||||
}
|
||||
|
||||
// Check if we need to update IDM
|
||||
def auditedRequired = attributesToAudit.size() > 0 || propertiesToAudit.size() > 0
|
||||
|
||||
if(auditedRequired){
|
||||
// update attributes in idm & transition to User notification
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
|
||||
String baseUrl = parameters.get("baseUrl")
|
||||
String clientExtId = parameters.get("clientExtId")
|
||||
String endPoint = "$baseUrl/api/core/v1"
|
||||
String userExtId = sess.getAttribute("ch.nevis.idm.User.extId")
|
||||
|
||||
String requestUrl = "$endPoint/$clientExtId/users/$userExtId"
|
||||
|
||||
|
||||
|
||||
def binding = [
|
||||
"firstName": sess.getAttribute('agov.eid.User.firstName'),
|
||||
"familyName": sess.getAttribute('agov.eid.User.lastName'),
|
||||
"svnr": sess.getAttribute('agov.eid.User.svnr'),
|
||||
"placeOfBirth": sess.getAttribute('agov.eid.User.placeOfBirth'),
|
||||
"nationality": sess.getAttribute('agov.eid.User.nationality'),
|
||||
"eIdNumber": sess.getAttribute('agov.eid.User.eIdNumber'),
|
||||
"gender": sess.getAttribute('agov.eid.User.gender').toLowerCase(),
|
||||
"birthDate": sess.getAttribute('agov.eid.User.birthDate'),
|
||||
"request": requestId
|
||||
]
|
||||
|
||||
def templateEngine = new SimpleTemplateEngine()
|
||||
def userUpdateDto = templateEngine.createTemplate(user_update_dto_template).make(binding).toString()
|
||||
|
||||
try {
|
||||
idmRestClient.patch(requestUrl, userUpdateDto)
|
||||
|
||||
}catch(Exception e) {
|
||||
LOG.error("Failed to update User data in IDM: ${e}")
|
||||
LOG.error("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='Failed to update User data in IDM'")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
String printKeys = attributesToAudit.toListString()
|
||||
LOG.debug("AuditedAttributes: $printKeys")
|
||||
|
||||
// Transform gender back to number
|
||||
if(sess.get('ch.nevis.idm.User.gender') == 'MALE'){
|
||||
sess.setAttribute('ch.nevis.idm.User.gender', '1')
|
||||
}
|
||||
if(sess.get('ch.nevis.idm.User.gender') == 'FEMALE'){
|
||||
sess.setAttribute('ch.nevis.idm.User.gender', '2')
|
||||
}
|
||||
if(sess.get('ch.nevis.idm.User.gender') == 'OTHER'){
|
||||
sess.setAttribute('ch.nevis.idm.User.gender', '3')
|
||||
}
|
||||
|
||||
response.setResult('audited')
|
||||
}else{
|
||||
// Attributes match & no notification needed => continue by updating the linking credential and sending the saml assertion
|
||||
// NOTE/aca/2025/06/19: We skip checking the account state, recovery code, mobile number and LoA
|
||||
LOG.debug("No Audit Required: Logging user in")
|
||||
response.setResult('noChange')
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import groovy.text.SimpleTemplateEngine
|
||||
|
||||
import ch.nevis.idm.client.IdmRestClient
|
||||
import ch.nevis.idm.client.IdmRestClientFactory
|
||||
|
||||
def getDateWithoutTimestamp(String date){
|
||||
def result = date
|
||||
if(date.matches('^[0-9-]+[+]{1}.*')){
|
||||
result = date.replaceAll('[+]{1}.*', "")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// NOTE/aca/2025/06/19: We could also reload the data from idm after the update instead of updating the session variables manualy -> probably better and less error-prone
|
||||
def compareAndUpdateSessionVariables(sess, keys, isProperty){
|
||||
def updatedKeys = []
|
||||
for(key in keys){
|
||||
def idmkey = isProperty ? "ch.nevis.idm.User.prop.$key" : "ch.nevis.idm.User.$key"
|
||||
def eidValue = session["agov.eid.User.$key"] ?: ""
|
||||
def idmValue = session[idmkey] ?: ""
|
||||
if(!idmValue || eidValue != idmValue){
|
||||
sess.setAttribute(idmkey, eidValue)
|
||||
updatedKeys.add(key)
|
||||
}
|
||||
}
|
||||
return updatedKeys
|
||||
}
|
||||
|
||||
// TODO/haburger/2025-07-01: we should also set the verificationMethod, etc. of the level400 role
|
||||
String user_update_dto_template = '''
|
||||
{
|
||||
"name": {
|
||||
"firstName": "$firstName",
|
||||
"familyName": "$familyName"
|
||||
},
|
||||
"properties": {
|
||||
"svnr": "$svnr",
|
||||
"placeOfBirth": "$placeOfBirth",
|
||||
"nationality": "$nationality",
|
||||
"eIdNumber": "$eIdNumber"
|
||||
},
|
||||
"gender": "$gender",
|
||||
"birthDate": "$birthDate",
|
||||
|
||||
"modificationComment": "updated user information with eid attributes during request $request"
|
||||
}
|
||||
'''
|
||||
|
||||
// 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 sess = request.getAuthSession(true)
|
||||
|
||||
// Convert EID gender format to IDM
|
||||
if(sess.get('agov.eid.User.gender') == '1'){
|
||||
sess.setAttribute('agov.eid.User.gender', 'MALE')
|
||||
}
|
||||
if(sess.get('agov.eid.User.gender') == '2'){
|
||||
sess.setAttribute('agov.eid.User.gender', 'FEMALE')
|
||||
}
|
||||
if(sess.get('agov.eid.User.gender') == '3'){
|
||||
sess.setAttribute('agov.eid.User.gender', 'OTHER')
|
||||
}
|
||||
|
||||
// Compare eid and idm attributes + update idm session variables if they differ
|
||||
def attributesToAudit = compareAndUpdateSessionVariables(sess, ["firstName", "lastName", "gender"], false)
|
||||
// NOTE/aca/2025/06/14/: Potentally Throw a DATA ERROR if the properties are different? -> should the svnr number ever change?
|
||||
def propertiesToAudit = compareAndUpdateSessionVariables(sess, ["svnr", "eIdNumber", "nationality", "placeOfBirth"], true)
|
||||
|
||||
|
||||
// Handle birthdate seperately, since it can contain a timestamp -> we probably don't want to update if only the timestamp is wrong
|
||||
String eidBirthdate = getDateWithoutTimestamp(session["agov.eid.User.birthDate"] ?: "")
|
||||
String idmBirthdate = getDateWithoutTimestamp(session["ch.nevis.idm.User.birthDate"] ?: "")
|
||||
LOG.debug("eidBirthdate: $eidBirthdate idmBirthdate: $idmBirthdate")
|
||||
if(eidBirthdate != idmBirthdate){
|
||||
sess.setAttribute("ch.nevis.idm.User.birthDate", eidBirthdate)
|
||||
// For some reson IdmGetPropertyState uses a different date format than IdmSetPropertyState?
|
||||
//def date = new SimpleDateFormat('yyyy-MM-dd').parse(eidBirthdate)
|
||||
//def idmFromatedBirthDate = new SimpleDateFormat('dd.MM.yyyy').format(date)
|
||||
//sess.setAttribute("ch.nevis.idm.User.birthDate.idmFormat", idmFromatedBirthDate)
|
||||
attributesToAudit.add("birthDate")
|
||||
}
|
||||
|
||||
// Check if we need to update IDM
|
||||
def auditedRequired = attributesToAudit.size() > 0 || propertiesToAudit.size() > 0
|
||||
|
||||
if(auditedRequired){
|
||||
// update attributes in idm & transition to User notification
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
|
||||
String baseUrl = parameters.get("baseUrl")
|
||||
String clientExtId = parameters.get("clientExtId")
|
||||
String endPoint = "$baseUrl/api/core/v1"
|
||||
String userExtId = sess.getAttribute("ch.nevis.idm.User.extId")
|
||||
|
||||
String requestUrl = "$endPoint/$clientExtId/users/$userExtId"
|
||||
|
||||
|
||||
|
||||
def binding = [
|
||||
"firstName": sess.getAttribute('agov.eid.User.firstName'),
|
||||
"familyName": sess.getAttribute('agov.eid.User.lastName'),
|
||||
"svnr": sess.getAttribute('agov.eid.User.svnr'),
|
||||
"placeOfBirth": sess.getAttribute('agov.eid.User.placeOfBirth'),
|
||||
"nationality": sess.getAttribute('agov.eid.User.nationality'),
|
||||
"eIdNumber": sess.getAttribute('agov.eid.User.eIdNumber'),
|
||||
"gender": sess.getAttribute('agov.eid.User.gender').toLowerCase(),
|
||||
"birthDate": sess.getAttribute('agov.eid.User.birthDate'),
|
||||
"request": requestId
|
||||
]
|
||||
|
||||
def templateEngine = new SimpleTemplateEngine()
|
||||
def userUpdateDto = templateEngine.createTemplate(user_update_dto_template).make(binding).toString()
|
||||
|
||||
try {
|
||||
idmRestClient.patch(requestUrl, userUpdateDto)
|
||||
|
||||
}catch(Exception e) {
|
||||
LOG.error("Failed to update User data in IDM: ${e}")
|
||||
LOG.error("Event='DATAERROR', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${user}, CredentialType='${credentialType}', SourceIp=${sourceIp}, UserAgent='${userAgent}', reason='Failed to update User data in IDM'")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
String printKeys = attributesToAudit.toListString()
|
||||
LOG.debug("AuditedAttributes: $printKeys")
|
||||
|
||||
// Transform gender back to number
|
||||
if(sess.get('ch.nevis.idm.User.gender') == 'MALE'){
|
||||
sess.setAttribute('ch.nevis.idm.User.gender', '1')
|
||||
}
|
||||
if(sess.get('ch.nevis.idm.User.gender') == 'FEMALE'){
|
||||
sess.setAttribute('ch.nevis.idm.User.gender', '2')
|
||||
}
|
||||
if(sess.get('ch.nevis.idm.User.gender') == 'OTHER'){
|
||||
sess.setAttribute('ch.nevis.idm.User.gender', '3')
|
||||
}
|
||||
|
||||
response.setResult('audited')
|
||||
}else{
|
||||
// Attributes match & no notification needed => continue by updating the linking credential and sending the saml assertion
|
||||
// NOTE/aca/2025/06/19: We skip checking the account state, recovery code, mobile number and LoA
|
||||
LOG.debug("No Audit Required: Logging user in")
|
||||
response.setResult('noChange')
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,200 +1,201 @@
|
|||
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 getAccounts(json, String svnr) {
|
||||
String svnrWithPrefix = "urn:ch-agov-eid:$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 || cred["issuerNameId"] == svnrWithPrefix )){
|
||||
// we found more than one 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"])
|
||||
|
||||
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%20%28%20urn:nevis:idm:scim:schemas:v1:extension:User.credentials.issuerNameId%20==%20'$svnr'%20OR%20urn:nevis:idm:scim:schemas:v1:extension:User.credentials.issuerNameId%20==%20'urn:ch-agov-eid:$svnr'%29"
|
||||
|
||||
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 == null){
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
def numAccounts = accounts.size()
|
||||
|
||||
LOG.debug("Linked accounts found: " + frontend_dto.toString())
|
||||
|
||||
if(numAccounts == 0){
|
||||
// No account found => show account linking dialog options
|
||||
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"])
|
||||
|
||||
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
|
||||
}
|
||||
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 getAccounts(json, String svnr) {
|
||||
String svnrWithPrefix = "urn:ch-agov-eid:$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 || cred["issuerNameId"] == svnrWithPrefix )){
|
||||
// we found more than one 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"])
|
||||
|
||||
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%20%28%20urn:nevis:idm:scim:schemas:v1:extension:User.credentials.issuerNameId%20==%20'$svnr'%20OR%20urn:nevis:idm:scim:schemas:v1:extension:User.credentials.issuerNameId%20==%20'urn:ch-agov-eid:$svnr'%29"
|
||||
|
||||
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 == null){
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
def numAccounts = accounts.size()
|
||||
|
||||
LOG.debug("Linked accounts found: " + frontend_dto.toString())
|
||||
|
||||
if(numAccounts == 0){
|
||||
// No account found => show account linking dialog options
|
||||
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"])
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
import ch.nevis.idm.client.IdmRestClient
|
||||
import ch.nevis.idm.client.IdmRestClientFactory
|
||||
|
||||
|
||||
String user_notification_dto = '''
|
||||
{
|
||||
"clientExtId": "{{clientExtId}}",
|
||||
"userExtId": "{{userExtId}}",
|
||||
"notificationType": "userNotification4",
|
||||
"sendingMethod": [
|
||||
"Email"
|
||||
],
|
||||
"async": false
|
||||
}
|
||||
'''
|
||||
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
String baseUrl = parameters.get("baseUrl")
|
||||
String clientExtId = parameters.get("clientExtId")
|
||||
String endPoint = "$baseUrl/api/notification/v1/"
|
||||
|
||||
String userExtId = sess.getAttribute("agov.eid.linkedAccountExtId")
|
||||
|
||||
String restRequest = user_notification_dto.replaceAll("\\{\\{clientExtId}}", clientExtId).replaceAll("\\{\\{userExtId}}", userExtId)
|
||||
|
||||
try {
|
||||
idmRestClient.post(endPoint, restRequest)
|
||||
|
||||
}catch(Exception e) {
|
||||
LOG.error("Failed to send User Notification: First Login: ${e}")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
response.setResult('ok')
|
||||
import ch.nevis.idm.client.IdmRestClient
|
||||
import ch.nevis.idm.client.IdmRestClientFactory
|
||||
|
||||
|
||||
String user_notification_dto = '''
|
||||
{
|
||||
"clientExtId": "{{clientExtId}}",
|
||||
"userExtId": "{{userExtId}}",
|
||||
"notificationType": "userNotification4",
|
||||
"sendingMethod": [
|
||||
"Email"
|
||||
],
|
||||
"async": false
|
||||
}
|
||||
'''
|
||||
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
String baseUrl = parameters.get("baseUrl")
|
||||
String clientExtId = parameters.get("clientExtId")
|
||||
String endPoint = "$baseUrl/api/notification/v1/"
|
||||
|
||||
String userExtId = sess.getAttribute("agov.eid.linkedAccountExtId")
|
||||
|
||||
String restRequest = user_notification_dto.replaceAll("\\{\\{clientExtId}}", clientExtId).replaceAll("\\{\\{userExtId}}", userExtId)
|
||||
|
||||
try {
|
||||
idmRestClient.post(endPoint, restRequest)
|
||||
|
||||
}catch(Exception e) {
|
||||
LOG.error("Failed to send User Notification: First Login: ${e}")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
response.setResult('ok')
|
||||
return
|
|
@ -1,38 +1,38 @@
|
|||
import ch.nevis.idm.client.IdmRestClient
|
||||
import ch.nevis.idm.client.IdmRestClientFactory
|
||||
|
||||
|
||||
String user_notification_dto = '''
|
||||
{
|
||||
"clientExtId": "{{clientExtId}}",
|
||||
"userExtId": "{{userExtId}}",
|
||||
"notificationType": "userNotification3",
|
||||
"sendingMethod": [
|
||||
"Email"
|
||||
],
|
||||
"async": false
|
||||
}
|
||||
'''
|
||||
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
String baseUrl = parameters.get("baseUrl")
|
||||
String clientExtId = parameters.get("clientExtId")
|
||||
String endPoint = "$baseUrl/api/notification/v1/"
|
||||
|
||||
String userExtId = sess.getAttribute("ch.nevis.idm.User.extId")
|
||||
|
||||
String restRequest = user_notification_dto.replaceAll("\\{\\{clientExtId}}", clientExtId).replaceAll("\\{\\{userExtId}}", userExtId)
|
||||
|
||||
try {
|
||||
idmRestClient.post(endPoint, restRequest)
|
||||
|
||||
}catch(Exception e) {
|
||||
LOG.error("Failed to send User Notification: Idm Update with EId data: ${e}")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
response.setResult('ok')
|
||||
import ch.nevis.idm.client.IdmRestClient
|
||||
import ch.nevis.idm.client.IdmRestClientFactory
|
||||
|
||||
|
||||
String user_notification_dto = '''
|
||||
{
|
||||
"clientExtId": "{{clientExtId}}",
|
||||
"userExtId": "{{userExtId}}",
|
||||
"notificationType": "userNotification3",
|
||||
"sendingMethod": [
|
||||
"Email"
|
||||
],
|
||||
"async": false
|
||||
}
|
||||
'''
|
||||
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
String baseUrl = parameters.get("baseUrl")
|
||||
String clientExtId = parameters.get("clientExtId")
|
||||
String endPoint = "$baseUrl/api/notification/v1/"
|
||||
|
||||
String userExtId = sess.getAttribute("ch.nevis.idm.User.extId")
|
||||
|
||||
String restRequest = user_notification_dto.replaceAll("\\{\\{clientExtId}}", clientExtId).replaceAll("\\{\\{userExtId}}", userExtId)
|
||||
|
||||
try {
|
||||
idmRestClient.post(endPoint, restRequest)
|
||||
|
||||
}catch(Exception e) {
|
||||
LOG.error("Failed to send User Notification: Idm Update with EId data: ${e}")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
response.setResult('ok')
|
||||
return
|
|
@ -1,17 +1,17 @@
|
|||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
if(inargs['cancel']){
|
||||
LOG.debug("Account registration canceled: Send response with error")
|
||||
response.setResult('back')
|
||||
return
|
||||
}
|
||||
|
||||
if(inargs['register'] == "agov"){
|
||||
LOG.debug("AGOV account registration was selected")
|
||||
response.setResult('register')
|
||||
return
|
||||
}
|
||||
|
||||
LOG.debug("Show GUI")
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
if(inargs['cancel']){
|
||||
LOG.debug("Account registration canceled: Send response with error")
|
||||
response.setResult('back')
|
||||
return
|
||||
}
|
||||
|
||||
if(inargs['register'] == "agov"){
|
||||
LOG.debug("AGOV account registration was selected")
|
||||
response.setResult('register')
|
||||
return
|
||||
}
|
||||
|
||||
LOG.debug("Show GUI")
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
|
@ -1,27 +1,27 @@
|
|||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
if(inargs['cancelEid']){
|
||||
LOG.debug("Account registration canceled: Send response with error")
|
||||
response.setResult('cancel')
|
||||
return
|
||||
}
|
||||
|
||||
if(inargs['continue'] == 'link_account'){
|
||||
LOG.debug("AGOV account linking")
|
||||
//sess.setAttribute("eid.placeholder.text", "EId: Implicit account linking not implemented yet")
|
||||
response.setResult('link')
|
||||
return
|
||||
}
|
||||
|
||||
if(inargs['continue'] == 'register_account'){
|
||||
LOG.debug("AGOV account registration was selected")
|
||||
sess.setAttribute("eid.placeholder.text", "EId: Account registration with implicit linking not implemented yet")
|
||||
response.setResult('register')
|
||||
return
|
||||
}
|
||||
|
||||
LOG.debug("Show GUI")
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
if(inargs['cancelEid']){
|
||||
LOG.debug("Account registration canceled: Send response with error")
|
||||
response.setResult('cancel')
|
||||
return
|
||||
}
|
||||
|
||||
if(inargs['continue'] == 'link_account'){
|
||||
LOG.debug("AGOV account linking")
|
||||
//sess.setAttribute("eid.placeholder.text", "EId: Implicit account linking not implemented yet")
|
||||
response.setResult('link')
|
||||
return
|
||||
}
|
||||
|
||||
if(inargs['continue'] == 'register_account'){
|
||||
LOG.debug("AGOV account registration was selected")
|
||||
sess.setAttribute("eid.placeholder.text", "EId: Account registration with implicit linking not implemented yet")
|
||||
response.setResult('register')
|
||||
return
|
||||
}
|
||||
|
||||
LOG.debug("Show GUI")
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
|
@ -1,36 +1,36 @@
|
|||
import ch.nevis.idm.client.IdmRestClient
|
||||
import ch.nevis.idm.client.IdmRestClientFactory
|
||||
|
||||
|
||||
String login_info_update_dto = '''
|
||||
{
|
||||
"success": true,
|
||||
"credentialExtId": "{{credentialExtId}}"
|
||||
}
|
||||
'''
|
||||
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
String baseUrl = parameters.get("baseUrl")
|
||||
String clientExtId = parameters.get("clientExtId")
|
||||
String endPoint = "$baseUrl/api/core/v1"
|
||||
|
||||
String userExtId = sess.getAttribute("ch.nevis.idm.User.extId")
|
||||
String linkingCredentialExtId = sess.getAttribute("agov.eid.linkingCredentialExtId")
|
||||
|
||||
String requestUrl = "$endPoint/$clientExtId/users/$userExtId/login-info"
|
||||
|
||||
String restRequest = login_info_update_dto.replaceAll("\\{\\{credentialExtId}}", linkingCredentialExtId)
|
||||
|
||||
try {
|
||||
idmRestClient.post(requestUrl, restRequest)
|
||||
|
||||
}catch(Exception e) {
|
||||
LOG.error("Failed to Update Linking Credential info: ${e}")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
response.setResult('ok')
|
||||
import ch.nevis.idm.client.IdmRestClient
|
||||
import ch.nevis.idm.client.IdmRestClientFactory
|
||||
|
||||
|
||||
String login_info_update_dto = '''
|
||||
{
|
||||
"success": true,
|
||||
"credentialExtId": "{{credentialExtId}}"
|
||||
}
|
||||
'''
|
||||
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
String baseUrl = parameters.get("baseUrl")
|
||||
String clientExtId = parameters.get("clientExtId")
|
||||
String endPoint = "$baseUrl/api/core/v1"
|
||||
|
||||
String userExtId = sess.getAttribute("ch.nevis.idm.User.extId")
|
||||
String linkingCredentialExtId = sess.getAttribute("agov.eid.linkingCredentialExtId")
|
||||
|
||||
String requestUrl = "$endPoint/$clientExtId/users/$userExtId/login-info"
|
||||
|
||||
String restRequest = login_info_update_dto.replaceAll("\\{\\{credentialExtId}}", linkingCredentialExtId)
|
||||
|
||||
try {
|
||||
idmRestClient.post(requestUrl, restRequest)
|
||||
|
||||
}catch(Exception e) {
|
||||
LOG.error("Failed to Update Linking Credential info: ${e}")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
response.setResult('ok')
|
||||
return
|
|
@ -1,455 +1,456 @@
|
|||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
import ch.nevis.esauth.sess.Session
|
||||
import ch.nevis.esauth.util.httpclient.api.HttpClient
|
||||
import groovy.json.JsonSlurper
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
|
||||
import com.fasterxml.uuid.Generators
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
// returns true on success and false on failure
|
||||
def getNewVerification(Session sess, HttpClient httpClient, String verification_request_template, String traceparent){
|
||||
// Initialize the verification session on the verifier
|
||||
def endPoint = "${parameters.get('eidVerifierBaseUrl')}/api/v1/verifications"
|
||||
|
||||
try {
|
||||
def httpResponse = Http.post()
|
||||
.url(endPoint)
|
||||
.header("Accept", "application/json")
|
||||
.header("traceparent", traceparent)
|
||||
.entity(Http.entity()
|
||||
.content(verification_request_template.replaceAll("\\{\\{UUID}}", UUID.randomUUID().toString()))
|
||||
.contentType("application/json")
|
||||
.build())
|
||||
.build()
|
||||
.send(httpClient)
|
||||
|
||||
|
||||
if (httpResponse.code() != 200) {
|
||||
LOG.debug("Result: ${httpResponse}")
|
||||
return false
|
||||
}
|
||||
|
||||
def json = new JsonSlurper().parseText(httpResponse.bodyAsString())
|
||||
LOG.debug("Result: ${json}")
|
||||
|
||||
sess.setAttribute('agov.eid.verification', 'true')
|
||||
sess.setAttribute('agov.eid.verification.id', json.id)
|
||||
sess.setAttribute('agov.eid.verification.link', json.verification_url)
|
||||
|
||||
|
||||
// TODO/aca/2025-04-04:This could probably also be INITIATED, once the verifier supports this status
|
||||
if (json.state != 'PENDING') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOG.error("Eid verification failed: $e")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
def clearEidSession(){
|
||||
def s = request.getAuthSession(true)
|
||||
s.removeAttribute('agov.eid.verification')
|
||||
s.removeAttribute('agov.eid.verification.id')
|
||||
s.removeAttribute('agov.eid.verification.link')
|
||||
}
|
||||
|
||||
def verification_request_template = '''
|
||||
{ "presentation_definition": {
|
||||
"id": "{{UUID}}",
|
||||
"name": "AGOV Verification",
|
||||
"purpose": "AGOV Login",
|
||||
"format": {
|
||||
"vc+sd-jwt": {
|
||||
"sd-jwt_alg_values": [
|
||||
"ES256"
|
||||
],
|
||||
"kb-jwt_alg_values": [
|
||||
"ES256"
|
||||
]
|
||||
}
|
||||
},
|
||||
"input_descriptors": [
|
||||
{
|
||||
"id": "agov-all-attributes",
|
||||
"name": "AGOV Identity Verification",
|
||||
"purpose": "verification and authentication",
|
||||
"format": {
|
||||
"vc+sd-jwt": {
|
||||
"sd-jwt_alg_values": [
|
||||
"ES256"
|
||||
],
|
||||
"kb-jwt_alg_values": [
|
||||
"ES256"
|
||||
]
|
||||
}
|
||||
},
|
||||
"constraints": {
|
||||
"fields": [
|
||||
{
|
||||
"path": [
|
||||
"$.family_name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.given_name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.birth_date"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.sex"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.place_of_origin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.birth_place"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.nationality"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.personal_administrative_number"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.document_number"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.issuance_date"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.expiry_date"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.issuing_authority"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.issuing_country"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
def ERROR_CODE_TO_STATUS_MAPPER = [
|
||||
'CREDENTIAL_INVALID' : 'FAILED',
|
||||
'JWT_EXPIRED' : 'ERROR',
|
||||
'INVALID_FORMAT' : 'ERROR',
|
||||
'CREDENTIAL_EXPIRED' : 'FAILED',
|
||||
'MISSING_NONCE' : 'ERROR',
|
||||
'UNSUPPORTED_FORMAT' : 'ERROR',
|
||||
'CREDENTIAL_REVOKED' : 'FAILED',
|
||||
'CREDENTIAL_SUSPENDED' : 'FAILED',
|
||||
'HOLDER_BINDING_MISMATCH' : 'ERROR',
|
||||
'CREDENTIAL_MISSING_DATA' : 'FAILED',
|
||||
'UNRESOLVABLE_STATUS_LIST' : 'ERROR',
|
||||
'PUBLIC_KEY_OF_ISSUER_UNRESOLVABLE': 'ERROR',
|
||||
'CLIENT_REJECTED' : 'CANCELED',
|
||||
'ISSUER_NOT_ACCEPTED' : 'ERROR'
|
||||
]
|
||||
|
||||
// ---------------
|
||||
// check, whether we are still processing the correct AuthnRequest
|
||||
// or if the frontend requested a timeout
|
||||
if ( (inargs.containsKey('authRequestId') && (inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'])) || inargs['oid4vp'] == 'TIMEOUT') {
|
||||
// wrong request, "force" a timeout
|
||||
LOG.debug('authentication timeout enforced, due to concurrent requests (authRequestId missmatch) -> return a 408')
|
||||
|
||||
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 sess = request.getAuthSession(true)
|
||||
|
||||
if (inargs['oid4vp'] == 'ERROR') {
|
||||
LOG.debug("oid4vp error")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['oid4vp'] == 'SUCCEEDED') {
|
||||
LOG.debug("oid4vp succeeded")
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
// switch to access App
|
||||
if (inargs['accessApp'] == 'accessApp') {
|
||||
//TODO/aca/2025/06/19: In theory we could also land here when we send 'SUCCESS' to the frontend -> would be better to clear all session vaiables that can be set in this Authstate
|
||||
//TODO/aca/2025/06/19: Should we here rather set the LOGINMETHOD cookie and send an error assertion, since otherwise we might swich states too often and Nevis will kill the session?
|
||||
clearEidSession()
|
||||
LOG.debug("Switch to Access App")
|
||||
sess.setAttribute('agov.lastLoginMethod', 'accessApp')
|
||||
response.setResult('agovLogin')
|
||||
return
|
||||
}
|
||||
|
||||
// switch to fido2
|
||||
if (inargs['securityKey'] == 'securityKey') {
|
||||
clearEidSession()
|
||||
LOG.debug("Switch to Security Key")
|
||||
sess.setAttribute('agov.lastLoginMethod', 'securityKey')
|
||||
response.setResult('agovLogin')
|
||||
return
|
||||
}
|
||||
|
||||
// switch to registration
|
||||
if (inargs['fallback'] == 'register') {
|
||||
clearEidSession()
|
||||
LOG.debug("Switch to registration")
|
||||
response.setResult('register')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
HttpClient httpClient = HttpClients.create(parameters)
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
|
||||
if (getHeader('Content-Type') == 'application/json' && inargs.containsKey('o.id.v')) {
|
||||
LOG.debug("Request Status Update")
|
||||
// request for a status update from the verifier
|
||||
def result
|
||||
|
||||
// FE requested a new verification
|
||||
if (inargs['o.id.v'] == 'NEW' || inargs['o.id.v'] == 'RESET') {
|
||||
LOG.debug("Initializing new verification")
|
||||
if(!getNewVerification(sess, httpClient, verification_request_template, traceparent)){
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
def idvalue = (!inargs['o.id.v'] || inargs['o.id.v'] == 'NEW' || inargs['o.id.v'] == 'RESET') ? session['agov.eid.verification.id'] : inargs['o.id.v']
|
||||
|
||||
LOG.error("IDValSent: " + idvalue)
|
||||
|
||||
// check, whether we are still processing the same verification request or if a new one was generated in e.g. another Tab
|
||||
if(inargs['o.id.v'] && inargs['o.id.v'] != 'NEW' && inargs['o.id.v'] != 'RESET' && inargs['o.id.v'] != session['agov.eid.verification.id']){
|
||||
// wrong request, tell fe to stop polling and request a timeout
|
||||
LOG.debug('authentication timeout enforced, due to concurrent requests (verificationRequest missmatch) -> Notify FE & then return a 408')
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "TIMEOUT",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "REQUEST-MISMATCH",
|
||||
"error_message": "Request Mismatch Detected: Forcing Timeout"
|
||||
}}"""
|
||||
|
||||
response.setContent(result.toString())
|
||||
response.setContentType('application/json')
|
||||
response.setHttpStatusCode(200)
|
||||
response.setIsDirectResponse(true)
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
def endPoint = "${parameters.get('eidVerifierBaseUrl')}/api/v1/verifications/${idvalue}"
|
||||
|
||||
def httpResponse = Http.get()
|
||||
.url(endPoint)
|
||||
.header("Accept", "application/json")
|
||||
.header("traceparent", traceparent)
|
||||
.build()
|
||||
.send(httpClient)
|
||||
|
||||
|
||||
// 404 -> request a new verification
|
||||
if(httpResponse.code() == 404){
|
||||
// Frontend should know that we are starting a new request and not recieve an error
|
||||
def status = "FAILED"
|
||||
// Delete session variable to start a new verification
|
||||
sess.removeAttribute('agov.eid.verification')
|
||||
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "${status}",
|
||||
"verification_url": "",
|
||||
"id": "",
|
||||
"error_code": "HTTP-ERROR",
|
||||
"error_message": "Faild to verify status of verification, http status: ${httpResponse.code()}"
|
||||
}}"""
|
||||
LOG.warn("<== Response: ${httpResponse.code()}")
|
||||
}
|
||||
else if (httpResponse.code() != 200) {
|
||||
LOG.debug("Result: ${httpResponse}")
|
||||
|
||||
def status = "ERROR"
|
||||
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "${status}",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "HTTP-ERROR",
|
||||
"error_message": "failed to verify status of verification ${idvalue}, http status: ${httpResponse.code()}"
|
||||
}}"""
|
||||
LOG.warn("<== Response: ${httpResponse.code()}")
|
||||
}
|
||||
else {
|
||||
|
||||
def json = new JsonSlurper().parseText(httpResponse.bodyAsString())
|
||||
LOG.debug(httpResponse.bodyAsString())
|
||||
if (json.state == 'SUCCESS') {
|
||||
def claims = json.wallet_response.credential_subject_data
|
||||
LOG.debug("Store user data in session")
|
||||
|
||||
def validFrom = LocalDate.parse(claims.issuance_date, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(ZoneId.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||
def validTo = LocalDate.parse(claims.expiry_date, DateTimeFormatter.ISO_LOCAL_DATE).atTime(23,59,59).atOffset(ZoneOffset.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||
|
||||
sess.setAttribute('agov.eid.User.firstName', claims.given_name)
|
||||
sess.setAttribute('agov.eid.User.lastName', claims.family_name)
|
||||
sess.setAttribute('agov.eid.User.birthDate', claims.birth_date)
|
||||
sess.setAttribute('agov.eid.User.gender', claims.sex)
|
||||
sess.setAttribute('agov.eid.User.svnr', claims.personal_administrative_number.replace('.',''))
|
||||
sess.setAttribute('agov.eid.User.placeOfBirth', claims.birth_place)
|
||||
sess.setAttribute('agov.eid.User.placeOfOrigin', claims.place_of_origin)
|
||||
sess.setAttribute('agov.eid.User.eIdNumber', claims.document_number)
|
||||
// Simpler for later comparison -> Is converted again to upper case in the saml assertion
|
||||
sess.setAttribute('agov.eid.User.nationality', claims.nationality.toString().toLowerCase())
|
||||
|
||||
sess.setAttribute('ValidFrom', validFrom)
|
||||
sess.setAttribute('ValidTo', validTo)
|
||||
sess.setAttribute('authenticatedWith', "urn:qa.agov.ch:names:tc:authfactor:eid")
|
||||
sess.setAttribute('idVerification', "Eid")
|
||||
|
||||
// BUNDBITBK-5203 Dynamic aq levels
|
||||
def requestedRoleLevel = session['agov.requestedRoleLevel']
|
||||
if(requestedRoleLevel == "600"){
|
||||
sess.setAttribute('contextClassRefToSet', "urn:qa.agov.ch:names:tc:ac:classes:600")
|
||||
}else{
|
||||
sess.setAttribute('contextClassRefToSet', "urn:qa.agov.ch:names:tc:ac:classes:500")
|
||||
}
|
||||
|
||||
// subjectUUID v5
|
||||
def namespace = UUID.fromString(parameters.get('eidUUIDNamespace'))
|
||||
def uuid = Generators.nameBasedGenerator(namespace).generate(claims.personal_administrative_number)
|
||||
LOG.debug("UUID derived from svnr: ${uuid}")
|
||||
String uuidString = uuid.toString()
|
||||
sess.setAttribute('agov.subjectUUID', '' + uuidString)
|
||||
|
||||
response.setUserId(uuidString)
|
||||
sess.setAttribute('ch.adnovum.nevisidm.user.extId', uuidString)
|
||||
response.setLoginId(claims.document_number)
|
||||
response.setAuthLevel("EID")
|
||||
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "SUCCEEDED",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "NONE"
|
||||
}}"""
|
||||
}
|
||||
else if (json.state == 'FAILED') {
|
||||
LOG
|
||||
.error("Eid verification failed: ${json.wallet_response.error_code} (${json.wallet_response.error_description})")
|
||||
|
||||
def status = ERROR_CODE_TO_STATUS_MAPPER[json.wallet_response.error_code] ?: 'ERROR'
|
||||
|
||||
// Send new request & return variables with new id and url
|
||||
if(status == 'FAILED' || status == 'CANCELED'){
|
||||
// Delete session variable to start a new verification
|
||||
sess.removeAttribute('agov.eid.verification')
|
||||
|
||||
// Clear variables for for a cleaner result
|
||||
sess.removeAttribute('agov.eid.verification.link')
|
||||
}
|
||||
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "${status}",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "${json.wallet_response.error_code}",
|
||||
"error_message": "${json.wallet_response.error_description}"
|
||||
}}"""
|
||||
}
|
||||
else {
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "${inargs['o.id.v'] == 'NEW' || inargs['o.id.v'] == 'RESET' ? 'INITIATED' : 'PENDING'}",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "NONE"
|
||||
}}"""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOG.error("Eid verification failed: ${e}")
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "ERROR",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "HTTP-ERROR",
|
||||
"error_message": "failed to verify status of verification ${idvalue}, http exception"
|
||||
}}"""
|
||||
}
|
||||
|
||||
response.setContent(result.toString())
|
||||
response.setContentType('application/json')
|
||||
response.setHttpStatusCode(200)
|
||||
response.setIsDirectResponse(true)
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
||||
}
|
||||
|
||||
// if we reach this place, display GUI
|
||||
LOG.debug("Show GUI")
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
||||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
import ch.nevis.esauth.sess.Session
|
||||
import ch.nevis.esauth.util.httpclient.api.HttpClient
|
||||
import groovy.json.JsonSlurper
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
|
||||
import com.fasterxml.uuid.Generators
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
// returns true on success and false on failure
|
||||
def getNewVerification(Session sess, HttpClient httpClient, String verification_request_template, String traceparent){
|
||||
// Initialize the verification session on the verifier
|
||||
def endPoint = "${parameters.get('eidVerifierBaseUrl')}/api/v1/verifications"
|
||||
|
||||
try {
|
||||
def httpResponse = Http.post()
|
||||
.url(endPoint)
|
||||
.header("Accept", "application/json")
|
||||
.header("traceparent", traceparent)
|
||||
.entity(Http.entity()
|
||||
.content(verification_request_template.replaceAll("\\{\\{UUID}}", UUID.randomUUID().toString()))
|
||||
.contentType("application/json")
|
||||
.build())
|
||||
.build()
|
||||
.send(httpClient)
|
||||
|
||||
|
||||
if (httpResponse.code() != 200) {
|
||||
LOG.debug("Result: ${httpResponse}")
|
||||
return false
|
||||
}
|
||||
|
||||
def json = new JsonSlurper().parseText(httpResponse.bodyAsString())
|
||||
LOG.debug("Result: ${json}")
|
||||
|
||||
sess.setAttribute('agov.eid.verification', 'true')
|
||||
sess.setAttribute('agov.eid.verification.id', json.id)
|
||||
sess.setAttribute('agov.eid.verification.link', json.verification_url)
|
||||
|
||||
|
||||
// TODO/aca/2025-04-04:This could probably also be INITIATED, once the verifier supports this status
|
||||
if (json.state != 'PENDING') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOG.error("Eid verification failed: $e")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
def clearEidSession(){
|
||||
def s = request.getAuthSession(true)
|
||||
s.removeAttribute('agov.eid.verification')
|
||||
s.removeAttribute('agov.eid.verification.id')
|
||||
s.removeAttribute('agov.eid.verification.link')
|
||||
}
|
||||
|
||||
def verification_request_template = '''
|
||||
{ "presentation_definition": {
|
||||
"id": "{{UUID}}",
|
||||
"name": "AGOV Verification",
|
||||
"purpose": "AGOV Login",
|
||||
"format": {
|
||||
"vc+sd-jwt": {
|
||||
"sd-jwt_alg_values": [
|
||||
"ES256"
|
||||
],
|
||||
"kb-jwt_alg_values": [
|
||||
"ES256"
|
||||
]
|
||||
}
|
||||
},
|
||||
"input_descriptors": [
|
||||
{
|
||||
"id": "agov-all-attributes",
|
||||
"name": "AGOV Identity Verification",
|
||||
"purpose": "verification and authentication",
|
||||
"format": {
|
||||
"vc+sd-jwt": {
|
||||
"sd-jwt_alg_values": [
|
||||
"ES256"
|
||||
],
|
||||
"kb-jwt_alg_values": [
|
||||
"ES256"
|
||||
]
|
||||
}
|
||||
},
|
||||
"constraints": {
|
||||
"fields": [
|
||||
{
|
||||
"path": [
|
||||
"$.family_name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.given_name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.birth_date"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.sex"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.place_of_origin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.birth_place"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.nationality"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.personal_administrative_number"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.document_number"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.issuance_date"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.expiry_date"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.issuing_authority"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"$.issuing_country"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
def ERROR_CODE_TO_STATUS_MAPPER = [
|
||||
'CREDENTIAL_INVALID' : 'FAILED',
|
||||
'JWT_EXPIRED' : 'ERROR',
|
||||
'INVALID_FORMAT' : 'ERROR',
|
||||
'CREDENTIAL_EXPIRED' : 'FAILED',
|
||||
'MISSING_NONCE' : 'ERROR',
|
||||
'UNSUPPORTED_FORMAT' : 'ERROR',
|
||||
'CREDENTIAL_REVOKED' : 'FAILED',
|
||||
'CREDENTIAL_SUSPENDED' : 'FAILED',
|
||||
'HOLDER_BINDING_MISMATCH' : 'ERROR',
|
||||
'CREDENTIAL_MISSING_DATA' : 'FAILED',
|
||||
'UNRESOLVABLE_STATUS_LIST' : 'ERROR',
|
||||
'PUBLIC_KEY_OF_ISSUER_UNRESOLVABLE': 'ERROR',
|
||||
'CLIENT_REJECTED' : 'CANCELED',
|
||||
'ISSUER_NOT_ACCEPTED' : 'ERROR'
|
||||
]
|
||||
|
||||
// ---------------
|
||||
// check, whether we are still processing the correct AuthnRequest
|
||||
// or if the frontend requested a timeout
|
||||
if ( (inargs.containsKey('authRequestId') && (inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'])) || inargs['oid4vp'] == 'TIMEOUT') {
|
||||
// wrong request, "force" a timeout
|
||||
LOG.debug('authentication timeout enforced, due to concurrent requests (authRequestId missmatch) -> return a 408')
|
||||
|
||||
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 sess = request.getAuthSession(true)
|
||||
|
||||
if (inargs['oid4vp'] == 'ERROR') {
|
||||
LOG.debug("oid4vp error")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['oid4vp'] == 'SUCCEEDED') {
|
||||
LOG.debug("oid4vp succeeded")
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
// switch to access App
|
||||
if (inargs['accessApp'] == 'accessApp') {
|
||||
//TODO/aca/2025/06/19: In theory we could also land here when we send 'SUCCESS' to the frontend -> would be better to clear all session vaiables that can be set in this Authstate
|
||||
//TODO/aca/2025/06/19: Should we here rather set the LOGINMETHOD cookie and send an error assertion, since otherwise we might swich states too often and Nevis will kill the session?
|
||||
clearEidSession()
|
||||
LOG.debug("Switch to Access App")
|
||||
sess.setAttribute('agov.lastLoginMethod', 'accessApp')
|
||||
response.setResult('agovLogin')
|
||||
return
|
||||
}
|
||||
|
||||
// switch to fido2
|
||||
if (inargs['securityKey'] == 'securityKey') {
|
||||
clearEidSession()
|
||||
LOG.debug("Switch to Security Key")
|
||||
sess.setAttribute('agov.lastLoginMethod', 'securityKey')
|
||||
response.setResult('agovLogin')
|
||||
return
|
||||
}
|
||||
|
||||
// switch to registration
|
||||
if (inargs['fallback'] == 'register') {
|
||||
clearEidSession()
|
||||
LOG.debug("Switch to registration")
|
||||
response.setResult('register')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
HttpClient httpClient = HttpClients.create(parameters)
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
|
||||
if (getHeader('Content-Type') == 'application/json' && inargs.containsKey('o.id.v')) {
|
||||
LOG.debug("Request Status Update")
|
||||
// request for a status update from the verifier
|
||||
def result
|
||||
|
||||
// FE requested a new verification
|
||||
if (inargs['o.id.v'] == 'NEW' || inargs['o.id.v'] == 'RESET') {
|
||||
LOG.debug("Initializing new verification")
|
||||
if(!getNewVerification(sess, httpClient, verification_request_template, traceparent)){
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
def idvalue = (!inargs['o.id.v'] || inargs['o.id.v'] == 'NEW' || inargs['o.id.v'] == 'RESET') ? session['agov.eid.verification.id'] : inargs['o.id.v']
|
||||
|
||||
LOG.error("IDValSent: " + idvalue)
|
||||
|
||||
// check, whether we are still processing the same verification request or if a new one was generated in e.g. another Tab
|
||||
if(inargs['o.id.v'] && inargs['o.id.v'] != 'NEW' && inargs['o.id.v'] != 'RESET' && inargs['o.id.v'] != session['agov.eid.verification.id']){
|
||||
// wrong request, tell fe to stop polling and request a timeout
|
||||
LOG.debug('authentication timeout enforced, due to concurrent requests (verificationRequest missmatch) -> Notify FE & then return a 408')
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "TIMEOUT",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "REQUEST-MISMATCH",
|
||||
"error_message": "Request Mismatch Detected: Forcing Timeout"
|
||||
}}"""
|
||||
|
||||
response.setContent(result.toString())
|
||||
response.setContentType('application/json')
|
||||
response.setHttpStatusCode(200)
|
||||
response.setIsDirectResponse(true)
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
def endPoint = "${parameters.get('eidVerifierBaseUrl')}/api/v1/verifications/${idvalue}"
|
||||
|
||||
def httpResponse = Http.get()
|
||||
.url(endPoint)
|
||||
.header("Accept", "application/json")
|
||||
.header("traceparent", traceparent)
|
||||
.build()
|
||||
.send(httpClient)
|
||||
|
||||
|
||||
// 404 -> request a new verification
|
||||
if(httpResponse.code() == 404){
|
||||
// Frontend should know that we are starting a new request and not recieve an error
|
||||
def status = "FAILED"
|
||||
// Delete session variable to start a new verification
|
||||
sess.removeAttribute('agov.eid.verification')
|
||||
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "${status}",
|
||||
"verification_url": "",
|
||||
"id": "",
|
||||
"error_code": "HTTP-ERROR",
|
||||
"error_message": "Faild to verify status of verification, http status: ${httpResponse.code()}"
|
||||
}}"""
|
||||
LOG.warn("<== Response: ${httpResponse.code()}")
|
||||
}
|
||||
else if (httpResponse.code() != 200) {
|
||||
LOG.debug("Result: ${httpResponse}")
|
||||
|
||||
def status = "ERROR"
|
||||
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "${status}",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "HTTP-ERROR",
|
||||
"error_message": "failed to verify status of verification ${idvalue}, http status: ${httpResponse.code()}"
|
||||
}}"""
|
||||
LOG.warn("<== Response: ${httpResponse.code()}")
|
||||
}
|
||||
else {
|
||||
|
||||
def json = new JsonSlurper().parseText(httpResponse.bodyAsString())
|
||||
LOG.debug(httpResponse.bodyAsString())
|
||||
if (json.state == 'SUCCESS') {
|
||||
def claims = json.wallet_response.credential_subject_data
|
||||
LOG.debug("Store user data in session")
|
||||
|
||||
def validFrom = LocalDate.parse(claims.issuance_date, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(ZoneId.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||
def validTo = LocalDate.parse(claims.expiry_date, DateTimeFormatter.ISO_LOCAL_DATE).atTime(23,59,59).atOffset(ZoneOffset.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||
|
||||
sess.setAttribute('agov.eid.User.firstName', claims.given_name)
|
||||
sess.setAttribute('agov.eid.User.lastName', claims.family_name)
|
||||
sess.setAttribute('agov.eid.User.birthDate', claims.birth_date)
|
||||
sess.setAttribute('agov.eid.User.gender', claims.sex)
|
||||
sess.setAttribute('agov.eid.User.svnr', claims.personal_administrative_number.replace('.',''))
|
||||
sess.setAttribute('agov.eid.User.placeOfBirth', claims.birth_place)
|
||||
sess.setAttribute('agov.eid.User.placeOfOrigin', claims.place_of_origin)
|
||||
sess.setAttribute('agov.eid.User.eIdNumber', claims.document_number)
|
||||
// Simpler for later comparison -> Is converted again to upper case in the saml assertion
|
||||
sess.setAttribute('agov.eid.User.nationality', claims.nationality.toString().toLowerCase())
|
||||
|
||||
sess.setAttribute('ValidFrom', validFrom)
|
||||
sess.setAttribute('ValidTo', validTo)
|
||||
sess.setAttribute('authenticatedWith', "urn:qa.agov.ch:names:tc:authfactor:eid")
|
||||
sess.setAttribute('idVerification', "Eid")
|
||||
|
||||
// BUNDBITBK-5203 Dynamic aq levels
|
||||
def requestedRoleLevel = session['agov.requestedRoleLevel']
|
||||
if(requestedRoleLevel == "600"){
|
||||
sess.setAttribute('contextClassRefToSet', "urn:qa.agov.ch:names:tc:ac:classes:600")
|
||||
}else{
|
||||
sess.setAttribute('contextClassRefToSet', "urn:qa.agov.ch:names:tc:ac:classes:500")
|
||||
}
|
||||
|
||||
// subjectUUID v5
|
||||
def namespace = UUID.fromString(parameters.get('eidUUIDNamespace'))
|
||||
def uuid = Generators.nameBasedGenerator(namespace).generate(claims.personal_administrative_number)
|
||||
LOG.debug("UUID derived from svnr: ${uuid}")
|
||||
String uuidString = uuid.toString()
|
||||
sess.setAttribute('agov.subjectUUID', '' + uuidString)
|
||||
|
||||
response.setUserId(uuidString)
|
||||
sess.setAttribute('ch.adnovum.nevisidm.user.extId', uuidString)
|
||||
response.setLoginId(claims.document_number)
|
||||
response.setAuthLevel("EID")
|
||||
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "SUCCEEDED",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "NONE"
|
||||
}}"""
|
||||
}
|
||||
else if (json.state == 'FAILED') {
|
||||
LOG
|
||||
.error("Eid verification failed: ${json.wallet_response.error_code} (${json.wallet_response.error_description})")
|
||||
|
||||
def status = ERROR_CODE_TO_STATUS_MAPPER[json.wallet_response.error_code] ?: 'ERROR'
|
||||
|
||||
// Send new request & return variables with new id and url
|
||||
if(status == 'FAILED' || status == 'CANCELED'){
|
||||
// Delete session variable to start a new verification
|
||||
sess.removeAttribute('agov.eid.verification')
|
||||
|
||||
// Clear variables for for a cleaner result
|
||||
sess.removeAttribute('agov.eid.verification.link')
|
||||
}
|
||||
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "${status}",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "${json.wallet_response.error_code}",
|
||||
"error_message": "${json.wallet_response.error_description}"
|
||||
}}"""
|
||||
}
|
||||
else {
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "${inargs['o.id.v'] == 'NEW' || inargs['o.id.v'] == 'RESET' ? 'INITIATED' : 'PENDING'}",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "NONE"
|
||||
}}"""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOG.error("Eid verification failed: ${e}")
|
||||
result = """{
|
||||
"oid4vp": {
|
||||
"status": "ERROR",
|
||||
"verification_url": "${session['agov.eid.verification.link']}",
|
||||
"id": "${idvalue}",
|
||||
"error_code": "HTTP-ERROR",
|
||||
"error_message": "failed to verify status of verification ${idvalue}, http exception"
|
||||
}}"""
|
||||
}
|
||||
|
||||
response.setContent(result.toString())
|
||||
response.setContentType('application/json')
|
||||
response.setHttpStatusCode(200)
|
||||
response.setIsDirectResponse(true)
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
||||
}
|
||||
|
||||
// if we reach this place, display GUI
|
||||
LOG.debug("Show GUI")
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
||||
|
||||
|
|
|
@ -122,4 +122,4 @@ if (inargs['submit']) {
|
|||
}
|
||||
|
||||
// show the GUI
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
|
|
|
@ -13,8 +13,9 @@ JAVA_OPTS=(
|
|||
"-javaagent:/opt/agent/opentelemetry-javaagent.jar"
|
||||
"-Dotel.javaagent.logging=application"
|
||||
"-Dotel.javaagent.configuration-file=/var/opt/nevisauth/default/conf/otel.properties"
|
||||
"-Dotel.resource.attributes=service.version=8.2411.3,service.instance.id=$HOSTNAME"
|
||||
"-Dotel.resource.attributes=service.version=8.2505.5,service.instance.id=$HOSTNAME"
|
||||
"-Djavax.net.ssl.trustStore=/var/opt/keys/trust/auth-idp-extended-truststore/truststore.p12"
|
||||
"-Djavax.net.ssl.trustStorePassword=\${exec:/var/opt/keys/trust/auth-idp-extended-truststore/keypass}"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -45,6 +45,8 @@
|
|||
<!-- source: pattern://94e0b7b92ff2593f958c1eec -->
|
||||
<field src="session" key="ch.adnovum.nevisidm.clientId" as="clientId"/>
|
||||
<!-- source: pattern://94e0b7b92ff2593f958c1eec -->
|
||||
<field src="session" key="ch.nevis.session.domain" as="domain"/>
|
||||
<!-- source: pattern://94e0b7b92ff2593f958c1eec -->
|
||||
<field src="request" key="ActualRoles" as="roles"/>
|
||||
</TokenSpec>
|
||||
<!-- source: pattern://94e0b7b92ff2593f958c1eec -->
|
||||
|
@ -65,6 +67,8 @@
|
|||
<!-- source: pattern://94e0b7b92ff2593f958c1eec -->
|
||||
<field src="session" key="ch.adnovum.nevisidm.clientId" as="clientId"/>
|
||||
<!-- source: pattern://94e0b7b92ff2593f958c1eec -->
|
||||
<field src="session" key="ch.nevis.session.domain" as="domain"/>
|
||||
<!-- source: pattern://94e0b7b92ff2593f958c1eec -->
|
||||
<field src="request" key="ActualRoles" as="roles"/>
|
||||
</TokenSpec>
|
||||
<!-- source: pattern://94e0b7b92ff2593f958c1eec -->
|
||||
|
@ -3282,8 +3286,6 @@
|
|||
<!-- source: pattern://e0fda9336be9c69dafc9b69e -->
|
||||
<ResultCond name="SOAP:showGui" next="NotUsed_Auth_Realm_Prepare_Done"/>
|
||||
<!-- source: pattern://e0fda9336be9c69dafc9b69e -->
|
||||
<ResultCond name="default" next="NotUsed_Auth_Realm_Prepare_Done"/>
|
||||
<!-- source: pattern://e0fda9336be9c69dafc9b69e -->
|
||||
<ResultCond name="ok" next="NotUsed_Auth_Realm_Prepare_Done" startOver="true"/>
|
||||
<!-- source: pattern://e0fda9336be9c69dafc9b69e -->
|
||||
<ResultCond name="showGui" next="NotUsed_Auth_Realm_NotUsed_Pwd_Login-IdmPostProcessing"/>
|
||||
|
@ -3302,6 +3304,12 @@
|
|||
<property name="detaillevel.default" value="EXCLUDE"/>
|
||||
<!-- source: pattern://e0fda9336be9c69dafc9b69e -->
|
||||
<property name="detaillevel.user" value="MEDIUM"/>
|
||||
<!-- source: pattern://e0fda9336be9c69dafc9b69e -->
|
||||
<property name="detaillevel.profile" value="MEDIUM"/>
|
||||
<!-- source: pattern://e0fda9336be9c69dafc9b69e -->
|
||||
<property name="detaillevel.role" value="LOW"/>
|
||||
<!-- source: pattern://e0fda9336be9c69dafc9b69e -->
|
||||
<property name="forceDataReload" value="true"/>
|
||||
</AuthState>
|
||||
<AuthState name="NotUsed_Auth_Realm_NotUsed_Pwd_Login-IdmPasswordChange" class="ch.nevis.idm.authstate.IdmChangePasswordState" final="false">
|
||||
<!-- source: pattern://e0fda9336be9c69dafc9b69e -->
|
||||
|
@ -3379,7 +3387,7 @@
|
|||
<!-- source: pattern://e0fda9336be9c69dafc9b69e -->
|
||||
<GuiElem name="isiwebnewpw2" type="pw-text" label="prompt.newpassword.confirm"/>
|
||||
<!-- source: pattern://e0fda9336be9c69dafc9b69e -->
|
||||
<GuiElem name="submit" type="submit" label="button.submit"/>
|
||||
<GuiElem name="submit" type="submit" label="submit.button.label"/>
|
||||
</Gui>
|
||||
</Response>
|
||||
<propertyRef name="nevisIDM_Connector"/>
|
||||
|
@ -3442,4 +3450,6 @@
|
|||
<!-- source: pattern://ab5a82719993921822e95751 -->
|
||||
<property name="out.keyobjectref" value="Signer_IDP_AGOV"/>
|
||||
</WebService>
|
||||
<!-- source: pattern://7022472ae407577ae604bbb8 -->
|
||||
<RESTService name="ManagementService" class="ch.nevis.esauth.rest.service.session.ManagementService"/>
|
||||
</esauth-server>
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
import groovy.json.JsonSlurper
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
def jsonSlurper = new JsonSlurper()
|
||||
|
||||
|
||||
def lang = (session['ch.nevis.idm.User.language']?:'DE').trim()
|
||||
def endppoint = "${parameters.get('baseurl')}/api/v1/countries?lang=${lang.toUpperCase()}"
|
||||
def countryCode = (session['ch.nevis.idm.User.country']?:'CH').trim().toLowerCase()
|
||||
|
||||
try {
|
||||
LOG.debug("UTILITY: Countries: Request url: ${endppoint}")
|
||||
|
||||
def httpClient = HttpClients.create(parameters)
|
||||
def httpResponse = Http.get().url(endppoint).header('traceparent', traceparent).build().send(httpClient)
|
||||
|
||||
LOG.debug('UTILITY: Countries: Response Message: ' + httpResponse.reasonPhrase())
|
||||
LOG.debug('UTILITY: Countries: Response Status Code: ' + httpResponse.code())
|
||||
LOG.debug('UTILITY: Countries: Response: ' + httpResponse.bodyAsString())
|
||||
|
||||
if (httpResponse.code() == 200) {
|
||||
def json = jsonSlurper.parseText(httpResponse.bodyAsString())
|
||||
// {"country.af":"Afghanistan","country.al":"Albanie"... }
|
||||
def countryName = json["country.${countryCode}"]
|
||||
LOG.debug("UTILITY: Countries: countryName for ${countryCode}: ${countryName}")
|
||||
if (countryName) {
|
||||
sess.setAttribute('agov.countryName', countryName)
|
||||
}
|
||||
} else {
|
||||
LOG.warn("UTILITY: Countries: Failed to fetch country translations. (httpResponse.code: ${httpResponse.code()})")
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("UTILITY: Countries: Failed to fetch country translations. (${e})")
|
||||
}
|
||||
import groovy.json.JsonSlurper
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
def jsonSlurper = new JsonSlurper()
|
||||
|
||||
|
||||
def lang = (session['ch.nevis.idm.User.language']?:'DE').trim()
|
||||
def endppoint = "${parameters.get('baseurl')}/api/v1/countries?lang=${lang.toUpperCase()}"
|
||||
def countryCode = (session['ch.nevis.idm.User.country']?:'CH').trim().toLowerCase()
|
||||
|
||||
try {
|
||||
LOG.debug("UTILITY: Countries: Request url: ${endppoint}")
|
||||
|
||||
def httpClient = HttpClients.create(parameters)
|
||||
def httpResponse = Http.get().url(endppoint).header('traceparent', traceparent).build().send(httpClient)
|
||||
|
||||
LOG.debug('UTILITY: Countries: Response Message: ' + httpResponse.reasonPhrase())
|
||||
LOG.debug('UTILITY: Countries: Response Status Code: ' + httpResponse.code())
|
||||
LOG.debug('UTILITY: Countries: Response: ' + httpResponse.bodyAsString())
|
||||
|
||||
if (httpResponse.code() == 200) {
|
||||
def json = jsonSlurper.parseText(httpResponse.bodyAsString())
|
||||
// {"country.af":"Afghanistan","country.al":"Albanie"... }
|
||||
def countryName = json["country.${countryCode}"]
|
||||
LOG.debug("UTILITY: Countries: countryName for ${countryCode}: ${countryName}")
|
||||
if (countryName) {
|
||||
sess.setAttribute('agov.countryName', countryName)
|
||||
}
|
||||
} else {
|
||||
LOG.warn("UTILITY: Countries: Failed to fetch country translations. (httpResponse.code: ${httpResponse.code()})")
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("UTILITY: Countries: Failed to fetch country translations. (${e})")
|
||||
}
|
||||
response.setResult('ok')
|
|
@ -1,38 +1,38 @@
|
|||
import groovy.json.JsonSlurper
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
def url = parameters.get('url')
|
||||
def realIpHttpHeaderName = parameters.get('realIpHttpHeaderName') ?: 'X-Real-IP'
|
||||
def ip = request.getLoginContext()['connection.HttpHeader.X-Real-IP'] ?: 'unknown'
|
||||
|
||||
try {
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
def jsonSlurper = new JsonSlurper()
|
||||
def httpClient = HttpClients.create(parameters)
|
||||
def httpResponse = Http.get().url(url).header('traceparent', traceparent)
|
||||
.header(realIpHttpHeaderName, ip).build().send(httpClient)
|
||||
|
||||
LOG.debug('Response Status Code: ' + httpResponse.code())
|
||||
LOG.debug('Response: ' + httpResponse.bodyAsString())
|
||||
|
||||
if (httpResponse.code() == 200) {
|
||||
def json = jsonSlurper.parseText(httpResponse.bodyAsString())
|
||||
|
||||
response.setSessionAttribute('agov.fido2.captchaSettings.enabled', String.valueOf(json.friendlyCaptureClientSettings.enabled))
|
||||
response.setSessionAttribute('agov.fido2.captchaSettings.siteKey', json.friendlyCaptureClientSettings.siteKey)
|
||||
response.setSessionAttribute('agov.fido2.captchaSettings.puzzleUrl', json.friendlyCaptureClientSettings.puzzleUrl)
|
||||
|
||||
response.setResult('ok')
|
||||
} else {
|
||||
LOG.error('Unexcpected HTTP response code: ' + httpResponse.code())
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Unexpected HTTP reponse')
|
||||
}
|
||||
} catch (all) {
|
||||
// Handle exception and set the transition
|
||||
LOG.error('error: ' + all, all)
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Exception during HTTP call')
|
||||
import groovy.json.JsonSlurper
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
def url = parameters.get('url')
|
||||
def realIpHttpHeaderName = parameters.get('realIpHttpHeaderName') ?: 'X-Real-IP'
|
||||
def ip = request.getLoginContext()['connection.HttpHeader.X-Real-IP'] ?: 'unknown'
|
||||
|
||||
try {
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
def jsonSlurper = new JsonSlurper()
|
||||
def httpClient = HttpClients.create(parameters)
|
||||
def httpResponse = Http.get().url(url).header('traceparent', traceparent)
|
||||
.header(realIpHttpHeaderName, ip).build().send(httpClient)
|
||||
|
||||
LOG.debug('Response Status Code: ' + httpResponse.code())
|
||||
LOG.debug('Response: ' + httpResponse.bodyAsString())
|
||||
|
||||
if (httpResponse.code() == 200) {
|
||||
def json = jsonSlurper.parseText(httpResponse.bodyAsString())
|
||||
|
||||
response.setSessionAttribute('agov.fido2.captchaSettings.enabled', String.valueOf(json.friendlyCaptureClientSettings.enabled))
|
||||
response.setSessionAttribute('agov.fido2.captchaSettings.siteKey', json.friendlyCaptureClientSettings.siteKey)
|
||||
response.setSessionAttribute('agov.fido2.captchaSettings.puzzleUrl', json.friendlyCaptureClientSettings.puzzleUrl)
|
||||
|
||||
response.setResult('ok')
|
||||
} else {
|
||||
LOG.error('Unexcpected HTTP response code: ' + httpResponse.code())
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Unexpected HTTP reponse')
|
||||
}
|
||||
} catch (all) {
|
||||
// Handle exception and set the transition
|
||||
LOG.error('error: ' + all, all)
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Exception during HTTP call')
|
||||
}
|
|
@ -1,63 +1,63 @@
|
|||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
def url = parameters.get('url')
|
||||
|
||||
def email = inargs['userInputValue_prompt.email']
|
||||
def token = inargs['captcha_response']?: 'MISSING'
|
||||
def enabled = (session['agov.fido2.captchaSettings.enabled']?:'true').toBoolean()
|
||||
|
||||
def ip = request.getLoginContext()['connection.HttpHeader.X-Real-IP'] ?: 'unknown'
|
||||
def userAgent = request.getLoginContext()['connection.HttpHeader.user-agent'] ?: request.getLoginContext()['connection.HttpHeader.User-Agent'] ?: 'unknown'
|
||||
|
||||
def payload = "{ \"userIp\": \"${ip}\", \"email\": \"${email}\", \"userAgent\": \"${userAgent}\" }"
|
||||
|
||||
LOG.debug('Token: ' + token)
|
||||
LOG.debug('Payload: ' + payload)
|
||||
|
||||
try {
|
||||
|
||||
if (!enabled) {
|
||||
LOG.info("FriendlyCAPTCHA is disabled, allowing operation for ${payload}")
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
def httpClient = HttpClients.create(parameters)
|
||||
def httpResponse = Http.post()
|
||||
.url(url)
|
||||
.header("Accept", "application/json")
|
||||
.header("X-FriendlyCAPTCHA-Token", token)
|
||||
.header("traceparent", traceparent)
|
||||
.entity(Http.entity()
|
||||
.content(payload)
|
||||
.contentType("application/json")
|
||||
.build())
|
||||
.build()
|
||||
.send(httpClient)
|
||||
|
||||
LOG.debug('Response Status Code: ' + httpResponse.code())
|
||||
LOG.debug('Response: ' + httpResponse.bodyAsString())
|
||||
|
||||
if (httpResponse.code() == 200) {
|
||||
if (httpResponse.bodyAsString().contains('SUCCESSFUL')) {
|
||||
response.setResult('ok')
|
||||
return
|
||||
} else {
|
||||
LOG.warn("Friendly captcha not successful for '{ \"userIp\": \"${ip}\", \"email\": \"${email}\", \"userAgent\": \"${userAgent}\" }'")
|
||||
response.setResult('exit.1')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
LOG.error("Friendly captcha failed with statuscode ${httpResponse.code()} for '{ \"userIp\": \"${ip}\", \"email\": \"${email}\", \"userAgent\": \"${userAgent}\" }'")
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Unexpected HTTP reponse')
|
||||
}
|
||||
} catch (all) {
|
||||
// Handle exception and set the transition
|
||||
LOG.error("Friendly captcha failed with a general error '${all}' for '{ \"userIp\": \"${ip}\", \"email\": \"${email}\", \"userAgent\": \"${userAgent}\" }', service-url: ${url}")
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Exception during HTTP call')
|
||||
}
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
def url = parameters.get('url')
|
||||
|
||||
def email = inargs['userInputValue_prompt.email']
|
||||
def token = inargs['captcha_response']?: 'MISSING'
|
||||
def enabled = (session['agov.fido2.captchaSettings.enabled']?:'true').toBoolean()
|
||||
|
||||
def ip = request.getLoginContext()['connection.HttpHeader.X-Real-IP'] ?: 'unknown'
|
||||
def userAgent = request.getLoginContext()['connection.HttpHeader.user-agent'] ?: request.getLoginContext()['connection.HttpHeader.User-Agent'] ?: 'unknown'
|
||||
|
||||
def payload = "{ \"userIp\": \"${ip}\", \"email\": \"${email}\", \"userAgent\": \"${userAgent}\" }"
|
||||
|
||||
LOG.debug('Token: ' + token)
|
||||
LOG.debug('Payload: ' + payload)
|
||||
|
||||
try {
|
||||
|
||||
if (!enabled) {
|
||||
LOG.info("FriendlyCAPTCHA is disabled, allowing operation for ${payload}")
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
def httpClient = HttpClients.create(parameters)
|
||||
def httpResponse = Http.post()
|
||||
.url(url)
|
||||
.header("Accept", "application/json")
|
||||
.header("X-FriendlyCAPTCHA-Token", token)
|
||||
.header("traceparent", traceparent)
|
||||
.entity(Http.entity()
|
||||
.content(payload)
|
||||
.contentType("application/json")
|
||||
.build())
|
||||
.build()
|
||||
.send(httpClient)
|
||||
|
||||
LOG.debug('Response Status Code: ' + httpResponse.code())
|
||||
LOG.debug('Response: ' + httpResponse.bodyAsString())
|
||||
|
||||
if (httpResponse.code() == 200) {
|
||||
if (httpResponse.bodyAsString().contains('SUCCESSFUL')) {
|
||||
response.setResult('ok')
|
||||
return
|
||||
} else {
|
||||
LOG.warn("Friendly captcha not successful for '{ \"userIp\": \"${ip}\", \"email\": \"${email}\", \"userAgent\": \"${userAgent}\" }'")
|
||||
response.setResult('exit.1')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
LOG.error("Friendly captcha failed with statuscode ${httpResponse.code()} for '{ \"userIp\": \"${ip}\", \"email\": \"${email}\", \"userAgent\": \"${userAgent}\" }'")
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Unexpected HTTP reponse')
|
||||
}
|
||||
} catch (all) {
|
||||
// Handle exception and set the transition
|
||||
LOG.error("Friendly captcha failed with a general error '${all}' for '{ \"userIp\": \"${ip}\", \"email\": \"${email}\", \"userAgent\": \"${userAgent}\" }', service-url: ${url}")
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Exception during HTTP call')
|
||||
}
|
||||
|
|
|
@ -24,3 +24,4 @@ else {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -20,4 +20,4 @@ if(outargs.containsKey('saml.SAMLResponse')) {
|
|||
}
|
||||
else {
|
||||
response.setResult('ok')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,3 +32,4 @@ else {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,168 +1,168 @@
|
|||
import groovy.xml.XmlSlurper
|
||||
import groovy.xml.slurpersupport.GPathResult
|
||||
import groovy.xml.slurpersupport.NodeChild
|
||||
|
||||
import java.util.zip.Inflater
|
||||
import java.util.zip.InflaterInputStream
|
||||
|
||||
/**
|
||||
* Gets the value of the Referer header.
|
||||
* If the header is missing the fallback is returned
|
||||
*
|
||||
* This method is used when SAML IDP / Dispatch Error Redirect is not set
|
||||
*
|
||||
* @param fallback - value to return if the Referer header is missing
|
||||
* @return value of header or fallback
|
||||
*/
|
||||
def getReferer(String fallback) {
|
||||
return request.getHttpHeader('Referer') ?: fallback
|
||||
}
|
||||
|
||||
def redirect(String url) {
|
||||
outargs.put('nevis.transfer.type', 'redirect')
|
||||
outargs.put('nevis.transfer.destination', url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the content of the Issuer element from a parsed SAML message.
|
||||
* The Issuer is optional according to SAML specification but we need it for dispatching.
|
||||
*
|
||||
* @param xml - as parsed by Groovy XmlSlurper
|
||||
* @return text content of Issuer element converted or null
|
||||
*/
|
||||
String getIssuer(GPathResult xml) {
|
||||
return xml.depthFirst().find { GPathResult node -> {
|
||||
node.name().endsWith(":Issuer") || node.name().equalsIgnoreCase("Issuer")
|
||||
}
|
||||
}?.text()
|
||||
}
|
||||
|
||||
String getIssuer(String value) {
|
||||
if (value == null) {
|
||||
return
|
||||
}
|
||||
String text
|
||||
byte[] decoded
|
||||
def parser = new XmlSlurper()
|
||||
// if value is raw xml then continue otherwise try to parse the base64 encoding
|
||||
if (value.startsWith("<")) {
|
||||
text = new String(value)
|
||||
}
|
||||
else {
|
||||
decoded = value.decodeBase64()
|
||||
text = new String(decoded)
|
||||
LOG.info("received SAML request $value")
|
||||
}
|
||||
|
||||
// after decoded, if redirect binding, we need to parse string to xml
|
||||
if (text.startsWith("<")) {
|
||||
LOG.debug("assuming POST/SOAP binding")
|
||||
// plain String (POST/SOAP parameter)
|
||||
def xml = parser.parseText(text)
|
||||
return getIssuer(xml)
|
||||
}
|
||||
else {
|
||||
LOG.debug("assuming redirect binding")
|
||||
// should be deflate encoded (query parameter)
|
||||
def is = new InflaterInputStream(new ByteArrayInputStream(decoded), new Inflater(true))
|
||||
def xml = parser.parse(is)
|
||||
return getIssuer(xml)
|
||||
}
|
||||
}
|
||||
|
||||
def dispatchIssuer(i2s, String issuer) {
|
||||
def result = i2s.get(issuer)
|
||||
if (result == null) {
|
||||
LOG.info("No SP found for issuer '$issuer'. Hint: check SAML SP Connector patterns.")
|
||||
}
|
||||
|
||||
// dispatch different idp if artifact binding is enabled
|
||||
if(parameters.get('epdMode') == 'artifact' && result == 'epd'){
|
||||
LOG.debug("EPD: Artifact mode")
|
||||
result = result + "_artifact"
|
||||
}else{
|
||||
LOG.debug("EPD: POST mode")
|
||||
}
|
||||
response.setResult(result)
|
||||
session.put("saml.inbound.issuer", issuer)
|
||||
session.put('saml.idp.result', result) // remember decision for sub-sequent requests without a SAML message
|
||||
|
||||
}
|
||||
|
||||
def dispatchMessage(i2s, String message) {
|
||||
def issuer = getIssuer(message)
|
||||
if (issuer == null) {
|
||||
LOG.info("No issuer found in incoming SAML message. Giving up.")
|
||||
}
|
||||
session.put("saml.inbound.issuer", issuer)
|
||||
dispatchIssuer(i2s, issuer)
|
||||
}
|
||||
|
||||
if (parameters.get('logoutConfirmation') == 'true' && "stepup" == request.getMethod()) {
|
||||
String url = request.currentResource
|
||||
def path = new URL(url).getPath()
|
||||
if (path.endsWith("/logout")) {
|
||||
// next AuthState will show a logout confirmation GUI
|
||||
response.setResult('confirm')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ensure session exists
|
||||
if (request.getSession(false) == null) {
|
||||
session = request.getSession(true).getData()
|
||||
}
|
||||
|
||||
// issuer (any case) -> ResultCond name
|
||||
def i2s = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER)
|
||||
|
||||
|
||||
i2s.put(parameters.get('atb'), 'main')
|
||||
i2s.put(parameters.get('epd_atb'), 'epd')
|
||||
|
||||
if (parameters.get('spInitiated') == 'true' && inargs.containsKey('SAMLRequest')) { // SP-initiated authentication
|
||||
LOG.debug("found SAMLRequest parameter for SP-initiated authentication")
|
||||
String message = inargs.get('SAMLRequest')
|
||||
dispatchMessage(i2s, message)
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs.containsKey('SAMLResponse')) { // response to IDP-initiated SAML Logout
|
||||
LOG.debug("found SAMLResponse parameter")
|
||||
String message = inargs.get('SAMLResponse')
|
||||
dispatchMessage(i2s, message)
|
||||
return
|
||||
}
|
||||
|
||||
if (parameters.get('spInitiated') == 'true' && inargs.containsKey('soapheader')) { // SP-initiated SOAP with soapheader
|
||||
LOG.debug("found soapheader parameter for SP-initiated")
|
||||
String message = inargs.get('soapheader')
|
||||
dispatchMessage(i2s, message)
|
||||
return
|
||||
}
|
||||
|
||||
if (parameters.get('spInitiated') == 'true' && inargs.containsKey('')) { // SP-initiated SOAP with empty
|
||||
LOG.debug("found empty parameter for SP-initiated SOAP message")
|
||||
String message = inargs.get('')
|
||||
dispatchMessage(i2s, message)
|
||||
return
|
||||
}
|
||||
|
||||
String issuer = inargs['Issuer'] ?: inargs['issuer']
|
||||
if (parameters.get('idpInitiated') == 'true' && issuer != null) { // IDP-initiated authentication
|
||||
LOG.debug("found Issuer parameter for IDP-initiated authentication")
|
||||
dispatchIssuer(i2s, issuer)
|
||||
return
|
||||
}
|
||||
|
||||
// used as fallback in case of ?logout (we need an IdentityProviderState)
|
||||
if (inargs.containsKey("logout") && session.containsKey('saml.idp.result')) {
|
||||
def result = session.get('saml.idp.result')
|
||||
LOG.debug("dispatching to last used ResultCond: $result")
|
||||
response.setResult(result)
|
||||
return
|
||||
}
|
||||
|
||||
def location = getReferer('/')
|
||||
LOG.info("Unable to dispatch request. Giving up and redirecting (back) to $location")
|
||||
import groovy.xml.XmlSlurper
|
||||
import groovy.xml.slurpersupport.GPathResult
|
||||
import groovy.xml.slurpersupport.NodeChild
|
||||
|
||||
import java.util.zip.Inflater
|
||||
import java.util.zip.InflaterInputStream
|
||||
|
||||
/**
|
||||
* Gets the value of the Referer header.
|
||||
* If the header is missing the fallback is returned
|
||||
*
|
||||
* This method is used when SAML IDP / Dispatch Error Redirect is not set
|
||||
*
|
||||
* @param fallback - value to return if the Referer header is missing
|
||||
* @return value of header or fallback
|
||||
*/
|
||||
def getReferer(String fallback) {
|
||||
return request.getHttpHeader('Referer') ?: fallback
|
||||
}
|
||||
|
||||
def redirect(String url) {
|
||||
outargs.put('nevis.transfer.type', 'redirect')
|
||||
outargs.put('nevis.transfer.destination', url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the content of the Issuer element from a parsed SAML message.
|
||||
* The Issuer is optional according to SAML specification but we need it for dispatching.
|
||||
*
|
||||
* @param xml - as parsed by Groovy XmlSlurper
|
||||
* @return text content of Issuer element converted or null
|
||||
*/
|
||||
String getIssuer(GPathResult xml) {
|
||||
return xml.depthFirst().find { GPathResult node -> {
|
||||
node.name().endsWith(":Issuer") || node.name().equalsIgnoreCase("Issuer")
|
||||
}
|
||||
}?.text()
|
||||
}
|
||||
|
||||
String getIssuer(String value) {
|
||||
if (value == null) {
|
||||
return
|
||||
}
|
||||
String text
|
||||
byte[] decoded
|
||||
def parser = new XmlSlurper()
|
||||
// if value is raw xml then continue otherwise try to parse the base64 encoding
|
||||
if (value.startsWith("<")) {
|
||||
text = new String(value)
|
||||
}
|
||||
else {
|
||||
decoded = value.decodeBase64()
|
||||
text = new String(decoded)
|
||||
LOG.info("received SAML request $value")
|
||||
}
|
||||
|
||||
// after decoded, if redirect binding, we need to parse string to xml
|
||||
if (text.startsWith("<")) {
|
||||
LOG.debug("assuming POST/SOAP binding")
|
||||
// plain String (POST/SOAP parameter)
|
||||
def xml = parser.parseText(text)
|
||||
return getIssuer(xml)
|
||||
}
|
||||
else {
|
||||
LOG.debug("assuming redirect binding")
|
||||
// should be deflate encoded (query parameter)
|
||||
def is = new InflaterInputStream(new ByteArrayInputStream(decoded), new Inflater(true))
|
||||
def xml = parser.parse(is)
|
||||
return getIssuer(xml)
|
||||
}
|
||||
}
|
||||
|
||||
def dispatchIssuer(i2s, String issuer) {
|
||||
def result = i2s.get(issuer)
|
||||
if (result == null) {
|
||||
LOG.info("No SP found for issuer '$issuer'. Hint: check SAML SP Connector patterns.")
|
||||
}
|
||||
|
||||
// dispatch different idp if artifact binding is enabled
|
||||
if(parameters.get('epdMode') == 'artifact' && result == 'epd'){
|
||||
LOG.debug("EPD: Artifact mode")
|
||||
result = result + "_artifact"
|
||||
}else{
|
||||
LOG.debug("EPD: POST mode")
|
||||
}
|
||||
response.setResult(result)
|
||||
session.put("saml.inbound.issuer", issuer)
|
||||
session.put('saml.idp.result', result) // remember decision for sub-sequent requests without a SAML message
|
||||
|
||||
}
|
||||
|
||||
def dispatchMessage(i2s, String message) {
|
||||
def issuer = getIssuer(message)
|
||||
if (issuer == null) {
|
||||
LOG.info("No issuer found in incoming SAML message. Giving up.")
|
||||
}
|
||||
session.put("saml.inbound.issuer", issuer)
|
||||
dispatchIssuer(i2s, issuer)
|
||||
}
|
||||
|
||||
if (parameters.get('logoutConfirmation') == 'true' && "stepup" == request.getMethod()) {
|
||||
String url = request.currentResource
|
||||
def path = new URL(url).getPath()
|
||||
if (path.endsWith("/logout")) {
|
||||
// next AuthState will show a logout confirmation GUI
|
||||
response.setResult('confirm')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ensure session exists
|
||||
if (request.getSession(false) == null) {
|
||||
session = request.getSession(true).getData()
|
||||
}
|
||||
|
||||
// issuer (any case) -> ResultCond name
|
||||
def i2s = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER)
|
||||
|
||||
|
||||
i2s.put(parameters.get('atb'), 'main')
|
||||
i2s.put(parameters.get('epd_atb'), 'epd')
|
||||
|
||||
if (parameters.get('spInitiated') == 'true' && inargs.containsKey('SAMLRequest')) { // SP-initiated authentication
|
||||
LOG.debug("found SAMLRequest parameter for SP-initiated authentication")
|
||||
String message = inargs.get('SAMLRequest')
|
||||
dispatchMessage(i2s, message)
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs.containsKey('SAMLResponse')) { // response to IDP-initiated SAML Logout
|
||||
LOG.debug("found SAMLResponse parameter")
|
||||
String message = inargs.get('SAMLResponse')
|
||||
dispatchMessage(i2s, message)
|
||||
return
|
||||
}
|
||||
|
||||
if (parameters.get('spInitiated') == 'true' && inargs.containsKey('soapheader')) { // SP-initiated SOAP with soapheader
|
||||
LOG.debug("found soapheader parameter for SP-initiated")
|
||||
String message = inargs.get('soapheader')
|
||||
dispatchMessage(i2s, message)
|
||||
return
|
||||
}
|
||||
|
||||
if (parameters.get('spInitiated') == 'true' && inargs.containsKey('')) { // SP-initiated SOAP with empty
|
||||
LOG.debug("found empty parameter for SP-initiated SOAP message")
|
||||
String message = inargs.get('')
|
||||
dispatchMessage(i2s, message)
|
||||
return
|
||||
}
|
||||
|
||||
String issuer = inargs['Issuer'] ?: inargs['issuer']
|
||||
if (parameters.get('idpInitiated') == 'true' && issuer != null) { // IDP-initiated authentication
|
||||
LOG.debug("found Issuer parameter for IDP-initiated authentication")
|
||||
dispatchIssuer(i2s, issuer)
|
||||
return
|
||||
}
|
||||
|
||||
// used as fallback in case of ?logout (we need an IdentityProviderState)
|
||||
if (inargs.containsKey("logout") && session.containsKey('saml.idp.result')) {
|
||||
def result = session.get('saml.idp.result')
|
||||
LOG.debug("dispatching to last used ResultCond: $result")
|
||||
response.setResult(result)
|
||||
return
|
||||
}
|
||||
|
||||
def location = getReferer('/')
|
||||
LOG.info("Unable to dispatch request. Giving up and redirecting (back) to $location")
|
||||
redirect(location)
|
|
@ -1,33 +1,33 @@
|
|||
if (inargs['authRequestId'] && (!session['ch.nevis.auth.saml.request.id'] || inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'])) {
|
||||
// make sure we start from scratch
|
||||
def mInargs = request.getInArgs()
|
||||
mInargs.remove('email')
|
||||
mInargs.remove('recaptcha_sitekey')
|
||||
mInargs.remove('recaptcha_response')
|
||||
mInargs.remove('continue')
|
||||
mInargs.remove('authRequestId')
|
||||
mInargs.remove('cancel')
|
||||
}
|
||||
|
||||
if (inargs['cd'] && session['agov.recovery.code']) {
|
||||
// we are called with a new URL --> make sure we start from scratch
|
||||
def s = request.getAuthSession(true)
|
||||
def sessionKeySet = new HashSet(session.keySet())
|
||||
sessionKeySet.each { key ->
|
||||
if ( key ==~ /ch.nevis.idm.*/ || key ==~ /ch.adnovum.nevisidm.*/ || key ==~ /agov.recovery.*/ ) {
|
||||
s.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!session['ch.nevis.auth.saml.request.id']) {
|
||||
response.setSessionAttribute('ch.nevis.auth.saml.request.id', java.util.UUID.randomUUID().toString())
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
response.setSessionAttribute('agov.recovery.ip', '' + sourceIp)
|
||||
response.setSessionAttribute('agov.recovery.userAgent', '' + userAgent)
|
||||
|
||||
if (inargs['authRequestId'] && (!session['ch.nevis.auth.saml.request.id'] || inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'])) {
|
||||
// make sure we start from scratch
|
||||
def mInargs = request.getInArgs()
|
||||
mInargs.remove('email')
|
||||
mInargs.remove('recaptcha_sitekey')
|
||||
mInargs.remove('recaptcha_response')
|
||||
mInargs.remove('continue')
|
||||
mInargs.remove('authRequestId')
|
||||
mInargs.remove('cancel')
|
||||
}
|
||||
|
||||
if (inargs['cd'] && session['agov.recovery.code']) {
|
||||
// we are called with a new URL --> make sure we start from scratch
|
||||
def s = request.getAuthSession(true)
|
||||
def sessionKeySet = new HashSet(session.keySet())
|
||||
sessionKeySet.each { key ->
|
||||
if ( key ==~ /ch.nevis.idm.*/ || key ==~ /ch.adnovum.nevisidm.*/ || key ==~ /agov.recovery.*/ ) {
|
||||
s.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!session['ch.nevis.auth.saml.request.id']) {
|
||||
response.setSessionAttribute('ch.nevis.auth.saml.request.id', java.util.UUID.randomUUID().toString())
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
response.setSessionAttribute('agov.recovery.ip', '' + sourceIp)
|
||||
response.setSessionAttribute('agov.recovery.userAgent', '' + userAgent)
|
||||
|
||||
response.setResult('default')
|
|
@ -1,10 +1,10 @@
|
|||
def requester = 'unknown'
|
||||
def requestId = session['ch.nevis.auth.saml.request.id'] ?: 'unknown'
|
||||
def user = session['ch.adnovum.nevisidm.user.extId'] ?: '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 reason = session['agov.recovery.reason'] ?: 'unknown'
|
||||
|
||||
LOG.info("Event='RECOVERY-REASON', Requester='${requester}', RequestId='${requestId}', User=${user}, SourceIp=${sourceIp}, UserAgent='${userAgent}', Reason='${reason}'")
|
||||
|
||||
def requester = 'unknown'
|
||||
def requestId = session['ch.nevis.auth.saml.request.id'] ?: 'unknown'
|
||||
def user = session['ch.adnovum.nevisidm.user.extId'] ?: '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 reason = session['agov.recovery.reason'] ?: 'unknown'
|
||||
|
||||
LOG.info("Event='RECOVERY-REASON', Requester='${requester}', RequestId='${requestId}', User=${user}, SourceIp=${sourceIp}, UserAgent='${userAgent}', Reason='${reason}'")
|
||||
|
||||
response.setResult('ok')
|
|
@ -16,12 +16,6 @@ Configuration:
|
|||
level: "INFO"
|
||||
- name: "EsAuthStart"
|
||||
level: "INFO"
|
||||
- name: "org.apache.catalina.loader.WebappClassLoader"
|
||||
level: "FATAL"
|
||||
- name: "org.apache.catalina.startup.HostConfig"
|
||||
level: "ERROR"
|
||||
- name: "ch.nevis.esauth.events"
|
||||
level: "FATAL"
|
||||
- name: "AGOV-ACCT"
|
||||
level: "DEBUG"
|
||||
- name: "AgovCaptcha"
|
||||
|
|
|
@ -1,64 +1,64 @@
|
|||
def redirect(location) {
|
||||
outargs.put('nevis.transfer.type', 'redirect')
|
||||
outargs.put('nevis.transfer.destination', location)
|
||||
}
|
||||
|
||||
def getReturnURL() {
|
||||
if (inargs.containsKey('return')) {
|
||||
return inargs.get('return')
|
||||
}
|
||||
// determine returnURL based on Referer header (if present and not pointing to this page)
|
||||
def referer = request.getHttpHeader('Referer')
|
||||
if (referer == null) {
|
||||
LOG.debug('no Referer header found')
|
||||
return null
|
||||
}
|
||||
// strip query String for comparison
|
||||
String previous = referer.contains('?') ? referer.substring(0, referer.indexOf("?")) : referer
|
||||
def current = request.getCurrentResource()
|
||||
if (current.startsWith(previous)) {
|
||||
LOG.debug("Referer header $referer cannot be used as return URL - cyclic redirect")
|
||||
return null
|
||||
}
|
||||
return referer
|
||||
}
|
||||
|
||||
if (inargs.containsKey('logout-confirm')) {
|
||||
def current = request.getCurrentResource()
|
||||
// user has confirmed logout -> replace /logout with /?logout
|
||||
String location
|
||||
if (current.contains('?')) {
|
||||
location = current.replace("/logout?", "/?logout&")
|
||||
}
|
||||
else {
|
||||
location = current.replace("/logout", "/?logout")
|
||||
}
|
||||
redirect(location)
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs.containsKey('logout-abort')) {
|
||||
// user has aborted logout -> redirect to stored return URL
|
||||
def location = session.get('logout-abort-url')
|
||||
redirect(location)
|
||||
return
|
||||
}
|
||||
|
||||
// user has not clicked any button -> render GUI
|
||||
response.setGuiName('saml_logout_confirm')
|
||||
response.setGuiLabel('title.logout.confirmation')
|
||||
// not setting a target as the API has been removed
|
||||
response.addInfoGuiField('info', 'info.logout.confirmation', null)
|
||||
response.addButtonGuiField('logout-confirm', 'continue.button.label', 'true')
|
||||
|
||||
def returnURL = getReturnURL()
|
||||
|
||||
if (returnURL != null) {
|
||||
// store return URL in session
|
||||
session.put('logout-abort-url', returnURL)
|
||||
}
|
||||
|
||||
if (session.containsKey('logout-abort-url')) {
|
||||
// add cancel button to go back
|
||||
response.addButtonGuiField('logout-abort', 'cancel.button.label', 'true')
|
||||
def redirect(location) {
|
||||
outargs.put('nevis.transfer.type', 'redirect')
|
||||
outargs.put('nevis.transfer.destination', location)
|
||||
}
|
||||
|
||||
def getReturnURL() {
|
||||
if (inargs.containsKey('return')) {
|
||||
return inargs.get('return')
|
||||
}
|
||||
// determine returnURL based on Referer header (if present and not pointing to this page)
|
||||
def referer = request.getHttpHeader('Referer')
|
||||
if (referer == null) {
|
||||
LOG.debug('no Referer header found')
|
||||
return null
|
||||
}
|
||||
// strip query String for comparison
|
||||
String previous = referer.contains('?') ? referer.substring(0, referer.indexOf("?")) : referer
|
||||
def current = request.getCurrentResource()
|
||||
if (current.startsWith(previous)) {
|
||||
LOG.debug("Referer header $referer cannot be used as return URL - cyclic redirect")
|
||||
return null
|
||||
}
|
||||
return referer
|
||||
}
|
||||
|
||||
if (inargs.containsKey('logout-confirm')) {
|
||||
def current = request.getCurrentResource()
|
||||
// user has confirmed logout -> replace /logout with /?logout
|
||||
String location
|
||||
if (current.contains('?')) {
|
||||
location = current.replace("/logout?", "/?logout&")
|
||||
}
|
||||
else {
|
||||
location = current.replace("/logout", "/?logout")
|
||||
}
|
||||
redirect(location)
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs.containsKey('logout-abort')) {
|
||||
// user has aborted logout -> redirect to stored return URL
|
||||
def location = session.get('logout-abort-url')
|
||||
redirect(location)
|
||||
return
|
||||
}
|
||||
|
||||
// user has not clicked any button -> render GUI
|
||||
response.setGuiName('saml_logout_confirm')
|
||||
response.setGuiLabel('title.logout.confirmation')
|
||||
// not setting a target as the API has been removed
|
||||
response.addInfoGuiField('info', 'info.logout.confirmation', null)
|
||||
response.addButtonGuiField('logout-confirm', 'continue.button.label', 'true')
|
||||
|
||||
def returnURL = getReturnURL()
|
||||
|
||||
if (returnURL != null) {
|
||||
// store return URL in session
|
||||
session.put('logout-abort-url', returnURL)
|
||||
}
|
||||
|
||||
if (session.containsKey('logout-abort-url')) {
|
||||
// add cancel button to go back
|
||||
response.addButtonGuiField('logout-abort', 'cancel.button.label', 'true')
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
otel.service.name = auth
|
||||
otel.traces.sampler = always_on
|
||||
otel.traces.exporter = none
|
||||
otel.metrics.exporter = none
|
||||
otel.logs.exporter = none
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
if (inargs['cancel'] && inargs['cancel'] == 'cancel') {
|
||||
def s = request.getAuthSession(true)
|
||||
s.removeAttribute('agov.recovery.moreThanOneLf')
|
||||
|
||||
response.setResult('doCancel')
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['continue'] && inargs['continue'] == 'yes') {
|
||||
response.setSessionAttribute('agov.recovery.moreThanOneLf', 'yes')
|
||||
response.setResult('loginFactorYes')
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['continue'] && inargs['continue'] == 'no') {
|
||||
response.setSessionAttribute('agov.recovery.moreThanOneLf', 'no')
|
||||
response.setResult('loginFactorNo')
|
||||
return
|
||||
}
|
||||
|
||||
// if we reach this, display the GUI again
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
if (inargs['cancel'] && inargs['cancel'] == 'cancel') {
|
||||
def s = request.getAuthSession(true)
|
||||
s.removeAttribute('agov.recovery.moreThanOneLf')
|
||||
|
||||
response.setResult('doCancel')
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['continue'] && inargs['continue'] == 'yes') {
|
||||
response.setSessionAttribute('agov.recovery.moreThanOneLf', 'yes')
|
||||
response.setResult('loginFactorYes')
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['continue'] && inargs['continue'] == 'no') {
|
||||
response.setSessionAttribute('agov.recovery.moreThanOneLf', 'no')
|
||||
response.setResult('loginFactorNo')
|
||||
return
|
||||
}
|
||||
|
||||
// if we reach this, display the GUI again
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
|
@ -1,28 +1,28 @@
|
|||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
if (inargs['reason']) {
|
||||
response.setSessionAttribute('agov.recovery.reason', '' + inargs['reason'])
|
||||
}
|
||||
|
||||
if (inargs['cancel'] && inargs['cancel'] == 'cancel') {
|
||||
def s = request.getAuthSession(true)
|
||||
s.removeAttribute('agov.recovery.moreThanOneLf')
|
||||
s.removeAttribute('agov.recovery.reason')
|
||||
|
||||
response.setResult('doCancel')
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['continue'] && inargs['continue'] == 'yes') {
|
||||
response.setResult('validReasons')
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['continue'] && inargs['continue'] == 'no') {
|
||||
response.setResult('invalidReasons')
|
||||
return
|
||||
}
|
||||
|
||||
// if we reach this, display the GUI again
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
if (inargs['reason']) {
|
||||
response.setSessionAttribute('agov.recovery.reason', '' + inargs['reason'])
|
||||
}
|
||||
|
||||
if (inargs['cancel'] && inargs['cancel'] == 'cancel') {
|
||||
def s = request.getAuthSession(true)
|
||||
s.removeAttribute('agov.recovery.moreThanOneLf')
|
||||
s.removeAttribute('agov.recovery.reason')
|
||||
|
||||
response.setResult('doCancel')
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['continue'] && inargs['continue'] == 'yes') {
|
||||
response.setResult('validReasons')
|
||||
return
|
||||
}
|
||||
|
||||
if (inargs['continue'] && inargs['continue'] == 'no') {
|
||||
response.setResult('invalidReasons')
|
||||
return
|
||||
}
|
||||
|
||||
// if we reach this, display the GUI again
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
|
@ -76,4 +76,4 @@ if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null
|
|||
response.setResult('error')
|
||||
return
|
||||
|
||||
// new
|
||||
// new
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
if (session['agov.recovery.redirectDone']) {
|
||||
// user navigated back from AGOV.me, go again for the code
|
||||
|
||||
// clean up SAML state first,
|
||||
// IdentityProviderState sets session attributes as follows
|
||||
// <IDP-State-Name>-session-participants.<SAML-RP-ISSUER> = <ACS-URL>
|
||||
// State name contains the name of the pattern 'Recovery_redirectAgovMe'
|
||||
def s = request.getAuthSession(true)
|
||||
def sessionKeySet = new HashSet(session.keySet())
|
||||
sessionKeySet.each { key ->
|
||||
if ( key ==~ /.*Recovery_redirectAgovMe-session-participants.*/ ) {
|
||||
LOG.debug("Deleted session attribute '${key}'")
|
||||
s.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
s.removeAttribute('agov.recovery.redirectDone')
|
||||
response.setResult('back')
|
||||
} else {
|
||||
// redirect
|
||||
response.setSessionAttribute('agov.recovery.redirectDone', 'true')
|
||||
response.setResult('redirect')
|
||||
}
|
||||
if (session['agov.recovery.redirectDone']) {
|
||||
// user navigated back from AGOV.me, go again for the code
|
||||
|
||||
// clean up SAML state first,
|
||||
// IdentityProviderState sets session attributes as follows
|
||||
// <IDP-State-Name>-session-participants.<SAML-RP-ISSUER> = <ACS-URL>
|
||||
// State name contains the name of the pattern 'Recovery_redirectAgovMe'
|
||||
def s = request.getAuthSession(true)
|
||||
def sessionKeySet = new HashSet(session.keySet())
|
||||
sessionKeySet.each { key ->
|
||||
if ( key ==~ /.*Recovery_redirectAgovMe-session-participants.*/ ) {
|
||||
LOG.debug("Deleted session attribute '${key}'")
|
||||
s.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
s.removeAttribute('agov.recovery.redirectDone')
|
||||
response.setResult('back')
|
||||
} else {
|
||||
// redirect
|
||||
response.setSessionAttribute('agov.recovery.redirectDone', 'true')
|
||||
response.setResult('redirect')
|
||||
}
|
||||
|
|
|
@ -1,267 +1,267 @@
|
|||
import groovy.json.JsonSlurper
|
||||
import groovy.xml.XmlSlurper
|
||||
import org.codehaus.groovy.runtime.StackTraceUtils
|
||||
|
||||
import ch.nevis.idm.client.IdmRestClient
|
||||
import ch.nevis.idm.client.IdmRestClientFactory
|
||||
|
||||
|
||||
// AGOVaq conversion
|
||||
def maxLoiRoleToCtxClssConvertorMap = [
|
||||
"level100": "urn:qa.agov.ch:names:tc:ac:classes:100",
|
||||
"level200": "urn:qa.agov.ch:names:tc:ac:classes:200",
|
||||
"level300": "urn:qa.agov.ch:names:tc:ac:classes:300",
|
||||
"level400": "urn:qa.agov.ch:names:tc:ac:classes:400",
|
||||
"level500": "urn:qa.agov.ch:names:tc:ac:classes:500"
|
||||
]
|
||||
|
||||
// https://docs.nevis.net/nevisidm/Developer-Guide/SOAP-Interface/Interface-specification/Value-types#enum-value-types
|
||||
def blockingCredentialStates = ['DISABLED', 'EXPIRED', 'LOCKED', 'ARCHIVED', 'RESET_CODE']
|
||||
|
||||
def getUserIdVerificationForRecovery(currentLoaRole) {
|
||||
// application is AGOV-AccountStatus
|
||||
def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
def result = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-AccountStatus,mustRecover'}?.value?.text()
|
||||
|
||||
if (!result) {
|
||||
// fallback if not explicitly set
|
||||
def chDomicile = list.country.text() == 'ch'
|
||||
def lastIdVerification = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + currentLoaRole}?.value?.text() ?: 'missing'
|
||||
switch (currentLoaRole) {
|
||||
case 'level100':
|
||||
result = chDomicile ? 'SimpleLetter' : 'Video'
|
||||
break
|
||||
case 'level200':
|
||||
result = chDomicile ? 'Bmid' : 'Video'
|
||||
break
|
||||
case 'level300':
|
||||
case 'level400':
|
||||
result = chDomicile ? lastIdVerification : 'Video'
|
||||
break
|
||||
default:
|
||||
LOG.warn("unexpected loa on account: ${currentLoaRole}")
|
||||
// safest default, should work in any case
|
||||
result = 'Video'
|
||||
}
|
||||
LOG.warn("Recovery method not set, choosing ${result} (based on currentLoad: ${currentLoaRole}, CH-domicile: ${chDomicile}, last verification method: ${lastIdVerification})")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def getAqLevelBasedOnIdVerificationForRecovery(idVerification, highestRoleLevel) {
|
||||
def result = 'level'
|
||||
|
||||
switch (idVerification) {
|
||||
case 'None':
|
||||
result = result.concat('100')
|
||||
break
|
||||
case 'SimpleLetter':
|
||||
result = result.concat('200')
|
||||
break
|
||||
case 'Video':
|
||||
case 'VideoSelfPaid':
|
||||
case 'Bmid':
|
||||
case 'BmidSelfPaid':
|
||||
case 'Counter':
|
||||
result = result.concat((highestRoleLevel == 'level400') ? '400' : '300')
|
||||
break
|
||||
default:
|
||||
LOG.warn("unexpected idVerification for recovery on account: ${idVerification}")
|
||||
// safest default, should work in any case
|
||||
result = highestRoleLevel
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
def getUserMustRecoverValidFrom() {
|
||||
// set attibutes from DTO: -> validFrom
|
||||
def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
def authzNode = payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == 'mustRecover'}
|
||||
return (authzNode) ? ((authzNode.validFrom && !authzNode.validFrom.text().isEmpty()) ? authzNode.validFrom?.text() : authzNode.ctlCreDat?.text()) : ''
|
||||
}
|
||||
|
||||
def userHasNewLoginFactor() {
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
|
||||
String baseUrl = parameters.get('baseUrl')
|
||||
String clientExtId = session.get('ch.adnovum.nevisidm.user.clientExtId')
|
||||
String userExtId = session.get('ch.adnovum.nevisidm.user.extId')
|
||||
String baseEndPoint = "$baseUrl/api/core/v1/$clientExtId/users/$userExtId"
|
||||
|
||||
response.setSessionAttribute('agov.recovery.newLoginFactor', 'NONE')
|
||||
|
||||
try {
|
||||
def credInfoArray = new JsonSlurper().parseText(idmRestClient.get("$baseEndPoint/generic-credentials"))
|
||||
|
||||
def accessApp = credInfoArray['items'].find( it -> it.stateName == "active")
|
||||
if (accessApp) {
|
||||
response.setSessionAttribute('agov.recovery.accessapp', accessApp.properties.fidouaf_name)
|
||||
response.setSessionAttribute('agov.recovery.accessapp.dispatchTargetId', accessApp.identification.replaceAll('dispatch_target_', ''))
|
||||
response.setSessionAttribute('agov.recovery.newLoginFactor', 'ACCESS_APP')
|
||||
return true
|
||||
}
|
||||
|
||||
credInfoArray = new JsonSlurper().parseText(idmRestClient.get("$baseEndPoint/fido2"))
|
||||
|
||||
def fido2Key = credInfoArray['items'].find( it -> it.stateName == "active")
|
||||
if (fido2Key) {
|
||||
response.setSessionAttribute('agov.recovery.securityKey', fido2Key.userFriendlyName)
|
||||
response.setSessionAttribute('agov.recovery.newLoginFactor', 'FIDO2')
|
||||
return true
|
||||
}
|
||||
|
||||
} catch(Exception e) {
|
||||
LOG.error(e.toString())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// for autditing
|
||||
def user = session['ch.adnovum.nevisidm.user.extId'] ?: '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 maxLoi = null
|
||||
|
||||
|
||||
// new
|
||||
if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null) {
|
||||
try {
|
||||
def userDto = new XmlSlurper().parseText(session['ch.adnovum.nevisidm.userDto'])
|
||||
def userState = userDto.state
|
||||
def recoveryCode = userDto.'**'.find {node -> node.name() == 'credentials' && node.type.text() == 'CONTEXT_PASSWORD' && node.context.text() == 'RECOVERY'}
|
||||
|
||||
LOG.debug("Recovery: Dto is '${userDto}")
|
||||
LOG.debug("Recovery: state is '${userState}")
|
||||
LOG.debug("Recovery: RecoveryCode is '${recoveryCode ? recoveryCode : 'none'}'")
|
||||
|
||||
if (userState == 'ACTIVE') {
|
||||
|
||||
response.setSessionAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recovery')
|
||||
response.setSessionAttribute('agov.recovery.authenticatedWith', 'urn:qa.agov.ch:names:tc:authfactor:email')
|
||||
response.setSessionAttribute('agov.recovery.codeStatus', 'notNeeded')
|
||||
response.setSessionAttribute('agov.recovery.codeDetailStatus', 'n/a')
|
||||
response.setSessionAttribute('agov.recovery.newLoginFactor', 'NONE')
|
||||
|
||||
def maxLoiList = userDto.'**'.findAll { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-Loi' }.collect({ node -> node.name.text() })
|
||||
maxLoi = (maxLoiList == null || maxLoiList.isEmpty()) ? null : maxLoiList.sort().last()
|
||||
|
||||
def idVerification = null
|
||||
def agovAqValidFrom = null
|
||||
if (maxLoi) {
|
||||
if (maxLoi != 'level100') {
|
||||
response.setSessionAttribute('agov.recovery.codeDetailStatus', '' + maxLoi)
|
||||
}
|
||||
|
||||
idVerification = userDto.'**'.find { node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + maxLoi}?.value?.text()
|
||||
idVerification = idVerification ?: 'None'
|
||||
agovAqValidFrom = userDto.'**'.find { node -> node.name() == 'authorizations' && node.role.name.text() == maxLoi}?.validFrom?.text()
|
||||
agovAqValidFrom = agovAqValidFrom?: userDto.'**'.find { node -> node.name() == 'authorizations' && node.role.name.text() == maxLoi}?.ctlCreDat?.text()
|
||||
}
|
||||
|
||||
def mustRecover = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'mustRecover' }
|
||||
|
||||
def hasRecoveryRole = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'recovery' }
|
||||
|
||||
def hasRecoveryCascadeRole = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'recoveryCascade' }
|
||||
|
||||
def hasNewLoginFactor = hasRecoveryRole && userHasNewLoginFactor()
|
||||
|
||||
if (mustRecover) {
|
||||
// attributes are defined over the mustRecover authorization
|
||||
response.setSessionAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:mustRecover')
|
||||
response.setSessionAttribute('agov.recovery.codeDetailStatus', 'mustRecover')
|
||||
|
||||
idVerification = getUserIdVerificationForRecovery(maxLoi ?: 'level100') ?: idVerification
|
||||
|
||||
agovAqValidFrom = getUserMustRecoverValidFrom()
|
||||
|
||||
maxLoi = getAqLevelBasedOnIdVerificationForRecovery(idVerification, maxLoi)
|
||||
} else if (hasRecoveryCascadeRole && hasNewLoginFactor) {
|
||||
response.setSessionAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recoveryCascade')
|
||||
}
|
||||
|
||||
LOG.debug("Recovery: MaxLoi is '${maxLoi}'")
|
||||
LOG.debug("Recovery: IdVerification is ${idVerification}")
|
||||
LOG.debug("Recovery: agovAqValidFrom is ${agovAqValidFrom}")
|
||||
LOG.debug("Recovery: mustRecover is '${mustRecover}'")
|
||||
LOG.debug("Recovery: hasRecoveryRole is '${hasRecoveryRole}'")
|
||||
LOG.debug("Recovery: hasNewLoginFactor is '${hasNewLoginFactor}'")
|
||||
|
||||
if (maxLoi != null) {
|
||||
if (maxLoiRoleToCtxClssConvertorMap.containsKey(maxLoi)) {
|
||||
LOG.debug("Recovery: MaxLoiMapping is " + maxLoiRoleToCtxClssConvertorMap[maxLoi])
|
||||
response.setSessionAttribute('agov.recovery.currentAgovAq', '' + maxLoiRoleToCtxClssConvertorMap[maxLoi])
|
||||
response.setSessionAttribute('agov.recovery.currentIdVerification', '' + idVerification)
|
||||
response.setSessionAttribute('agov.recovery.currentAgovAqRoleValidFrom', '' + agovAqValidFrom)
|
||||
|
||||
if ((maxLoi == 'level100') && (mustRecover == null) && !hasNewLoginFactor) {
|
||||
// AQ100 accounts need to use the recovery code, if they can
|
||||
// check the status of recoveryCode credential
|
||||
if (recoveryCode && !blockingCredentialStates.contains(recoveryCode.state.text())) {
|
||||
LOG.debug("Recovery: emailAndCode")
|
||||
response.setResult('needCode')
|
||||
return
|
||||
} else {
|
||||
LOG.warn("AGOVaq100 recovery: skipped Recovery-Code check '${recoveryCode ? recoveryCode.state.text() : 'MISSING'}'")
|
||||
response.setSessionAttribute('agov.recovery.codeStatus', 'skipped')
|
||||
response.setSessionAttribute('agov.recovery.codeDetailStatus', "unusable (state: ${recoveryCode ? recoveryCode.state.text() : 'MISSING'})")
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
} else if ((maxLoi == 'level100') && hasNewLoginFactor) {
|
||||
LOG.debug("Recovery: new Login Factor")
|
||||
response.setSessionAttribute('agov.recovery.codeStatus', 'skipped')
|
||||
response.setSessionAttribute('agov.recovery.codeDetailStatus', "new login factor already registered (${session['agov.recovery.newLoginFactor']})")
|
||||
response.setResult('ok')
|
||||
return
|
||||
|
||||
} else {
|
||||
LOG.debug("Recovery: email")
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG.error("Recovery: Failed to convert '${maxLoi}' to AGOVaq")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// maxLoi is null
|
||||
LOG.debug("Recovery: no 'AGOV-Loi'-role assigned to user ${user}")
|
||||
if ((hasRecoveryRole != null) && (mustRecover == null)) {
|
||||
response.setResult('notFullyRegistered')
|
||||
return
|
||||
} else {
|
||||
LOG.error("Recovery: no 'AGOV-Loi'-role assigned to user ${user} and no recovery role ")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// state != ACTIVE and no lasterror should not happen
|
||||
LOG.error("Recovery: state='${userState}' but not lasterror set")
|
||||
response.setNote('lasterror', '9909')
|
||||
response.setNote('lasterrorinfo', 'internal error')
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e = StackTraceUtils.sanitize(e)
|
||||
def affectedLines = e.stackTrace.findAll { it.className.startsWith('Script') }.collect { "${it.methodName}:${it.lineNumber}" }
|
||||
LOG.error("FATAL: Recovery processing failed (at lines: ${affectedLines})", e)
|
||||
response.setNote('lasterror', '9909')
|
||||
response.setNote('lasterrorinfo', 'internal error')
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
LOG.error("Recovery: userDto missing or failure before (lasterror='${notes.getProperty('lasterror', '-')}')")
|
||||
response.setNote('lasterror', '9909')
|
||||
response.setNote('lasterrorinfo', 'internal error')
|
||||
response.setResult('error')
|
||||
return
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.xml.XmlSlurper
|
||||
import org.codehaus.groovy.runtime.StackTraceUtils
|
||||
|
||||
import ch.nevis.idm.client.IdmRestClient
|
||||
import ch.nevis.idm.client.IdmRestClientFactory
|
||||
|
||||
|
||||
// AGOVaq conversion
|
||||
def maxLoiRoleToCtxClssConvertorMap = [
|
||||
"level100": "urn:qa.agov.ch:names:tc:ac:classes:100",
|
||||
"level200": "urn:qa.agov.ch:names:tc:ac:classes:200",
|
||||
"level300": "urn:qa.agov.ch:names:tc:ac:classes:300",
|
||||
"level400": "urn:qa.agov.ch:names:tc:ac:classes:400",
|
||||
"level500": "urn:qa.agov.ch:names:tc:ac:classes:500"
|
||||
]
|
||||
|
||||
// https://docs.nevis.net/nevisidm/Developer-Guide/SOAP-Interface/Interface-specification/Value-types#enum-value-types
|
||||
def blockingCredentialStates = ['DISABLED', 'EXPIRED', 'LOCKED', 'ARCHIVED', 'RESET_CODE']
|
||||
|
||||
def getUserIdVerificationForRecovery(currentLoaRole) {
|
||||
// application is AGOV-AccountStatus
|
||||
def list = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
def result = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-AccountStatus,mustRecover'}?.value?.text()
|
||||
|
||||
if (!result) {
|
||||
// fallback if not explicitly set
|
||||
def chDomicile = list.country.text() == 'ch'
|
||||
def lastIdVerification = list.'**'.find {node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + currentLoaRole}?.value?.text() ?: 'missing'
|
||||
switch (currentLoaRole) {
|
||||
case 'level100':
|
||||
result = chDomicile ? 'SimpleLetter' : 'Video'
|
||||
break
|
||||
case 'level200':
|
||||
result = chDomicile ? 'Bmid' : 'Video'
|
||||
break
|
||||
case 'level300':
|
||||
case 'level400':
|
||||
result = chDomicile ? lastIdVerification : 'Video'
|
||||
break
|
||||
default:
|
||||
LOG.warn("unexpected loa on account: ${currentLoaRole}")
|
||||
// safest default, should work in any case
|
||||
result = 'Video'
|
||||
}
|
||||
LOG.warn("Recovery method not set, choosing ${result} (based on currentLoad: ${currentLoaRole}, CH-domicile: ${chDomicile}, last verification method: ${lastIdVerification})")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def getAqLevelBasedOnIdVerificationForRecovery(idVerification, highestRoleLevel) {
|
||||
def result = 'level'
|
||||
|
||||
switch (idVerification) {
|
||||
case 'None':
|
||||
result = result.concat('100')
|
||||
break
|
||||
case 'SimpleLetter':
|
||||
result = result.concat('200')
|
||||
break
|
||||
case 'Video':
|
||||
case 'VideoSelfPaid':
|
||||
case 'Bmid':
|
||||
case 'BmidSelfPaid':
|
||||
case 'Counter':
|
||||
result = result.concat((highestRoleLevel == 'level400') ? '400' : '300')
|
||||
break
|
||||
default:
|
||||
LOG.warn("unexpected idVerification for recovery on account: ${idVerification}")
|
||||
// safest default, should work in any case
|
||||
result = highestRoleLevel
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
def getUserMustRecoverValidFrom() {
|
||||
// set attibutes from DTO: -> validFrom
|
||||
def payload = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto'))
|
||||
def authzNode = payload.'**'.find {node -> node.name() == 'authorizations' && node.role.name.text() == 'mustRecover'}
|
||||
return (authzNode) ? ((authzNode.validFrom && !authzNode.validFrom.text().isEmpty()) ? authzNode.validFrom?.text() : authzNode.ctlCreDat?.text()) : ''
|
||||
}
|
||||
|
||||
def userHasNewLoginFactor() {
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
|
||||
String baseUrl = parameters.get('baseUrl')
|
||||
String clientExtId = session.get('ch.adnovum.nevisidm.user.clientExtId')
|
||||
String userExtId = session.get('ch.adnovum.nevisidm.user.extId')
|
||||
String baseEndPoint = "$baseUrl/api/core/v1/$clientExtId/users/$userExtId"
|
||||
|
||||
response.setSessionAttribute('agov.recovery.newLoginFactor', 'NONE')
|
||||
|
||||
try {
|
||||
def credInfoArray = new JsonSlurper().parseText(idmRestClient.get("$baseEndPoint/generic-credentials"))
|
||||
|
||||
def accessApp = credInfoArray['items'].find( it -> it.stateName == "active")
|
||||
if (accessApp) {
|
||||
response.setSessionAttribute('agov.recovery.accessapp', accessApp.properties.fidouaf_name)
|
||||
response.setSessionAttribute('agov.recovery.accessapp.dispatchTargetId', accessApp.identification.replaceAll('dispatch_target_', ''))
|
||||
response.setSessionAttribute('agov.recovery.newLoginFactor', 'ACCESS_APP')
|
||||
return true
|
||||
}
|
||||
|
||||
credInfoArray = new JsonSlurper().parseText(idmRestClient.get("$baseEndPoint/fido2"))
|
||||
|
||||
def fido2Key = credInfoArray['items'].find( it -> it.stateName == "active")
|
||||
if (fido2Key) {
|
||||
response.setSessionAttribute('agov.recovery.securityKey', fido2Key.userFriendlyName)
|
||||
response.setSessionAttribute('agov.recovery.newLoginFactor', 'FIDO2')
|
||||
return true
|
||||
}
|
||||
|
||||
} catch(Exception e) {
|
||||
LOG.error(e.toString())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// for autditing
|
||||
def user = session['ch.adnovum.nevisidm.user.extId'] ?: '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 maxLoi = null
|
||||
|
||||
|
||||
// new
|
||||
if (session['ch.adnovum.nevisidm.userDto'] != null && notes['lasterror'] == null) {
|
||||
try {
|
||||
def userDto = new XmlSlurper().parseText(session['ch.adnovum.nevisidm.userDto'])
|
||||
def userState = userDto.state
|
||||
def recoveryCode = userDto.'**'.find {node -> node.name() == 'credentials' && node.type.text() == 'CONTEXT_PASSWORD' && node.context.text() == 'RECOVERY'}
|
||||
|
||||
LOG.debug("Recovery: Dto is '${userDto}")
|
||||
LOG.debug("Recovery: state is '${userState}")
|
||||
LOG.debug("Recovery: RecoveryCode is '${recoveryCode ? recoveryCode : 'none'}'")
|
||||
|
||||
if (userState == 'ACTIVE') {
|
||||
|
||||
response.setSessionAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recovery')
|
||||
response.setSessionAttribute('agov.recovery.authenticatedWith', 'urn:qa.agov.ch:names:tc:authfactor:email')
|
||||
response.setSessionAttribute('agov.recovery.codeStatus', 'notNeeded')
|
||||
response.setSessionAttribute('agov.recovery.codeDetailStatus', 'n/a')
|
||||
response.setSessionAttribute('agov.recovery.newLoginFactor', 'NONE')
|
||||
|
||||
def maxLoiList = userDto.'**'.findAll { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-Loi' }.collect({ node -> node.name.text() })
|
||||
maxLoi = (maxLoiList == null || maxLoiList.isEmpty()) ? null : maxLoiList.sort().last()
|
||||
|
||||
def idVerification = null
|
||||
def agovAqValidFrom = null
|
||||
if (maxLoi) {
|
||||
if (maxLoi != 'level100') {
|
||||
response.setSessionAttribute('agov.recovery.codeDetailStatus', '' + maxLoi)
|
||||
}
|
||||
|
||||
idVerification = userDto.'**'.find { node -> node.name() == 'properties' && node.name.text() == 'idVerification' && node.scopeName.text() == 'AGOV-Loi,' + maxLoi}?.value?.text()
|
||||
idVerification = idVerification ?: 'None'
|
||||
agovAqValidFrom = userDto.'**'.find { node -> node.name() == 'authorizations' && node.role.name.text() == maxLoi}?.validFrom?.text()
|
||||
agovAqValidFrom = agovAqValidFrom?: userDto.'**'.find { node -> node.name() == 'authorizations' && node.role.name.text() == maxLoi}?.ctlCreDat?.text()
|
||||
}
|
||||
|
||||
def mustRecover = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'mustRecover' }
|
||||
|
||||
def hasRecoveryRole = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'recovery' }
|
||||
|
||||
def hasRecoveryCascadeRole = userDto.'**'.find { node -> node.name() == 'roles' && node.applicationName.text() == 'AGOV-AccountStatus' && node.name.text() == 'recoveryCascade' }
|
||||
|
||||
def hasNewLoginFactor = hasRecoveryRole && userHasNewLoginFactor()
|
||||
|
||||
if (mustRecover) {
|
||||
// attributes are defined over the mustRecover authorization
|
||||
response.setSessionAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:mustRecover')
|
||||
response.setSessionAttribute('agov.recovery.codeDetailStatus', 'mustRecover')
|
||||
|
||||
idVerification = getUserIdVerificationForRecovery(maxLoi ?: 'level100') ?: idVerification
|
||||
|
||||
agovAqValidFrom = getUserMustRecoverValidFrom()
|
||||
|
||||
maxLoi = getAqLevelBasedOnIdVerificationForRecovery(idVerification, maxLoi)
|
||||
} else if (hasRecoveryCascadeRole && hasNewLoginFactor) {
|
||||
response.setSessionAttribute('agov.recovery.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:recoveryCascade')
|
||||
}
|
||||
|
||||
LOG.debug("Recovery: MaxLoi is '${maxLoi}'")
|
||||
LOG.debug("Recovery: IdVerification is ${idVerification}")
|
||||
LOG.debug("Recovery: agovAqValidFrom is ${agovAqValidFrom}")
|
||||
LOG.debug("Recovery: mustRecover is '${mustRecover}'")
|
||||
LOG.debug("Recovery: hasRecoveryRole is '${hasRecoveryRole}'")
|
||||
LOG.debug("Recovery: hasNewLoginFactor is '${hasNewLoginFactor}'")
|
||||
|
||||
if (maxLoi != null) {
|
||||
if (maxLoiRoleToCtxClssConvertorMap.containsKey(maxLoi)) {
|
||||
LOG.debug("Recovery: MaxLoiMapping is " + maxLoiRoleToCtxClssConvertorMap[maxLoi])
|
||||
response.setSessionAttribute('agov.recovery.currentAgovAq', '' + maxLoiRoleToCtxClssConvertorMap[maxLoi])
|
||||
response.setSessionAttribute('agov.recovery.currentIdVerification', '' + idVerification)
|
||||
response.setSessionAttribute('agov.recovery.currentAgovAqRoleValidFrom', '' + agovAqValidFrom)
|
||||
|
||||
if ((maxLoi == 'level100') && (mustRecover == null) && !hasNewLoginFactor) {
|
||||
// AQ100 accounts need to use the recovery code, if they can
|
||||
// check the status of recoveryCode credential
|
||||
if (recoveryCode && !blockingCredentialStates.contains(recoveryCode.state.text())) {
|
||||
LOG.debug("Recovery: emailAndCode")
|
||||
response.setResult('needCode')
|
||||
return
|
||||
} else {
|
||||
LOG.warn("AGOVaq100 recovery: skipped Recovery-Code check '${recoveryCode ? recoveryCode.state.text() : 'MISSING'}'")
|
||||
response.setSessionAttribute('agov.recovery.codeStatus', 'skipped')
|
||||
response.setSessionAttribute('agov.recovery.codeDetailStatus', "unusable (state: ${recoveryCode ? recoveryCode.state.text() : 'MISSING'})")
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
} else if ((maxLoi == 'level100') && hasNewLoginFactor) {
|
||||
LOG.debug("Recovery: new Login Factor")
|
||||
response.setSessionAttribute('agov.recovery.codeStatus', 'skipped')
|
||||
response.setSessionAttribute('agov.recovery.codeDetailStatus', "new login factor already registered (${session['agov.recovery.newLoginFactor']})")
|
||||
response.setResult('ok')
|
||||
return
|
||||
|
||||
} else {
|
||||
LOG.debug("Recovery: email")
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG.error("Recovery: Failed to convert '${maxLoi}' to AGOVaq")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// maxLoi is null
|
||||
LOG.debug("Recovery: no 'AGOV-Loi'-role assigned to user ${user}")
|
||||
if ((hasRecoveryRole != null) && (mustRecover == null)) {
|
||||
response.setResult('notFullyRegistered')
|
||||
return
|
||||
} else {
|
||||
LOG.error("Recovery: no 'AGOV-Loi'-role assigned to user ${user} and no recovery role ")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// state != ACTIVE and no lasterror should not happen
|
||||
LOG.error("Recovery: state='${userState}' but not lasterror set")
|
||||
response.setNote('lasterror', '9909')
|
||||
response.setNote('lasterrorinfo', 'internal error')
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e = StackTraceUtils.sanitize(e)
|
||||
def affectedLines = e.stackTrace.findAll { it.className.startsWith('Script') }.collect { "${it.methodName}:${it.lineNumber}" }
|
||||
LOG.error("FATAL: Recovery processing failed (at lines: ${affectedLines})", e)
|
||||
response.setNote('lasterror', '9909')
|
||||
response.setNote('lasterrorinfo', 'internal error')
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
LOG.error("Recovery: userDto missing or failure before (lasterror='${notes.getProperty('lasterror', '-')}')")
|
||||
response.setNote('lasterror', '9909')
|
||||
response.setNote('lasterrorinfo', 'internal error')
|
||||
response.setResult('error')
|
||||
return
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
import groovy.json.JsonSlurper
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
def url = parameters.get('url')
|
||||
def realIpHttpHeaderName = parameters.get('realIpHttpHeaderName') ?: 'X-Real-IP'
|
||||
def ip = request.getLoginContext()['connection.HttpHeader.X-Real-IP'] ?: 'unknown'
|
||||
|
||||
try {
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
def jsonSlurper = new JsonSlurper()
|
||||
def httpClient = HttpClients.create(parameters)
|
||||
def httpResponse = Http.get().url(url).header('traceparent', traceparent)
|
||||
.header(realIpHttpHeaderName, ip).build().send(httpClient)
|
||||
|
||||
LOG.debug('Response Status Code: ' + httpResponse.code())
|
||||
LOG.debug('Response: ' + httpResponse.bodyAsString())
|
||||
|
||||
if (httpResponse.code() == 200) {
|
||||
def json = jsonSlurper.parseText(httpResponse.bodyAsString())
|
||||
|
||||
response.setSessionAttribute('agov.recovery.captchaSettings.enabled', String.valueOf(json.friendlyCaptureClientSettings.enabled))
|
||||
response.setSessionAttribute('agov.recovery.captchaSettings.siteKey', json.friendlyCaptureClientSettings.siteKey)
|
||||
response.setSessionAttribute('agov.recovery.captchaSettings.puzzleUrl', json.friendlyCaptureClientSettings.puzzleUrl)
|
||||
|
||||
|
||||
response.setResult('ok')
|
||||
} else {
|
||||
LOG.error('Unexcpected HTTP response code: ' + httpResponse.code())
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Unexpected HTTP reponse')
|
||||
}
|
||||
} catch (all) {
|
||||
// Handle exception and set the transition
|
||||
LOG.error('error: ' + all, all)
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Exception during HTTP call')
|
||||
import groovy.json.JsonSlurper
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
def url = parameters.get('url')
|
||||
def realIpHttpHeaderName = parameters.get('realIpHttpHeaderName') ?: 'X-Real-IP'
|
||||
def ip = request.getLoginContext()['connection.HttpHeader.X-Real-IP'] ?: 'unknown'
|
||||
|
||||
try {
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
def jsonSlurper = new JsonSlurper()
|
||||
def httpClient = HttpClients.create(parameters)
|
||||
def httpResponse = Http.get().url(url).header('traceparent', traceparent)
|
||||
.header(realIpHttpHeaderName, ip).build().send(httpClient)
|
||||
|
||||
LOG.debug('Response Status Code: ' + httpResponse.code())
|
||||
LOG.debug('Response: ' + httpResponse.bodyAsString())
|
||||
|
||||
if (httpResponse.code() == 200) {
|
||||
def json = jsonSlurper.parseText(httpResponse.bodyAsString())
|
||||
|
||||
response.setSessionAttribute('agov.recovery.captchaSettings.enabled', String.valueOf(json.friendlyCaptureClientSettings.enabled))
|
||||
response.setSessionAttribute('agov.recovery.captchaSettings.siteKey', json.friendlyCaptureClientSettings.siteKey)
|
||||
response.setSessionAttribute('agov.recovery.captchaSettings.puzzleUrl', json.friendlyCaptureClientSettings.puzzleUrl)
|
||||
|
||||
|
||||
response.setResult('ok')
|
||||
} else {
|
||||
LOG.error('Unexcpected HTTP response code: ' + httpResponse.code())
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Unexpected HTTP reponse')
|
||||
}
|
||||
} catch (all) {
|
||||
// Handle exception and set the transition
|
||||
LOG.error('error: ' + all, all)
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Exception during HTTP call')
|
||||
}
|
|
@ -60,4 +60,4 @@ try {
|
|||
LOG.error("Friendly captcha failed with a general error '${all}' for '{ \"userIp\": \"${ip}\", \"email\": \"${email}\", \"userAgent\": \"${userAgent}\" }', service-url: ${url}")
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Exception during HTTP call')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,4 +21,4 @@ if (inargs['cd'] != null) {
|
|||
if (inargs['cd'] == null && session['agov.recovery.code'] != null) {
|
||||
response.setResult('exit.1')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
if (inargs['recovery'] != null && inargs['recovery'] == 'recovery' ) {
|
||||
// clean up SAML state, to make sure the redirect will really be processed
|
||||
// IdentityProviderState sets session attributes as follows
|
||||
// <IDP-State-Name>-session-participants.<SAML-RP-ISSUER> = <ACS-URL>
|
||||
// State name contains the name of the pattern 'Recovery_redirectAgovMe'
|
||||
def s = request.getAuthSession(true)
|
||||
def sessionKeySet = new HashSet(session.keySet())
|
||||
sessionKeySet.each { key ->
|
||||
if ( key ==~ /.*Recovery_redirectAgovMe-session-participants.*/ ) {
|
||||
LOG.debug("Deleted session attribute '${key}'")
|
||||
s.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
// if we reach this, display the GUI again
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
if (inargs['recovery'] != null && inargs['recovery'] == 'recovery' ) {
|
||||
// clean up SAML state, to make sure the redirect will really be processed
|
||||
// IdentityProviderState sets session attributes as follows
|
||||
// <IDP-State-Name>-session-participants.<SAML-RP-ISSUER> = <ACS-URL>
|
||||
// State name contains the name of the pattern 'Recovery_redirectAgovMe'
|
||||
def s = request.getAuthSession(true)
|
||||
def sessionKeySet = new HashSet(session.keySet())
|
||||
sessionKeySet.each { key ->
|
||||
if ( key ==~ /.*Recovery_redirectAgovMe-session-participants.*/ ) {
|
||||
LOG.debug("Deleted session attribute '${key}'")
|
||||
s.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
// if we reach this, display the GUI again
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
|
@ -1,41 +1,41 @@
|
|||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
def url = parameters.get('url')
|
||||
def email = inargs['email']
|
||||
def language = session['ch.nevis.session.user.language'] ?: 'en'
|
||||
def payload = '{ "email": "' + email + '", "language": "' + language + '"}'
|
||||
|
||||
try {
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
def httpClient = HttpClients.create(parameters)
|
||||
def httpResponse = Http.post()
|
||||
.url(url)
|
||||
.header("Accept", "application/json")
|
||||
.header("traceparent", traceparent)
|
||||
.entity(Http.entity()
|
||||
.content(payload)
|
||||
.contentType("application/json")
|
||||
// .charSet("utf-8")
|
||||
.build())
|
||||
.build()
|
||||
.send(httpClient)
|
||||
|
||||
LOG.info('Response Message: ' + httpResponse.reasonPhrase())
|
||||
LOG.info('Response Status Code: ' + httpResponse.code())
|
||||
LOG.info('Response: ' + httpResponse.bodyAsString())
|
||||
|
||||
if (httpResponse.code() == 200) {
|
||||
response.setResult('ok')
|
||||
} else {
|
||||
LOG.error('Unexcpected HTTP response code: ' + httpResponse.code())
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Unexpected HTTP reponse')
|
||||
}
|
||||
} catch (all) {
|
||||
// Handle exception and set the transition
|
||||
LOG.error('error: ' + all, all)
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Exception during HTTP call')
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
def url = parameters.get('url')
|
||||
def email = inargs['email']
|
||||
def language = session['ch.nevis.session.user.language'] ?: 'en'
|
||||
def payload = '{ "email": "' + email + '", "language": "' + language + '"}'
|
||||
|
||||
try {
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
def traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
def httpClient = HttpClients.create(parameters)
|
||||
def httpResponse = Http.post()
|
||||
.url(url)
|
||||
.header("Accept", "application/json")
|
||||
.header("traceparent", traceparent)
|
||||
.entity(Http.entity()
|
||||
.content(payload)
|
||||
.contentType("application/json")
|
||||
// .charSet("utf-8")
|
||||
.build())
|
||||
.build()
|
||||
.send(httpClient)
|
||||
|
||||
LOG.info('Response Message: ' + httpResponse.reasonPhrase())
|
||||
LOG.info('Response Status Code: ' + httpResponse.code())
|
||||
LOG.info('Response: ' + httpResponse.bodyAsString())
|
||||
|
||||
if (httpResponse.code() == 200) {
|
||||
response.setResult('ok')
|
||||
} else {
|
||||
LOG.error('Unexcpected HTTP response code: ' + httpResponse.code())
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Unexpected HTTP reponse')
|
||||
}
|
||||
} catch (all) {
|
||||
// Handle exception and set the transition
|
||||
LOG.error('error: ' + all, all)
|
||||
response.setResult('error')
|
||||
response.setError(1, 'Exception during HTTP call')
|
||||
}
|
|
@ -8,4 +8,4 @@ response.setHeader('IDP-AUTH', 'Timeout')
|
|||
|
||||
// CONTINUE to keep the other request beeing processed
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
||||
return
|
||||
|
|
|
@ -29,3 +29,4 @@ if ( inargs['submit'] && inargs['submit'] == 'submit' ) {
|
|||
|
||||
response.setResult('stay')
|
||||
return
|
||||
|
||||
|
|
|
@ -22,4 +22,4 @@ if ( inargs['continue'] && inargs['continue'] == 'continue' ) {
|
|||
}
|
||||
|
||||
response.setResult('stay')
|
||||
return
|
||||
return
|
||||
|
|
|
@ -11,8 +11,8 @@ metadata:
|
|||
spec:
|
||||
type: "NevisFIDO"
|
||||
replicas: 1
|
||||
version: "8.2411.2"
|
||||
gitInitVersion: "1.3.0"
|
||||
version: "8.2505.5"
|
||||
gitInitVersion: "1.4.0"
|
||||
runAsNonRoot: true
|
||||
ports:
|
||||
rest: 9443
|
||||
|
@ -40,18 +40,19 @@ spec:
|
|||
management:
|
||||
httpGet:
|
||||
path: "/nevisfido/health"
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 6
|
||||
failureThreshold: 50
|
||||
failureThreshold: 30
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: "50%"
|
||||
git:
|
||||
tag: "r-8c160b6ed06647cec021e38b8bc8f4dffaab04c1"
|
||||
tag: "r-5e17b7ae74eadb8800587a4f4db74406a7e21e95"
|
||||
dir: "DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/fido-uaf"
|
||||
credentials: "git-credentials"
|
||||
database:
|
||||
name: "fido-uaf"
|
||||
requiredVersion: "8.2411.1"
|
||||
requiredVersion: "8.2505.5"
|
||||
keystores:
|
||||
- "fido-uaf-default-server-identity"
|
||||
- "fido-uaf-default-client-identity"
|
||||
|
|
|
@ -11,7 +11,7 @@ metadata:
|
|||
spec:
|
||||
type: "NevisFIDO"
|
||||
databaseType: "MariaDB"
|
||||
version: "8.2411.2"
|
||||
version: "8.2505.5"
|
||||
url: "mariadb-session-store-service.adn-agov-nevisidm-ob-01-uat"
|
||||
port: 3306
|
||||
database: "nevisfido_uaf"
|
||||
|
|
|
@ -9,4 +9,4 @@
|
|||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/agov-dev%40agov-test.iam.gserviceaccount.com"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@ JAVA_OPTS=(
|
|||
"-javaagent:/opt/agent/opentelemetry-javaagent.jar"
|
||||
"-Dotel.javaagent.logging=application"
|
||||
"-Dotel.javaagent.configuration-file=/var/opt/nevisfido/default/conf/otel.properties"
|
||||
"-Dotel.resource.attributes=service.version=8.2411.2,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
"-Dotel.resource.attributes=service.version=8.2505.5,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
|
|
|
@ -3,14 +3,13 @@
|
|||
"aaid" : "F1D0#0001",
|
||||
"description" : "Android NEVIS Mobile Authentication PIN Authenticator",
|
||||
"assertionScheme" : "UAFV1TLV",
|
||||
"attestationRootCertificates" : [
|
||||
"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"
|
||||
"attestationRootCertificates" : [],
|
||||
"supportedExtensions" : [
|
||||
{
|
||||
"id" : "ch.nevis.auth.fido.uaf.google-attestation-root-keys",
|
||||
"fail_if_unknown" : false,
|
||||
"data" : "[ \"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5UmAGMCAwEAAQ==\" ]"
|
||||
}
|
||||
],
|
||||
"attestationTypes" : [ 15879, 15880 ],
|
||||
"upv" : [ {
|
||||
|
@ -34,14 +33,13 @@
|
|||
"aaid" : "F1D0#0002",
|
||||
"description" : "Android NEVIS Mobile Authentication Fingerprint Authenticator",
|
||||
"assertionScheme" : "UAFV1TLV",
|
||||
"attestationRootCertificates" : [
|
||||
"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"
|
||||
"attestationRootCertificates" : [],
|
||||
"supportedExtensions" : [
|
||||
{
|
||||
"id" : "ch.nevis.auth.fido.uaf.google-attestation-root-keys",
|
||||
"fail_if_unknown" : false,
|
||||
"data" : "[ \"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5UmAGMCAwEAAQ==\" ]"
|
||||
}
|
||||
],
|
||||
"attestationTypes" : [ 15879, 15880 ],
|
||||
"upv" : [ {
|
||||
|
@ -65,14 +63,13 @@
|
|||
"aaid" : "F1D0#0003",
|
||||
"description" : "Android NEVIS Mobile Authentication Biometric Authenticator",
|
||||
"assertionScheme" : "UAFV1TLV",
|
||||
"attestationRootCertificates" : [
|
||||
"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"
|
||||
"attestationRootCertificates" : [],
|
||||
"supportedExtensions" : [
|
||||
{
|
||||
"id" : "ch.nevis.auth.fido.uaf.google-attestation-root-keys",
|
||||
"fail_if_unknown" : false,
|
||||
"data" : "[ \"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5UmAGMCAwEAAQ==\" ]"
|
||||
}
|
||||
],
|
||||
"attestationTypes" : [ 15879, 15880 ],
|
||||
"upv" : [ {
|
||||
|
@ -96,14 +93,13 @@
|
|||
"aaid" : "F1D0#0004",
|
||||
"description" : "Android NEVIS Mobile Authentication Device Passcode Authenticator",
|
||||
"assertionScheme" : "UAFV1TLV",
|
||||
"attestationRootCertificates" : [
|
||||
"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"
|
||||
"attestationRootCertificates" : [],
|
||||
"supportedExtensions" : [
|
||||
{
|
||||
"id" : "ch.nevis.auth.fido.uaf.google-attestation-root-keys",
|
||||
"fail_if_unknown" : false,
|
||||
"data" : "[ \"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5UmAGMCAwEAAQ==\" ]"
|
||||
}
|
||||
],
|
||||
"attestationTypes" : [ 15879, 15880 ],
|
||||
"upv" : [ {
|
||||
|
@ -127,14 +123,13 @@
|
|||
"aaid" : "F1D0#0005",
|
||||
"description" : "Android NEVIS Mobile Authentication Password Authenticator",
|
||||
"assertionScheme" : "UAFV1TLV",
|
||||
"attestationRootCertificates" : [
|
||||
"MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYyODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5UmAGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYDVR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lkLmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQADggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfBPb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00mqC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rYDBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPmQUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4uJU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyDCdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79IyZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxDqwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23UaicMDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk",
|
||||
"MIIFHDCCAwSgAwIBAgIJANUP8luj8tazMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTkxMTIyMjAzNzU4WhcNMzQxMTE4MjAzNzU4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5UmAGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1UdIwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBOMaBc8oumXb2voc7XCWnuXKhBBK3e2KMGz39t7lA3XXRe2ZLLAkLM5y3J7tURkf5a1SutfdOyXAmeE6SRo83Uh6WszodmMkxK5GM4JGrnt4pBisu5igXEydaW7qq2CdC6DOGjG+mEkN8/TA6p3cnoL/sPyz6evdjLlSeJ8rFBH6xWyIZCbrcpYEJzXaUOEaxxXxgYz5/cTiVKN2M1G2okQBUIYSY6bjEL4aUN5cfo7ogP3UvliEo3Eo0YgwuzR2v0KR6C1cZqZJSTnghIC/vAD32KdNQ+c3N+vl2OTsUVMC1GiWkngNx1OO1+kXW+YTnnTUOtOIswUP/Vqd5SYgAImMAfY8U9/iIgkQj6T2W6FsScy94IN9fFhE1UtzmLoBIuUFsVXJMTz+Jucth+IqoWFua9v1R93/k98p41pjtFX+H8DslVgfP097vju4KDlqN64xV1grw3ZLl4CiOe/A91oeLm2UHOq6wn3esB4r2EIQKb6jTVGu5sYCcdWpXr0AUVqcABPdgL+H7qJguBw09ojm6xNIrw2OocrDKsudk/okr/AwqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUBZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCHex0SdDrx+tWUDqG8At2JHA==",
|
||||
"MIIFHDCCAwSgAwIBAgIJAMNrfES5rhgxMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMjExMTE3MjMxMDQyWhcNMzYxMTEzMjMxMDQyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5UmAGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1UdIwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBTNNZe5cuf8oiq+jV0itTGzWVhSTjOBEk2FQvh11J3o3lna0o7rd8RFHnN00q4hi6TapFhh4qaw/iG6Xg+xOan63niLWIC5GOPFgPeYXM9+nBb3zZzC8ABypYuCusWCmt6Tn3+Pjbz3MTVhRGXuT/TQH4KGFY4PhvzAyXwdjTOCXID+aHud4RLcSySr0Fq/L+R8TWalvM1wJJPhyRjqRCJerGtfBagiALzvhnmY7U1qFcS0NCnKjoO7oFedKdWlZz0YAfu3aGCJd4KHT0MsGiLZez9WP81xYSrKMNEsDK+zK5fVzw6jA7cxmpXcARTnmAuGUeI7VVDhDzKeVOctf3a0qQLwC+d0+xrETZ4r2fRGNw2YEs2W8Qj6oDcfPvq9JySe7pJ6wcHnl5EZ0lwc4xH7Y4Dx9RA1JlfooLMw3tOdJZH0enxPXaydfAD3YifeZpFaUzicHeLzVJLt9dvGB0bHQLE4+EqKFgOZv2EoP686DQqbVS1u+9k0p2xbMA105TBIk7npraa8VM0fnrRKi7wlZKwdH+aNAyhbXRW9xsnODJ+g8eF452zvbiKKngEKirK5LGieoXBX7tZ9D1GNBH2Ob3bKOwwIWdEFle/YF/h6zWgdeoaNGDqVBrLr2+0DtWoiB1aDEjLWl9FmyIUyUm7mD/vFDkzF+wm7cyWpQpCVQ==",
|
||||
"MIIFHDCCAwSgAwIBAgIJAPHBcqaZ6vUdMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMjIwMzIwMTgwNzQ4WhcNNDIwMzE1MTgwNzQ4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5UmAGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1UdIwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQB8cMqTllHc8U+qCrOlg3H7174lmaCsbo/bJ0C17JEgMLb4kvrqsXZs01U3mB/qABg/1t5Pd5AORHARs1hhqGICW/nKMav574f9rZN4PC2ZlufGXb7sIdJpGiO9ctRhiLuYuly10JccUZGEHpHSYM2GtkgYbZba6lsCPYAAP83cyDV+1aOkTf1RCp/lM0PKvmxYN10RYsK631jrleGdcdkxoSK//mSQbgcWnmAEZrzHoF1/0gso1HZgIn0YLzVhLSA/iXCX4QT2h3J5z3znluKG1nv8NQdxei2DIIhASWfu804CA96cQKTTlaae2fweqXjdN1/v2nqOhngNyz1361mFmr4XmaKH/ItTwOe72NI9ZcwS1lVaCvsIkTDCEXdm9rCNPAY10iTunIHFXRh+7KPzlHGewCq/8TOohBRn0/NNfh7uRslOSZ/xKbN9tMBtw37Z8d2vvnXq/YWdsm1+JLVwn6yYD/yacNJBlwpddla8eaVMjsF6nBnIgQOf9zKSe06nSTqvgwUHosgOECZJZ1EuzbH4yswbt02tKtKEFhx+v+OTge/06V+jGsqTWLsfrOCNLuA8H++z+pUENmpqnnHovaI47gC+TNpkgYGkkBT6B/m/U01BuOBBTzhIlMEZq9qkDWuM2cA5kW5V3FJUcfHnw1IdYIg2Wxg7yHcQZemFQg==",
|
||||
"MIIC8jCCAdqgAwIBAgIGAZFrLh2fMA0GCSqGSIb3DQEBCwUAMDoxDjAMBgNVBAMMBXRlc3R5MQswCQYDVQQGEwJVUzEbMBkGCSqGSIb3DQEJARYMYWJjQGFjbWUuY29tMB4XDTI0MDgxOTE1MDc1MFoXDTI1MDgxOTE1MDc1MFowOjEOMAwGA1UEAwwFdGVzdHkxCzAJBgNVBAYTAlVTMRswGQYJKoZIhvcNAQkBFgxhYmNAYWNtZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDqitlYBzaxbPF389ZT5xkSS9Le1qdIOuc+dLVpBSWP9PEJhVZROgdOHs5f666iAcBedQm73sew3rpl+02J4fSgGmPkIYm1G2vkIrpt0eB9KzSc0AiLZbrPcFZOLHcOLoqVTfoRhnmAksHDC2f8euNKhCyriK8xlJb/xPfAfCn4r58ZGsQPUS7cJL6FLYh7FjrqfYDS10VOrQvGOALrG5NUj1DdqRq0M+klgs+6oJdUZTtY62BKkWh3N+7moNvrqykpv+ydFUJltgezDcb4Br8Nkw/breSPnomRfyHIcAcfATZcOPJlI8pO0zFZDIz8r7ESMnBhAxNaZgsUhR2XbaqbAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGw5XLY6GeFJMP350+djhcVqAw+E4HZqCJu1BMpYC0qS2D85fFi3gNuV0TnqB52abX1WBDDJK1CA0SPdyo/nX+qQzP6Dba1AVRKpRzdcsDsMDN3eMC08tajHgIIf5tNDv+HGE/MT2br4o5oducmQMOfV1NTJO1xhXYVqbsUnyrq3S6kD9WS8zRl6ruY1rT26eCQ4hTLHPaAiVsoXh5TBRXYCvGlAw7o2d9cmsbySforZ2wgdZwmu43B5eHNnt4NlDxZRyz6iEDP0nT877aB2ffsOKHAkJNuTvF5JSfnVzLmiyfa/7NI1ujfzcpA2UUXoWa7WN0wACiZQot8Zmswonjc=",
|
||||
"MIIC8jCCAdqgAwIBAgIGAZFrJblQMA0GCSqGSIb3DQEBCwUAMDoxDTALBgNVBAMMBHRlc3QxCzAJBgNVBAYTAkNIMRwwGgYJKoZIhvcNAQkBFg1mYWtlQGFjbWUuY29tMB4XDTI0MDgxOTE0NTg0MFoXDTI1MDgxOTE0NTg0MFowOjENMAsGA1UEAwwEdGVzdDELMAkGA1UEBhMCQ0gxHDAaBgkqhkiG9w0BCQEWDWZha2VAYWNtZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcWDBNmdq13fYHnhsmLndAW+MfbI6PeU4OenqfbrTtQUxqpyqhP6QccPYKX2SK3JeQo5uuF1jRD/9i9vAXI9NyiMMHSItjt9LjRs7bWnY4lokYGCAcSZooR9fGZX63dBSQo73V7MC8LDFGy5rw6dGDOmh0ktKxFzaT/nav8/Mx8FyG7M9+b5OPIBo2yze5Rd5cdErGJuUYa9No93BBr5tq+JfnmR/gwgCOke97ovhNj+sMu5bt946AxC6t00wNyPNVlJHKi1os0c/pWztTQkoRAx/w0JYKS9Afl0ZnGWQQ5PNLHHecp2GzriBpQAPXq81QTbOh5H7SzvhkaFQ4oxstAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD8GOaeMDqj2mzMmCqR6Cr3ChkbDAkdsBa5lOAikMKs7/tJyaw8iA5yH0nyobC58Jb61IATuxABPUALhP3RiNsUhnQQF/Dh+6CnCTD/2wsZmr8vUvNqyCLom+xkMT6Wayd9LYW4UONARv1qCLVI4RhiAr5kcomwqZnuj2DRF697lbSQDoz3iuKrCyBYSCBhS+k7UXpqpMyB2D6quRuPqh7JNtMjGSeMiNpMXhx5f4kl1YWb8NU93LDwHFR2kwnGmPA3M272VitcJC4dz3itGRKm9EYGd6d5D7kdC6lqpZPSIopChvXDyVrXjQgckvgtSGKscs6AvYgjthJGsR2z3Eao=",
|
||||
"MIIC8jCCAdqgAwIBAgIGAZFrLh2fMA0GCSqGSIb3DQEBCwUAMDoxDjAMBgNVBAMMBXRlc3R5MQswCQYDVQQGEwJVUzEbMBkGCSqGSIb3DQEJARYMYWJjQGFjbWUuY29tMB4XDTI0MDgxOTE1MDc1MFoXDTI1MDgxOTE1MDc1MFowOjEOMAwGA1UEAwwFdGVzdHkxCzAJBgNVBAYTAlVTMRswGQYJKoZIhvcNAQkBFgxhYmNAYWNtZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDqitlYBzaxbPF389ZT5xkSS9Le1qdIOuc+dLVpBSWP9PEJhVZROgdOHs5f666iAcBedQm73sew3rpl+02J4fSgGmPkIYm1G2vkIrpt0eB9KzSc0AiLZbrPcFZOLHcOLoqVTfoRhnmAksHDC2f8euNKhCyriK8xlJb/xPfAfCn4r58ZGsQPUS7cJL6FLYh7FjrqfYDS10VOrQvGOALrG5NUj1DdqRq0M+klgs+6oJdUZTtY62BKkWh3N+7moNvrqykpv+ydFUJltgezDcb4Br8Nkw/breSPnomRfyHIcAcfATZcOPJlI8pO0zFZDIz8r7ESMnBhAxNaZgsUhR2XbaqbAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGw5XLY6GeFJMP350+djhcVqAw+E4HZqCJu1BMpYC0qS2D85fFi3gNuV0TnqB52abX1WBDDJK1CA0SPdyo/nX+qQzP6Dba1AVRKpRzdcsDsMDN3eMC08tajHgIIf5tNDv+HGE/MT2br4o5oducmQMOfV1NTJO1xhXYVqbsUnyrq3S6kD9WS8zRl6ruY1rT26eCQ4hTLHPaAiVsoXh5TBRXYCvGlAw7o2d9cmsbySforZ2wgdZwmu43B5eHNnt4NlDxZRyz6iEDP0nT877aB2ffsOKHAkJNuTvF5JSfnVzLmiyfa/7NI1ujfzcpA2UUXoWa7WN0wACiZQot8Zmswonjc="
|
||||
"attestationRootCertificates" : [],
|
||||
"supportedExtensions" : [
|
||||
{
|
||||
"id" : "ch.nevis.auth.fido.uaf.google-attestation-root-keys",
|
||||
"fail_if_unknown" : false,
|
||||
"data" : "[ \"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5UmAGMCAwEAAQ==\" ]"
|
||||
}
|
||||
],
|
||||
"attestationTypes" : [ 15879, 15880 ],
|
||||
"upv" : [ {
|
||||
|
@ -268,4 +263,5 @@
|
|||
"publicKeyAlgAndEncodings" : [ 257 ],
|
||||
"tcDisplay" : 1,
|
||||
"tcDisplayContentType" : "text/plain"
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -37,7 +37,7 @@ fido-uaf:
|
|||
max-text-length: 2000
|
||||
metadata:
|
||||
path: "conf/metadata/metadata.json"
|
||||
idm-connection-type: "soap"
|
||||
idm-connection-type: "rest"
|
||||
dispatchers:
|
||||
- type: "firebase-cloud-messaging"
|
||||
dry-run: false
|
||||
|
@ -45,6 +45,7 @@ fido-uaf:
|
|||
registration-redeem-url: "https://auth.agov-w.azure.adnovum.net/nevisfido/token/redeem/registration"
|
||||
authentication-redeem-url: "https://auth.agov-w.azure.adnovum.net/nevisfido/token/redeem/authentication"
|
||||
deregistration-redeem-url: "https://auth.agov-w.azure.adnovum.net/nevisfido/token/redeem/deregistration"
|
||||
message-ttl: "180s"
|
||||
- type: "png-qr-code"
|
||||
registration-redeem-url: "https://auth.agov-w.azure.adnovum.net/nevisfido/token/redeem/registration"
|
||||
authentication-redeem-url: "https://auth.agov-w.azure.adnovum.net/nevisfido/token/redeem/authentication"
|
||||
|
@ -54,8 +55,11 @@ fido-uaf:
|
|||
authentication-redeem-url: "https://auth.agov-w.azure.adnovum.net/nevisfido/token/redeem/authentication"
|
||||
deregistration-redeem-url: "https://auth.agov-w.azure.adnovum.net/nevisfido/token/redeem/deregistration"
|
||||
base-url: "ch.agov.access-t://x-callback-url/authenticate"
|
||||
basic-full-attestation:
|
||||
android-verification-level: "strict"
|
||||
full-basic-attestation:
|
||||
android-verification-level: "default"
|
||||
android-permissive-mode-enabled: true
|
||||
android-attestation-key-revocation:
|
||||
reload-interval: "21600s"
|
||||
authorization:
|
||||
registration:
|
||||
type: "sectoken"
|
||||
|
@ -95,18 +99,18 @@ fido-uaf:
|
|||
session-repository:
|
||||
type: "sql"
|
||||
jdbc-url: "jdbc:mariadb://mariadb-session-store-service.adn-agov-nevisidm-ob-01-uat:3306/nevisfido_uaf?sslMode=disable&autocommit=true"
|
||||
max-connection-lifetime: "10m"
|
||||
user: "${exec:/var/opt/nevisfido/default/conf/credentials/dbUser}"
|
||||
password: "${exec:/var/opt/nevisfido/default/conf/credentials/dbPassword}"
|
||||
schema-user: ""
|
||||
schema-user-password: ""
|
||||
automatic-db-schema-setup: false
|
||||
max-connection-lifetime: "1800s"
|
||||
connection-timeout: "30s"
|
||||
min-connection-pool-size: 10
|
||||
max-connection-pool-size: 10
|
||||
max-connection-idle-time: "600s"
|
||||
credential-repository:
|
||||
type: "nevisidm"
|
||||
client-id: "cfa9c9b9-119f-4dff-9bb8-86d7c0cf2720"
|
||||
client-id: 1000
|
||||
user-attribute: "extId"
|
||||
administration-url: "https://idm.adn-agov-nevisidm-admin-01-uat:8989/nevisidm/services/v1_46/AdminService"
|
||||
admin-service-version: "v1_46"
|
||||
rest-url: "https://idm.adn-agov-nevisidm-admin-01-uat:8989/nevisidm"
|
||||
keystore: "/var/opt/keys/own/fido-uaf-default-client-identity/keystore.p12"
|
||||
keystore-type: "pkcs12"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
otel.service.name = fido-uaf
|
||||
otel.traces.sampler = always_on
|
||||
otel.traces.exporter = none
|
||||
otel.metrics.exporter = none
|
||||
otel.logs.exporter = none
|
||||
|
|
|
@ -21,4 +21,4 @@
|
|||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,4 +41,4 @@
|
|||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,4 +11,4 @@
|
|||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,4 +44,4 @@ if is_nevisfido_healthy():
|
|||
sys.exit(0)
|
||||
else:
|
||||
raise_last_error_in_log()
|
||||
sys.exit(1)
|
||||
sys.exit(1)
|
||||
|
|
|
@ -11,8 +11,8 @@ metadata:
|
|||
spec:
|
||||
type: "NevisFIDO"
|
||||
replicas: 1
|
||||
version: "8.2411.2"
|
||||
gitInitVersion: "1.3.0"
|
||||
version: "8.2505.5"
|
||||
gitInitVersion: "1.4.0"
|
||||
runAsNonRoot: true
|
||||
ports:
|
||||
management: 9089
|
||||
|
@ -40,13 +40,14 @@ spec:
|
|||
management:
|
||||
httpGet:
|
||||
path: "/nevisfido/health"
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 6
|
||||
failureThreshold: 50
|
||||
failureThreshold: 30
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: "50%"
|
||||
git:
|
||||
tag: "r-317ed268556b37656f27fb58fcffd4797cea27e4"
|
||||
tag: "r-5e17b7ae74eadb8800587a4f4db74406a7e21e95"
|
||||
dir: "DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/fido2"
|
||||
credentials: "git-credentials"
|
||||
keystores:
|
||||
|
|
|
@ -6,5 +6,5 @@ JAVA_OPTS=(
|
|||
"-javaagent:/opt/agent/opentelemetry-javaagent.jar"
|
||||
"-Dotel.javaagent.logging=application"
|
||||
"-Dotel.javaagent.configuration-file=/var/opt/nevisfido/default/conf/otel.properties"
|
||||
"-Dotel.resource.attributes=service.version=8.2411.2,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
"-Dotel.resource.attributes=service.version=8.2505.5,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
|
|
|
@ -1,3 +1,21 @@
|
|||
fido2:
|
||||
enabled: true
|
||||
user-presence-requirement: "always"
|
||||
rp-name: "AGOV-RelPartName"
|
||||
rp-id: "adnovum.net"
|
||||
origins:
|
||||
- "https://ob.agov-w.azure.adnovum.net"
|
||||
- "https://auth.agov-w.azure.adnovum.net"
|
||||
- "https://nevisidm.agov-w.azure.adnovum.net"
|
||||
signature-algorithms:
|
||||
- "ES256"
|
||||
- "EdDSA"
|
||||
display-name-source: "email"
|
||||
metadata:
|
||||
allow-listing-enabled: false
|
||||
timeout:
|
||||
user-verification: "300s"
|
||||
no-user-verification: "120s"
|
||||
server:
|
||||
port: 9443
|
||||
protocol: "https"
|
||||
|
@ -15,7 +33,7 @@ management:
|
|||
enabled: true
|
||||
credential-repository:
|
||||
type: "nevisidm"
|
||||
client-id: "cfa9c9b9-119f-4dff-9bb8-86d7c0cf2720"
|
||||
client-id: 1000
|
||||
rest-url: "https://idm.adn-agov-nevisidm-admin-01-uat:8989/nevisidm"
|
||||
keystore: "/var/opt/keys/own/fido2-default-client-identity/keystore.p12"
|
||||
keystore-passphrase: "${exec:/var/opt/keys/own/fido2-default-client-identity/keypass}"
|
||||
|
@ -24,27 +42,5 @@ credential-repository:
|
|||
truststore-passphrase: "${exec:/var/opt/keys/trust/fido2-idp-extended-truststore/keypass}"
|
||||
truststore-type: "pkcs12"
|
||||
user-attribute: "extId"
|
||||
fido2:
|
||||
enabled: true
|
||||
rp-name: "AGOV-RelPartName"
|
||||
rp-id: "adnovum.net"
|
||||
origins:
|
||||
- "https://ob.agov-w.azure.adnovum.net"
|
||||
- "https://auth.agov-w.azure.adnovum.net"
|
||||
- "https://nevisidm.agov-w.azure.adnovum.net"
|
||||
signature-algorithms:
|
||||
- "RS1"
|
||||
- "RS256"
|
||||
- "RS384"
|
||||
- "RS512"
|
||||
- "ES256"
|
||||
- "ES384"
|
||||
- "ES512"
|
||||
display-name-source: "email"
|
||||
metadata:
|
||||
allow-listing-enabled: false
|
||||
timeout:
|
||||
user-verification: "300s"
|
||||
no-user-verification: "120s"
|
||||
session-repository:
|
||||
type: "in-memory"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
otel.service.name = fido2
|
||||
otel.traces.sampler = always_on
|
||||
otel.traces.exporter = none
|
||||
otel.metrics.exporter = none
|
||||
otel.logs.exporter = none
|
||||
|
|
|
@ -44,4 +44,4 @@ if is_nevisfido_healthy():
|
|||
sys.exit(0)
|
||||
else:
|
||||
raise_last_error_in_log()
|
||||
sys.exit(1)
|
||||
sys.exit(1)
|
||||
|
|
|
@ -11,8 +11,8 @@ metadata:
|
|||
spec:
|
||||
type: "NevisLogrend"
|
||||
replicas: 1
|
||||
version: "8.2411.2"
|
||||
gitInitVersion: "1.3.0"
|
||||
version: "8.2505.5"
|
||||
gitInitVersion: "1.4.0"
|
||||
runAsNonRoot: true
|
||||
ports:
|
||||
server: 8988
|
||||
|
@ -38,13 +38,14 @@ spec:
|
|||
startupProbe:
|
||||
server:
|
||||
tcpSocket: true
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 4
|
||||
failureThreshold: 50
|
||||
failureThreshold: 30
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: "50%"
|
||||
git:
|
||||
tag: "r-e157935e7f17a778cb613627a645fe400a85af4d"
|
||||
tag: "r-5e17b7ae74eadb8800587a4f4db74406a7e21e95"
|
||||
dir: "DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/logrend"
|
||||
credentials: "git-credentials"
|
||||
podSecurity:
|
||||
|
|
|
@ -10,5 +10,5 @@ JAVA_OPTS=(
|
|||
"-javaagent:/opt/agent/opentelemetry-javaagent.jar"
|
||||
"-Dotel.javaagent.logging=application"
|
||||
"-Dotel.javaagent.configuration-file=/var/opt/nevislogrend/default/conf/otel.properties"
|
||||
"-Dotel.resource.attributes=service.version=8.2411.2,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
"-Dotel.resource.attributes=service.version=8.2505.5,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
|
||||
ico=image/x-icon
|
||||
json=application/json
|
||||
woff=font/woff
|
||||
woff2=font/woff2
|
||||
woff2=font/woff2
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
otel.service.name = logrend
|
||||
otel.traces.sampler = always_on
|
||||
otel.traces.exporter = none
|
||||
otel.metrics.exporter = none
|
||||
otel.logs.exporter = none
|
||||
|
|
|
@ -9,7 +9,6 @@ agov-ident.invalid-url.message=Link kann nicht verarbeitet werden
|
|||
agov-ident.invalid-url.title=Ungültiger Link
|
||||
agov-ident.onboarding=Registrierung & Verifikation
|
||||
agov-ident.retry=Versuchen Sie es erneut
|
||||
button.submit=Senden
|
||||
darkModeSwitch.aria.label=Dark-Mode-Schalter
|
||||
error.policy.failed=Das neue Passwort stimmt nicht mit der Richtlinie überein.
|
||||
error_1=Bitte überprüfen Sie Ihre Eingaben.
|
||||
|
@ -246,6 +245,7 @@ recovery_questionnaire_reason_selection.instruction=Bitte wählen Sie einen
|
|||
recovery_start_info.banner.warning=Sie können Ihr Konto nicht nutzen, bis der Wiederherstellungsprozess abgeschlossen ist.
|
||||
recovery_start_info.instruction=Während des Wiederherstellungsprozesses werden Sie einen neuen Login-Faktor registrieren. Wenn Ihr Konto verifizierte Informationen enthält, müssen Sie zum Abschluss des Wiederherstellungsprozesses möglicherweise auch einen Verifikationsprozess durchlaufen.
|
||||
recovery_start_info.title=Sie sind dabei, den Wiederherstellungsprozess zu starten
|
||||
submit.button.label=Senden
|
||||
title=NEVIS SSO Portal
|
||||
title.login=Login
|
||||
title.pwchange.label=Passwort ändern
|
||||
|
|
|
@ -9,7 +9,6 @@ agov-ident.invalid-url.message=Link kann nicht verarbeitet werden
|
|||
agov-ident.invalid-url.title=Ungültiger Link
|
||||
agov-ident.onboarding=Registrierung & Verifikation
|
||||
agov-ident.retry=Versuchen Sie es erneut
|
||||
button.submit=Senden
|
||||
darkModeSwitch.aria.label=Dark-Mode-Schalter
|
||||
error.policy.failed=Das neue Passwort stimmt nicht mit der Richtlinie überein.
|
||||
error_1=Bitte überprüfen Sie Ihre Eingaben.
|
||||
|
@ -246,6 +245,7 @@ recovery_questionnaire_reason_selection.instruction=Bitte wählen Sie einen
|
|||
recovery_start_info.banner.warning=Sie können Ihr Konto nicht nutzen, bis der Wiederherstellungsprozess abgeschlossen ist.
|
||||
recovery_start_info.instruction=Während des Wiederherstellungsprozesses werden Sie einen neuen Login-Faktor registrieren. Wenn Ihr Konto verifizierte Informationen enthält, müssen Sie zum Abschluss des Wiederherstellungsprozesses möglicherweise auch einen Verifikationsprozess durchlaufen.
|
||||
recovery_start_info.title=Sie sind dabei, den Wiederherstellungsprozess zu starten
|
||||
submit.button.label=Senden
|
||||
title=NEVIS SSO Portal
|
||||
title.login=Login
|
||||
title.pwchange.label=Passwort ändern
|
||||
|
|
|
@ -9,7 +9,6 @@ agov-ident.invalid-url.message=Link can't be processed
|
|||
agov-ident.invalid-url.title=Invalid Link
|
||||
agov-ident.onboarding=Registration & Verification
|
||||
agov-ident.retry=Try again
|
||||
button.submit=Submit
|
||||
darkModeSwitch.aria.label=Dark mode toggle
|
||||
error.policy.failed=The new password does not comply with the policy.
|
||||
error_1=Please check your input.
|
||||
|
@ -246,6 +245,7 @@ recovery_questionnaire_reason_selection.instruction=Please select the reason you
|
|||
recovery_start_info.banner.warning=You will not be able to use your account until the recovery process has been concluded.
|
||||
recovery_start_info.instruction=During the recovery process you will register a new login factor. If your account contains any verified information you might also have to go through a verification process to finish the recovery.
|
||||
recovery_start_info.title=You are about to start the recovery process
|
||||
submit.button.label=Submit
|
||||
title=NEVIS SSO Portal
|
||||
title.login=Login
|
||||
title.pwchange.label=Password Change
|
||||
|
|
|
@ -9,7 +9,6 @@ agov-ident.invalid-url.message=Le lien ne peut pas être traité
|
|||
agov-ident.invalid-url.title=Lien non valide
|
||||
agov-ident.onboarding=Enregistrement et vérification
|
||||
agov-ident.retry=Essayez à nouveau
|
||||
button.submit=Envoyer
|
||||
darkModeSwitch.aria.label=Activer l'apparence sombre
|
||||
error.policy.failed=Votre nouveau mot de passe ne conforme pas aux mesures de sécurité
|
||||
error_1=Veuillez vérifier votre saisie.
|
||||
|
@ -246,6 +245,7 @@ recovery_questionnaire_reason_selection.instruction=Veuillez sélectionner
|
|||
recovery_start_info.banner.warning=Vous ne pourrez pas utiliser votre compte tant que le processus de récupération n'aura pas été terminé.
|
||||
recovery_start_info.instruction=Le processus de récupération nécessitera l’enregistrement d’un nouveau facteur d’authentification. Si votre compte contient des informations ayant déjà été vérifiées, il se peut que vous deviez les faire vérifier à nouveau pour terminer la récupération.
|
||||
recovery_start_info.title=Vous êtes sur le point de démarrer le processus de récupération.
|
||||
submit.button.label=Envoyer
|
||||
title=NEVIS SSO Portal
|
||||
title.login=Login
|
||||
title.pwchange.label=Changer mot de passe
|
||||
|
|
|
@ -9,7 +9,6 @@ agov-ident.invalid-url.message=Il link non può essere elaborato
|
|||
agov-ident.invalid-url.title=Link non valido
|
||||
agov-ident.onboarding=Registrazione e verifica
|
||||
agov-ident.retry=Riprova
|
||||
button.submit=Continua
|
||||
darkModeSwitch.aria.label=Attivare la modalità scura
|
||||
error.policy.failed=La nuova password non è stata accettata. Scegliere una password che sia conforme ai criteri di password.
|
||||
error_1=Verificare i dati inseriti.
|
||||
|
@ -246,6 +245,7 @@ recovery_questionnaire_reason_selection.instruction=Selezioni il motivo per cui
|
|||
recovery_start_info.banner.warning=Non è possibile utilizzare l’account finché il processo di ripristino non sarà concluso.
|
||||
recovery_start_info.instruction=Durante il processo di ripristino registrerà un nuovo fattore di login. Se il suo account contiene informazioni verificate, potrebbe dover effettuare anche un processo di verificazione per completare il ripristino.
|
||||
recovery_start_info.title=Sta per iniziare il processo di ripristino
|
||||
submit.button.label=Continua
|
||||
title=NEVIS SSO Portal
|
||||
title.login=Login
|
||||
title.pwchange.label=Cambiare Password
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M13.9697 17.2808C12.9941 18.2276 11.9177 18.08 10.8917 17.6336C9.80091 17.1782 8.80371 17.1494 7.65171 17.6336C6.21711 18.2528 5.45571 18.0728 4.59171 17.2808C-0.28628 12.2588 0.433719 4.60879 5.97771 4.32079C7.32231 4.39279 8.26371 5.06419 9.05571 5.11999C10.2329 4.88059 11.3597 4.19479 12.6197 4.28479C14.1335 4.40719 15.2657 5.00479 16.0217 6.07938C12.9077 7.95138 13.6457 12.0554 16.5059 13.2074C15.9335 14.7104 15.1991 16.1954 13.9679 17.2934L13.9697 17.2808ZM8.94771 4.26679C8.80191 2.03479 10.6109 0.198798 12.6917 0.0187988C12.9779 2.59279 10.3517 4.51879 8.94771 4.26679Z" fill="#1F2F33"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="15.156" height="18" fill="white" transform="translate(1.3335)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 872 B |
|
@ -66,7 +66,7 @@ const Status = {
|
|||
};
|
||||
|
||||
function setDeepLinkLabel(button) {
|
||||
const text = document.getElementsByName('info.deeplink')[0].value;
|
||||
const text = document.getElementById('info.login.access_app').innerText;
|
||||
button.innerHTML = text;
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,13 @@ function messageCheckPhone() {
|
|||
infoElement.innerHTML = text;
|
||||
}
|
||||
|
||||
const Element = {
|
||||
function showError() {
|
||||
const text = document.getElementsByName('error.authcloud.login')[0].value;
|
||||
errorElement.innerHTML = text;
|
||||
infoElement.style.display = "none";
|
||||
}
|
||||
|
||||
const AccessAppElement = {
|
||||
|
||||
_elem: null, // QR code or deep link depending on device
|
||||
|
||||
|
@ -91,8 +97,11 @@ const Element = {
|
|||
if (isAndroid || isIphone) {
|
||||
this._elem = document.createElement('a');
|
||||
this._elem.setAttribute('href', appLink);
|
||||
this._elem.setAttribute('class', 'btn btn-primary');
|
||||
this._elem.setAttribute('class', 'btn btn-primary w-100 mt-4');
|
||||
this._elem.setAttribute('target', '_blank');
|
||||
// distinguishes style for platforms
|
||||
dispatcherElement.classList.add('mobile-platform');
|
||||
|
||||
dispatcherElement.appendChild(this._elem);
|
||||
setDeepLinkLabel(this._elem);
|
||||
}
|
||||
|
@ -103,13 +112,23 @@ const Element = {
|
|||
}
|
||||
else {
|
||||
messageScanQR();
|
||||
const qrSize = 280;
|
||||
// Element to render the QR code
|
||||
this._elem = document.createElement('canvas');
|
||||
dispatcherElement.appendChild(this._elem);
|
||||
var qrcode = new QRious({
|
||||
// Wrapper div to render corners
|
||||
const qrCodeWrapper = document.createElement('div');
|
||||
qrCodeWrapper.setAttribute('id','qr-code-wrapper');
|
||||
qrCodeWrapper.style.width = `${qrSize}px`;
|
||||
qrCodeWrapper.style.height = `${qrSize}px`;
|
||||
qrCodeWrapper.appendChild(this._elem)
|
||||
dispatcherElement.style.height = `${qrSize}px`;
|
||||
dispatcherElement.appendChild(qrCodeWrapper);
|
||||
const qrcode = new QRious({
|
||||
element: this._elem,
|
||||
foreground: "#168CA9",
|
||||
// use --nevis-gray-900 CSS variable value
|
||||
foreground: getComputedStyle(document.body).getPropertyValue('--nevis-gray-900'),
|
||||
level: "M",
|
||||
size: 280,
|
||||
size: qrSize,
|
||||
value: appLink
|
||||
});
|
||||
}
|
||||
|
@ -125,20 +144,31 @@ const Element = {
|
|||
};
|
||||
|
||||
function authenticateUser(appLink) {
|
||||
Element.show(appLink);
|
||||
console.log('Starting Authentication Cloud status polling...');
|
||||
|
||||
AccessAppElement.show(appLink);
|
||||
|
||||
console.log('Starting Auth Cloud status polling...');
|
||||
|
||||
Status.startPolling(statusToken, (st, done) => {
|
||||
|
||||
if (st.status === 'succeeded') {
|
||||
console.log('Authentication Cloud login done.');
|
||||
|
||||
console.log('Auth Cloud success.');
|
||||
|
||||
// auto submit form with outcome
|
||||
submitStatus('succeeded')
|
||||
}
|
||||
else if (st.status === 'failed') {
|
||||
|
||||
// failed: The transaction failed, either by timeout or because the user did not accept.
|
||||
console.warn('Authentication Cloud login failed. User abort or timeout.');
|
||||
console.warn('Auth Cloud login failed. User abort or timeout.');
|
||||
|
||||
submitStatus('failed')
|
||||
}
|
||||
else if (st.status === 'unknown') {
|
||||
console.error('Authentication Cloud login failed. Unknown status.');
|
||||
|
||||
console.error('Auth Cloud login failed. Unknown status.');
|
||||
|
||||
submitStatus('unknown')
|
||||
}
|
||||
});
|
||||
|
|
|
@ -75,7 +75,12 @@ function messageScanQR() {
|
|||
infoElement.innerHTML = text;
|
||||
}
|
||||
|
||||
const Element = {
|
||||
function messageInstalledAccessApp() {
|
||||
const text = document.getElementById('info.access_app.installed').innerText;
|
||||
infoElement.innerHTML = text;
|
||||
}
|
||||
|
||||
const AccessAppElement = {
|
||||
|
||||
_elem: null, // QR code or deep link depending on device
|
||||
|
||||
|
@ -84,22 +89,47 @@ const Element = {
|
|||
const isIphone = 'iPhone' === navigator.platform;
|
||||
const isAndroid = /android/i.test(userAgent) && /mobile/i.test(userAgent);
|
||||
if (isAndroid || isIphone) {
|
||||
if (isAndroid) {
|
||||
document.getElementById('install_apple').style.display = 'none';
|
||||
}
|
||||
if (isIphone) {
|
||||
document.getElementById('install_google').style.display = 'none';
|
||||
}
|
||||
this._elem = document.createElement('a');
|
||||
this._elem.setAttribute('href', appLink);
|
||||
this._elem.setAttribute('class', 'btn btn-primary');
|
||||
this._elem.setAttribute('class', 'btn btn-primary w-100');
|
||||
this._elem.setAttribute('target', '_blank');
|
||||
// distinguishes style for platforms
|
||||
dispatcherElement.classList.add('mobile-platform');
|
||||
const accessApplinks = document.getElementById('access-app-download-link');
|
||||
accessApplinks.classList.add('access-app-download-link-mobile-spacing');
|
||||
|
||||
dispatcherElement.appendChild(this._elem);
|
||||
setDeepLinkLabel(this._elem);
|
||||
|
||||
// info text is displayed before access app links
|
||||
accessApplinks.parentNode.insertBefore(infoElement.parentNode, accessApplinks);
|
||||
messageInstalledAccessApp();
|
||||
}
|
||||
else {
|
||||
messageScanQR();
|
||||
const qrSize = 280;
|
||||
// Element to render the QR code
|
||||
this._elem = document.createElement('canvas');
|
||||
dispatcherElement.appendChild(this._elem);
|
||||
var qrcode = new QRious({
|
||||
// Wrapper div to render corners
|
||||
const qrCodeWrapper = document.createElement('div');
|
||||
qrCodeWrapper.setAttribute('id','qr-code-wrapper');
|
||||
qrCodeWrapper.style.width = `${qrSize}px`;
|
||||
qrCodeWrapper.style.height = `${qrSize}px`;
|
||||
qrCodeWrapper.appendChild(this._elem)
|
||||
dispatcherElement.style.height = `${qrSize}px`;
|
||||
dispatcherElement.appendChild(qrCodeWrapper);
|
||||
const qrcode = new QRious({
|
||||
element: this._elem,
|
||||
foreground: "#168CA9",
|
||||
// use --nevis-gray-900 CSS variable value
|
||||
foreground: getComputedStyle(document.body).getPropertyValue('--nevis-gray-900'),
|
||||
level: "M",
|
||||
size: 280,
|
||||
size: qrSize,
|
||||
value: appLink
|
||||
});
|
||||
}
|
||||
|
@ -114,25 +144,47 @@ const Element = {
|
|||
};
|
||||
|
||||
function onboardUser(appLink) {
|
||||
Element.show(appLink);
|
||||
console.log('Starting Authentication Cloud status polling...');
|
||||
|
||||
AccessAppElement.show(appLink);
|
||||
|
||||
console.log('Starting Auth Cloud status polling...');
|
||||
|
||||
Status.startPolling(statusToken, (st, done) => {
|
||||
|
||||
if (st.status === 'succeeded') {
|
||||
console.log('Authentication Cloud onboarding done.');
|
||||
|
||||
console.log('Auth Cloud success.');
|
||||
|
||||
// auto submit form with outcome
|
||||
submitStatus('succeeded')
|
||||
}
|
||||
else if (st.status === 'failed') {
|
||||
|
||||
// failed: The transaction failed, either by timeout or because the user did not accept.
|
||||
console.warn('Authentication Cloud onboarding failed. User abort or timeout.');
|
||||
|
||||
submitStatus('failed')
|
||||
}
|
||||
else if (st.status === 'unknown') {
|
||||
|
||||
console.error('Authentication Cloud onboarding failed. Unknown status.');
|
||||
|
||||
submitStatus('unknown')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const swap = function (nodeA, nodeB) {
|
||||
const parentA = nodeA.parentNode;
|
||||
const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling;
|
||||
|
||||
// Move `nodeA` to before the `nodeB`
|
||||
nodeB.parentNode.insertBefore(nodeA, nodeB);
|
||||
|
||||
// Move `nodeB` to before the sibling of `nodeA`
|
||||
parentA.insertBefore(nodeB, siblingA);
|
||||
};
|
||||
|
||||
function init() {
|
||||
|
||||
const form = document.getElementById('authcloud_onboard');
|
||||
|
@ -145,6 +197,9 @@ function init() {
|
|||
|
||||
dispatcherElement = document.getElementById('authcloud_dispatch');
|
||||
|
||||
// info texts are displayed underneath QR code
|
||||
swap(infoElement.parentNode, dispatcherElement.parentNode);
|
||||
|
||||
const appLink = form.appLink.value;
|
||||
onboardUser(appLink);
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6667 6L8 10.6667L3.33333 6" stroke="#1F2F33" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 227 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 3.33332L10.6667 7.99999L6 12.6667" stroke="#1F2F33" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 235 B |
|
@ -0,0 +1,27 @@
|
|||
function copyToClipboard(containerid) {
|
||||
if (document.selection) {
|
||||
var range = document.body.createTextRange();
|
||||
range.moveToElementText(document.getElementById(containerid));
|
||||
range.select().createTextRange();
|
||||
document.execCommand("copy");
|
||||
} else if (window.getSelection) {
|
||||
var range = document.createRange();
|
||||
range.selectNode(document.getElementById(containerid));
|
||||
window.getSelection().addRange(range);
|
||||
document.execCommand("copy");
|
||||
}
|
||||
|
||||
// clear selection
|
||||
if (window.getSelection) {
|
||||
if (window.getSelection().empty) {
|
||||
// Chrome
|
||||
window.getSelection().empty();
|
||||
} else if (window.getSelection().removeAllRanges) {
|
||||
// Firefox
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
} else if (document.selection) {
|
||||
// IE
|
||||
document.selection.empty();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,755 @@
|
|||
/*!
|
||||
* Bootstrap v5.1.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file contains customized bootstrap classes which are in the same name, however differ in the implementation.
|
||||
* Classes use CSS custom properties from :root to be runtime modifiable.
|
||||
* Used a portion of bootstrap classes which satisfy the requirements without to include the whole bootstrap bundle.
|
||||
* If you would like to add new classes as "override" or extension please use the bootstrap naming convention.
|
||||
*/
|
||||
|
||||
/* Form controls */
|
||||
.form-label {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.form-check:has(.form-check-label) {
|
||||
padding: 1em 1em 1em 1.6em;
|
||||
border-top: solid 1px lightgray;
|
||||
margin: 0 1em 0 1em;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.form-group {}
|
||||
|
||||
.form-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.5625rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.25rem;
|
||||
color: var(--nevis-black);
|
||||
background-color: var(--nevis-white);
|
||||
background-clip: padding-box;
|
||||
border: 0.0625rem solid var(--nevis-form-control-border-color);
|
||||
border-radius: var(--nevis-border-radius);
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.form-control {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
color: var(--nevis-black);
|
||||
background-color: var(--nevis-white);
|
||||
border-color: var(--nevis-primary);
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.0625rem var(--nevis-primary);
|
||||
}
|
||||
|
||||
.form-control::-webkit-date-and-time-value {
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
.form-control::-moz-placeholder {
|
||||
color: var(--nevis-secondary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: var(--nevis-secondary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.form-control:disabled {
|
||||
font-size: 0.875rem;
|
||||
background-color: #e9ecef;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.form-control[readonly] {
|
||||
background: var(--nevis-readonly-bg-color);
|
||||
border-color: var(--nevis-readonly-border-color);
|
||||
border-radius: var(--nevis-border-radius);
|
||||
color: var(--nevis-gray-900);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-control[readonly]:focus {
|
||||
box-shadow: 0 0 0 0.0625rem var(--nevis-readonly-box-shadow-color);
|
||||
}
|
||||
|
||||
/* Valdiation */
|
||||
.invalid-feedback {
|
||||
display: none;
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--nevis-danger);
|
||||
}
|
||||
|
||||
.was-validated :invalid~.invalid-feedback,
|
||||
.was-validated :invalid~.invalid-tooltip,
|
||||
.is-invalid~.invalid-feedback,
|
||||
.is-invalid~.invalid-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Added for 3rd party International Telephone Input */
|
||||
.was-validated .iti~.invalid-feedback.invalid-feedback-ready,
|
||||
.was-validated .iti~.invalid-tooltip.invalid-feedback-ready {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.was-validated .form-control:invalid,
|
||||
.form-control.is-invalid {
|
||||
border-color: var(--nevis-danger);
|
||||
border-width: 0.125rem;
|
||||
padding-right: inherit;
|
||||
background-image: none;
|
||||
background-repeat: no-repeat;
|
||||
background-position: inherit;
|
||||
background-size: inherit;
|
||||
}
|
||||
|
||||
.was-validated .form-control:invalid:focus,
|
||||
.form-control.is-invalid:focus {
|
||||
border-color: var(--nevis-danger);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-control:valid,
|
||||
.form-control.is-valid {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
/* remove valid feedback classes */
|
||||
.was-validated .form-control:valid,
|
||||
.form-control.is-valid {
|
||||
border-color: var(--nevis-gray-400);
|
||||
padding-right: inherit;
|
||||
background-image: inherit;
|
||||
background-repeat: no-repeat;
|
||||
background-position: inherit;
|
||||
background-size: inherit;
|
||||
}
|
||||
|
||||
.was-validated .form-control:valid:focus,
|
||||
.form-control.is-valid:focus {
|
||||
border-color: var(--nevis-gray-400);
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.was-validated textarea.form-control:valid,
|
||||
textarea.form-control.is-valid {
|
||||
padding-right: inherit;
|
||||
background-position: inherit;
|
||||
}
|
||||
|
||||
.was-validated .form-select:valid,
|
||||
.form-select.is-valid {
|
||||
border-color: var(--nevis-gray-400);
|
||||
}
|
||||
|
||||
.was-validated .form-select:valid:not([multiple]):not([size]),
|
||||
.was-validated .form-select:valid:not([multiple])[size="1"],
|
||||
.form-select.is-valid:not([multiple]):not([size]),
|
||||
.form-select.is-valid:not([multiple])[size="1"] {
|
||||
padding-right: inherit;
|
||||
background-image: none;
|
||||
background-position: inherit;
|
||||
background-size: inherit;
|
||||
}
|
||||
|
||||
.was-validated .form-select:valid:focus,
|
||||
.form-select.is-valid:focus {
|
||||
border-color: var(--nevis-gray-400);
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.was-validated .form-check-input:valid,
|
||||
.form-check-input.is-valid {
|
||||
border-color: var(--nevis-gray-400);
|
||||
}
|
||||
|
||||
.was-validated .form-check-input:valid:checked,
|
||||
.form-check-input.is-valid:checked {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.was-validated .form-check-input:valid:focus,
|
||||
.form-check-input.is-valid:focus {
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.was-validated .form-check-input:valid~.form-check-label,
|
||||
.form-check-input.is-valid~.form-check-label {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
line-height: 1.5rem;
|
||||
color: var(--nevis-black);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
border: 0.0625rem solid transparent;
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 1rem;
|
||||
border-radius: var(--nevis-border-radius);
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.btn {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
color: var(--nevis-black);
|
||||
}
|
||||
|
||||
.btn:disabled,
|
||||
.btn.disabled,
|
||||
fieldset:disabled .btn {
|
||||
pointer-events: none;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* remove box-shadows by default, enable later by colors */
|
||||
.btn:focus {
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.btn-check:checked+.btn-primary:focus,
|
||||
.btn-check:active+.btn-primary:focus,
|
||||
.btn-primary:active:focus,
|
||||
.btn-primary.active:focus,
|
||||
.show>.btn-primary.dropdown-toggle:focus {
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
/* Primary Button */
|
||||
.btn-primary {
|
||||
color: var(--nevis-white);
|
||||
background-color: var(--nevis-primary);
|
||||
border-color: var(--nevis-primary);
|
||||
box-shadow: 0rem 0.25rem 1.875rem -0.625rem var(--nevis-primary);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
color: var(--nevis-white);
|
||||
filter: brightness(110%);
|
||||
background-color: var(--nevis-primary);
|
||||
border-color: var(--nevis-primary);
|
||||
box-shadow: 0rem 0.25rem 1.875rem -0.625rem var(--nevis-primary);
|
||||
}
|
||||
|
||||
.btn-primary:focus {
|
||||
color: var(--nevis-white);
|
||||
background-color: var(--nevis-primary);
|
||||
border-color: var(--nevis-primary);
|
||||
filter: brightness(110%);
|
||||
box-shadow: 0rem 0.25rem 1.875rem -0.625rem var(--nevis-primary);
|
||||
}
|
||||
|
||||
.btn-primary:active,
|
||||
.btn-primary.active {
|
||||
color: var(--nevis-white);
|
||||
background-color: var(--nevis-primary);
|
||||
border-color: var(--nevis-primary);
|
||||
filter: brightness(90%);
|
||||
}
|
||||
|
||||
.btn-primary:active:focus,
|
||||
.btn-primary.active:focus {
|
||||
box-shadow: 0rem 0.25rem 1.875rem -0.625rem var(--nevis-primary);
|
||||
}
|
||||
|
||||
.btn-primary:disabled,
|
||||
.btn-primary.disabled {
|
||||
color: var(--nevis-secondary);
|
||||
background-color: var(--nevis-gray-100);
|
||||
border-color: var(--nevis-gray-100);
|
||||
box-shadow: none;
|
||||
filter: brightness(1);
|
||||
}
|
||||
|
||||
/* Secondary Button */
|
||||
.btn-secondary {
|
||||
color: var(--nevis-gray-900);
|
||||
background-color: var(--nevis-gray-200);
|
||||
border-color: var(--nevis-gray-200);
|
||||
box-shadow: 0rem 0.25rem 1.875rem -0.625rem var(--nevis-gray-200);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
color: var(--nevis-gray-900);
|
||||
filter: brightness(110%);
|
||||
background-color: var(--nevis-gray-200);
|
||||
border-color: var(--nevis-gray-200);
|
||||
box-shadow: 0rem 0.25rem 1.875rem -0.625rem var(--nevis-gray-200);
|
||||
}
|
||||
|
||||
.btn-secondary:focus {
|
||||
color: var(--nevis-gray-900);
|
||||
background-color: var(--nevis-gray-200);
|
||||
border-color: var(--nevis-gray-200);
|
||||
filter: brightness(110%);
|
||||
box-shadow: 0rem 0.25rem 1.875rem -0.625rem var(--nevis-gray-200);
|
||||
}
|
||||
|
||||
.btn-secondary:active,
|
||||
.btn-secondary.active {
|
||||
color: var(--nevis-gray-900);
|
||||
background-color: var(--nevis-gray-200);
|
||||
border-color: var(--nevis-gray-200);
|
||||
filter: brightness(90%);
|
||||
}
|
||||
|
||||
.btn-secondary:active:focus,
|
||||
.btn-secondary.active:focus {
|
||||
box-shadow: 0rem 0.25rem 1.875rem -0.625rem var(--nevis-gray-200);
|
||||
}
|
||||
|
||||
.btn-secondary:disabled,
|
||||
.btn-secondary.disabled {
|
||||
color: var(--nevis-secondary);
|
||||
background-color: var(--nevis-gray-100);
|
||||
border-color: var(--nevis-gray-100);
|
||||
box-shadow: none;
|
||||
filter: brightness(1);
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
font-size: 0.875rem !important;
|
||||
vertical-align: baseline;
|
||||
border: none;
|
||||
color: var(--nevis-primary);
|
||||
background: none;
|
||||
text-decoration: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Componentes */
|
||||
.dropdown-toggle::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
h6,
|
||||
.h6,
|
||||
h5,
|
||||
.h5,
|
||||
h4,
|
||||
.h4,
|
||||
h3,
|
||||
.h3,
|
||||
h2,
|
||||
.h2,
|
||||
h1,
|
||||
.h1 {
|
||||
margin-top: 0;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1,
|
||||
.h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
|
||||
h1,
|
||||
.h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2,
|
||||
.h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
|
||||
h2,
|
||||
.h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3,
|
||||
.h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
|
||||
h3,
|
||||
.h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4,
|
||||
.h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
|
||||
h4,
|
||||
.h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5,
|
||||
.h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6,
|
||||
.h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
small,
|
||||
.small {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--nevis-primary) !important;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--nevis-secondary) !important;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--nevis-success) !important;
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: var(--nevis-info) !important;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: var(--nevis-warning) !important;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--nevis-danger) !important;
|
||||
}
|
||||
|
||||
.text-light {
|
||||
color: var(--nevis-light) !important;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: var(--nevis-dark) !important;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
color: var(--nevis-white) !important;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--nevis-primary) !important;
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
background-color: var(--nevis-secondary) !important;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: var(--nevis-success) !important;
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background-color: var(--nevis-info) !important;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: var(--nevis-warning) !important;
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: var(--nevis-danger) !important;
|
||||
}
|
||||
|
||||
.bg-light {
|
||||
background-color: var(--nevis-light) !important;
|
||||
}
|
||||
|
||||
.bg-dark {
|
||||
background-color: var(--nevis-dark) !important;
|
||||
}
|
||||
|
||||
.bg-body {
|
||||
background-color: var(--nevis-white) !important;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: var(--nevis-white) !important;
|
||||
}
|
||||
|
||||
.link-primary {
|
||||
color: var(--nevis-primary);
|
||||
}
|
||||
|
||||
.link-primary:hover,
|
||||
.link-primary:focus {
|
||||
color: var(--nevis-primary);
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
.link-secondary {
|
||||
color: var(--nevis-secondary);
|
||||
}
|
||||
|
||||
.link-secondary:hover,
|
||||
.link-secondary:focus {
|
||||
color: var(--nevis-secondary);
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
.link-success {
|
||||
color: var(--nevis-success);
|
||||
}
|
||||
|
||||
.link-success:hover,
|
||||
.link-success:focus {
|
||||
color: var(--nevis-success);
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
.link-info {
|
||||
color: var(--nevis-info);
|
||||
}
|
||||
|
||||
.link-info:hover,
|
||||
.link-info:focus {
|
||||
color: var(--nevis-info);
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
.link-warning {
|
||||
color: var(--nevis-warning);
|
||||
}
|
||||
|
||||
.link-warning:hover,
|
||||
.link-warning:focus {
|
||||
color: var(--nevis-warning);
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
.link-danger {
|
||||
color: var(--nevis-danger);
|
||||
}
|
||||
|
||||
.link-danger:hover,
|
||||
.link-danger:focus {
|
||||
color: var(--nevis-danger);
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
.link-light {
|
||||
color: var(--nevis-light);
|
||||
}
|
||||
|
||||
.link-light:hover,
|
||||
.link-light:focus {
|
||||
color: var(--nevis-light);
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
.link-dark {
|
||||
color: var(--nevis-dark);
|
||||
}
|
||||
|
||||
.link-dark:hover,
|
||||
.link-dark:focus {
|
||||
color: var(--nevis-dark);
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
.border-primary {
|
||||
border-color: var(--nevis-primary) !important;
|
||||
}
|
||||
|
||||
.border-secondary {
|
||||
border-color: var(--nevis-secondary) !important;
|
||||
}
|
||||
|
||||
.border-success {
|
||||
border-color: var(--nevis-success) !important;
|
||||
}
|
||||
|
||||
.border-info {
|
||||
border-color: var(--nevis-info) !important;
|
||||
}
|
||||
|
||||
.border-warning {
|
||||
border-color: var(--nevis-warning) !important;
|
||||
}
|
||||
|
||||
.border-danger {
|
||||
border-color: var(--nevis-danger) !important;
|
||||
border-width: 0.125rem;
|
||||
}
|
||||
|
||||
.border-light {
|
||||
border-color: var(--nevis-light) !important;
|
||||
}
|
||||
|
||||
.border-dark {
|
||||
border-color: var(--nevis-dark) !important;
|
||||
}
|
||||
|
||||
.border-white {
|
||||
border-color: var(--nevis-white) !important;
|
||||
}
|
||||
|
||||
|
||||
/* EXTENSION PART */
|
||||
|
||||
/* Spacing */
|
||||
.mt-20 {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.me-5px {
|
||||
margin-right: 0.3125rem;
|
||||
}
|
||||
|
||||
.my-40 {
|
||||
margin: 2.5rem 0;
|
||||
}
|
||||
|
||||
.mb-40 {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* Colors */
|
||||
|
||||
.text-nevis-blue {
|
||||
color: var(--nevis-blue-600) !important;
|
||||
}
|
||||
|
||||
.bg-nevis-blue {
|
||||
background-color: var(--nevis-blue-600) !important;
|
||||
}
|
||||
|
||||
.border-nevis-blue {
|
||||
border-color: var(--nevis-blue-600) !important;
|
||||
}
|
||||
|
||||
.link-nevis-blue {
|
||||
color: var(--nevis-blue-600);
|
||||
}
|
||||
|
||||
.link-nevis-blue:hover,
|
||||
.link-nevis-blue:focus {
|
||||
color: var(--nevis-blue-600);
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
.btn-language-selector {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
font-size: 0.875rem !important;
|
||||
line-height: 1.25rem;
|
||||
font-weight: normal;
|
||||
outline: none;
|
||||
border: none;
|
||||
vertical-align: baseline;
|
||||
text-align: center;
|
||||
background-color: initial;
|
||||
color: var(--nevis-gray-900);
|
||||
}
|
||||
|
||||
.btn-language-selector:hover {
|
||||
background: initial;
|
||||
}
|
||||
|
||||
.btn-language-selector:active {
|
||||
background: initial;
|
||||
}
|
||||
|
||||
.btn-language-selector:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-language-selector+.dropdown-menu {
|
||||
min-width: 0;
|
||||
width: 10rem;
|
||||
padding: 0.25rem 0;
|
||||
/* centering the dropdown */
|
||||
margin-left: -0.5rem !important;
|
||||
margin-top: 0.5rem !important;
|
||||
overflow: hidden;
|
||||
box-shadow: 0rem 0rem 0rem 0.0625rem var(--nevis-gray-200),
|
||||
0rem 0.1875rem 1.25rem -0.625rem var(--nevis-gray-900);
|
||||
border-radius: var(--nevis-border-radius);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.btn-language-selector+.dropdown-menu>li {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-language-selector+.dropdown-menu .dropdown-item {
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
padding: 0.5rem 1rem;
|
||||
color: var(--nevis-gray-900);
|
||||
}
|
||||
|
||||
.btn-language-selector+.dropdown-menu .dropdown-item:hover {
|
||||
background: var(--nevis-blue-100);
|
||||
}
|
||||
|
||||
.btn-language-selector+.dropdown-menu .dropdown-item:focus {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.btn-language-selector+.dropdown-menu .dropdown-item:active,
|
||||
.btn-language-selector+.dropdown-menu .dropdown-item.active {
|
||||
background: var(--nevis-blue-100);
|
||||
filter: brightness(90%);
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
/********************************************************
|
||||
* Layout
|
||||
********************************************************/
|
||||
|
||||
html { /* magic to position footer */
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-bottom: 76px; /* == footer height */
|
||||
}
|
||||
|
||||
.container, .container-fluid {
|
||||
padding-left: 36px;
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
nav {
|
||||
min-height: 100px;
|
||||
padding: 36px;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 16px; /* h1.logintitle adds 20px => 36px */
|
||||
}
|
||||
|
||||
.container {
|
||||
min-width: 260px;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 0 36px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
* Header
|
||||
********************************************************/
|
||||
|
||||
header .logo {
|
||||
/* width: 20%;*/
|
||||
/*max-width: 600px;*/
|
||||
max-height: 150px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
* Dropdown
|
||||
********************************************************/
|
||||
a.dropdown-toggle {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.dropdown-toggle:hover {
|
||||
color: #168CA9;
|
||||
border-bottom: 3px solid #168CA9;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.dropdown-menu li > a {
|
||||
padding: 6px 28px;
|
||||
}
|
||||
|
||||
.dropdown-menu a > .prefix {
|
||||
display: inline-block;
|
||||
min-width: 22px;
|
||||
margin-right: 28px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
* Form
|
||||
********************************************************/
|
||||
|
||||
/* Labels should not be bold */
|
||||
label {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Make error messages bold */
|
||||
.has-error .help-block {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Change button size, by default 116px in width */
|
||||
.btn {
|
||||
min-width: 116px;
|
||||
padding: 3px 12px;
|
||||
}
|
||||
|
||||
/* Disable gradient in buttons, ughhhh */
|
||||
.btn.btn-primary {
|
||||
border-color: transparent;
|
||||
background-image: none;
|
||||
text-shadow: none;
|
||||
box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
}
|
||||
|
||||
.help-block a, .help-block a:visited {
|
||||
color: #168CA9;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.help-block a:hover {
|
||||
color: #168CA9;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
* Footer
|
||||
********************************************************/
|
||||
footer .row {
|
||||
margin: 36px 0 0 0;
|
||||
height: 40px;
|
||||
padding-top: 14px;
|
||||
line-height: 26px; /* to center text: height - padding-top = 26px */
|
||||
border-top: 1px solid #168CA9;
|
||||
}
|
||||
|
||||
footer .row > div { /* Fix alignment between border + text on Bootstrap grid */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
footer .logo-round-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
footer .logo-round {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -33px; /* found visually with Chrome Dev Tools */
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
border: 1px solid #00868c;
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
footer .logo-round > img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#dispatchTargets {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
* Social login
|
||||
********************************************************/
|
||||
.btn.line {
|
||||
background-color: transparent;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 1.5em 0 1em;
|
||||
border: 0.5px solid #ccc;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn.socialLogin {
|
||||
background-color: #fff;
|
||||
border: thin solid #ccc;
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
margin: 5px;
|
||||
min-width: 140px;
|
||||
width: 210px;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.socialLogin img {
|
||||
width: 1.5em;
|
||||
height: 108%;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.btn.apple img {
|
||||
width: 1.2em;
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
* Show password
|
||||
********************************************************/
|
||||
.icon-inside {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-inside input {
|
||||
padding-right: calc(0.75rem + 1.25rem + 0.75rem);
|
||||
}
|
||||
|
||||
.icon-inside button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin-top: 0.45rem;
|
||||
margin-right: 0.45rem;
|
||||
background: #FFFFFF;
|
||||
border: #FFFFFF;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
function displayRecoveryCodes() {
|
||||
const recoverCodes = document.getElementById("recovery-codes-raw");
|
||||
// early return if recoverCodes not found
|
||||
if (!recoverCodes) {
|
||||
return;
|
||||
}
|
||||
|
||||
var recoveryCodesContent = recoverCodes.innerHTML;
|
||||
recoveryCodesContent = recoveryCodesContent.replace("[", "");
|
||||
recoveryCodesContent = recoveryCodesContent.replace("]", "");
|
||||
recoveryCodesContent = recoveryCodesContent.split(",");
|
||||
for (let i = 0; i < recoveryCodesContent.length; i++) {
|
||||
if (i % 2 == 0) {
|
||||
document.getElementById("recovery-codes").innerHTML += "<div class=\"recovery-code-gray printable\">" + recoveryCodesContent[i] + "</div>";
|
||||
}
|
||||
else {
|
||||
document.getElementById("recovery-codes").innerHTML += "<div class=\"recovery-code-white printable\">" + recoveryCodesContent[i] + "</div>";
|
||||
}
|
||||
}
|
||||
recoverCodes.remove();
|
||||
}
|
||||
|
||||
displayRecoveryCodes();
|
|
@ -0,0 +1,26 @@
|
|||
function downloadRecoveryCodes(contentContainerId) {
|
||||
const textToDownload = document.getElementById(contentContainerId).innerText;
|
||||
// It is necessary to create a new blob object with mime-type explicitly set
|
||||
// otherwise only Chrome works like it should
|
||||
const newBlob = new Blob([textToDownload], { type: "text/plain" });
|
||||
|
||||
// IE doesn't allow using a blob object directly as link href
|
||||
// instead it is necessary to use msSaveOrOpenBlob
|
||||
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
|
||||
window.navigator.msSaveOrOpenBlob(newBlob, "recovery-codes.txt");
|
||||
return;
|
||||
}
|
||||
|
||||
// For other browsers:
|
||||
// Create a link pointing to the ObjectURL containing the blob.
|
||||
const data = window.URL.createObjectURL(newBlob);
|
||||
const link = document.createElement("a");
|
||||
link.href = data;
|
||||
link.download = "recovery-codes.txt";
|
||||
link.click();
|
||||
setTimeout(() => {
|
||||
// For Firefox it is necessary to delay revoking the ObjectURL
|
||||
window.URL.revokeObjectURL(data);
|
||||
}, 400);
|
||||
link.remove();
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue