...
Bloc de code |
---|
build.gradle
gradle.properties
gradle/springboot.gradle
lib/jcifs-ext.jar
src/main/resources/static/images/cas-logo.png
src/main/resources/templates/fragments/footer.html
src/main/resources/templates/fragments/pmlinks.html
src/main/resources/templates/layout.html |
Notre build.gradle comporte les modules supplémentaires que l'on a décidé décidés d'ajouter.
Pour les modules ESUP, on a utilisé https://jitpack.io , on a ainsi ajouté dans repositories :
...
Bloc de code |
---|
####################### ## Parametre serveur ## ####################### cas.server.name: https://cas.univ-rouen.fr cas.server.prefix: https://cas.univ-rouen.fr cas.audit.ignoreAuditFailures=false cas.audit.appCode=CAS cas.audit.numberOfDaysInHistory=30 cas.audit.includeValidationAssertion=false cas.audit.alternateServerAddrHeaderName= cas.audit.engine.alternate-client-addr-header-name=X-Forwarded-For cas.audit.useServerHostAddress=false # clearpass cas.clearpass.cacheCredential=true cas.clearpass.crypto.encryption.key=xxxxxxxxxxxxxxxxxxxxxxxxx cas.clearpass.crypto.signing.key=xxxxxxxxxxxxxxxxxxxxxxxxx # décommenter pour tester le modifs html sans redémarrer tomcat (doit être égal à false) spring #spring.thymeleaf.cache=truefalse ### On active le cache pour le formulaire d'authentication (par défaut:true) # When true, will inject the following headers into the response for non-static resources. # Cache-Control: no-cache, no-store, max-age=0, must-revalidate # Pragma: no-cache # Expires: 0 cas.http-web-request.header.cache=true ### Modification du timeout pour l'affichage du session report dans le dashboard (par défaut: 5s) cas.http-client.asyncTimeout=PT60S cas.http-client.connectionTimeout=PT5S cas.http-client.readTimeout=PT5S ### Valeur d'expiration des TGTs # Set to a negative value to never expire tickets cas.ticket.tgt.primary.max-time-to-live-in-seconds=1209600 cas.ticket.tgt.primary.time-to-kill-in-seconds=28800 ### Valeur expiration tickets service/proxy : 10sec par défaut, on augmente car latence sur les sogo ... cas.ticket.pt.timeToKillInSeconds=60 cas.ticket.st.timeToKillInSeconds=60 # rememberme cas.ticket.tgt.rememberMe.enabled=true cas.ticket.tgt.rememberMe.timeToKillInSeconds=1209600 # rememberme 2 semaines ok pour le cookie tgc.remember.me.maxAge=1209600 cas.tgc.rememberMeMaxAge=1209600 # pinToSession = false pour que ça fonctionne même si chgt d'IP par exemple. cas.tgc.pinToSession=false # Redirection vers l'url du service au lieu de la page de déconnexion du cas après logout cas.logout.follow-service-redirects=true ######### ## SLO ## ######### cas.slo.asynchronous=true cas.slo.disabled=false ###################### ## Access dashboard ## ###################### # https://apereo.github.io/2018/11/06/cas6-admin-endpoints-security/ management.endpoints.web.exposure.include=* management.endpoints.enabled-by-default=true cas.monitor.endpoints.endpoint.defaults.access=IP_ADDRESS cas.monitor.endpoints.endpoint.defaults.requiredIpAddresses=127.0.0.1,10.0.0.3 ######### ## Log ## ######### logging.config: file:/etc/cas/config/log4j2.xml ############################# ## parametre generique CAS ## ############################# cas.authn.accept.users= # Liste des atributs à renvoyer par défaut (si non défini dans le service) cas.authn.attributeRepository.core.defaultAttributesToRelease=uid,mail,displayName,eduPersonPrincipalName,eduPersonAffiliation,sn,givenname # throttle (pour limiter le nombre de tentative d'authentification) cas.authn.throttle.failure.range-seconds=30 cas.authn.throttle.failure.threshold=12 # pour throttle sur IP *et* username cas.authn.throttle.core.username-parameter=username ########################### ## Authentification LDAP ## ########################### cas.authn.ldap[0].name=openldap cas.authn.ldap[0].order=0 cas.authn.ldap[0].type=DIRECT cas.authn.ldap[0].ldapUrl=ldap://ldap.univ-rouen.fr ldap://ldap-clone.univ-rouen.fr cas.authn.ldap[0].baseDn=ou=people,dc=univ-rouen,dc=fr cas.authn.ldap[0].searchFilter=(&(uid={user})(objectclass=eduPerson)) cas.authn.ldap[0].dnFormat=uid=%s,ou=people,dc=univ-rouen,dc=fr cas.authn.ldap[0].connectionStrategy=ROUND_ROBIN cas.authn.ldap[0].useSsl=false cas.authn.ldap[0].useStartTls=false cas.authn.ldap[0].connectTimeout=20 cas.authn.ldap[0].subtreeSearch=false #cas.authn.ldap[0].principalAttributeId=uid # limite le nombre d'attributs demandés (c'est l'attribute filter pour le ldapsearch)lors du bind cas.authn.ldap[0].principalAttributeList=uid,mail,displayName,eduPersonPrincipalName,eduPersonAffiliation,radiusFilterId,memberOf # le cache utilise en clef un hash non injectif (collisions possibles) cas.authn.attribute-repository.core.expiration-time = 0 # Attribute repository pour récupérer les attributs (utile si authentification autre que openldap) cas.authn.attributeRepository.maximum-cache-size=0 # hack pour ne PAS récupérer TOUS les attributs LDAP (permet aussi de mapper les attributs, ex: xxx.ldap[0].attributes.nom=givenName) # pour spnego, le .* DOIT être évité #cas.authn.attributeRepository.ldap[0].attributes.*= cas.authn.attributeRepository.ldap[0].attributes.uid = uid cas.authn.attributeRepository.ldap[0].attributes.mail = mail cas.authn.attributeRepository.ldap[0].attributes.displayName = displayName cas.authn.attributeRepository.ldap[0].attributes.eduPersonPrincipalName = eduPersonPrincipalName cas.authn.attributeRepository.ldap[0].attributes.eduPersonAffiliation = eduPersonAffiliation cas.authn.attributeRepository.ldap[0].attributes.radiusFilterId = radiusFilterId cas.authn.attributeRepository.ldap[0].attributes.memberOf = memberOf cas.authn.attributeRepository.ldap[0].attributes.dn = dn cas.authn.attributeRepository.ldap[0].attributes.givenname = givenname cas.authn.attributeRepository.ldap[0].attributes.sn = sn cas.authn.attributeRepository.ldap[0].attributes.mobile = mobile cas.authn.attributeRepository.ldap[0].ldapUrl=ldap://ldap.univ-rouen.fr cas.authn.attributeRepository.ldap[0].connectionStrategy=ROUND_ROBIN cas.authn.attributeRepository.ldap[0].order=0 cas.authn.attributeRepository.ldap[0].useSsl=false cas.authn.attributeRepository.ldap[0].useStartTls=false cas.authn.attributeRepository.ldap[0].connectTimeout=20 cas.authn.attributeRepository.ldap[0].baseDn=ou=people,dc=univ-rouen,dc=fr cas.authn.attributeRepository.ldap[0].searchFilter=(&(uid={user})(objectclass=eduPerson)) cas.authn.attributeRepository.ldap[0].subtreeSearch=false cas.authn.attributeRepository.ldap[0].bindDn=cn=cas,dc=univ-rouen,dc=fr cas.authn.attributeRepository.ldap[0].bindCredential=xxxxxxxxxxxxxxxxxxxxxxxxx cas.authn.attributeRepository.ldap[0].poolPassivator=NONE cas.authn.attributeRepository.ldap[0].minPoolSize=2 cas.authn.attributeRepository.ldap[0].maxPoolSize=10 cas.authn.attributeRepository.ldap[0].validateOnCheckout=true cas.authn.attributeRepository.ldap[0].validatePeriodically=true cas.authn.attributeRepository.ldap[0].validatePeriod=60 cas.authn.attributeRepository.ldap[0].validateTimeout=30 cas.authn.attributeRepository.ldap[0].failFast=true cas.authn.attributeRepository.ldap[0].idleTime=300 cas.authn.attributeRepository.ldap[0].prunePeriod=300 cas.authn.attributeRepository.ldap[0].blockWaitTime=300 cas.authn.attributeRepository.ldap[0].providerClass=org.ldaptive.provider.unboundid.UnboundIDProvider ## ETAT de sante LDAPs cas.authn.ldap[0].validator.type=SEARCH cas.authn.ldap[0].validator.baseDn=ou=people,dc=univ-rouen,dc=fr cas.authn.ldap[0].validator.searchFilter=(objectClass=organizationalUnit) cas.authn.ldap[0].validator.scope=OBJECT cas.authn.ldap[0].validator.attributeName=objectClass cas.authn.ldap[0].validator.attributeValues=top cas.authn.ldap[0].validator.dn=ou=people,dc=univ-rouen,dc=fr ## Passivator (utilisé si utilisation de DIRECT ou AUTHENTICATED pour l'authn Ldap) # permet de passifier les connexions dans le pool (https://apereo.github.io/cas/56.24.x/installation/Configuration-Properties.html#passivatorsintegration/Attribute-Release-Consent-Storage-LDAP.html#configuration) cas.authn.ldap[0].poolPassivator=NONE cas.authn.ldap[0].minPoolSize=1 cas.authn.ldap[0].maxPoolSize=10 cas.authn.ldap[0].validateOnCheckout=false cas.authn.ldap[0].validatePeriodically=true cas.authn.ldap[0].validatePeriod=60 cas.authn.ldap[0].validateTimeout=30 cas.authn.ldap[0].failFast=true cas.authn.ldap[0].idleTime=300 cas.authn.ldap[0].prunePeriod=300 cas.authn.ldap[0].blockWaitTime=300 ###################### ## SPNEGO ## ###################### cas.authn.spnego.mixedModeAuthentication=true #cas.authn.spnego.supportedBrowsers=MSIE,Trident,Firefox,AppleWebKit cas.authn.spnego.supportedBrowsers=Firefox cas.authn.spnego.send401OnAuthenticationFailure=true cas.authn.spnego.ntlmAllowed=false cas.authn.spnego.principalWithDomainName=false cas.authn.spnego.name=spnego cas.authn.spnego.ntlm=false cas.authn.spnego.order=0 cas.authn.spnego.system.kerberosConf=file:/etc/krb5.conf cas.authn.spnego.system.loginConf=file:/etc/cas/config/login.conf cas.authn.spnego.system.kerberosRealm=UR.UNIV-ROUEN.FR cas.authn.spnego.system.kerberosDebug=false cas.authn.spnego.system.useSubjectCredsOnly=false cas.authn.spnego.system.kerberosKdc=10.0.0.12 cas.authn.spnego.properties[0].jcifsUsername=cas-spnego cas.authn.spnego.properties[0].jcifsDomainController=notread.univ-rouen.fr cas.authn.spnego.properties[0].jcifsDomain=UR.UNIV-ROUEN.FR cas.authn.spnego.properties[0].jcifsServicePassword=xxxxxxxxxxxxxxxxxxxxxxxxx cas.authn.spnego.properties[0].jcifsPassword=xxxxxxxxxxxxxxxxxxxxxxxxx cas.authn.spnego.properties[0].jcifsServicePrincipal=HTTP/nommachinecas.univ-rouen.fr@UR.UNIV-ROUEN.FR cas.authn.spnego.properties[0].cachePolicy=600 cas.authn.spnego.properties[0].timeout=300000 cas.authn.spnego.properties[0].jcifsNetbiosWins= ### SPNEGO Client Selection Strategy cas.authn.spnego.hostNameClientActionStrategy=hostnameSpnegoClientAction ### SPNEGO Client Selection Hostname cas.authn.spnego.alternativeRemoteHostAttribute=alternateRemoteHeader # IPs internes sans VPN ? TODO : à fixer ... :-/ #cas.authn.spnego.ipsToCheckPattern=10\.[1-9][0-9]\d{0,3}\.[0-9]\d{0,3}\.[0-9]\d{0,3} cas.authn.spnego.ipsToCheckPattern=.+ cas.authn.spnego.dnsTimeout=2000 # hostname du boitier vpn à exclure de spnego #cas.authn.spnego.hostNamePatternString=^(?!.*vpn[ct]-aa\.univ-rouen\.fr).*$ cas.authn.spnego.hostNamePatternString=^(?!.*(vpn|vqn|vrn|vsn)-aa\.univ-rouen\.fr).*$ ###################### ## Service REGISTRY ## ###################### # Pour relire les services à la volée cas.serviceRegistry.watcherEnabled=false cas.serviceRegistry.schedule.repeatIntervalcas.serviceRegistry.schedule.repeatInterval=10000 cas.serviceRegistry.schedule.startDelay=1000 ## Utile pour créer les premiers services en BDD lorsqu'on utilise cas-management-overlay # Auto-initialize the registry from default JSON service definitions # cas.serviceRegistry.initFromJson=false ####################################################### ## Parametrage JSON pour la persistence des services ## ####################################################### cas.serviceRegistry.json.location=file:/etc/cas/services ######################################################################### ## Paramétrage WebFlow (Client-side Sessions) ## ######################################################################### cas.webflow.crypto.enabled=true cas.webflow.crypto.signing.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx cas.webflow.crypto.signing.keySize=512 cas.webflow.crypto.encryption.keySize=16 cas.webflow.crypto.encryption.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx cas.webflow.crypto.alg=AES ##################### ## Paramétrage TGC ## ##################### # Positione les flags secure et httpOnly cas.tgc.secure=true cas.tgc.httpOnly=true # Crypte Chiffre les TGC ... à noter que lorsque cryptéchiffré, le TGC contient l'ip et le user-agent du demandeur. # Si pour x raisons il y a un changement d'user-agent (rare) ou d'adresse IP (plus fréquent avec l'itinérance) pendant la session, # CAS le détecte lors de la présentation du cookie et celui-ci est invalidé. Une réauthentification est alors nécessaire. # true pour activer cas.tgc.crypto.enabled=true cas.tgc.crypto.encryption.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx cas.tgc.crypto.signing.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx ####################################################### ## Parametrage REDIS pour la persistence des tickets ## ####################################################### cas.ticket.registry.redis.host=localhost cas.ticket.registry.redis.database=0 cas.ticket.registry.redis.port=6379 cas.ticket.registry.redis.password= cas.ticket.registry.redis.timeout=2000 cas.ticket.registry.redis.useSsl=false cas.ticket.registry.redis.crypto.enabled=false cas.ticket.registry.redis.crypto.signing.key= cas.ticket.registry.redis.crypto.signing.keySize=512 cas.ticket.registry.redis.crypto.encryption.key= cas.ticket.registry.redis.crypto.encryption.keySize=16 cas.ticket.registry.redis.crypto.alg=AES ############################## ## Locale ## ############################## cas.locale.defaultValue=fr ############################## ## ESUP-SMSU ## ############################## cas.smsProvider.rest.url=http://esup-smsu-api.univ-rouen.fr/apereo-cas #cas.smsProvider.rest.url=http://esup-smsu-api-test.univ-rouen.fr/apereo-cas cas.smsProvider.rest.basicAuthUsername=sms-cas-account cas.smsProvider.rest.basicAuthPassword=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx ############################## ## MFA ## ############################## # cas.authn.mfa.globalFailureModeglobalProviderId=OPENmfa-esupotp cas.authn.mfa.groovy-script.location=file:/etc/cas/config/mfaGroovyTrigger.groovy cas.authn.mfa.simple.name=smsu cas.authn.mfa.simple.order=0 cas.authn.mfa.simple.timeToKillInSeconds=300 cas.authn.mfa.simple.sms.from=Université de Rouen Normandie cas.authn.mfa.simple.sms.text=Bonjour, voici le code SMS requis pour votre authentification CAS : %s cas.authn.mfa.simple.sms.attributeName=mobile # Add translations, you will need to check what are the default from CAS "Message Bundles" properties # Add translations, you will need to check what are the default from CAS "Message Bundles" properties cas.messageBundle.baseNames=classpath:custom_messages,classpath:messages,classpath:esupnfccas_messages,classpath:esupotp_message ############################## ## MFA TRUSTED DEVICES ## ############################## cas.authn.mfa.trusted.mongo.clientUri=mongodb://localhost/cas-mongo-database cas.authn.mfa.trusted.authenticationContextAttribute=isFromTrustedMultifactorAuthentication cas.authn.mfa.trusted.deviceRegistrationEnabled=true cas.authn.mfa.trusted.expiration=7 cas.authn.mfa.trusted.timeUnit=DAYS cas.authn.mfa.trusted.crypto.enabled=true cas.authn.mfa.trusted.crypto.encryption.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx cas.authn.mfa.trusted.crypto.signing.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx cas.authn.mfa.trusted.deviceFingerprint.cookie.crypto.encryption.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx cas.authn.mfa.trusted.deviceFingerprint.cookie.crypto.signing.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
...
Bloc de code |
---|
import java.util.* class SampleGroovyEventResolver { def String run(service, registeredService, authentication, httpRequest, logger, ... other_args) { def mobile = authentication.principal.attributes.mobile def ip = httpRequest.getRemoteAddr() def memberOf = authentication.principal.attributes.memberOf /* logger.info("ip : [{}]", httpRequest.getRemoteAddr()) logger.info("mobile : [{}]", mobile) logger.info("registeredService.id : [{}]", registeredService.id) */ if ((int)registeredService.id in [// d'abord les services avec MFA obligatoire if ((int)registeredService.id in [22] && 'cn=from.grouper.admin,ou=groups,dc=univ-rouen,dc=fr' in memberOf) { logger.warn("mfa required for grouper !", authentication.principal.id) return "mfa-esupotp" } // maintenant les services avec MFA uniquement si activé if(!('cn=from.cas.otp,ou=groups,dc=univ-rouen,dc=fr' in memberOf)) { return null; } // l'utilisateur n'a pas activé le MFA (fonctionne grâce à un groupe LDAP reflétant l'activation du MFA dans esup-otp-manager) return null; } if ((int)registeredService.id in [12,13,14,18,21,22] && !ip.startsWith("10.0.1.")) { logger.warn("mfa for [{}] !", authentication.principal.id) return "mfa-esupotp" } if ((int)registeredService.id in [11, 18] && !ip.startsWith("10.0.1.") && 'cn=for.multipass.admin,ou=groups,dc=univ-rouen,dc=fr' in memberOf) { logger.warn("mfa for [{}] !", authentication.principal.id) return "mfa-esupotp" } return null } } |
...
La désacivtation des sessions au niveau de l'IdP est également nécessaire si on souhaite qu'effectivement chaque authentification d'un SP auprès de l'IdP soit réévaluée côté CAS (poure activation pour activation ou non du MFA dans notre cas) :
...
Pour l'ENT avec la partie esup-filemanager qui utilise les mécanismes de proxy-cas / clearpass, la fonctionnalité du passage de mot de passe en tant qu'attribut (chiffré) dit "clearpass" est configuré configurée ainsi dans le fichier /etc/cas/services/esup_filemanager_univ_rouen_fr-2.json
...
Bloc de code |
---|
<bean name="univ_rouen_cas_clearpass_auth" class="org.esupportail.portlet.filemanager.services.auth.cas.ClearPassUserCasAuthenticatorService" scope="session"> <property name="domain" value="ur"/> <property name="userCasAuthenticatorServiceRoot" ref="casUserAuthenticationServiceRoot"/> <property name="pkcs8Key" value="/opt/tomcat-esup/webapps/esup-filemanager/WEB-INF/classes/univ-rouen-esup-filemanager-private.p8"/> </bean> |
Rappel : la mise en oeuvre côté CAS (dont la génération des clefs) est documentée dans la documentation CAS - clearpass.
ldap policy
A noter l'usage de ldap policy à l'Université de Rouen Normandie : on ajoute l'attribut ldap PwdAccountLockedTime pour verrouiller les comptes détectées détectés comme corrompues corrompus de manière automatique (via l'usage de fail2ban notamment).
Apereo CAS avec cas-server-support-ldap supporte en effet les codes d'erreur portés par les ldap policy sur les bind ldap.
Pb de lisibilité des entrées username/password du formulaire de login dans Rocket.Chat
Cf la copie d'écran ci-dessous, le formulaire d'authentification de CAS 6.4 pose un problème de lisibilité au travers du client lourd Rocket.Chat.
La cause est indéterminée, le problème est sans doute lié à electron utilisé par le client lourd Rocket.Chat.
Pour contourner la chose, et en s'inspirant de https://github.com/material-components/material-components-web/issues/4447 nous avons ajouté le code javascript suivant dans notre footer.html :
Bloc de code | ||||
---|---|---|---|---|
| ||||
window.setTimeout(() => {
document
.querySelectorAll('.mdc-text-field__input')
.forEach(el => {
const textField = el.parentNode;
const label = textField.querySelector('.mdc-floating-label');
const spanOutline = textField.querySelector('.mdc-notched-outline');
if (label) {
label.classList.add('mdc-floating-label--float-above');
}
if (spanOutline) {
spanOutline.classList.add('mdc-notched-outline--notched');
}
if (textField.MDCTextField) {
textField.MDCTextField.foundation_.notchOutline(true);
}
});
}, 300); |
Ainsi au bout de 300ms les labels Identifiant / Mot de passe se positionnent au dessus par défaut et ne restent donc pas/plus en placeholder.
Le comportement global est un peu moins ergonomique mais ne pose plus de problème dans Rocket.Chat ainsi que dans d'autres contextes d'usage (utilisation d'extension navigateur comme Bitwarden par exemple).
Suppression des sessions CAS d'un utilisateur (en cas de phishing ou autre)
CAS 6.4 propose via un actuator endpoint (API REST poussée par spring boot) la possibilité de "détruire" une session d'un utilisateur via un appel REST.
Cet appel direct est beaucoup plus performant que ce qu'on pouvait faire avec la 6.0.
De plus, CAS déclenche également maintenant la procédure de Single LogOut (SLO) au travers de cet appel (attention toutefois à prendre une version > 6.4.5 pour que le TGT soit bien supprimé / bug introduit en cours de 6.4).
On implémente maintenant la destruction des sessions CAS d'un utilisateur depuis notre application de gestion de comptes (codée en java/spring) ainsi :
Bloc de code | ||||
---|---|---|---|---|
| ||||
public class CasService {
protected Logger log = Logger.getLogger(CasService.class);
RestTemplate restTemplate;
String casSsoSessionsUrl;
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public void setCasUrl(String casUrl) {
casSsoSessionsUrl = casUrl + "/actuator/ssoSessions?type={type}&username={username}";
}
public synchronized String destroySsoSessions(String login) {
log.info(String.format("Call Cas Destroy tickets for user %s", login));
Map<String, String> urlVariables = new HashMap<String, String>();
urlVariables.put("type", "ALL");
urlVariables.put("username", login);
ResponseEntity resp = restTemplate.exchange(casSsoSessionsUrl, HttpMethod.DELETE, null, String.class, urlVariables);
String msg = String.format("Cas Destroyed tickets of user %s - resp : %s", login, resp.getBody());
log.info(msg);
return msg;
}
}
|
Note : pour que la récupération du JSON de retour puisse se faire en simple String comme proposé ici, on a un RestTemplate défini avec en messageConverter org.springframework.http.converter.StringHttpMessageConverter