Compare commits
4 Commits
r-36c19390
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
8863ec6dca | |
|
|
c02d2980f8 | |
|
|
b350c5a025 | |
|
|
69dbd2b4e0 |
|
|
@ -11,8 +11,8 @@ metadata:
|
|||
spec:
|
||||
type: "NevisAuth"
|
||||
replicas: 1
|
||||
version: "8.2505.5"
|
||||
gitInitVersion: "1.4.0"
|
||||
version: "8.2411.3"
|
||||
gitInitVersion: "1.3.0"
|
||||
runAsNonRoot: true
|
||||
ports:
|
||||
management: 9000
|
||||
|
|
@ -39,14 +39,13 @@ spec:
|
|||
management:
|
||||
httpGet:
|
||||
path: "/nevisauth/liveness"
|
||||
initialDelaySeconds: 50
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 6
|
||||
failureThreshold: 30
|
||||
failureThreshold: 50
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: "50%"
|
||||
git:
|
||||
tag: "r-d6878093aefa2bfb8cc241b61fff5fe94bc95282"
|
||||
tag: "r-ac938692d8edd6d7a3c23c703a8b0ad0b4510414"
|
||||
dir: "DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth-sts"
|
||||
credentials: "git-credentials"
|
||||
keystores:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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.
|
||||
|
|
@ -71,8 +70,6 @@ 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
|
||||
|
|
@ -80,5 +77,4 @@ 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,7 +3,6 @@ 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.
|
||||
|
|
@ -71,8 +70,6 @@ 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
|
||||
|
|
@ -80,5 +77,4 @@ 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,7 +3,6 @@ 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.
|
||||
|
|
@ -71,8 +70,6 @@ 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
|
||||
|
|
@ -80,5 +77,4 @@ 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,7 +3,6 @@ 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.
|
||||
|
|
@ -71,8 +70,6 @@ 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
|
||||
|
|
@ -80,5 +77,4 @@ 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,9 +1,8 @@
|
|||
|
||||
accept.button.label=Accetta
|
||||
cancel.button.label=Annulla
|
||||
accept.button.label=Accettare
|
||||
cancel.button.label=Abortire
|
||||
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.
|
||||
|
|
@ -70,9 +69,7 @@ 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=Rifiuta
|
||||
signup.button.label=Iscriviti
|
||||
skip.button.label=Salta
|
||||
reject.button.label=Rifiuti
|
||||
submit.button.label=Continua
|
||||
tan.sent=Inserisci il codice di sicurezza che è stato inviato al tuo telefono cellulare.
|
||||
title.logout=Logout
|
||||
|
|
@ -80,5 +77,4 @@ 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,9 +13,8 @@ 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.2505.5,service.instance.id=$HOSTNAME"
|
||||
"-Dotel.resource.attributes=service.version=8.2411.3,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,6 +431,4 @@
|
|||
<!-- 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,6 +16,12 @@ 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"
|
||||
|
|
@ -26,6 +32,8 @@ Configuration:
|
|||
level: "INFO"
|
||||
- name: "AuthPerf"
|
||||
level: "INFO"
|
||||
- name: "DIM-REG"
|
||||
level: "DEBUG"
|
||||
- name: "IdmAuth"
|
||||
level: "DEBUG"
|
||||
- name: "OpTrace"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
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')
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@ spec:
|
|||
namespace: "adn-agov-nevisidm-01-uat"
|
||||
- name: "proxy-idp-auth-realm-mobile-fido-uaf-identity"
|
||||
namespace: "adn-agov-nevisidm-01-uat"
|
||||
- name: "proxy-idp-auth-realm-dimilar-identity"
|
||||
namespace: "adn-agov-nevisidm-01-uat"
|
||||
- name: "proxy-idp-auth-realm-recovery-identity"
|
||||
namespace: "adn-agov-nevisidm-01-uat"
|
||||
extraCerts:
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ metadata:
|
|||
spec:
|
||||
type: "NevisAuth"
|
||||
replicas: 1
|
||||
version: "8.2505.5"
|
||||
gitInitVersion: "1.4.0"
|
||||
version: "8.2411.3"
|
||||
gitInitVersion: "1.3.0"
|
||||
runAsNonRoot: true
|
||||
ports:
|
||||
management: 9000
|
||||
|
|
@ -39,19 +39,15 @@ spec:
|
|||
management:
|
||||
httpGet:
|
||||
path: "/nevisauth/liveness"
|
||||
initialDelaySeconds: 50
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 6
|
||||
failureThreshold: 30
|
||||
failureThreshold: 50
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: "50%"
|
||||
git:
|
||||
tag: "r-36c19390c674892b1c236998382911bcbca6d5e3"
|
||||
tag: "r-ac938692d8edd6d7a3c23c703a8b0ad0b4510414"
|
||||
dir: "DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/auth"
|
||||
credentials: "git-credentials"
|
||||
database:
|
||||
name: "auth"
|
||||
requiredVersion: "8.2505.5"
|
||||
keystores:
|
||||
- "auth-sh4r3d-internal-idp-auth-signer"
|
||||
- "auth-auth-realm-mobile-fido-uaf-tls-client-nevisfido"
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
apiVersion: "operator.nevis-security.ch/v1"
|
||||
kind: "NevisDatabase"
|
||||
metadata:
|
||||
name: "auth"
|
||||
namespace: "adn-agov-nevisidm-01-uat"
|
||||
labels:
|
||||
deploymentTarget: "auth"
|
||||
annotations:
|
||||
projectKey: "DEFAULT-ADN-AGOV-PROJECT"
|
||||
patternId: "b7b59e97b3fd18bb60178573"
|
||||
spec:
|
||||
type: "NevisAuth"
|
||||
databaseType: "MariaDB"
|
||||
version: "8.2505.5"
|
||||
url: "session-db-primary-service.adn-agov-database-01-uat"
|
||||
port: 3306
|
||||
database: "nevisauth"
|
||||
bootstrap: true
|
||||
migrate: true
|
||||
rootCredentials:
|
||||
name: "root-mariadb-session-store"
|
||||
namespace: "adn-agov-nevisidm-ob-01-uat"
|
||||
podSecurity:
|
||||
policy: "baseline"
|
||||
automountServiceAccountToken: false
|
||||
timeZone: "Europe/Zurich"
|
||||
|
|
@ -10,11 +10,30 @@ 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.
|
||||
dimilar.confirm_identity.checkbox=I confirm this is my data
|
||||
dimilar.confirm_identity.description=Please confirm the data below is yours in order to proceed:
|
||||
dimilar.confirm_identity.error=Please confirm the data is yours to proceed.
|
||||
dimilar.confirm_identity.link=If this is not your data, please visit <a class='link' href='https://agov.ch/dimilar' target='_blank'>https://agov.ch/dimilar</a>.
|
||||
dimilar.confirm_identity.title=Confirm data
|
||||
dimilar.select_onboarding.description=Welcome to AGOV. Please complete your onboarding by connecting to an existing or new AGOV account.
|
||||
dimilar.select_onboarding.error-banner=Please select one option to continue
|
||||
dimilar.select_onboarding.existing-account=Onboard with an existing AGOV account
|
||||
dimilar.select_onboarding.proceeding=How would you like to proceed?
|
||||
dimilar.select_onboarding.registering-account=Onboard with a new AGOV account
|
||||
dimilar.select_onboarding.title=Hello !!!FIRSTNAME!!! !!!LASTNAME!!!,
|
||||
dimilar.token_error.support=For support please visit <a class='link' href='https://agov.ch/dimilar' target='_blank'>https://agov.ch/dimilar</a>.
|
||||
dimilar.token_error.token_expired=Token expired or already used.
|
||||
dimilar_onboarding.aborted.link=If you require support please visit <a class='link' href='https://agov.ch/dimilar' target='_blank'>https://agov.ch/dimilar</a>.
|
||||
dimilar_onboarding.aborted.message=Onboarding aborted. Please try again.
|
||||
dimilar_onboarding.failed.link=<a class='link' href='https://agov.ch/dimilar' target='_blank'>https://agov.ch/dimilar</a>.
|
||||
dimilar_onboarding.failed.message=Onboarding aborted. Please contact support at
|
||||
dimilar_onboarding.successful.message=Onboarding with AGOV account successful. You are now able to log in to Dimilar at <a class='link' href='https://www.armee.ch/dim' target='_blank'>https://www.armee.ch/dim</a>.
|
||||
dimilar_onboarding.title=Register
|
||||
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.
|
||||
|
|
@ -25,8 +44,16 @@ error_11=Please use another certficate or login with another credential type.
|
|||
error_2=Please select another login name.
|
||||
error_3=Your account will be locked if next authentication fails.
|
||||
error_4=Your new password does not comply with the security policy. Please choose a different password.
|
||||
error_403.description=You are not authorised to access this application.
|
||||
error_403.title=Not authorised
|
||||
error_404.description=The page you are looking for does not exist.
|
||||
error_404.title=Page not found
|
||||
error_5=Error in password confirmation.
|
||||
error_50=The new password is too short.
|
||||
error_500.description=There is currently an outage. We are working on it.
|
||||
error_500.title=Something went wrong.
|
||||
error_502.description=We are working on it. Please try again later.
|
||||
error_502.title=Something went wrong.
|
||||
error_55=The new password has to differ from old passwords.
|
||||
error_6=Password change required.
|
||||
error_7=Change of login ID required.
|
||||
|
|
@ -61,11 +88,17 @@ general.cancel=Cancel
|
|||
general.confirm=Confirm
|
||||
general.contactSupport=Contact Support
|
||||
general.continue=Continue
|
||||
general.data.birthDate=Date of birth
|
||||
general.data.birthDateFormat=DD.MM.YYYY
|
||||
general.data.enrollmentNumber=Enrolment number (SSN/AHV number)
|
||||
general.data.firstname=First name
|
||||
general.data.lastname=Last name
|
||||
general.edit=Edit
|
||||
general.email=Email
|
||||
general.email.address=Email address
|
||||
general.entryCode=Code entry
|
||||
general.fieldRequired=Field required
|
||||
general.generalAccessApp=Access app
|
||||
general.getStarted=Get started
|
||||
general.goAGOVHelp=Go to AGOV help
|
||||
general.goAccessApp=Login with AGOV access
|
||||
|
|
@ -98,7 +131,7 @@ general.skip.content=Skip to main content
|
|||
general.wrongPhoneNumber=Please enter a valid phone number
|
||||
generic.auth.error.message=There was a service interruption. We are working on it.
|
||||
generic.auth.error.next.steps=Please try again later. Please consult AGOV help if the problem persists.
|
||||
generic.auth.error.subtitle=Something went wrong
|
||||
generic.auth.error.subtitle=Something went wrong.
|
||||
generic.auth.error.title=Error
|
||||
info.login=Please enter your authentication information.
|
||||
info.logout.confirmation=Please confirm that you want to log out.
|
||||
|
|
@ -119,6 +152,8 @@ loainfo.later=Later
|
|||
loainfo.startNow=Do you want to start the process now?
|
||||
loainfo.startVerification=Start verification
|
||||
loainfo.title=Verify your data
|
||||
loggedout.description=You have been successfully logged out.
|
||||
loggedout.title=Logged out
|
||||
login.button.label=Login
|
||||
logout.label=Logout
|
||||
logout.text=You have successfully logged out.
|
||||
|
|
@ -147,6 +182,16 @@ method.recovery.label=Recovery Codes
|
|||
method.safeword.label=SafeWord
|
||||
method.securid.label=SecurID
|
||||
method.ticket.label=Ticket
|
||||
onboard_linking_account_auth.fido_instructions=A physical security key offers a secure way to onboard with your account without having to use a phone.
|
||||
onboard_linking_account_auth.instructions=Onboard with your AGOV account by scanning the QR code with your AGOV access app
|
||||
onboarding.cancel-onboarding=Are you sure you want to cancel the onboarding process?
|
||||
onboarding.cancel-onboarding-description=In order to proceed with an account recovery, you will have to cancel the onboarding process.
|
||||
onboarding.cancel-proceed-recovery=Yes, cancel and proceed to recovery
|
||||
onboarding.login-factor=Step 1 - Login factor
|
||||
onboarding.with-agov.title=Onboard with AGOV account
|
||||
onboarding_account.switchLinking=Switch to onboard with
|
||||
onboarding_account_auth.loginSecurityKey=Start onboarding with security key
|
||||
onboarding_account_auth.useSecurityKey=Use a security key to onboard with your AGOV account
|
||||
op-admin.login=AGOV op admin
|
||||
op-admin.login.intro.message=Login with your username and password
|
||||
op-admin.login.loginid=LoginId
|
||||
|
|
@ -284,7 +329,7 @@ recovery_questionnaire_no_recovery.instruction2=If you have several login factor
|
|||
recovery_questionnaire_reason_selection.answer1=I have trouble logging in, even though I have my app / security key
|
||||
recovery_questionnaire_reason_selection.answer10=I lost one of my login factors (AGOV access app or security key)
|
||||
recovery_questionnaire_reason_selection.answer2=I was unable to finish my registration
|
||||
recovery_questionnaire_reason_selection.answer3=I have deleted, reinstalled, or reset my AGOV access app
|
||||
recovery_questionnaire_reason_selection.answer3=I have deleted, reinstalled, or reset my AGOV access app, or it shows there are no accounts defined
|
||||
recovery_questionnaire_reason_selection.answer4=I have lost my phone / security key
|
||||
recovery_questionnaire_reason_selection.answer5=I have a new phone and forgot to transfer my AGOV access app
|
||||
recovery_questionnaire_reason_selection.answer6=I forgot my PIN for the AGOV access app
|
||||
|
|
@ -297,10 +342,10 @@ 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.
|
||||
timeout.description=Your session has timed out. Please close this window and try logging in again.
|
||||
timeout.title=Session expired
|
||||
title.login=Login
|
||||
title.logout=Logout
|
||||
title.logout.confirmation=Logout
|
||||
|
|
@ -309,7 +354,6 @@ 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,30 @@ 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.
|
||||
dimilar.confirm_identity.checkbox=Ich bestätige, dass dies meine Angaben sind
|
||||
dimilar.confirm_identity.description=Bitte bestätigen Sie, dass die folgenden Angaben Ihnen gehören, um fortzufahren:
|
||||
dimilar.confirm_identity.error=Bitte bestätigen Sie, dass die Angaben Ihnen gehören, um fortzufahren.
|
||||
dimilar.confirm_identity.link=Wenn diese nicht Ihre Angaben sind, besuchen Sie bitte <a class='link' href='https://agov.ch/dim' target='_blank'>https://agov.ch/dim</a>.
|
||||
dimilar.confirm_identity.title=Angaben bestätigen
|
||||
dimilar.select_onboarding.description=Willkommen bei AGOV. Bitte komplettieren Sie Ihr Onboarding, indem Sie ein bestehendes oder neues AGOV Konto verbinden.
|
||||
dimilar.select_onboarding.error-banner=Bitte wählen Sie eine Option aus, um fortzufahren
|
||||
dimilar.select_onboarding.existing-account=Onboarding mit einem existierenden AGOV-Konto
|
||||
dimilar.select_onboarding.proceeding=Wie möchten Sie fortfahren?
|
||||
dimilar.select_onboarding.registering-account=Onboarding mit einem neuen AGOV-Konto
|
||||
dimilar.select_onboarding.title=Hallo !!!FIRSTNAME!!! !!!LASTNAME!!!
|
||||
dimilar.token_error.support=Um Hilfe zu erhalten, besuchen Sie bitte <a class='link' href='https://agov.ch/dim' target='_blank'>agov.ch/dim</a>.
|
||||
dimilar.token_error.token_expired=Token abgelaufen oder bereits verwendet.
|
||||
dimilar_onboarding.aborted.link=Wenn Sie Hilfe benötigen, besuchen Sie bitte <a class='link' href='https://agov.ch/dim' target='_blank'>https://agov.ch/dim</a>.
|
||||
dimilar_onboarding.aborted.message=Onboarding abgebrochen. Bitte versuchen Sie es erneut.
|
||||
dimilar_onboarding.failed.link=<a class='link' href='https://agov.ch/dim' target='_blank'>agov.ch/dim</a>.
|
||||
dimilar_onboarding.failed.message=Onboarding abgebrochen. Bitte kontaktieren Sie den Support unter
|
||||
dimilar_onboarding.successful.message=Onboarding mit AGOV-Konto erfolgreich. Sie können sich nun bei Dimilar unter <a class='link' href='https://www.armee.ch/de/dim' target='_blank'>https://www.armee.ch/de/dim</a> einloggen.
|
||||
dimilar_onboarding.title=Registrieren
|
||||
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.
|
||||
|
|
@ -25,8 +44,16 @@ error_11=Bitte verwenden Sie ein anderes Zertifikat oder melden Sie sich mit ein
|
|||
error_2=Bitte wählen Sie einen anderen Login-Namen.
|
||||
error_3=Wenn die nächste Authentifizierung fehlschlägt, wird Ihr Konto gesperrt.
|
||||
error_4=Ihr neues Passwort verstösst gegen die Sicherheitsrichtlinien. Bitte wählen Sie ein anderes Passwort.
|
||||
error_403.description=Sie sind nicht berechtigt, auf diese Anwendung zuzugreifen.
|
||||
error_403.title=Nicht zugelassen
|
||||
error_404.description=Die von Ihnen gesuchte Seite existiert nicht.
|
||||
error_404.title=Seite nicht gefunden
|
||||
error_5=Fehler bei der Passwortbestätigung.
|
||||
error_50=Das neue Passwort ist zu kurz.
|
||||
error_500.description=Zurzeit liegt eine Störung vor. Wir arbeiten daran.
|
||||
error_500.title=Etwas ist schiefgegangen.
|
||||
error_502.description=Wir arbeiten daran. Bitte versuchen Sie es später noch einmal.
|
||||
error_502.title=Etwas ist schiefgegangen.
|
||||
error_55=Das neue Passwort muss sich von alten Passwörtern unterscheiden.
|
||||
error_6=Passwortänderung erforderlich.
|
||||
error_7=Änderung der Login-ID erforderlich.
|
||||
|
|
@ -61,11 +88,17 @@ general.cancel=Abbrechen
|
|||
general.confirm=Bestätigen
|
||||
general.contactSupport=Support kontaktieren
|
||||
general.continue=Weiter
|
||||
general.data.birthDate=Geburtsdatum
|
||||
general.data.birthDateFormat=TT.MM.JJJJ
|
||||
general.data.enrollmentNumber=AHV-Nummer (Dienstmanager)
|
||||
general.data.firstname=Vorname
|
||||
general.data.lastname=Nachname
|
||||
general.edit=Ändern
|
||||
general.email=E-Mail
|
||||
general.email.address=E-Mail-Adresse
|
||||
general.entryCode=Code-Eingabe
|
||||
general.fieldRequired=Erforderliches Feld
|
||||
general.generalAccessApp=Access App
|
||||
general.getStarted=Los geht's
|
||||
general.goAGOVHelp=Weiter zur AGOV help
|
||||
general.goAccessApp=Login mit AGOV access
|
||||
|
|
@ -98,7 +131,7 @@ general.skip.content=Direkt zum Hauptteil
|
|||
general.wrongPhoneNumber=Bitte geben Sie eine gültige Telefonnummer ein
|
||||
generic.auth.error.message=Es gab eine Service-Unterbrechung. Wir arbeiten daran.
|
||||
generic.auth.error.next.steps=Versuchen Sie es bitte später noch einmal. Bitte besuchen Sie die AGOV-Hilfe, wenn das Problem weiterhin besteht.
|
||||
generic.auth.error.subtitle=Etwas ist schiefgegangen
|
||||
generic.auth.error.subtitle=Etwas ist schiefgegangen.
|
||||
generic.auth.error.title=Fehler
|
||||
info.login=Bitte geben Sie Ihre persönlichen Zugangsdaten ein.
|
||||
info.logout.confirmation=Bitte bestätigen Sie, dass Sie sich abmelden möchten.
|
||||
|
|
@ -119,6 +152,8 @@ loainfo.later=Später
|
|||
loainfo.startNow=Möchten Sie den Prozess jetzt starten?
|
||||
loainfo.startVerification=Verifikation starten
|
||||
loainfo.title=Verifizieren Sie Ihre Daten
|
||||
loggedout.description=Sie haben sich erfolgreich ausgeloggt.
|
||||
loggedout.title=Ausgeloggt
|
||||
login.button.label=Login
|
||||
logout.label=Logout
|
||||
logout.text=Sie haben sich erfolgreich abgemeldet.
|
||||
|
|
@ -147,6 +182,16 @@ method.recovery.label=Wiederherstellungscodes
|
|||
method.safeword.label=SafeWord
|
||||
method.securid.label=SecurID
|
||||
method.ticket.label=Ticket
|
||||
onboard_linking_account_auth.fido_instructions=Ein physischer Sicherheitsschlüssel bietet eine sichere Möglichkeit, das Onboarding mit Ihrem Konto ohne Telefon durchzuführen.
|
||||
onboard_linking_account_auth.instructions=Führen Sie das Onboarding mit Ihrem AGOV-Konto durch, indem Sie den QR-Code mit Ihrer AGOV access App scannen
|
||||
onboarding.cancel-onboarding=Sind Sie sicher, dass Sie den Onboarding-Prozess abbrechen möchten?
|
||||
onboarding.cancel-onboarding-description=Um mit der Kontowiederherstellung fortzufahren, müssen Sie den Onboarding-Prozess abbrechen.
|
||||
onboarding.cancel-proceed-recovery=Ja, abbrechen und mit der Wiederherstellung fortfahren
|
||||
onboarding.login-factor=Schritt 1 – Login-Faktor
|
||||
onboarding.with-agov.title=Onboarding mit AGOV-Konto
|
||||
onboarding_account.switchLinking=Wechseln zum Onboarding mit
|
||||
onboarding_account_auth.loginSecurityKey=Onboarding mit Sicherheitsschlüssel starten
|
||||
onboarding_account_auth.useSecurityKey=Benutzen Sie einen Sicherheitsschlüssel, um das Onboarding mit Ihrem AGOV-Konto durchzuführen
|
||||
op-admin.login=AGOV-op-Admin
|
||||
op-admin.login.intro.message=Login mit Ihrem Benutzernamen und Passwort
|
||||
op-admin.login.loginid=LoginID
|
||||
|
|
@ -284,7 +329,7 @@ recovery_questionnaire_no_recovery.instruction2=Wenn Sie mehrere Loginfaktoren r
|
|||
recovery_questionnaire_reason_selection.answer1=Ich habe Probleme mich anzumelden, obwohl ich meine App / meinen Sicherheitsschlüssel habe
|
||||
recovery_questionnaire_reason_selection.answer10=Ich habe einen meiner Loginfaktoren verloren (AGOV access App oder Sicherheitsschlüssel)
|
||||
recovery_questionnaire_reason_selection.answer2=Ich konnte meine Registrierung nicht abschliessen
|
||||
recovery_questionnaire_reason_selection.answer3=Ich habe meine AGOV access App gelöscht, neu installiert oder zurückgesetzt
|
||||
recovery_questionnaire_reason_selection.answer3=Ich habe meine AGOV access App gelöscht, neu installiert oder zurückgesetzt, oder es wird angezeigt, dass keine Konten definiert sind
|
||||
recovery_questionnaire_reason_selection.answer4=Ich habe mein Telefon / Sicherheitsschlüssel verloren
|
||||
recovery_questionnaire_reason_selection.answer5=Ich habe ein neues Telefon und habe vergessen, meine AGOV access App zu übertragen
|
||||
recovery_questionnaire_reason_selection.answer6=Ich habe die PIN für meine AGOV access App vergessen
|
||||
|
|
@ -297,10 +342,10 @@ 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.
|
||||
timeout.description=Ihre Sitzung ist abgelaufen. Bitte schliessen Sie dieses Fenster und versuchen Sie erneut, sich einzuloggen.
|
||||
timeout.title=Sitzung abgelaufen
|
||||
title.login=Login
|
||||
title.logout=Logout
|
||||
title.logout.confirmation=Logout
|
||||
|
|
@ -309,7 +354,6 @@ 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,30 @@ 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.
|
||||
dimilar.confirm_identity.checkbox=I confirm this is my data
|
||||
dimilar.confirm_identity.description=Please confirm the data below is yours in order to proceed:
|
||||
dimilar.confirm_identity.error=Please confirm the data is yours to proceed.
|
||||
dimilar.confirm_identity.link=If this is not your data, please visit <a class='link' href='https://agov.ch/dimilar' target='_blank'>https://agov.ch/dimilar</a>.
|
||||
dimilar.confirm_identity.title=Confirm data
|
||||
dimilar.select_onboarding.description=Welcome to AGOV. Please complete your onboarding by connecting to an existing or new AGOV account.
|
||||
dimilar.select_onboarding.error-banner=Please select one option to continue
|
||||
dimilar.select_onboarding.existing-account=Onboard with an existing AGOV account
|
||||
dimilar.select_onboarding.proceeding=How would you like to proceed?
|
||||
dimilar.select_onboarding.registering-account=Onboard with a new AGOV account
|
||||
dimilar.select_onboarding.title=Hello !!!FIRSTNAME!!! !!!LASTNAME!!!,
|
||||
dimilar.token_error.support=For support please visit <a class='link' href='https://agov.ch/dimilar' target='_blank'>https://agov.ch/dimilar</a>.
|
||||
dimilar.token_error.token_expired=Token expired or already used.
|
||||
dimilar_onboarding.aborted.link=If you require support please visit <a class='link' href='https://agov.ch/dimilar' target='_blank'>https://agov.ch/dimilar</a>.
|
||||
dimilar_onboarding.aborted.message=Onboarding aborted. Please try again.
|
||||
dimilar_onboarding.failed.link=<a class='link' href='https://agov.ch/dimilar' target='_blank'>https://agov.ch/dimilar</a>.
|
||||
dimilar_onboarding.failed.message=Onboarding aborted. Please contact support at
|
||||
dimilar_onboarding.successful.message=Onboarding with AGOV account successful. You are now able to log in to Dimilar at <a class='link' href='https://www.armee.ch/dim' target='_blank'>https://www.armee.ch/dim</a>.
|
||||
dimilar_onboarding.title=Register
|
||||
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.
|
||||
|
|
@ -25,8 +44,16 @@ error_11=Please use another certficate or login with another credential type.
|
|||
error_2=Please select another login name.
|
||||
error_3=Your account will be locked if next authentication fails.
|
||||
error_4=Your new password does not comply with the security policy. Please choose a different password.
|
||||
error_403.description=You are not authorised to access this application.
|
||||
error_403.title=Not authorised
|
||||
error_404.description=The page you are looking for does not exist.
|
||||
error_404.title=Page not found
|
||||
error_5=Error in password confirmation.
|
||||
error_50=The new password is too short.
|
||||
error_500.description=There is currently an outage. We are working on it.
|
||||
error_500.title=Something went wrong.
|
||||
error_502.description=We are working on it. Please try again later.
|
||||
error_502.title=Something went wrong.
|
||||
error_55=The new password has to differ from old passwords.
|
||||
error_6=Password change required.
|
||||
error_7=Change of login ID required.
|
||||
|
|
@ -61,11 +88,17 @@ general.cancel=Cancel
|
|||
general.confirm=Confirm
|
||||
general.contactSupport=Contact Support
|
||||
general.continue=Continue
|
||||
general.data.birthDate=Date of birth
|
||||
general.data.birthDateFormat=DD.MM.YYYY
|
||||
general.data.enrollmentNumber=Enrolment number (SSN/AHV number)
|
||||
general.data.firstname=First name
|
||||
general.data.lastname=Last name
|
||||
general.edit=Edit
|
||||
general.email=Email
|
||||
general.email.address=Email address
|
||||
general.entryCode=Code entry
|
||||
general.fieldRequired=Field required
|
||||
general.generalAccessApp=Access app
|
||||
general.getStarted=Get started
|
||||
general.goAGOVHelp=Go to AGOV help
|
||||
general.goAccessApp=Login with AGOV access
|
||||
|
|
@ -98,7 +131,7 @@ general.skip.content=Skip to main content
|
|||
general.wrongPhoneNumber=Please enter a valid phone number
|
||||
generic.auth.error.message=There was a service interruption. We are working on it.
|
||||
generic.auth.error.next.steps=Please try again later. Please consult AGOV help if the problem persists.
|
||||
generic.auth.error.subtitle=Something went wrong
|
||||
generic.auth.error.subtitle=Something went wrong.
|
||||
generic.auth.error.title=Error
|
||||
info.login=Please enter your authentication information.
|
||||
info.logout.confirmation=Please confirm that you want to log out.
|
||||
|
|
@ -119,6 +152,8 @@ loainfo.later=Later
|
|||
loainfo.startNow=Do you want to start the process now?
|
||||
loainfo.startVerification=Start verification
|
||||
loainfo.title=Verify your data
|
||||
loggedout.description=You have been successfully logged out.
|
||||
loggedout.title=Logged out
|
||||
login.button.label=Login
|
||||
logout.label=Logout
|
||||
logout.text=You have successfully logged out.
|
||||
|
|
@ -147,6 +182,16 @@ method.recovery.label=Recovery Codes
|
|||
method.safeword.label=SafeWord
|
||||
method.securid.label=SecurID
|
||||
method.ticket.label=Ticket
|
||||
onboard_linking_account_auth.fido_instructions=A physical security key offers a secure way to onboard with your account without having to use a phone.
|
||||
onboard_linking_account_auth.instructions=Onboard with your AGOV account by scanning the QR code with your AGOV access app
|
||||
onboarding.cancel-onboarding=Are you sure you want to cancel the onboarding process?
|
||||
onboarding.cancel-onboarding-description=In order to proceed with an account recovery, you will have to cancel the onboarding process.
|
||||
onboarding.cancel-proceed-recovery=Yes, cancel and proceed to recovery
|
||||
onboarding.login-factor=Step 1 - Login factor
|
||||
onboarding.with-agov.title=Onboard with AGOV account
|
||||
onboarding_account.switchLinking=Switch to onboard with
|
||||
onboarding_account_auth.loginSecurityKey=Start onboarding with security key
|
||||
onboarding_account_auth.useSecurityKey=Use a security key to onboard with your AGOV account
|
||||
op-admin.login=AGOV op admin
|
||||
op-admin.login.intro.message=Login with your username and password
|
||||
op-admin.login.loginid=LoginId
|
||||
|
|
@ -284,7 +329,7 @@ recovery_questionnaire_no_recovery.instruction2=If you have several login factor
|
|||
recovery_questionnaire_reason_selection.answer1=I have trouble logging in, even though I have my app / security key
|
||||
recovery_questionnaire_reason_selection.answer10=I lost one of my login factors (AGOV access app or security key)
|
||||
recovery_questionnaire_reason_selection.answer2=I was unable to finish my registration
|
||||
recovery_questionnaire_reason_selection.answer3=I have deleted, reinstalled, or reset my AGOV access app
|
||||
recovery_questionnaire_reason_selection.answer3=I have deleted, reinstalled, or reset my AGOV access app, or it shows there are no accounts defined
|
||||
recovery_questionnaire_reason_selection.answer4=I have lost my phone / security key
|
||||
recovery_questionnaire_reason_selection.answer5=I have a new phone and forgot to transfer my AGOV access app
|
||||
recovery_questionnaire_reason_selection.answer6=I forgot my PIN for the AGOV access app
|
||||
|
|
@ -297,10 +342,10 @@ 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.
|
||||
timeout.description=Your session has timed out. Please close this window and try logging in again.
|
||||
timeout.title=Session expired
|
||||
title.login=Login
|
||||
title.logout=Logout
|
||||
title.logout.confirmation=Logout
|
||||
|
|
@ -309,7 +354,6 @@ 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,30 @@ 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.
|
||||
dimilar.confirm_identity.checkbox=Je confirme que ce sont mes données
|
||||
dimilar.confirm_identity.description=Veuillez confirmer que les données ci-dessous vous appartiennent afin de poursuivre :
|
||||
dimilar.confirm_identity.error=Veuillez confirmer que les données vous appartiennent afin de poursuivre.
|
||||
dimilar.confirm_identity.link=Si ces données ne sont pas les vôtres, veuillez vous rendre sur <a class='link' href='https://agov.ch/fr/dim' target='_blank'>https://agov.ch/fr/dim</a>.
|
||||
dimilar.confirm_identity.title=Confirmer les données
|
||||
dimilar.select_onboarding.description=Bienvenue sur AGOV. Veuillez terminer votre intégration en vous connectant à un compte AGOV existant ou en créant un nouveau compte.
|
||||
dimilar.select_onboarding.error-banner=Veuillez sélectionner une option pour continuer
|
||||
dimilar.select_onboarding.existing-account=Se connecter avec un compte AGOV existant
|
||||
dimilar.select_onboarding.proceeding=Comment voulez-vous procéder ?
|
||||
dimilar.select_onboarding.registering-account=Se connecter avec un nouveau compte AGOV
|
||||
dimilar.select_onboarding.title=Bonjour !!!FIRSTNAME!!! !!!LASTNAME!!!,
|
||||
dimilar.token_error.support=Si vous avez besoin d'aide veuillez vous rendre sur <a class='link' href='https://agov.ch/fr/dimf' target='_blank'>https://agov.ch/fr/dimf</a>.
|
||||
dimilar.token_error.token_expired=Jeton expiré ou déjà utilisé.
|
||||
dimilar_onboarding.aborted.link=Si vous avez besoin d'aide veuillez vous rendre sur <a class='link' href='https://agov.ch/fr/dimf' target='_blank'>https://agov.ch/fr/dimf</a>.
|
||||
dimilar_onboarding.aborted.message=Le processus d’intégration a été annulé. Veuillez réessayer.
|
||||
dimilar_onboarding.failed.link=<a class='link' href='https://agov.ch/fr/dimf' target='_blank'>https://agov.ch/fr/dimf</a>.
|
||||
dimilar_onboarding.failed.message=Le processus d'intégration a été annulé. Veuillez contacter le service de support à
|
||||
dimilar_onboarding.successful.message=L’intégration avec le compte AGOV a réussi. Vous pouvez maintenant vous connecter sur le gestionnaire de service <a class='link' href='https://www.armee.ch/fr/dimf' target='_blank'>https://www.armee.ch/fr/dimf</a>.
|
||||
dimilar_onboarding.title=Créer un compte
|
||||
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.
|
||||
|
|
@ -25,8 +44,16 @@ error_11=Veuillez utiliser un autre certificat ou vous connecter au moyen d&rsqu
|
|||
error_2=Veuillez sélectionner un autre nom d’utilisateur.
|
||||
error_3=Votre compte sera bloqué si la prochaine tentative d’authentification échoue.
|
||||
error_4=Votre nouveau mot de passe n’est pas conforme à la politique de sécurité. Veuillez choisir un autre mot de passe.
|
||||
error_403.description=Vous n’êtes pas autorisé à accéder à cette ressource.
|
||||
error_403.title=Pas autorisé
|
||||
error_404.description=La page que vous recherchez n'existe pas.
|
||||
error_404.title=Page introuvable
|
||||
error_5=Erreur de confirmation du mot de passe
|
||||
error_50=Le nouveau mot de passe est trop court.
|
||||
error_500.description=Un incident est survenu. Nous mettons tout en œuvre pour le résoudre.
|
||||
error_500.title=Un problème s’est produit.
|
||||
error_502.description=Nous y travaillons. Veuillez réessayer plus tard.
|
||||
error_502.title=Un problème s’est produit.
|
||||
error_55=Le nouveau mot de passe doit être différent des précédents.
|
||||
error_6=Changement de mot de passe requis.
|
||||
error_7=Changement d’identifiant de connexion requis.
|
||||
|
|
@ -61,11 +88,17 @@ general.cancel=Annuler
|
|||
general.confirm=Confirmer
|
||||
general.contactSupport=Contacter le service d'assistance
|
||||
general.continue=Continuer
|
||||
general.data.birthDate=Date de naissance
|
||||
general.data.birthDateFormat=JJ.MM.AAAA
|
||||
general.data.enrollmentNumber=Numéro AVS (Gestionnaire de service)
|
||||
general.data.firstname=Prénom
|
||||
general.data.lastname=Nom
|
||||
general.edit=Editer
|
||||
general.email=E-mail
|
||||
general.email.address=Adresse e-mail
|
||||
general.entryCode=Entrer le code
|
||||
general.fieldRequired=Champ requis
|
||||
general.generalAccessApp=Access app
|
||||
general.getStarted=Démarrer
|
||||
general.goAGOVHelp=Rendez-vous sur AGOV help
|
||||
general.goAccessApp=Login avec AGOV access
|
||||
|
|
@ -98,7 +131,7 @@ general.skip.content=Passer au contenu principal
|
|||
general.wrongPhoneNumber=Veuillez saisir un numéro de téléphone valable
|
||||
generic.auth.error.message=Une interruption de service s’est produite. Nous nous employons à résoudre le problème.
|
||||
generic.auth.error.next.steps=Veuillez réessayer plus tard. Veuillez vous rendre sur AGOV help si le problème persiste.
|
||||
generic.auth.error.subtitle=Un problème s’est produit
|
||||
generic.auth.error.subtitle=Un problème s’est produit.
|
||||
generic.auth.error.title=Erreur
|
||||
info.login=Veuillez entrer vos éléments de sécurité ci-après.
|
||||
info.logout.confirmation=Veuillez confirmer que vous souhaitez vous déconnecter.
|
||||
|
|
@ -119,6 +152,8 @@ loainfo.later=Plus tard
|
|||
loainfo.startNow=Voulez-vous commencer le processus maintenant?
|
||||
loainfo.startVerification=Démarrer la vérification
|
||||
loainfo.title=Vérifiez vos données
|
||||
loggedout.description=Vous vous êtes déconnecté avec succès.
|
||||
loggedout.title=Déconnecté
|
||||
login.button.label=Login
|
||||
logout.label=Logout
|
||||
logout.text=Au revoir
|
||||
|
|
@ -147,6 +182,16 @@ method.recovery.label=Codes de récupération
|
|||
method.safeword.label=SafeWord
|
||||
method.securid.label=SecurID
|
||||
method.ticket.label=Ticket
|
||||
onboard_linking_account_auth.fido_instructions=Une clé de sécurité physique offre un moyen sûr de se connecter à son compte sans devoir utiliser son téléphone.
|
||||
onboard_linking_account_auth.instructions=Connectez-vous avec votre compte AGOV en scannant le code QR avec votre application AGOV access
|
||||
onboarding.cancel-onboarding=Êtes-vous sûr de vouloir annuler la procédure d'intégration ?
|
||||
onboarding.cancel-onboarding-description=Pour procéder à la récupération de votre compte, vous devrez annuler le processus d’intégration.
|
||||
onboarding.cancel-proceed-recovery=Oui, annuler et procéder à la récupération
|
||||
onboarding.login-factor=Étape 1 - Facteur de connexion
|
||||
onboarding.with-agov.title=Se connecter avec un compte AGOV
|
||||
onboarding_account.switchLinking=Passer à l’intégration avec
|
||||
onboarding_account_auth.loginSecurityKey=Commencez l'intégration avec une clé de sécurité
|
||||
onboarding_account_auth.useSecurityKey=Utilisez une clé de sécurité pour se connecter avec votre compte AGOV
|
||||
op-admin.login=Administration de l’accès à AGOV op
|
||||
op-admin.login.intro.message=Connectez-vous avec votre nom d’utilisateur et votre mot de passe
|
||||
op-admin.login.loginid=Identifiant de connexion
|
||||
|
|
@ -284,7 +329,7 @@ recovery_questionnaire_no_recovery.instruction2=Si vous avez enregistré p
|
|||
recovery_questionnaire_reason_selection.answer1=Je n'arrive pas à me connecter, même si j'ai mon application / ma clé de sécurité
|
||||
recovery_questionnaire_reason_selection.answer10=J'ai perdu l'un de mes facteurs d'authentification (application AGOV access ou clé de sécurité)
|
||||
recovery_questionnaire_reason_selection.answer2=Je n'ai pas pu terminer mon inscription
|
||||
recovery_questionnaire_reason_selection.answer3=J'ai supprimé, réinstallé ou réinitialisé mon application AGOV access
|
||||
recovery_questionnaire_reason_selection.answer3=J'ai supprimé, réinstallé, ou réinitialisé mon application AGOV access, ou cela indique qu'aucun compte n'est défini
|
||||
recovery_questionnaire_reason_selection.answer4=J'ai perdu mon téléphone / clé de sécurité
|
||||
recovery_questionnaire_reason_selection.answer5=J'ai un nouveau téléphone et j'ai oublié de transférer mon application AGOV access
|
||||
recovery_questionnaire_reason_selection.answer6=J'ai oublié mon PIN pour l'application AGOV access
|
||||
|
|
@ -297,10 +342,10 @@ 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.
|
||||
timeout.description=Votre session a expiré. Veuillez fermer cette fenêtre et essayer de vous reconnecter.
|
||||
timeout.title=Session expirée
|
||||
title.login=Login
|
||||
title.logout=Logout
|
||||
title.logout.confirmation=Logout
|
||||
|
|
@ -309,7 +354,6 @@ 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=Accetta
|
||||
accept.button.label=Accettare
|
||||
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,30 @@ 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
|
||||
cancel.button.label=Annulla
|
||||
button.submit=Continua
|
||||
cancel.button.label=Abortire
|
||||
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.
|
||||
dimilar.confirm_identity.checkbox=Confermo che questi sono i miei dati
|
||||
dimilar.confirm_identity.description=Confermi che i dati riportati di seguito le appartengono per poter procedere:
|
||||
dimilar.confirm_identity.error=Confermi che i dati sono i suoi per poter procedere.
|
||||
dimilar.confirm_identity.link=Se questi non sono i suoi dati, visiti <a class='link' href='https://agov.ch/dim' target='_blank'>https://agov.ch/dim</a>.
|
||||
dimilar.confirm_identity.title=Confermare i dati
|
||||
dimilar.select_onboarding.description=Benvenuto in AGOV. Completi la procedura di registrazione collegando un account AGOV esistente o creandone uno nuovo.
|
||||
dimilar.select_onboarding.error-banner=Selezioni un’opzione per continuare
|
||||
dimilar.select_onboarding.existing-account=Proceda con un account AGOV esistente
|
||||
dimilar.select_onboarding.proceeding=Come desidera procedere?
|
||||
dimilar.select_onboarding.registering-account=Proceda con un nuovo account AGOV
|
||||
dimilar.select_onboarding.title=Buongiorno !!!FIRSTNAME!!! !!!LASTNAME!!!,
|
||||
dimilar.token_error.support=Per assistenza visita <a class='link' href='https://agov.ch/dim' target='_blank'>https://agov.ch/dim</a>.
|
||||
dimilar.token_error.token_expired=Token scaduto o già utilizzato.
|
||||
dimilar_onboarding.aborted.link=Se ha bisogno di assistenza, visiti <a class='link' href='https://agov.ch/dim' target='_blank'>https://agov.ch/dim</a>.
|
||||
dimilar_onboarding.aborted.message=La procedura di registrazione è stata interrotta. Provi di nuovo.
|
||||
dimilar_onboarding.failed.link=<a class='link' href='https://agov.ch/dim' target='_blank'>https://agov.ch/dim</a>.
|
||||
dimilar_onboarding.failed.message=La procedura di registrazione è stata interrotta. Contatti il supporto al
|
||||
dimilar_onboarding.successful.message=Registrazione con l’account AGOV completata con successo. Ora può accedere alla Gestione dei servizi su <a class='link' href='https://www.armee.ch/dim' target='_blank'>https://www.armee.ch/dim</a>.
|
||||
dimilar_onboarding.title=Registrarsi
|
||||
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.
|
||||
|
|
@ -25,8 +44,16 @@ error_11=Utilizzare un altro certificato o accedere con altre credenziali.
|
|||
error_2=Selezionare un altro nome di accesso.
|
||||
error_3=Se la prossima autenticazione fallisce, l’account sarà bloccato.
|
||||
error_4=La nuova password non rispetta le norme di sicurezza. Scegliere un’altra password.
|
||||
error_403.description=Accesso non autorizzato a questa risorsa.
|
||||
error_403.title=Non è autorizatto
|
||||
error_404.description=La pagina che state cercando non esiste.
|
||||
error_404.title=Pagina non trovata
|
||||
error_5=Errore nella conferma della password.
|
||||
error_50=La nuova password è troppo corta.
|
||||
error_500.description=Al momento si è verificato un disservizio. Stiamo intervenendo.
|
||||
error_500.title=Qualcosa non ha funzionato.
|
||||
error_502.description=Stiamo intervenendo. Riprovi più tardi.
|
||||
error_502.title=Qualcosa non ha funzionato.
|
||||
error_55=La nuova password deve differire da quelle precedenti.
|
||||
error_6=È richiesta la modifica della password.
|
||||
error_7=È richiesta la modifica dell’ID di accesso.
|
||||
|
|
@ -61,11 +88,17 @@ general.cancel=Annullare
|
|||
general.confirm=Confermare
|
||||
general.contactSupport=Contattare il supporto
|
||||
general.continue=Continuare
|
||||
general.data.birthDate=Data di nascita
|
||||
general.data.birthDateFormat=GG.MM.AAAA
|
||||
general.data.enrollmentNumber=Numero AVS (Gestione dei servizi)
|
||||
general.data.firstname=Nome
|
||||
general.data.lastname=Cognome
|
||||
general.edit=Modificare
|
||||
general.email=e-mail
|
||||
general.email.address=Indirizzo e-mail
|
||||
general.entryCode=Codice
|
||||
general.fieldRequired=Campo obbligatorio
|
||||
general.generalAccessApp=App AGOV access
|
||||
general.getStarted=Iniziare
|
||||
general.goAGOVHelp=Vai ad AGOV help
|
||||
general.goAccessApp=Login con AGOV access
|
||||
|
|
@ -119,6 +152,8 @@ loainfo.later=Più tardi
|
|||
loainfo.startNow=Vuole iniziare il processo ora?
|
||||
loainfo.startVerification=Inizi la verificazione
|
||||
loainfo.title=Verificare i dati.
|
||||
loggedout.description=Disconnessione effettuata con successo.
|
||||
loggedout.title=Disconnessione eseguita
|
||||
login.button.label=Login
|
||||
logout.label=Logout
|
||||
logout.text=È uscito con successo.
|
||||
|
|
@ -147,6 +182,16 @@ method.recovery.label=Codici di ripristino
|
|||
method.safeword.label=SafeWord
|
||||
method.securid.label=SecurID
|
||||
method.ticket.label=Ticket
|
||||
onboard_linking_account_auth.fido_instructions=Una chiave di sicurezza fisica permette di accedere in modo sicuro senza utilizzare un telefono.
|
||||
onboard_linking_account_auth.instructions=Proceda con il suo account AGOV scansionando il codice QR con l’app AGOV access
|
||||
onboarding.cancel-onboarding=Sei sicuro di voler annullare la registrazione?
|
||||
onboarding.cancel-onboarding-description=Per procedere con il recupero dell’account, è necessario annullare la registrazione.
|
||||
onboarding.cancel-proceed-recovery=Sì, annulla e procedi con il recupero
|
||||
onboarding.login-factor=Passaggio 1 – Fattore di login
|
||||
onboarding.with-agov.title=Proceda con l’account AGOV
|
||||
onboarding_account.switchLinking=Passa alla registrazione con
|
||||
onboarding_account_auth.loginSecurityKey=Inizia la registrazione con la chiave di sicurezza
|
||||
onboarding_account_auth.useSecurityKey=Utilizzi una chiave di sicurezza per procedere con il suo account AGOV
|
||||
op-admin.login=AGOV op admin
|
||||
op-admin.login.intro.message=Accedere con nome utente e password
|
||||
op-admin.login.loginid=ID di accesso
|
||||
|
|
@ -284,7 +329,7 @@ recovery_questionnaire_no_recovery.instruction2=Se ha registrato più fatt
|
|||
recovery_questionnaire_reason_selection.answer1=Ho problemi ad accedere, anche se ho la mia app/chiave di sicurezza
|
||||
recovery_questionnaire_reason_selection.answer10=Ho perso uno dei miei fattori di accesso (app AGOV access o chiave di sicurezza)
|
||||
recovery_questionnaire_reason_selection.answer2=Non sono riuscito a completare la registrazione
|
||||
recovery_questionnaire_reason_selection.answer3=Ho eliminato, reinstallato o reimpostato la mia app AGOV access
|
||||
recovery_questionnaire_reason_selection.answer3=Ho eliminato, reinstallato o reimpostato l’app AGOV access, oppure risulta che non ci sono account definiti
|
||||
recovery_questionnaire_reason_selection.answer4=Ho perso il telefono/la chiave di sicurezza
|
||||
recovery_questionnaire_reason_selection.answer5=Ho un nuovo telefono e ho dimenticato di trasferire la mia app AGOV access
|
||||
recovery_questionnaire_reason_selection.answer6=Ho dimenticato il PIN dell'app AGOV access
|
||||
|
|
@ -296,11 +341,11 @@ 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=Rifiuta
|
||||
signup.button.label=Iscriviti
|
||||
skip.button.label=Salta
|
||||
reject.button.label=Rifiuti
|
||||
submit.button.label=Continua
|
||||
tan.sent=Inserisci il codice di sicurezza che è stato inviato al tuo telefono cellulare.
|
||||
timeout.description=La sessione è scaduta. Chiuda questa finestra e provi ad accedere nuovamente.
|
||||
timeout.title=Sessione scaduta
|
||||
title.login=Login
|
||||
title.logout=Logout
|
||||
title.logout.confirmation=Logout
|
||||
|
|
@ -309,7 +354,6 @@ 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,4 +50,3 @@ 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,287 @@
|
|||
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 'AutoIdent':
|
||||
case 'AutoIdentSelfPaid':
|
||||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
import groovy.json.JsonBuilder
|
||||
import groovy.json.JsonSlurper
|
||||
import java.util.UUID
|
||||
|
||||
if (inargs.containsKey('cancel_fido2')) {
|
||||
response.setResult('cancel')
|
||||
LOG.debug("Fido2Auth: authentication cancelled by user")
|
||||
return
|
||||
}
|
||||
|
||||
def base64url(uuid) {
|
||||
def msb = uuid.getMostSignificantBits()
|
||||
def lsb = uuid.getLeastSignificantBits()
|
||||
return new byte[] {
|
||||
(byte) msb,
|
||||
(byte) (msb >> 8),
|
||||
(byte) (msb >> 16),
|
||||
(byte) (msb >> 24),
|
||||
(byte) (msb >> 32),
|
||||
(byte) (msb >> 40),
|
||||
(byte) (msb >> 48),
|
||||
(byte) (msb >> 56),
|
||||
(byte) lsb,
|
||||
(byte) (lsb >> 8),
|
||||
(byte) (lsb >> 16),
|
||||
(byte) (lsb >> 24),
|
||||
(byte) (lsb >> 32),
|
||||
(byte) (lsb >> 40),
|
||||
(byte) (lsb >> 48),
|
||||
(byte) (lsb >> 56)
|
||||
}.encodeBase64Url().toString()
|
||||
}
|
||||
|
||||
def showGui() {
|
||||
response.setGuiName('dimilar_onboarding_fido_auth') // name is the trigger for including the JS
|
||||
response.setGuiLabel('title.login.fido2')
|
||||
response.addInfoGuiField('info', 'info.login.fido2', null)
|
||||
response.addHiddenGuiField('authRequestId', 'not used', session['ch.nevis.auth.saml.request.id'])
|
||||
response.addTextGuiField('email', 'email', session['ch.nevis.idm.User.email'])
|
||||
if (notes.containsKey('lasterrorinfo') || notes.containsKey('lasterror')) {
|
||||
response.addErrorGuiField('lasterror', notes['lasterrorinfo'], notes['lasterror'])
|
||||
}
|
||||
if (parameters.containsKey('cancel')) {
|
||||
response.addButtonGuiField('cancel_fido2', 'cancel.login.fido2.button.label', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
def getPath() {
|
||||
if (inargs.containsKey('path')) { // form POST
|
||||
return inargs['path']
|
||||
}
|
||||
if (inargs.containsKey('o.path.v')) { // AJAX POST
|
||||
return inargs['o.path.v']
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
def post(connection, json) {
|
||||
connection.setRequestMethod("POST")
|
||||
connection.setRequestProperty("Content-Type", "application/json")
|
||||
connection.setDoOutput(true) // required to write body
|
||||
String body = json.toString()
|
||||
LOG.debug("Fido2Auth: ==> Request: '${body}'")
|
||||
connection.getOutputStream().write(body.getBytes())
|
||||
}
|
||||
|
||||
String userExtId = session['ch.adnovum.nevisidm.user.extId'] ?: session['ch.nevis.idm.User.extId'] ?: request.getUserId() ?: notes['userid']
|
||||
if (userExtId == null) {
|
||||
LOG.error("Fido2Auth: missing extId of nevisIDM user. check your authentication flow.")
|
||||
}
|
||||
// without the user extId this script won't work and we can fail with a System Error
|
||||
Objects.requireNonNull(userExtId)
|
||||
|
||||
def path = getPath()
|
||||
if (path == null) {
|
||||
showGui() // POST from JavaScript not received
|
||||
return
|
||||
}
|
||||
|
||||
def connection = null
|
||||
try {
|
||||
def fullPath = "https://${parameters.get('fido')}${path}"
|
||||
LOG.debug("Fido2Auth: opening connection to '${fullPath}'")
|
||||
connection = new URL(fullPath).openConnection()
|
||||
} catch (Exception e) {
|
||||
LOG.error("Fido2Auth: opening connection failed", e)
|
||||
notes.setProperty('lasterrorinfo', 'FIDO2 authentication failed')
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
def json = new JsonBuilder()
|
||||
|
||||
if (path == '/nevisfido/fido2/attestation/options') {
|
||||
json {
|
||||
"username" userExtId
|
||||
"userVerification" "required"
|
||||
}
|
||||
post(connection, json)
|
||||
def responseCode = connection.responseCode
|
||||
def responseText = responseCode == 200 ? connection.inputStream.text : '{"allowCredentials":[]}'
|
||||
def jsonResponse = new JsonSlurper().parseText(responseText)
|
||||
def numOfKeys = jsonResponse.allowCredentials ? jsonResponse.allowCredentials.size() : 0
|
||||
|
||||
// non existing account, account without FIDO2 key , or account with disabled FIDO2 key case
|
||||
if (responseCode == 404 || responseCode == 400 || numOfKeys == 0) {
|
||||
|
||||
LOG.debug("Fido2Auth: <== Response: ${responseCode}")
|
||||
|
||||
// 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 tAuth = System.currentTimeMillis() - (request.getSession(true).getCreationTime().getEpochSecond() * 1000)
|
||||
def details = "no account (404)"
|
||||
if (responseCode == 400 ) {
|
||||
details = "no fido2 keys for account (400)"
|
||||
} else if (responseCode == 200) {
|
||||
details = "no active fido2 key for account (200, empty allowCredentials array)"
|
||||
}
|
||||
|
||||
LOG.info("Event='NOACCOUNT', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${session['ch.nevis.idm.User.email']}, CredentialType='${credentialType}', tAuth=${tAuth}ms, SourceIp=${sourceIp}, UserAgent='${userAgent}', Details='${details}'")
|
||||
|
||||
// returning a fake options structure, which shouldn't leak whether the user account exists or not
|
||||
// keyId is unique per environment and email, fido2SessionId and challenge are renewed each time
|
||||
def keyId = UUID.nameUUIDFromBytes("${parameters['rpId']}.${session['ch.nevis.idm.User.email']}".getBytes())
|
||||
responseText = """{"status": "ok",
|
||||
"errorMessage": "",
|
||||
"fido2SessionId": "${UUID.randomUUID()}",
|
||||
"challenge": "${base64url(UUID.randomUUID())}",
|
||||
"timeout": 300000,
|
||||
"rpId": "${parameters['rpId']}",
|
||||
"allowCredentials": [
|
||||
{
|
||||
"type": "public-key",
|
||||
"id": "${base64url(keyId)}",
|
||||
"transports": []
|
||||
}
|
||||
],
|
||||
"userVerification": "required"}"""
|
||||
}
|
||||
|
||||
LOG.debug("Fido2Auth: <== Response: ${responseCode} : ${responseText}")
|
||||
response.setContent(responseText)
|
||||
response.setContentType('application/json')
|
||||
response.setHttpStatusCode(200)
|
||||
response.setIsDirectResponse(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (path == '/nevisfido/fido2/assertion/result') {
|
||||
|
||||
if (inargs.containsKey('authRequestId') && (inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'])) {
|
||||
// wrong request, "force" a timeout
|
||||
LOG.debug('Fido2Auth: authentication timeout enforced, due to concurrent requests')
|
||||
|
||||
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 userHandleValue = userExtId.getBytes().encodeBase64Url().toString()
|
||||
LOG.debug("Fido2Auth: encoded userHandle: ${userHandleValue}")
|
||||
json {
|
||||
"id" inargs['id']
|
||||
"type" inargs['type']
|
||||
response {
|
||||
"clientDataJSON" inargs['response.clientDataJSON']
|
||||
"authenticatorData" inargs['response.authenticatorData']
|
||||
"signature" inargs['response.signature']
|
||||
"userHandle" userHandleValue
|
||||
}
|
||||
}
|
||||
post(connection, json)
|
||||
def responseCode = connection.responseCode
|
||||
// test if credentials exist
|
||||
if (responseCode != 400) {
|
||||
def responseText = connection.inputStream.text
|
||||
LOG.debug("Fido2Auth: <== Response: ${responseCode} : ${responseText}")
|
||||
if (responseCode == 200 && new JsonSlurper().parseText(responseText).status == 'ok') {
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
}
|
||||
//response.setHttpStatusCode(400)
|
||||
//response.setIsDirectResponse(true)
|
||||
// DEFINE how to handel error
|
||||
notes.setProperty('lasterror', '1')
|
||||
notes.setProperty('lasterrorinfo', 'FIDO2 authentication failed')
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
response.setError(1, "FIDO2 authentication failed")
|
||||
showGui()
|
||||
|
|
@ -0,0 +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')
|
||||
}
|
||||
|
|
@ -0,0 +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')
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
if (inargs['qr'] != null) {
|
||||
//cleanSession()
|
||||
response.setSessionAttribute('agov.dimilar.token', inargs['qr'])
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
response.setTransferDestination('/reg/')
|
||||
response.setIsRedirectTransfer(true)
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import groovy.json.JsonBuilder
|
||||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
def cleanSession(s){
|
||||
s.removeAttribute('agov.dimilar.verification')
|
||||
s.removeAttribute('agov.dimilar.User.firstName')
|
||||
s.removeAttribute('agov.dimilar.User.lastName')
|
||||
s.removeAttribute('agov.dimilar.User.birthDate')
|
||||
s.removeAttribute('agov.dimilar.User.militaryId')
|
||||
|
||||
s.removeAttribute('agov.dimilar.User.birthDate.formatted')
|
||||
s.removeAttribute('agov.dimilar.User.militaryId.formatted')
|
||||
|
||||
s.removeAttribute('agov.dimilar.User.identityConfirmed')
|
||||
s.removeAttribute('agov.dimilar.tokenVerified')
|
||||
|
||||
s.removeAttribute('agov.dimilar.token')
|
||||
}
|
||||
|
||||
def cleanSessionAndContinue(s){
|
||||
cleanSession(s)
|
||||
s.removeAttribute('agov.dimilar.failed')
|
||||
s.removeAttribute('agov.dimilar.aborted')
|
||||
s.removeAttribute('agov.dimilar.invalidToken')
|
||||
s.removeAttribute('agov.dimilar.linkExisting')
|
||||
}
|
||||
|
||||
def s = request.getAuthSession(true)
|
||||
|
||||
// TODO/2025/09/23: We don't need to clear the session here since 'qr' is configured as a clear condition for the entire Auth realm
|
||||
// -> cleanSessionAndContinue could be removed
|
||||
|
||||
// If we get a new token then we always invalidate the session and extract it
|
||||
// so we can always restart if the user provides a new token
|
||||
if(inargs.containsKey('qr')){
|
||||
cleanSessionAndContinue(s)
|
||||
LOG.debug("Dimilar: Clean Session and handle token")
|
||||
response.setResult('handleToken')
|
||||
return
|
||||
}
|
||||
|
||||
// cornercases, receiving an unexpected XHR request, return a json answer, and don't kill the session if we had one
|
||||
if (inargs.containsKey('o.fidoUafSessionId.v')) {
|
||||
// access app status polling
|
||||
LOG.debug("received polling for fido session ${inargs['o.fidoUafSessionId.v']} while auth was already canceled")
|
||||
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((s.getAttribute('agov.dimilar.token') != null) ? AuthResponse.AUTH_CONTINUE : AuthResponse.AUTH_ERROR)
|
||||
return
|
||||
}
|
||||
if (inargs.containsKey('o.path.v')) {
|
||||
// fido 2 call
|
||||
LOG.debug("received fido2 rest call on ${inargs['o.path.v']} while auth was already canceled")
|
||||
def json = new JsonBuilder()
|
||||
json {
|
||||
"status" "failed"
|
||||
"errorMessage" "no active fido2 session"
|
||||
}
|
||||
String body = json.toString()
|
||||
|
||||
response.setContent(body)
|
||||
response.setContentType('application/json')
|
||||
response.setHttpStatusCode(200)
|
||||
response.setIsDirectResponse(true)
|
||||
response.setStatus((s.getAttribute('agov.dimilar.token') != null) ? AuthResponse.AUTH_CONTINUE : AuthResponse.AUTH_ERROR)
|
||||
return
|
||||
}
|
||||
|
||||
// Agov me redirects back on different paths depending on the status
|
||||
String url = request.getCurrentResource()
|
||||
|
||||
if(url.contains('success')){
|
||||
response.setResult('ok')
|
||||
return
|
||||
}else if(url.contains('aborted')){
|
||||
// will redirect below to aborted
|
||||
s.setAttribute('agov.dimilar.aborted', 'true')
|
||||
}else if(url.contains('restart')){
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
response.setTransferDestination('/reg/')
|
||||
response.setIsRedirectTransfer(true)
|
||||
return
|
||||
}else if(url.contains('failed')){
|
||||
// Currently just for testing
|
||||
response.setResult('failed')
|
||||
return
|
||||
}else if(url.contains('link')) {
|
||||
// we clean the url by redirecting again
|
||||
response.setSessionAttribute('agov.dimilar.linkExisting', 'true')
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
response.setTransferDestination('/reg/')
|
||||
response.setIsRedirectTransfer(true)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// if an invalid token was detected we redirect to that error screen
|
||||
if(s.getAttribute('agov.dimilar.invalidToken') == "true"){
|
||||
cleanSession(s)
|
||||
response.setResult('invalidToken')
|
||||
return
|
||||
}
|
||||
|
||||
// if the user aborted we clean the session and redirect to the aborted error screen
|
||||
if(s.getAttribute('agov.dimilar.aborted') == "true"){
|
||||
cleanSession(s)
|
||||
response.setResult('aborted')
|
||||
return
|
||||
}
|
||||
|
||||
// if the onboarding faild because of some data error -> redirect to the failed screen
|
||||
if(s.getAttribute('agov.dimilar.failed') == "true"){
|
||||
cleanSession(s)
|
||||
response.setResult('failed')
|
||||
return
|
||||
}
|
||||
|
||||
// If the token was extracted for the url and we have not validated it yet -> continue with parsing and validation
|
||||
if(s.getAttribute('agov.dimilar.token') != null && s.getAttribute('agov.dimilar.tokenVerified') != "true"){
|
||||
response.setResult('validateToken')
|
||||
return
|
||||
}
|
||||
|
||||
// If the token was validated, but the identity has not yet been confirmed -> show confirmation screen
|
||||
if(s.getAttribute('agov.dimilar.tokenVerified') == "true" && s.getAttribute('agov.dimilar.User.identityConfirmed') != "true"){
|
||||
response.setResult('confirmIdentity')
|
||||
return
|
||||
}
|
||||
|
||||
// If the token is validated and the identity is confirmed then we ...
|
||||
if(s.getAttribute('agov.dimilar.tokenVerified') == "true" && s.getAttribute('agov.dimilar.User.identityConfirmed') == "true"){
|
||||
if (s.getAttribute('agov.dimilar.linkExisting') == "true") {
|
||||
s.removeAttribute('agov.dimilar.linkExisting')
|
||||
// ... back from reg with already existing account, go directly to linking
|
||||
response.setResult('linkExisting')
|
||||
} else {
|
||||
// ... else choose what you want to do
|
||||
response.setResult('selectOnboarding')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import ch.nevis.esauth.util.httpclient.api.HttpClient
|
||||
import groovy.json.JsonSlurper
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
|
||||
// Accounting
|
||||
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 s = request.getAuthSession(true)
|
||||
|
||||
String userExtId = s.getAttribute("ch.nevis.session.userid") ?: s.getAttribute('ch.adnovum.nevisidm.userExtId')
|
||||
String militaryId = s.getAttribute("agov.dimilar.User.militaryId")
|
||||
LOG.debug("Dimilar mobileId: " + userExtId)
|
||||
|
||||
|
||||
// Endpoint on the UtilityService to check and link an account
|
||||
String endPoint = parameters.get('utilityServiceDimilarLinkingUrl')
|
||||
|
||||
|
||||
String utilityServiceRequestTemplate = '{"agovId": "{{AGOVID}}", "militaryId": "{{MILITARYID}}"}'
|
||||
String utilityServiceRequest = utilityServiceRequestTemplate.replaceAll("\\{\\{AGOVID}}",userExtId)
|
||||
.replaceAll("\\{\\{MILITARYID}}",militaryId)
|
||||
|
||||
LOG.debug("DIMILAR: UtilityService linking request: " + utilityServiceRequest)
|
||||
|
||||
HttpClient httpClient = HttpClients.create(parameters)
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
String traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
try {
|
||||
def httpResponse = Http.post()
|
||||
.url(endPoint)
|
||||
.header("Accept", "application/json")
|
||||
.header("traceparent", traceparent)
|
||||
.entity(Http.entity()
|
||||
.content(utilityServiceRequest)
|
||||
.contentType("application/json")
|
||||
.charset("utf-8")
|
||||
.build())
|
||||
.build()
|
||||
.send(httpClient)
|
||||
|
||||
// an error occured on the utility -> linking not successfull
|
||||
if (httpResponse.code() != 200) {
|
||||
LOG.debug("DIMILAR: Linking on the Uitlity service failed: ${httpResponse.bodyAsString()}")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.debug("DIMILAR: Calling the Utility Service for linking failed: $e")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
LOG.info("Event='ACCT-LINKED', User=${user}, CredentialType='${credentialType}', SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
|
||||
response.setResult('ok')
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
import ch.nevis.esauth.util.httpclient.api.HttpClient
|
||||
import java.net.URLDecoder
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import groovy.json.JsonSlurper
|
||||
import io.opentelemetry.api.trace.Span
|
||||
|
||||
|
||||
// Accounting
|
||||
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 s = request.getAuthSession(true)
|
||||
|
||||
String token = session["agov.dimilar.token"]
|
||||
String[] splitToken = token.tokenize("|")
|
||||
// if the token has more than one potential signature/payload, we abort
|
||||
if(splitToken.size() != 2){
|
||||
LOG.warn("Event='INVALID-TOKEN', errorMessage='Multiple payloads/signatures detected', SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
s.setAttribute('agov.dimilar.invalidToken', 'true')
|
||||
response.setResult('invalidToken')
|
||||
return
|
||||
}
|
||||
|
||||
LOG.debug("DIMILAR Token Payload: " + splitToken[0])
|
||||
LOG.debug("DIMILAR Token Signature: " + splitToken[1])
|
||||
|
||||
String utilityServiceRequestTemplate = '{"payload": "{{PAYLOAD}}", "signature": "{{SIGNATURE}}"}'
|
||||
String utilityServiceRequest = utilityServiceRequestTemplate.replaceAll("\\{\\{PAYLOAD}}",splitToken[0])
|
||||
.replaceAll("\\{\\{SIGNATURE}}",splitToken[1])
|
||||
LOG.debug("DIMILAR: UtilityService request: " + utilityServiceRequest)
|
||||
|
||||
// to Call UtilityService to validate token
|
||||
String endPoint = parameters.get('utilityServiceTokenVerificationUrl')
|
||||
|
||||
HttpClient httpClient = HttpClients.create(parameters)
|
||||
def spanCtxt = Span.current().getSpanContext()
|
||||
String traceparent = "00-${spanCtxt.getTraceId()}-${spanCtxt.getSpanId()}-${spanCtxt.getTraceFlags().asHex()}"
|
||||
|
||||
String firstName = ''
|
||||
String lastName = ''
|
||||
LocalDateTime birthDate
|
||||
String militaryIdUnformatted = ''
|
||||
String utilityTraceId = ''
|
||||
|
||||
try {
|
||||
def httpResponse = Http.post()
|
||||
.url(endPoint)
|
||||
.header("Accept", "application/json")
|
||||
.header("traceparent", traceparent)
|
||||
.entity(Http.entity()
|
||||
.content(utilityServiceRequest)
|
||||
.contentType("application/json")
|
||||
.charset("utf-8")
|
||||
.build())
|
||||
.build()
|
||||
.send(httpClient)
|
||||
|
||||
|
||||
if (httpResponse.code() != 200) {
|
||||
LOG.warn("DIMILAR: Calling the Utility Service resulted in unexpected status code: ${httpResponse}")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
def json = new JsonSlurper().parseText(httpResponse.bodyAsString())
|
||||
LOG.debug("DIMILAR: UtilityService Result: ${json}")
|
||||
|
||||
if(json.trId != null){
|
||||
utilityTraceId = json.trId
|
||||
}
|
||||
|
||||
s.setAttribute('agov.dimilar.trId', utilityTraceId)
|
||||
|
||||
if(!json.isValid){
|
||||
LOG.warn("Event='INVALID-TOKEN', errorMessage='Token is not valid (validation service)', SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
s.setAttribute('agov.dimilar.invalidToken', "true")
|
||||
response.setResult('invalidToken')
|
||||
return
|
||||
}
|
||||
|
||||
firstName = json.userName.firstName
|
||||
lastName = json.userName.lastName
|
||||
birthDate = LocalDateTime.of(json.dateOfBirth[0], json.dateOfBirth[1], json.dateOfBirth[2],0,0)
|
||||
militaryIdUnformatted = json.militarySectorId
|
||||
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.error("DIMILAR: Calling the Utility Service failed: $e")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
s.setAttribute('agov.requestedRoleLevel', 'urn:qa.agov.ch:names:tc:ac:classes:100')
|
||||
|
||||
DateTimeFormatter frontendDateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
|
||||
DateTimeFormatter idmDateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
|
||||
|
||||
// For now we use hardcoded data instead of what is in the token
|
||||
s.setAttribute('agov.dimilar.User.firstName', firstName)
|
||||
s.setAttribute('agov.dimilar.User.lastName', lastName)
|
||||
s.setAttribute('agov.dimilar.User.birthDate', birthDate.format(idmDateFormatter))
|
||||
s.setAttribute('agov.dimilar.User.militaryId', "756" + militaryIdUnformatted)
|
||||
|
||||
s.setAttribute('agov.dimilar.User.birthDate.formatted', birthDate.format(frontendDateFormatter))
|
||||
String militaryIdFormatted = "756." + militaryIdUnformatted.substring(0, 2) + "****" + militaryIdUnformatted.substring(militaryIdUnformatted.length()-2, militaryIdUnformatted.length())
|
||||
s.setAttribute('agov.dimilar.User.militaryId.formatted', militaryIdFormatted)
|
||||
|
||||
s.setAttribute('agov.dimilar.tokenVerified', "true")
|
||||
|
||||
s.setAttribute('agov.dimilar.trId', utilityTraceId)
|
||||
|
||||
LOG.debug("Dimilar Utility trId: " + utilityTraceId)
|
||||
LOG.debug("Dimilar traceparent: " + traceparent)
|
||||
|
||||
|
||||
response.setResult('ok')
|
||||
return
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
if(outargs.containsKey('saml.SAMLResponse')) {
|
||||
// Accounting
|
||||
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='GOTOREGISTER-ATTR', 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{
|
||||
LOG.debug("DIMILAR: Got back from agov me in redirection state: ")
|
||||
|
||||
def s = request.getAuthSession(true)
|
||||
|
||||
// Decide what to do depending on the url that agov me redirects back to
|
||||
String url = request.getCurrentResource()
|
||||
|
||||
if(url.contains('success')){
|
||||
s.setAttribute("dimilar.placeholder.text", "AGOV me returned to: /success")
|
||||
response.setResult('redirect')
|
||||
return
|
||||
}else if(url.contains('aborted')){
|
||||
s.setAttribute("dimilar.placeholder.text", "AGOV me returned to: /aborted")
|
||||
response.setResult('redirect')
|
||||
return
|
||||
}else if(url.contains('restart')){
|
||||
s.setAttribute("dimilar.placeholder.text", "AGOV me returned to: /restart")
|
||||
response.setResult('redirect')
|
||||
return
|
||||
}
|
||||
|
||||
response.setResult('ok')
|
||||
|
||||
|
||||
}
|
||||
|
||||
// NOTE/aca/2025/09/21: Since resumeState is false redirection from agov me will go back to Dimilar_OnboardingAuth -> no handling needed here
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
def s = request.getAuthSession(true)
|
||||
|
||||
|
||||
if(inargs['continue'] == 'linking'){
|
||||
LOG.debug("DIMILAR Onboarding: Selected Linking")
|
||||
s.setAttribute("dimilar.placeholder.text", "DIMILAR: Linking not implemented yet")
|
||||
response.setResult('link')
|
||||
return
|
||||
}
|
||||
|
||||
if(inargs['continue'] == 'registration'){
|
||||
LOG.debug("DIMILAR Onboarding: Selected Registration")
|
||||
// generate new extId for the registration
|
||||
String uuidString = UUID.randomUUID().toString()
|
||||
s.setAttribute('agov.subjectUUID', uuidString)
|
||||
s.setAttribute('agov.authnContextClassRef', 'urn:qa.agov.ch:names:tc:ac:classes:null')
|
||||
response.setResult('register')
|
||||
return
|
||||
}
|
||||
|
||||
if(inargs['cancel']){
|
||||
LOG.debug("DIMILAR Onboarding: Abort")
|
||||
s.setAttribute("dimilar.placeholder.text", "DIMILAR: Onboarding cancelled while selecting the onboarding type")
|
||||
s.setAttribute("agov.dimilar.aborted", "true")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
LOG.debug("Show GUI")
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import ch.nevis.esauth.auth.engine.AuthResponse
|
||||
|
||||
def s = request.getAuthSession(true)
|
||||
|
||||
|
||||
if(inargs['confirmIdentity'] == 'yes'){
|
||||
LOG.debug("DIMILAR Onboarding: Identity was verified by user")
|
||||
//s.setAttribute("dimilar.placeholder.text", "DIMILAR: Onboarding Type Selection not implemented yet")
|
||||
s.setAttribute('agov.dimilar.User.identityConfirmed', "true")
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
|
||||
if(inargs['confirmIdentity'] == 'no'){
|
||||
LOG.debug("DIMILAR Onboarding: Identity not verified by user")
|
||||
s.setAttribute("dimilar.placeholder.text", "DIMILAR: Identity not verified by user")
|
||||
s.setAttribute("agov.dimilar.aborted", "true")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
LOG.debug("Show GUI")
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
return
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import ch.nevis.idm.client.IdmRestClient
|
||||
import ch.nevis.idm.client.IdmRestClientFactory
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.xml.XmlSlurper
|
||||
|
||||
// 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 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() })
|
||||
}
|
||||
|
||||
|
||||
|
||||
IdmRestClient idmRestClient = IdmRestClientFactory.get(parameters)
|
||||
def sess = request.getAuthSession(true)
|
||||
|
||||
|
||||
String baseUrl = parameters.get("baseUrl")
|
||||
String agovClientExtId = parameters.get("agovClientExtId")
|
||||
String shadowClientExtId = parameters.get("shadowClientExtId")
|
||||
|
||||
String userExtId = sess.getAttribute("ch.nevis.session.userid")
|
||||
|
||||
String endpoint = "$baseUrl/api/core/v1"
|
||||
|
||||
// Check if the account is flagged for recovery
|
||||
def recoveryRoleList = getUserAGOVRecoveryRoles()
|
||||
|
||||
if(recoveryRoleList.contains('mustRecover') || recoveryRoleList.contains('recovery')){
|
||||
LOG.debug("EID: User is flagged for recovery. Account linking not possible")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Check if there is an active shadow account for this user
|
||||
String queryParameters ="?property.agovId=$userExtId&userState=ACTIVE"
|
||||
String accountCtxtPwEndPoint = "$endpoint/clients/$shadowClientExtId/users/$queryParameters"
|
||||
try {
|
||||
|
||||
def idmResponse = idmRestClient.get(accountCtxtPwEndPoint)
|
||||
def json = new JsonSlurper().parseText(idmResponse)
|
||||
|
||||
def shadowAccounts = json.items
|
||||
if(shadowAccounts.size() > 0){
|
||||
LOG.debug("EID: User is undergoing a recovery process. Account linking not possible")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
}catch(Exception e) {
|
||||
LOG.error("EID: Failed Idm Shadow account lookup ${e}")
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
//TODO/aca/2025/09/02: Check if the user has active shadowaccounts
|
||||
|
||||
response.setResult('ok')
|
||||
return
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
import groovy.json.JsonBuilder
|
||||
import groovy.json.JsonSlurper
|
||||
import java.util.UUID
|
||||
|
||||
if (inargs.containsKey('cancel_fido2')) {
|
||||
response.setResult('cancel')
|
||||
LOG.debug("Fido2Auth: authentication cancelled by user")
|
||||
return
|
||||
}
|
||||
|
||||
def base64url(uuid) {
|
||||
def msb = uuid.getMostSignificantBits()
|
||||
def lsb = uuid.getLeastSignificantBits()
|
||||
return new byte[] {
|
||||
(byte) msb,
|
||||
(byte) (msb >> 8),
|
||||
(byte) (msb >> 16),
|
||||
(byte) (msb >> 24),
|
||||
(byte) (msb >> 32),
|
||||
(byte) (msb >> 40),
|
||||
(byte) (msb >> 48),
|
||||
(byte) (msb >> 56),
|
||||
(byte) lsb,
|
||||
(byte) (lsb >> 8),
|
||||
(byte) (lsb >> 16),
|
||||
(byte) (lsb >> 24),
|
||||
(byte) (lsb >> 32),
|
||||
(byte) (lsb >> 40),
|
||||
(byte) (lsb >> 48),
|
||||
(byte) (lsb >> 56)
|
||||
}.encodeBase64Url().toString()
|
||||
}
|
||||
|
||||
def showGui() {
|
||||
response.setGuiName('eid_linking_account_fido2_auth') // name is the trigger for including the JS
|
||||
response.setGuiLabel('title.login.fido2')
|
||||
response.addInfoGuiField('info', 'info.login.fido2', null)
|
||||
response.addHiddenGuiField('authRequestId', 'not used', session['ch.nevis.auth.saml.request.id'])
|
||||
response.addTextGuiField('email', 'email', session['ch.nevis.idm.User.email'])
|
||||
if (notes.containsKey('lasterrorinfo') || notes.containsKey('lasterror')) {
|
||||
response.addErrorGuiField('lasterror', notes['lasterrorinfo'], notes['lasterror'])
|
||||
}
|
||||
if (parameters.containsKey('cancel')) {
|
||||
response.addButtonGuiField('cancel_fido2', 'cancel.login.fido2.button.label', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
def getPath() {
|
||||
if (inargs.containsKey('path')) { // form POST
|
||||
return inargs['path']
|
||||
}
|
||||
if (inargs.containsKey('o.path.v')) { // AJAX POST
|
||||
return inargs['o.path.v']
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
def post(connection, json) {
|
||||
connection.setRequestMethod("POST")
|
||||
connection.setRequestProperty("Content-Type", "application/json")
|
||||
connection.setDoOutput(true) // required to write body
|
||||
String body = json.toString()
|
||||
LOG.debug("Fido2Auth: ==> Request: '${body}'")
|
||||
connection.getOutputStream().write(body.getBytes())
|
||||
}
|
||||
|
||||
String userExtId = session['ch.adnovum.nevisidm.user.extId'] ?: session['ch.nevis.idm.User.extId'] ?: request.getUserId() ?: notes['userid']
|
||||
if (userExtId == null) {
|
||||
LOG.error("Fido2Auth: missing extId of nevisIDM user. check your authentication flow.")
|
||||
}
|
||||
// without the user extId this script won't work and we can fail with a System Error
|
||||
Objects.requireNonNull(userExtId)
|
||||
|
||||
def path = getPath()
|
||||
if (path == null) {
|
||||
showGui() // POST from JavaScript not received
|
||||
return
|
||||
}
|
||||
|
||||
def connection = null
|
||||
try {
|
||||
def fullPath = "https://${parameters.get('fido')}${path}"
|
||||
LOG.debug("Fido2Auth: opening connection to '${fullPath}'")
|
||||
connection = new URL(fullPath).openConnection()
|
||||
} catch (Exception e) {
|
||||
LOG.error("Fido2Auth: opening connection failed", e)
|
||||
notes.setProperty('lasterrorinfo', 'FIDO2 authentication failed')
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
def json = new JsonBuilder()
|
||||
|
||||
if (path == '/nevisfido/fido2/attestation/options') {
|
||||
json {
|
||||
"username" userExtId
|
||||
"userVerification" "required"
|
||||
}
|
||||
post(connection, json)
|
||||
def responseCode = connection.responseCode
|
||||
def responseText = responseCode == 200 ? connection.inputStream.text : '{"allowCredentials":[]}'
|
||||
def jsonResponse = new JsonSlurper().parseText(responseText)
|
||||
def numOfKeys = jsonResponse.allowCredentials ? jsonResponse.allowCredentials.size() : 0
|
||||
|
||||
// non existing account, account without FIDO2 key , or account with disabled FIDO2 key case
|
||||
if (responseCode == 404 || responseCode == 400 || numOfKeys == 0) {
|
||||
|
||||
LOG.debug("Fido2Auth: <== Response: ${responseCode}")
|
||||
|
||||
// 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 tAuth = System.currentTimeMillis() - (request.getSession(true).getCreationTime().getEpochSecond() * 1000)
|
||||
def details = "no account (404)"
|
||||
if (responseCode == 400 ) {
|
||||
details = "no fido2 keys for account (400)"
|
||||
} else if (responseCode == 200) {
|
||||
details = "no active fido2 key for account (200, empty allowCredentials array)"
|
||||
}
|
||||
|
||||
LOG.info("Event='NOACCOUNT', Requester='${requester}', RequestId='${requestId}', RequestedAq=${requestedAq}, User=${session['ch.nevis.idm.User.email']}, CredentialType='${credentialType}', tAuth=${tAuth}ms, SourceIp=${sourceIp}, UserAgent='${userAgent}', Details='${details}'")
|
||||
|
||||
// returning a fake options structure, which shouldn't leak whether the user account exists or not
|
||||
// keyId is unique per environment and email, fido2SessionId and challenge are renewed each time
|
||||
def keyId = UUID.nameUUIDFromBytes("${parameters['rpId']}.${session['ch.nevis.idm.User.email']}".getBytes())
|
||||
responseText = """{"status": "ok",
|
||||
"errorMessage": "",
|
||||
"fido2SessionId": "${UUID.randomUUID()}",
|
||||
"challenge": "${base64url(UUID.randomUUID())}",
|
||||
"timeout": 300000,
|
||||
"rpId": "${parameters['rpId']}",
|
||||
"allowCredentials": [
|
||||
{
|
||||
"type": "public-key",
|
||||
"id": "${base64url(keyId)}",
|
||||
"transports": []
|
||||
}
|
||||
],
|
||||
"userVerification": "required"}"""
|
||||
}
|
||||
|
||||
LOG.debug("Fido2Auth: <== Response: ${responseCode} : ${responseText}")
|
||||
response.setContent(responseText)
|
||||
response.setContentType('application/json')
|
||||
response.setHttpStatusCode(200)
|
||||
response.setIsDirectResponse(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (path == '/nevisfido/fido2/assertion/result') {
|
||||
|
||||
if (inargs.containsKey('authRequestId') && (inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'])) {
|
||||
// wrong request, "force" a timeout
|
||||
LOG.debug('Fido2Auth: authentication timeout enforced, due to concurrent requests')
|
||||
|
||||
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 userHandleValue = userExtId.getBytes().encodeBase64Url().toString()
|
||||
LOG.debug("Fido2Auth: encoded userHandle: ${userHandleValue}")
|
||||
json {
|
||||
"id" inargs['id']
|
||||
"type" inargs['type']
|
||||
response {
|
||||
"clientDataJSON" inargs['response.clientDataJSON']
|
||||
"authenticatorData" inargs['response.authenticatorData']
|
||||
"signature" inargs['response.signature']
|
||||
"userHandle" userHandleValue
|
||||
}
|
||||
}
|
||||
post(connection, json)
|
||||
def responseCode = connection.responseCode
|
||||
// test if credentials exist
|
||||
if (responseCode != 400) {
|
||||
def responseText = connection.inputStream.text
|
||||
LOG.debug("Fido2Auth: <== Response: ${responseCode} : ${responseText}")
|
||||
if (responseCode == 200 && new JsonSlurper().parseText(responseText).status == 'ok') {
|
||||
response.setResult('ok')
|
||||
return
|
||||
}
|
||||
}
|
||||
//response.setHttpStatusCode(400)
|
||||
//response.setIsDirectResponse(true)
|
||||
// DEFINE how to handel error
|
||||
notes.setProperty('lasterror', '1')
|
||||
notes.setProperty('lasterrorinfo', 'FIDO2 authentication failed')
|
||||
response.setResult('error')
|
||||
return
|
||||
}
|
||||
|
||||
response.setError(1, "FIDO2 authentication failed")
|
||||
showGui()
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
def EMAIL_REGEXP = '^(([^<>()\\[\\]\\\\\\.,;:\\s@"]+(\\.[^<>()\\[\\]\\\\\\.,;:\\s@"]+)*)|(\\.\\+))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$'
|
||||
|
||||
|
||||
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['cancelFido2'] && inargs['cancelFido2'] == 'cancelFido2') {
|
||||
response.setResult('cancel')
|
||||
return
|
||||
}
|
||||
|
||||
if ( inargs['authRequestId'] && inargs['authRequestId'] != session['ch.nevis.auth.saml.request.id'] ) {
|
||||
response.setResult('timeout')
|
||||
return
|
||||
}
|
||||
|
||||
if ( inargs['submit'] && inargs['submit'] == 'submit' ) {
|
||||
if (inargs['userInputValue_prompt.email'] && inargs['userInputValue_prompt.email'].matches(EMAIL_REGEXP)) {
|
||||
response.setResult('verifyEmail')
|
||||
return
|
||||
} else {
|
||||
LOG.warn("User attempted to bypass frontend emailvalidation with inavlid email: '${inargs['userInputValue_prompt.email']}', SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
request.getInArgs().setProperty('userInputValue_prompt.email', 'inavalid@email.org')
|
||||
response.setResult('stay')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.setResult('stay')
|
||||
return
|
||||
|
|
@ -0,0 +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')
|
||||
}
|
||||
|
|
@ -0,0 +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')
|
||||
}
|
||||
|
|
@ -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,159 +1,158 @@
|
|||
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,201 +1,200 @@
|
|||
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,456 +1,458 @@
|
|||
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')
|
||||
}
|
||||
|
||||
// TODO/haburger/2025-09-25: we need to restrict the trusted issuer to the correct one
|
||||
// "accepted_issuer_dids": [ TODO ],
|
||||
// "jwt_secured_authorization_request": true,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import ch.nevis.idm.client.IdmRestClientFactory
|
|||
import ch.nevis.idm.client.HTTPRequestWrapper
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.xml.XmlSlurper
|
||||
|
||||
// Accounting
|
||||
def requester = session['ch.nevis.auth.saml.request.scoping.requesterId'] ?: 'unknown'
|
||||
|
|
@ -118,6 +119,14 @@ if (!session['ch.adnovum.nevisidm.userDto'].contains("<properties><name>idVerifi
|
|||
}
|
||||
}
|
||||
|
||||
// Processing militarySectorId (value is on credential, thus not automatically added to the session)
|
||||
def slurper = new XmlSlurper().parseText(session.get('ch.adnovum.nevisidm.userDto') ?: '<missing></missing>')
|
||||
def militarySectorId = slurper.'**'.find { node -> node.name() == 'samlFederations' && node.issuerNameId.text() == 'urn:ch-agov-link:military' }?.subjectNameId?.text()
|
||||
|
||||
if (militarySectorId) {
|
||||
def s = request.getAuthSession(true)
|
||||
s.setAttribute('agov.militarySectorId', militarySectorId)
|
||||
}
|
||||
|
||||
if (audited) {
|
||||
response.setResult('reload')
|
||||
|
|
|
|||
|
|
@ -122,4 +122,4 @@ if (inargs['submit']) {
|
|||
}
|
||||
|
||||
// show the GUI
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
response.setStatus(AuthResponse.AUTH_CONTINUE)
|
||||
|
|
@ -13,9 +13,8 @@ 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.2505.5,service.instance.id=$HOSTNAME"
|
||||
"-Dotel.resource.attributes=service.version=8.2411.3,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}"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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,4 +24,3 @@ else {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,4 +20,4 @@ if(outargs.containsKey('saml.SAMLResponse')) {
|
|||
}
|
||||
else {
|
||||
response.setResult('ok')
|
||||
}
|
||||
}
|
||||
|
|
@ -32,4 +32,3 @@ else {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,197 +1,207 @@
|
|||
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)
|
||||
}
|
||||
|
||||
String getNormalisedSamlMessage(String parameter) {
|
||||
if (parameter == null) {
|
||||
return
|
||||
}
|
||||
String text
|
||||
byte[] decoded
|
||||
|
||||
// if parameter is raw xml then continue otherwise try to parse the base64 encoding
|
||||
if (parameter.startsWith("<")) {
|
||||
text = new String(parameter)
|
||||
}
|
||||
else {
|
||||
decoded = parameter.decodeBase64()
|
||||
text = new String(decoded)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
|
||||
String getNodeText(GPathResult xml, String nodeName) {
|
||||
return xml.depthFirst().find { GPathResult node -> {
|
||||
node.name().endsWith(":${nodeName}") || node.name().equalsIgnoreCase(nodeName)
|
||||
}
|
||||
}?.text()?.trim()
|
||||
}
|
||||
|
||||
String getAttribute(GPathResult xml, String attributeName) {
|
||||
return xml.depthFirst().find { GPathResult node -> {
|
||||
node.attributes().containsKey(attributeName)
|
||||
}
|
||||
}?.attributes()?.get(attributeName)
|
||||
}
|
||||
|
||||
String getNodeText(String parameter, String nodeName) {
|
||||
String samlMessage = getNormalisedSamlMessage(parameter)
|
||||
if (samlMessage == null) {
|
||||
return
|
||||
}
|
||||
def parser = new XmlSlurper()
|
||||
def xml = parser.parseText(samlMessage)
|
||||
return getNodeText(xml, nodeName)
|
||||
}
|
||||
|
||||
String getAttribute(String parameter, String attributeName) {
|
||||
String samlMessage = getNormalisedSamlMessage(parameter)
|
||||
if (samlMessage == null) {
|
||||
return
|
||||
}
|
||||
def parser = new XmlSlurper()
|
||||
def xml = parser.parseText(samlMessage)
|
||||
return getAttribute(xml, attributeName)
|
||||
}
|
||||
|
||||
String getIssuer(String value) {
|
||||
return getNodeText(value, 'Issuer')
|
||||
}
|
||||
|
||||
String getAttributeConsumingServiceIndex(String value) {
|
||||
return getAttribute(value, 'AttributeConsumingServiceIndex')
|
||||
}
|
||||
|
||||
String getProtocolBinding(String value) {
|
||||
return getAttribute(value, 'ProtocolBinding')
|
||||
}
|
||||
|
||||
def dispatchIssuer(i2s, String issuer, boolean secureMode) {
|
||||
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 if (result == 'main' && secureMode) {
|
||||
LOG.debug("AGOV: Secure mode requested")
|
||||
result = result + "_secure"
|
||||
}
|
||||
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 dispatchIssuer(i2s, String issuer) {
|
||||
dispatchIssuer(i2s, issuer, false)
|
||||
}
|
||||
|
||||
def dispatchMessage(i2s, String message) {
|
||||
def issuer = getIssuer(message)
|
||||
def secureMode = (getAttributeConsumingServiceIndex(message) == '10101')
|
||||
def useArtifact = ('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' == getProtocolBinding(message))
|
||||
|
||||
LOG.info("secureMode requested: ${secureMode}")
|
||||
|
||||
if (issuer == null) {
|
||||
LOG.info("No issuer found in incoming SAML message. Giving up.")
|
||||
}
|
||||
session.put('saml.inbound.issuer', issuer)
|
||||
session.put('agov.idp.use.artifact', '' + useArtifact)
|
||||
dispatchIssuer(i2s, issuer, secureMode)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
String getNormalisedSamlMessage(String parameter) {
|
||||
if (parameter == null) {
|
||||
return
|
||||
}
|
||||
String text
|
||||
byte[] decoded
|
||||
|
||||
// if parameter is raw xml then continue otherwise try to parse the base64 encoding
|
||||
if (parameter.startsWith("<")) {
|
||||
text = new String(parameter)
|
||||
}
|
||||
else {
|
||||
decoded = parameter.decodeBase64()
|
||||
text = new String(decoded)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
|
||||
String getNodeText(GPathResult xml, String nodeName) {
|
||||
return xml.depthFirst().find { GPathResult node -> {
|
||||
node.name().endsWith(":${nodeName}") || node.name().equalsIgnoreCase(nodeName)
|
||||
}
|
||||
}?.text()?.trim()
|
||||
}
|
||||
|
||||
String getAttribute(GPathResult xml, String attributeName) {
|
||||
return xml.depthFirst().find { GPathResult node -> {
|
||||
node.attributes().containsKey(attributeName)
|
||||
}
|
||||
}?.attributes()?.get(attributeName)
|
||||
}
|
||||
|
||||
String getNodeText(String parameter, String nodeName) {
|
||||
String samlMessage = getNormalisedSamlMessage(parameter)
|
||||
if (samlMessage == null) {
|
||||
return
|
||||
}
|
||||
def parser = new XmlSlurper()
|
||||
def xml = parser.parseText(samlMessage)
|
||||
return getNodeText(xml, nodeName)
|
||||
}
|
||||
|
||||
String getAttribute(String parameter, String attributeName) {
|
||||
String samlMessage = getNormalisedSamlMessage(parameter)
|
||||
if (samlMessage == null) {
|
||||
return
|
||||
}
|
||||
def parser = new XmlSlurper()
|
||||
def xml = parser.parseText(samlMessage)
|
||||
return getAttribute(xml, attributeName)
|
||||
}
|
||||
|
||||
String getIssuer(String value) {
|
||||
return getNodeText(value, 'Issuer')
|
||||
}
|
||||
|
||||
String getAttributeConsumingServiceIndex(String value) {
|
||||
return getAttribute(value, 'AttributeConsumingServiceIndex')
|
||||
}
|
||||
|
||||
String getProtocolBinding(String value) {
|
||||
return getAttribute(value, 'ProtocolBinding')
|
||||
}
|
||||
|
||||
def dispatchIssuer(i2s, String issuer, boolean secureMode) {
|
||||
def result = i2s.get(issuer)
|
||||
if (result == null) {
|
||||
// TODO/22-09-2025/haburger: proper error handling
|
||||
LOG.error("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 if (result == 'main' && secureMode) {
|
||||
LOG.debug("AGOV: Secure mode requested")
|
||||
result = result + "_secure"
|
||||
}
|
||||
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 dispatchIssuer(i2s, String issuer) {
|
||||
dispatchIssuer(i2s, issuer, false)
|
||||
}
|
||||
|
||||
def dispatchMessage(i2s, String message) {
|
||||
def issuer = getIssuer(message)
|
||||
def secureMode = (getAttributeConsumingServiceIndex(message) == '10101')
|
||||
def useArtifact = ('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' == getProtocolBinding(message))
|
||||
|
||||
LOG.info("Response to be handled: secureMode: ${secureMode}, artifact binding: ${useArtifact}")
|
||||
|
||||
if (issuer == null) {
|
||||
// TODO/22-09-2025/haburger: proper error handling
|
||||
LOG.error("No issuer found in incoming SAML message. Giving up.")
|
||||
}
|
||||
session.put('saml.inbound.issuer', issuer)
|
||||
session.put('agov.idp.use.artifact', '' + useArtifact)
|
||||
dispatchIssuer(i2s, issuer, secureMode)
|
||||
}
|
||||
|
||||
// beef
|
||||
|
||||
// TODO/22-09-2025/haburger: not needed for AGOV (logout not supported)
|
||||
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
|
||||
}
|
||||
|
||||
// TODO/22-09-2025/haburger: not needed for AGOV (logout not supported)
|
||||
if (inargs.containsKey('SAMLResponse')) { // response to IDP-initiated SAML Logout
|
||||
LOG.debug("found SAMLResponse parameter")
|
||||
String message = inargs.get('SAMLResponse')
|
||||
dispatchMessage(i2s, message)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO/22-09-2025/haburger: not needed for AGOV (SOAP binding not supported ?)
|
||||
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
|
||||
}
|
||||
|
||||
// TODO/22-09-2025/haburger: not needed for AGOV (SOAP binding not supported ?)
|
||||
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']
|
||||
|
||||
// TODO/22-09-2025/haburger: not needed for AGOV (IDP-initiated not supported ?)
|
||||
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,43 @@
|
|||
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())
|
||||
}
|
||||
|
||||
if (!session['agov.recovery.redirectBackPath']) {
|
||||
def referer = request.getLoginContext()['connection.HttpHeader.referer'] ?: request.getLoginContext()['connection.HttpHeader.Referer'] ?: 'no-referer'
|
||||
// dim onboarding is using /dim as context
|
||||
if (referer.matches('^https\\:\\/\\/[^\\/]+\\/reg(\\/?|\\/[^\\/]+)?(\\?.+)?$')) {
|
||||
response.setSessionAttribute('agov.recovery.redirectBackPath', '/reg/')
|
||||
} else {
|
||||
response.setSessionAttribute('agov.recovery.redirectBackPath', '/SAML2/SSO/')
|
||||
}
|
||||
}
|
||||
|
||||
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,6 +16,12 @@ 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"
|
||||
|
|
@ -26,6 +32,8 @@ Configuration:
|
|||
level: "INFO"
|
||||
- name: "AuthPerf"
|
||||
level: "INFO"
|
||||
- name: "DIM-REG"
|
||||
level: "DEBUG"
|
||||
- name: "IdmAuth"
|
||||
level: "DEBUG"
|
||||
- name: "OpTrace"
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// nevisProxy replaces the entire AUTH: scope when new outargs are returned by nevisAuth.
|
||||
// Thus, we have to store tokens in the session (as a String) and restore them on subsequent step-ups.
|
||||
|
||||
// restore tokens
|
||||
session.each { key, value ->
|
||||
if (key.startsWith('outarg.token.')) {
|
||||
def name = key.substring(7)
|
||||
if (outargs.containsKey(name)) {
|
||||
LOG.debug("not restoring token (outarg: $name) from session: outarg already set")
|
||||
}
|
||||
else {
|
||||
LOG.debug("restoring token (outarg: $name) from session")
|
||||
outargs.put(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// store tokens
|
||||
outargs.each { name, value ->
|
||||
if (name.startsWith('token.')) {
|
||||
session.put('outarg.' + name, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
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')
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@ import ch.nevis.esauth.auth.engine.AuthResponse
|
|||
if (inargs['cancel'] == 'cancel') {
|
||||
//cleanSession()
|
||||
response.setStatus(AuthResponse.AUTH_ERROR)
|
||||
response.setTransferDestination('/SAML2/SSO/')
|
||||
def destination = session['agov.recovery.redirectBackPath'] ?: '/SAML2/SSO/'
|
||||
response.setTransferDestination(destination)
|
||||
response.setIsRedirectTransfer(true)
|
||||
return
|
||||
}
|
||||
|
|
@ -21,4 +22,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')
|
||||
}
|
||||
|
|
@ -56,7 +56,11 @@ def appRequiresBestTokenWithAddress = bestTokenAddressWhitelist.contains(','+req
|
|||
def bestTokenSvnrWhitelist = ',' + (parameters.get('bestTokenSvnrWhitelist') ?: '').replaceAll('\\s','') + ','
|
||||
def appRequiresBestTokenWithSvnr = bestTokenSvnrWhitelist.contains(','+requester+',')
|
||||
|
||||
LOG.info("Event='AUTHREQUEST', Requester='${requester}', RequestId='${requestId}', ReplacedRequestId='${replacedRequestId}', RequestedAq=${requestedAq}, BestTokenRequired='svnr: ${appRequiresBestTokenWithSvnr}; address: ${appRequiresBestTokenWithAddress}', SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
def militarySectorIdWhitelist = ',' + (parameters.get('militarySectorIdWhitelist') ?: '').replaceAll('\\s','') + ','
|
||||
def appRequiresMilitarySectorId = militarySectorIdWhitelist.contains(','+requester+',')
|
||||
session.setAttribute('agov.militarySectorIdRequired', appRequiresMilitarySectorId.toString())
|
||||
|
||||
LOG.info("Event='AUTHREQUEST', Requester='${requester}', RequestId='${requestId}', ReplacedRequestId='${replacedRequestId}', RequestedAq=${requestedAq}, BestTokenRequired='svnr: ${appRequiresBestTokenWithSvnr}; address: ${appRequiresBestTokenWithAddress}', militarySectorId=${appRequiresMilitarySectorId}, SourceIp=${sourceIp}, UserAgent='${userAgent}'")
|
||||
|
||||
|
||||
if (requestedRoleLevelNumber == 0 || session.get('ch.nevis.auth.saml.request.scoping.requesterId') == null) {
|
||||
|
|
|
|||
|
|
@ -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,4 +29,3 @@ 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.2505.5"
|
||||
gitInitVersion: "1.4.0"
|
||||
version: "8.2411.2"
|
||||
gitInitVersion: "1.3.0"
|
||||
runAsNonRoot: true
|
||||
ports:
|
||||
rest: 9443
|
||||
|
|
@ -40,19 +40,15 @@ spec:
|
|||
management:
|
||||
httpGet:
|
||||
path: "/nevisfido/health"
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 6
|
||||
failureThreshold: 30
|
||||
failureThreshold: 50
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: "50%"
|
||||
git:
|
||||
tag: "r-36c19390c674892b1c236998382911bcbca6d5e3"
|
||||
tag: "r-ac938692d8edd6d7a3c23c703a8b0ad0b4510414"
|
||||
dir: "DEFAULT-ADN-AGOV-PROJECT/DEFAULT-ADN-AGOV-INV/fido-uaf"
|
||||
credentials: "git-credentials"
|
||||
database:
|
||||
name: "fido-uaf"
|
||||
requiredVersion: "8.2505.5"
|
||||
keystores:
|
||||
- "fido-uaf-default-server-identity"
|
||||
- "fido-uaf-default-client-identity"
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
apiVersion: "operator.nevis-security.ch/v1"
|
||||
kind: "NevisDatabase"
|
||||
metadata:
|
||||
name: "fido-uaf"
|
||||
namespace: "adn-agov-nevisidm-01-uat"
|
||||
labels:
|
||||
deploymentTarget: "fido-uaf"
|
||||
annotations:
|
||||
projectKey: "DEFAULT-ADN-AGOV-PROJECT"
|
||||
patternId: "9385d1b33aefe975fb1c5914"
|
||||
spec:
|
||||
type: "NevisFIDO"
|
||||
databaseType: "MariaDB"
|
||||
version: "8.2505.5"
|
||||
url: "session-db-primary-service.adn-agov-database-01-uat"
|
||||
port: 3306
|
||||
database: "nevisfido_uaf"
|
||||
bootstrap: true
|
||||
migrate: true
|
||||
rootCredentials:
|
||||
name: "root-mariadb-session-store"
|
||||
namespace: "adn-agov-nevisidm-ob-01-uat"
|
||||
podSecurity:
|
||||
policy: "baseline"
|
||||
automountServiceAccountToken: false
|
||||
timeZone: "Europe/Zurich"
|
||||
|
|
@ -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.2505.5,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
"-Dotel.resource.attributes=service.version=8.2411.2,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
|
|
@ -3,13 +3,14 @@
|
|||
"aaid" : "F1D0#0001",
|
||||
"description" : "Android NEVIS Mobile Authentication PIN Authenticator",
|
||||
"assertionScheme" : "UAFV1TLV",
|
||||
"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==\" ]"
|
||||
}
|
||||
"attestationRootCertificates" : [
|
||||
"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"
|
||||
],
|
||||
"attestationTypes" : [ 15879, 15880 ],
|
||||
"upv" : [ {
|
||||
|
|
@ -33,13 +34,14 @@
|
|||
"aaid" : "F1D0#0002",
|
||||
"description" : "Android NEVIS Mobile Authentication Fingerprint Authenticator",
|
||||
"assertionScheme" : "UAFV1TLV",
|
||||
"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==\" ]"
|
||||
}
|
||||
"attestationRootCertificates" : [
|
||||
"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"
|
||||
],
|
||||
"attestationTypes" : [ 15879, 15880 ],
|
||||
"upv" : [ {
|
||||
|
|
@ -63,13 +65,14 @@
|
|||
"aaid" : "F1D0#0003",
|
||||
"description" : "Android NEVIS Mobile Authentication Biometric Authenticator",
|
||||
"assertionScheme" : "UAFV1TLV",
|
||||
"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==\" ]"
|
||||
}
|
||||
"attestationRootCertificates" : [
|
||||
"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"
|
||||
],
|
||||
"attestationTypes" : [ 15879, 15880 ],
|
||||
"upv" : [ {
|
||||
|
|
@ -93,13 +96,14 @@
|
|||
"aaid" : "F1D0#0004",
|
||||
"description" : "Android NEVIS Mobile Authentication Device Passcode Authenticator",
|
||||
"assertionScheme" : "UAFV1TLV",
|
||||
"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==\" ]"
|
||||
}
|
||||
"attestationRootCertificates" : [
|
||||
"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"
|
||||
],
|
||||
"attestationTypes" : [ 15879, 15880 ],
|
||||
"upv" : [ {
|
||||
|
|
@ -123,13 +127,14 @@
|
|||
"aaid" : "F1D0#0005",
|
||||
"description" : "Android NEVIS Mobile Authentication Password Authenticator",
|
||||
"assertionScheme" : "UAFV1TLV",
|
||||
"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==\" ]"
|
||||
}
|
||||
"attestationRootCertificates" : [
|
||||
"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"
|
||||
],
|
||||
"attestationTypes" : [ 15879, 15880 ],
|
||||
"upv" : [ {
|
||||
|
|
@ -263,5 +268,4 @@
|
|||
"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: "rest"
|
||||
idm-connection-type: "soap"
|
||||
dispatchers:
|
||||
- type: "firebase-cloud-messaging"
|
||||
dry-run: false
|
||||
|
|
@ -45,7 +45,6 @@ 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"
|
||||
|
|
@ -55,11 +54,8 @@ 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"
|
||||
full-basic-attestation:
|
||||
basic-full-attestation:
|
||||
android-verification-level: "default"
|
||||
android-permissive-mode-enabled: true
|
||||
android-attestation-key-revocation:
|
||||
reload-interval: "21600s"
|
||||
authorization:
|
||||
registration:
|
||||
type: "sectoken"
|
||||
|
|
@ -98,19 +94,19 @@ fido-uaf:
|
|||
- "userid"
|
||||
session-repository:
|
||||
type: "sql"
|
||||
jdbc-url: "jdbc:mariadb://session-db-primary-service.adn-agov-database-01-uat:3306/nevisfido_uaf?sslMode=disable&autocommit=true"
|
||||
user: "${exec:/var/opt/nevisfido/default/conf/credentials/dbUser}"
|
||||
password: "${exec:/var/opt/nevisfido/default/conf/credentials/dbPassword}"
|
||||
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: "adndbadmin"
|
||||
password: "not-used"
|
||||
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"
|
||||
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,5 +1,4 @@
|
|||
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.2505.5"
|
||||
gitInitVersion: "1.4.0"
|
||||
version: "8.2411.2"
|
||||
gitInitVersion: "1.3.0"
|
||||
runAsNonRoot: true
|
||||
ports:
|
||||
management: 9089
|
||||
|
|
@ -40,14 +40,13 @@ spec:
|
|||
management:
|
||||
httpGet:
|
||||
path: "/nevisfido/health"
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 6
|
||||
failureThreshold: 30
|
||||
failureThreshold: 50
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: "50%"
|
||||
git:
|
||||
tag: "r-484395a405f9f7123da379fa8df82e197d2dbd71"
|
||||
tag: "r-ac938692d8edd6d7a3c23c703a8b0ad0b4510414"
|
||||
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.2505.5,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
"-Dotel.resource.attributes=service.version=8.2411.2,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
|
|
@ -1,21 +1,3 @@
|
|||
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"
|
||||
|
|
@ -42,5 +24,27 @@ 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,5 +1,4 @@
|
|||
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.2505.5"
|
||||
gitInitVersion: "1.4.0"
|
||||
version: "8.2411.2"
|
||||
gitInitVersion: "1.3.0"
|
||||
runAsNonRoot: true
|
||||
ports:
|
||||
server: 8988
|
||||
|
|
@ -38,14 +38,13 @@ spec:
|
|||
startupProbe:
|
||||
server:
|
||||
tcpSocket: true
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 4
|
||||
failureThreshold: 30
|
||||
failureThreshold: 50
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: "50%"
|
||||
git:
|
||||
tag: "r-5e17b7ae74eadb8800587a4f4db74406a7e21e95"
|
||||
tag: "r-ac938692d8edd6d7a3c23c703a8b0ad0b4510414"
|
||||
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.2505.5,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
"-Dotel.resource.attributes=service.version=8.2411.2,service.instance.id=$HOSTNAME"
|
||||
)
|
||||
|
|
@ -11,7 +11,7 @@ application.language.cookie.it=LANG:it:.agov-d.azure.adnovum.net
|
|||
application.language.cookie.rm=LANG:rm:.agov-d.azure.adnovum.net
|
||||
application.languages=de,fr,it,rm,en
|
||||
application.loginapp.current=
|
||||
application.loginapp.default=Auth_Realm_Main_IDP
|
||||
application.loginapp.default=Auth_Realm_Dimilar
|
||||
application.loginapp.override=header:channel
|
||||
application.package.name=nevislogrend
|
||||
application.render.content.type=text/html; charset=UTF-8
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue