diff --git a/app/actions/LoginAction.scala b/app/actions/LoginAction.scala index 2aa8e3e6..ff4af96e 100644 --- a/app/actions/LoginAction.scala +++ b/app/actions/LoginAction.scala @@ -41,7 +41,7 @@ object LoginAction { List( Keys.Session.signupId, Keys.Session.signupLoginExpiresAt, - Keys.Session.signupAgentConnectSubject + Keys.Session.signupProConnectSubject ) } diff --git a/app/controllers/LoginController.scala b/app/controllers/LoginController.scala index 45f92d65..d62d255c 100644 --- a/app/controllers/LoginController.scala +++ b/app/controllers/LoginController.scala @@ -11,11 +11,11 @@ import helper.Time import java.time.Instant import javax.inject.{Inject, Singleton} import models.{ - AgentConnectClaims, Authorization, Error, EventType, LoginToken, + ProConnectClaims, SignupRequest, User, UserSession @@ -31,10 +31,10 @@ import scala.jdk.CollectionConverters._ import scala.util.Try import serializers.Keys import services.{ - AgentConnectService, EventService, NotificationService, PasswordService, + ProConnectService, ServicesDependencies, SignupService, TokenService, @@ -44,7 +44,6 @@ import views.home.LoginPanel @Singleton class LoginController @Inject() ( - agentConnectService: AgentConnectService, val config: AppConfig, val controllerComponents: ControllerComponents, dependencies: ServicesDependencies, @@ -53,6 +52,7 @@ class LoginController @Inject() ( tokenService: TokenService, eventService: EventService, passwordService: PasswordService, + proConnectService: ProConnectService, signupService: SignupService, )(implicit ec: ExecutionContext, webJarsUtil: WebJarsUtil) extends BaseController @@ -246,9 +246,9 @@ class LoginController @Inject() ( } def loginPage: Action[AnyContent] = Action { request => - val agentConnectErrorMessage = ( - request.flash.get(agentConnectErrorTitleFlashKey), - request.flash.get(agentConnectErrorDescriptionFlashKey) + val proConnectErrorMessage = ( + request.flash.get(proConnectErrorTitleFlashKey), + request.flash.get(proConnectErrorDescriptionFlashKey) ) match { case (Some(title), Some(description)) => Some((title, description)) case (Some(title), None) => Some((title, "")) @@ -257,21 +257,21 @@ class LoginController @Inject() ( } Ok( views.login.page( - featureAgentConnectEnabled = config.featureAgentConnectEnabled, - agentConnectErrorMessage = agentConnectErrorMessage + featureProConnectEnabled = config.featureProConnectEnabled, + proConnectErrorMessage = proConnectErrorMessage ) ) } /** Will send back a Redirect with the states in the session */ - def agentConnectLoginRedirection: Action[AnyContent] = Action.async { implicit request => - if (config.featureAgentConnectEnabled) - agentConnectService.authenticationRequestUrl.value + def proConnectLoginRedirection: Action[AnyContent] = Action.async { implicit request => + if (config.featureProConnectEnabled) + proConnectService.authenticationRequestUrl.value .flatMap( _.fold( error => - logAgentConnectError(error) >> - IO(agentConnectService.resetSessionKeys(agentConnectErrorRedirect(error))), + logProConnectError(error) >> + IO(proConnectService.resetSessionKeys(proConnectErrorRedirect(error))), { case (authenticationRequestUrl, withResultSettings) => IO(withResultSettings(Redirect(authenticationRequestUrl), request)) } @@ -283,17 +283,17 @@ class LoginController @Inject() ( } /** OAuth 2 / OpenID Connect "redirect_uri" */ - def agentConnectAuthenticationResponseCallback: Action[AnyContent] = Action.async { + def proConnectAuthenticationResponseCallback: Action[AnyContent] = Action.async { implicit request => - def noAccountError(userInfo: AgentConnectService.UserInfo) = EitherT.right[Error]( + def noAccountError(userInfo: ProConnectService.UserInfo) = EitherT.right[Error]( IO.blocking( eventService.logSystem( - EventType.AgentConnectUnknownEmail, - s"Connexion AgentConnect réussie avec l'email ${userInfo.email}, mais aucun compte actif à cette adresse [subject: ${userInfo.subject}]", + EventType.ProConnectUnknownEmail, + s"Connexion ProConnect réussie avec l'email ${userInfo.email}, mais aucun compte actif à cette adresse [subject: ${userInfo.subject}]", ) ) >> IO( - agentConnectErrorRedirectResult( + proConnectErrorRedirectResult( errorTitle = s"Aucun compte à l’adresse « ${userInfo.email} »", errorDescription = "Aucun compte actif n’est associé à cette adresse email. Veuillez noter que la création de compte doit être effectuée par votre responsable de structure ou départemental.", @@ -302,22 +302,22 @@ class LoginController @Inject() ( ) def logSignupIn( signup: SignupRequest, - userInfo: AgentConnectService.UserInfo + userInfo: ProConnectService.UserInfo ): EitherT[IO, Error, Result] = EitherT.right[Error]( IO.blocking( eventService.logSystem( - EventType.AgentConnectSignupLoginSuccessful, - s"Identification via AgentConnect, préinscription ${signup.id} [subject: ${userInfo.subject}]" + EventType.ProConnectSignupLoginSuccessful, + s"Identification via ProConnect, préinscription ${signup.id} [subject: ${userInfo.subject}]" ) ) >> IO.realTimeInstant - .flatMap(AgentConnectService.calculateExpiresAt) + .flatMap(ProConnectService.calculateExpiresAt) .map(expiresAt => Redirect(routes.SignupController.signupForm) .addingToSession( Keys.Session.signupId -> signup.id.toString, - Keys.Session.signupAgentConnectSubject -> userInfo.subject, + Keys.Session.signupProConnectSubject -> userInfo.subject, Keys.Session.signupLoginExpiresAt -> expiresAt.getEpochSecond.toString, ) ) @@ -325,8 +325,8 @@ class LoginController @Inject() ( def logUserIn( user: User, - idToken: AgentConnectService.IDToken, - userInfo: AgentConnectService.UserInfo + idToken: ProConnectService.IDToken, + userInfo: ProConnectService.UserInfo ): EitherT[IO, Error, Result] = { val userRights = Authorization.readUserRights(user) if (user.disabled) { @@ -335,12 +335,12 @@ class LoginController @Inject() ( EitherT.right[Error]( IO.blocking( eventService.log( - EventType.AgentConnectLoginDeactivatedUser, - s"Identification via AgentConnect de l'utilisateur désactivé ${user.id} [subject: ${userInfo.subject}]" + EventType.ProConnectLoginDeactivatedUser, + s"Identification via ProConnect de l'utilisateur désactivé ${user.id} [subject: ${userInfo.subject}]" )(requestWithUserData) ) >> IO( - agentConnectErrorRedirectResult( + proConnectErrorRedirectResult( errorTitle = s"Compte « ${userInfo.email} » désactivé", errorDescription = s"Le compte lié à l’adresse email que vous avez renseignée « ${userInfo.email} » est désactivé. Le responsable de votre structure ou le responsable départemental peut réactiver le compte. Alternativement, si vous possédez un compte actif, vous pouvez utiliser l’adresse email correspondante." @@ -352,7 +352,7 @@ class LoginController @Inject() ( request.session .get(Keys.Session.signupLoginExpiresAt) .flatMap(epoch => Try(Instant.ofEpochSecond(epoch.toLong)).toOption) match { - case None => AgentConnectService.calculateExpiresAt(now) + case None => ProConnectService.calculateExpiresAt(now) case Some(expiresAt) => IO.pure(expiresAt) } ) @@ -361,7 +361,7 @@ class LoginController @Inject() ( _ <- EitherT.right[Error](IO.blocking(userService.recordLogin(user.id))) session <- userService.createNewUserSession( user.id, - UserSession.LoginType.AgentConnect, + UserSession.LoginType.ProConnect, expiresAt, request.remoteAddress, request.headers.get(USER_AGENT), @@ -371,13 +371,13 @@ class LoginController @Inject() ( new RequestWithUserData(user, userRights, session.some, request) val idTokenClaimsNames = idToken.signedToken.getPayload.keySet.asScala.toSet val userInfoClaimsNames = userInfo.signedToken.getPayload.keySet.asScala.toSet - val agentConnectInfos = s"subject: ${userInfo.subject} ; " + + val proConnectInfos = s"subject: ${userInfo.subject} ; " + s"IDToken claims: $idTokenClaimsNames ; " + s"UserInfo claims: $userInfoClaimsNames" IO.blocking( eventService.log( - EventType.AgentConnectUserLoginSuccessful, - s"Identification via AgentConnect, utilisateur ${user.id} [$agentConnectInfos]" + EventType.ProConnectUserLoginSuccessful, + s"Identification via ProConnect, utilisateur ${user.id} [$proConnectInfos]" )(requestWithUserData) ) } @@ -389,15 +389,15 @@ class LoginController @Inject() ( } } - agentConnectService + proConnectService .handleAuthenticationResponse(request) { error => - logAgentConnectError(error) >> - IO(agentConnectErrorRedirect(error)) + logProConnectError(error) >> + IO(proConnectErrorRedirect(error)) } { case (idToken, userInfo) => IO.realTimeInstant.flatMap { now => IO.blocking(userService.byEmail(userInfo.email, includeDisabled = true)).flatMap { user => - val claims = AgentConnectClaims( + val claims = ProConnectClaims( subject = userInfo.subject, email = userInfo.email, givenName = userInfo.givenName, @@ -408,7 +408,7 @@ class LoginController @Inject() ( lastAuthTime = idToken.authTime.map(Instant.ofEpochSecond), userId = user.map(_.id), ) - EitherT(userService.saveAgentConnectClaims(claims)) + EitherT(userService.saveProConnectClaims(claims)) .flatMap(_ => user match { case None => @@ -422,7 +422,7 @@ class LoginController @Inject() ( .valueOrF(error => IO.blocking(eventService.logErrorNoUser(error)) .as( - agentConnectErrorRedirectResult( + proConnectErrorRedirectResult( errorTitle = "Erreur interne", errorDescription = "Une erreur interne est survenue. Celle-ci étant possiblement temporaire, nous vous invitons à réessayer plus tard.", @@ -803,29 +803,29 @@ class LoginController @Inject() ( ) } - private val agentConnectErrorTitleFlashKey = "agentConnectErrorTitle" - private val agentConnectErrorDescriptionFlashKey = "agentConnectErrorDescription" + private val proConnectErrorTitleFlashKey = "proConnectErrorTitle" + private val proConnectErrorDescriptionFlashKey = "proConnectErrorDescription" - private def agentConnectErrorRedirect(error: AgentConnectService.Error): Result = { - val (title, description) = agentConnectErrorMessage(error) - agentConnectErrorRedirectResult(title, description) + private def proConnectErrorRedirect(error: ProConnectService.Error): Result = { + val (title, description) = proConnectErrorMessage(error) + proConnectErrorRedirectResult(title, description) } - private def agentConnectErrorRedirectResult( + private def proConnectErrorRedirectResult( errorTitle: String, errorDescription: String ): Result = Redirect(routes.LoginController.loginPage) .flashing( - agentConnectErrorTitleFlashKey -> errorTitle, - agentConnectErrorDescriptionFlashKey -> errorDescription + proConnectErrorTitleFlashKey -> errorTitle, + proConnectErrorDescriptionFlashKey -> errorDescription ) - private val agentConnectErrorMessage = - s"Une erreur s’est produite lors de notre communication avec AgentConnect. Il n’est donc pas possible de vous connecter via AgentConnect. L’erreur étant probablement temporaire, vous pouvez réessayer plus tard, ou utiliser la connexion par lien à usage unique disponible sur notre page d’accueil. Si l’erreur venait à persister, vous pouvez contacter le support d’Administration+." + private val proConnectErrorMessage = + s"Une erreur s’est produite lors de notre communication avec ProConnect. Il n’est donc pas possible de vous connecter via ProConnect. L’erreur étant probablement temporaire, vous pouvez réessayer plus tard, ou utiliser la connexion par lien à usage unique disponible sur notre page d’accueil. Si l’erreur venait à persister, vous pouvez contacter le support d’Administration+." - private def agentConnectErrorMessage(error: AgentConnectService.Error): (String, String) = { - import AgentConnectService.Error._ + private def proConnectErrorMessage(error: ProConnectService.Error): (String, String) = { + import ProConnectService.Error._ error match { case FailedGeneratingFromSecureRandom(_) | AuthResponseMissingStateInSession | @@ -842,228 +842,228 @@ class LoginController @Inject() ( ) => ( "Erreur : échec de la connexion", - "Votre connexion via AgentConnect a échoué, vous pouvez réessayer ou utiliser la connexion par lien à usage unique disponible sur notre page d’accueil" + "Votre connexion via ProConnect a échoué, vous pouvez réessayer ou utiliser la connexion par lien à usage unique disponible sur notre page d’accueil" ) // Note: TokenResponseError error codes: // - https://www.rfc-editor.org/rfc/rfc6749.html#section-5.2 - case _ => ("Erreur : impossible de communiquer avec AgentConnect", agentConnectErrorMessage) + case _ => ("Erreur : impossible de communiquer avec ProConnect", proConnectErrorMessage) } } - private def logAgentConnectError( - error: AgentConnectService.Error + private def logProConnectError( + error: ProConnectService.Error )(implicit request: Request[_]): IO[Unit] = IO.blocking { - import AgentConnectService.Error._ + import ProConnectService.Error._ val (eventType, description, additionalUnsafeData, exception) : (EventType, String, Option[String], Option[Throwable]) = error match { case FailedGeneratingFromSecureRandom(error) => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect - Impossible de générer des données aléatoire avec le CSPRNG", + EventType.ProConnectSecurityWarning, + "ProConnect - Impossible de générer des données aléatoire avec le CSPRNG", None, Some(error) ) case ProviderConfigurationRequestFailure(error) => ( - EventType.AgentConnectError, - "AgentConnect (Configuration) - La connexion à l'url de configuration AgentConnect a échouée", + EventType.ProConnectError, + "ProConnect (Configuration) - La connexion à l'url de configuration ProConnect a échouée", None, Some(error) ) case ProviderConfigurationErrorResponse(status, body) => ( - EventType.AgentConnectError, - s"AgentConnect (Configuration) - L'url de configuration AgentConnect a renvoyé une erreur, status $status", + EventType.ProConnectError, + s"ProConnect (Configuration) - L'url de configuration ProConnect a renvoyé une erreur, status $status", Some(s"Body : $body"), None ) case ProviderConfigurationUnparsableJson(status, error) => ( - EventType.AgentConnectError, - s"AgentConnect (Configuration) - Impossible de lire le JSON reçu de l'url de configuration AgentConnect (status $status)", + EventType.ProConnectError, + s"ProConnect (Configuration) - Impossible de lire le JSON reçu de l'url de configuration ProConnect (status $status)", None, Some(error) ) case ProviderConfigurationInvalidJson(error) => ( - EventType.AgentConnectError, - s"AgentConnect (Configuration) - Formatage inattendu du JSON de configuration AgentConnect: ${error.errors}", + EventType.ProConnectError, + s"ProConnect (Configuration) - Formatage inattendu du JSON de configuration ProConnect: ${error.errors}", None, None ) case ProviderConfigurationInvalidIssuer(wantedIssuer, providedIssuer) => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect (Configuration) - Le champ iss de l'url de configuration AgentConnect ne correspond pas au notre", + EventType.ProConnectSecurityWarning, + "ProConnect (Configuration) - Le champ iss de l'url de configuration ProConnect ne correspond pas au notre", Some(s"Issuer attendu : '$wantedIssuer' ; Issuer reçu : '$providedIssuer'"), None ) case NotEnoughElapsedTimeBetweenDiscoveryCalls(lastFetchTime, now) => ( - EventType.AgentConnectError, - s"AgentConnect - Demande trop récente de rafraîchissement du cache de la configuration AgentConnect, dernière demande $lastFetchTime, date présente $now", + EventType.ProConnectError, + s"ProConnect - Demande trop récente de rafraîchissement du cache de la configuration ProConnect, dernière demande $lastFetchTime, date présente $now", None, None ) case AuthResponseMissingStateInSession => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect (Authorization Response) - La session de l'utilisateur n'a pas le state posé avant l'appel à AgentConnect", + EventType.ProConnectSecurityWarning, + "ProConnect (Authorization Response) - La session de l'utilisateur n'a pas le state posé avant l'appel à ProConnect", None, None ) case AuthResponseMissingNonceInSession => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect (Authorization Response) - La session de l'utilisateur n'a pas le nonce posé avant l'appel à AgentConnect", + EventType.ProConnectSecurityWarning, + "ProConnect (Authorization Response) - La session de l'utilisateur n'a pas le nonce posé avant l'appel à ProConnect", None, None ) case AuthResponseUnparseableState(requestState, error) => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect (Authorization Response) - Impossible de lire le state reçu", + EventType.ProConnectSecurityWarning, + "ProConnect (Authorization Response) - Impossible de lire le state reçu", Some(s"State reçu : $requestState"), Some(error) ) case AuthResponseInvalidState(sessionState, requestState) => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect (Authorization Response) - Le state reçu ne correspond pas au state en session", + EventType.ProConnectSecurityWarning, + "ProConnect (Authorization Response) - Le state reçu ne correspond pas au state en session", Some(s"State en session: $sessionState ; State reçu : $requestState"), None ) case AuthResponseMissingErrorQueryParam => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect (Authorization Response) - Erreur reçue mais le champ 'error' est manquant", + EventType.ProConnectSecurityWarning, + "ProConnect (Authorization Response) - Erreur reçue mais le champ 'error' est manquant", None, None ) case AuthResponseMissingStateQueryParam => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect (Authorization Response) - Erreur reçue mais le champ 'state' est manquant", + EventType.ProConnectSecurityWarning, + "ProConnect (Authorization Response) - Erreur reçue mais le champ 'state' est manquant", None, None ) case AuthResponseEndpointError(errorCode, errorDescription, errorUri) => ( - EventType.AgentConnectError, - s"AgentConnect (Authorization Response) - Erreur lors de l'authentification de l'utilisateur", + EventType.ProConnectError, + s"ProConnect (Authorization Response) - Erreur lors de l'authentification de l'utilisateur", Some(s"error: $errorCode ; error_description: $errorDescription ; error_uri: $errorUri"), None ) case JwksRequestFailure(error) => ( - EventType.AgentConnectError, - "AgentConnect (jwks) - La connexion à l'url jwks a échouée", + EventType.ProConnectError, + "ProConnect (jwks) - La connexion à l'url jwks a échouée", None, Some(error) ) case JwksUnparsableResponse(status, body, error) => ( - EventType.AgentConnectError, - s"AgentConnect (jwks) - Impossible de lire le JWK Set reçu (status $status)", + EventType.ProConnectError, + s"ProConnect (jwks) - Impossible de lire le JWK Set reçu (status $status)", Some(s"Body: $body"), Some(error) ) case TokenRequestFailure(error) => ( - EventType.AgentConnectError, - "AgentConnect (Token Request) - La connexion au token endpoint a échoué", + EventType.ProConnectError, + "ProConnect (Token Request) - La connexion au token endpoint a échoué", None, Some(error) ) case TokenResponseUnparsableJson(status, error) => ( - EventType.AgentConnectError, - s"AgentConnect (Token Response) - Impossible de lire le JSON (status $status)", + EventType.ProConnectError, + s"ProConnect (Token Response) - Impossible de lire le JSON (status $status)", None, Some(error) ) case TokenResponseInvalidJson(error) => ( - EventType.AgentConnectError, - s"AgentConnect (Token Response) - Formatage inattendu du JSON (status 200): ${error.errors}", + EventType.ProConnectError, + s"ProConnect (Token Response) - Formatage inattendu du JSON (status 200): ${error.errors}", None, None ) case TokenResponseErrorInvalidJson(error) => ( - EventType.AgentConnectError, - s"AgentConnect (Token Response) - Formatage inattendu du JSON (status 4xx): ${error.errors}", + EventType.ProConnectError, + s"ProConnect (Token Response) - Formatage inattendu du JSON (status 4xx): ${error.errors}", None, None ) case TokenResponseUnknown(status, body) => ( - EventType.AgentConnectError, - s"AgentConnect (Token Response) - Status inconnu $status", + EventType.ProConnectError, + s"ProConnect (Token Response) - Status inconnu $status", Some(s"Body: $body"), None ) case TokenResponseError(error) => ( - EventType.AgentConnectError, - "AgentConnect (Token Response) - Le serveur a renvoyé un JSON décrivant une erreur", + EventType.ProConnectError, + "ProConnect (Token Response) - Le serveur a renvoyé un JSON décrivant une erreur", Some(s"Erreur: $error"), None ) case TokenResponseInvalidTokenType(tokenType) => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect (Token Response) - Le token_type est invalide", + EventType.ProConnectSecurityWarning, + "ProConnect (Token Response) - Le token_type est invalide", Some(s"token_type: $tokenType"), None ) case InvalidIDToken(error, claimsNames) => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect (Token Response) - Le IDToken est invalide, mauvaise signature ou claims invalides", + EventType.ProConnectSecurityWarning, + "ProConnect (Token Response) - Le IDToken est invalide, mauvaise signature ou claims invalides", Some(s"Claims: $claimsNames"), Some(error) ) case IDTokenInvalidClaims(error, claimsNames) => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect (Token Response) - Le IDToken n'a pas de claim sub ou auth_time", + EventType.ProConnectSecurityWarning, + "ProConnect (Token Response) - Le IDToken n'a pas de claim sub ou auth_time", Some(s"Claims: $claimsNames"), error ) case UserInfoRequestFailure(error) => ( - EventType.AgentConnectError, - "AgentConnect (UserInfo Request) - La connexion a échoué", + EventType.ProConnectError, + "ProConnect (UserInfo Request) - La connexion a échoué", None, Some(error) ) case UserInfoResponseUnsuccessfulStatus(status, wwwAuthenticateHeader, body) => ( - EventType.AgentConnectError, - s"AgentConnect (UserInfo Response) - AgentConnect indique une erreur (status $status)", + EventType.ProConnectError, + s"ProConnect (UserInfo Response) - ProConnect indique une erreur (status $status)", Some(s"WWW-Authenticate: $wwwAuthenticateHeader ; Body: $body"), None ) case UserInfoResponseUnknownContentType(contentType) => ( - EventType.AgentConnectSecurityWarning, - "AgentConnect (UserInfo Response) - Le Content-Type reçu est inattendu", + EventType.ProConnectSecurityWarning, + "ProConnect (UserInfo Response) - Le Content-Type reçu est inattendu", Some(s"Content-Type: $contentType"), None ) case UserInfoInvalidClaims(error, claimsNames) => ( - EventType.AgentConnectError, - "AgentConnect (UserInfo Response) - Certaines claims sont manquantes (les claims nécessaires sont sub et email et celles nullables sont given_name, usual_name, uid, siret)", + EventType.ProConnectError, + "ProConnect (UserInfo Response) - Certaines claims sont manquantes (les claims nécessaires sont sub et email et celles nullables sont given_name, usual_name, uid, siret)", Some(s"Claims: $claimsNames"), error ) case InvalidJwsAlgorithm(invalidAlgorithm) => ( - EventType.AgentConnectSecurityWarning, - "L'algorithme de chiffrement utilisé par AgentConnect est inattendu", + EventType.ProConnectSecurityWarning, + "L'algorithme de chiffrement utilisé par ProConnect est inattendu", Some(s"Algorithme: $invalidAlgorithm"), None ) diff --git a/app/controllers/SignupController.scala b/app/controllers/SignupController.scala index f264e0d2..d9321a78 100644 --- a/app/controllers/SignupController.scala +++ b/app/controllers/SignupController.scala @@ -137,7 +137,7 @@ case class SignupController @Inject() ( LoginAction.readUserRights(user).flatMap { userRights => ( for { - loginType <- maybeLinkUserToAgentConnectClaims(user.id, request) + loginType <- maybeLinkUserToProConnectClaims(user.id, request) userSession <- userService .createNewUserSession( user.id, @@ -327,16 +327,16 @@ case class SignupController @Inject() ( ) ) - private def maybeLinkUserToAgentConnectClaims( + private def maybeLinkUserToProConnectClaims( userId: UUID, request: Request[_] ): EitherT[IO, Error, UserSession.LoginType] = - request.session.get(Keys.Session.signupAgentConnectSubject) match { + request.session.get(Keys.Session.signupProConnectSubject) match { case None => EitherT.rightT[IO, Error](UserSession.LoginType.MagicLink) case Some(subject) => EitherT( - userService.linkUserToAgentConnectClaims(userId, subject) - ).map(_ => UserSession.LoginType.AgentConnect) + userService.linkUserToProConnectClaims(userId, subject) + ).map(_ => UserSession.LoginType.ProConnect) } /** Note: parameter is curried to easily mark `Request` as implicit. */ @@ -406,7 +406,7 @@ case class SignupController @Inject() ( // (this case happen if the signup session has not been purged after user creation) ( for { - loginType <- maybeLinkUserToAgentConnectClaims( + loginType <- maybeLinkUserToProConnectClaims( existingUser.id, request ) diff --git a/app/models/EventType.scala b/app/models/EventType.scala index f22d3046..ea357964 100644 --- a/app/models/EventType.scala +++ b/app/models/EventType.scala @@ -228,15 +228,15 @@ object EventType { object FSMatriculeError extends Error object FSMatriculeChanged extends Info - // AgentConnect - object AgentConnectSecurityWarning extends Warn - object AgentConnectError extends Error - object AgentConnectUpdateProviderConfiguration extends Info - object AgentConnectUserLoginSuccessful extends Info - object AgentConnectSignupLoginSuccessful extends Info - object AgentConnectLoginDeactivatedUser extends Error - object AgentConnectUnknownEmail extends Warn - object AgentConnectClaimsSaveError extends Error + // ProConnect + object ProConnectSecurityWarning extends Warn + object ProConnectError extends Error + object ProConnectUpdateProviderConfiguration extends Info + object ProConnectUserLoginSuccessful extends Info + object ProConnectSignupLoginSuccessful extends Info + object ProConnectLoginDeactivatedUser extends Error + object ProConnectUnknownEmail extends Warn + object ProConnectClaimsSaveError extends Error val unauthenticatedEvents: List[EventType] = List( GenerateToken, diff --git a/app/models/AgentConnectClaims.scala b/app/models/ProConnectClaims.scala similarity index 59% rename from app/models/AgentConnectClaims.scala rename to app/models/ProConnectClaims.scala index 01ea05c9..d8809bb5 100644 --- a/app/models/AgentConnectClaims.scala +++ b/app/models/ProConnectClaims.scala @@ -3,14 +3,20 @@ package models import java.time.Instant import java.util.UUID -/** https://github.com/numerique-gouv/agentconnect-documentation/blob/main/doc_fs/donnees_fournies.md#le-champ-sub +/** https://github.com/numerique-gouv/proconnect-documentation/blob/main/doc_fs/donnees_fournies.md#le-champ-sub + * + * ProConnect transmet systématiquement au Fournisseur de Services un identifiant unique pour + * chaque agent (le sub) : cet identifiant est spécifique à chaque Fournisseur d'Identité. Il est + * recommandé de l'utiliser pour effectuer la réconciliation d'identité. + * + * Le code est basé sur l'ancienne documentation : * * AgentConnect transmet systématiquement au Fournisseur de Services un identifiant unique pour * chaque agent (le sub) : cet identifiant est spécifique à chaque couple Fournisseur de Services / * Fournisseur d'Identité. Il ne peut donc pas être utilisé pour faire de la réconciliation * d'identité : nous vous recommandons l'utilisation de l'email professionnel pour cet usage. */ -case class AgentConnectClaims( +case class ProConnectClaims( subject: String, email: String, givenName: Option[String], diff --git a/app/models/UserSession.scala b/app/models/UserSession.scala index 21ebb82e..feffa933 100644 --- a/app/models/UserSession.scala +++ b/app/models/UserSession.scala @@ -8,7 +8,7 @@ object UserSession { sealed abstract trait LoginType object LoginType { - case object AgentConnect extends LoginType + case object ProConnect extends LoginType case object InsecureDemoKey extends LoginType case object MagicLink extends LoginType case object Password extends LoginType diff --git a/app/modules/AppConfig.scala b/app/modules/AppConfig.scala index 58a844c9..84981413 100644 --- a/app/modules/AppConfig.scala +++ b/app/modules/AppConfig.scala @@ -162,40 +162,40 @@ class AppConfig @Inject() (configuration: Configuration) { .flatMap(UUIDHelper.fromString) .toSet - val featureAgentConnectEnabled: Boolean = - configuration.get[Boolean]("app.features.agentConnectEnabled") + val featureProConnectEnabled: Boolean = + configuration.get[Boolean]("app.features.proConnectEnabled") - def agentConnectConfig[A: ConfigLoader](key: String, defaultIfDisabled: => A): A = + def proConnectConfig[A: ConfigLoader](key: String, defaultIfDisabled: => A): A = configuration .getOptional[A](key) .getOrElse( - if (featureAgentConnectEnabled) - throw new Exception(s"Missing AgentConnect configuration key $key") + if (featureProConnectEnabled) + throw new Exception(s"Missing ProConnect configuration key $key") else defaultIfDisabled ) - val agentConnectIssuerUri: String = - agentConnectConfig[String]("app.agentConnect.issuerUri", "") + val proConnectIssuerUri: String = + proConnectConfig[String]("app.proConnect.issuerUri", "") - val agentConnectMinimumDurationBetweenDiscoveryCalls: FiniteDuration = - agentConnectConfig[Int]( - "app.agentConnect.minimumDurationBetweenDiscoveryCallsInSeconds", + val proConnectMinimumDurationBetweenDiscoveryCalls: FiniteDuration = + proConnectConfig[Int]( + "app.proConnect.minimumDurationBetweenDiscoveryCallsInSeconds", 10 ).seconds - val agentConnectClientId: String = - agentConnectConfig[String]("app.agentConnect.clientId", "") + val proConnectClientId: String = + proConnectConfig[String]("app.proConnect.clientId", "") - val agentConnectClientSecret: String = - agentConnectConfig[String]("app.agentConnect.clientSecret", "") + val proConnectClientSecret: String = + proConnectConfig[String]("app.proConnect.clientSecret", "") - val agentConnectRedirectUri: String = - agentConnectConfig[String]("app.agentConnect.redirectUri", "") + val proConnectRedirectUri: String = + proConnectConfig[String]("app.proConnect.redirectUri", "") - val agentConnectPostLogoutRedirectUri: String = - agentConnectConfig[String]("app.agentConnect.postLogoutRedirectUri", "") + val proConnectPostLogoutRedirectUri: String = + proConnectConfig[String]("app.proConnect.postLogoutRedirectUri", "") - val agentConnectSigningAlgorithm: String = - agentConnectConfig[String]("app.agentConnect.signingAlgorithm", "") + val proConnectSigningAlgorithm: String = + proConnectConfig[String]("app.proConnect.signingAlgorithm", "") } diff --git a/app/serializers/Keys.scala b/app/serializers/Keys.scala index cbe05156..59d42822 100644 --- a/app/serializers/Keys.scala +++ b/app/serializers/Keys.scala @@ -33,7 +33,7 @@ object Keys { val signupId: String = "signupId" val sessionId: String = "sessionId" val signupLoginExpiresAt: String = "signupLoginExpiresAt" - val signupAgentConnectSubject: String = "signupAgentConnectSubject" + val signupProConnectSubject: String = "signupProConnectSubject" val passwordEmail: String = "passwordEmail" } diff --git a/app/services/AgentConnectService.scala b/app/services/ProConnectService.scala similarity index 94% rename from app/services/AgentConnectService.scala rename to app/services/ProConnectService.scala index f72b7de5..6da6242a 100644 --- a/app/services/AgentConnectService.scala +++ b/app/services/ProConnectService.scala @@ -29,7 +29,7 @@ import play.api.libs.ws.WSClient import play.api.mvc.{Request, RequestHeader, Result} import scala.jdk.CollectionConverters._ -object AgentConnectService { +object ProConnectService { implicit val config: JsonConfiguration = JsonConfiguration(JsonNaming.SnakeCase) @@ -180,7 +180,7 @@ object AgentConnectService { case class UnknownKidInJwtHeader(message: String) extends Exception(message) /** Spec: - * - https://github.com/numerique-gouv/agentconnect-documentation/blob/main/doc_fs/implementation_technique.md#35-authentification-de-lutilisateur + * - https://github.com/numerique-gouv/proconnect-documentation/blob/main/doc_fs/implementation_technique.md#35-authentification-de-lutilisateur * - la session Agent Connect a une durée de 12 heures et se termine dans tous les cas à la fin * de la journée (minuit) */ @@ -196,14 +196,14 @@ object AgentConnectService { } @Singleton -class AgentConnectService @Inject() ( +class ProConnectService @Inject() ( config: AppConfig, db: Database, dependencies: ServicesDependencies, eventService: EventService, ws: WSClient, ) { - import AgentConnectService._ + import ProConnectService._ import dependencies.ioRuntime @@ -235,7 +235,7 @@ class AgentConnectService @Inject() ( case Right(metadata) if Duration .between(metadata.fetchTime, now) - .toMillis < config.agentConnectMinimumDurationBetweenDiscoveryCalls.toMillis => + .toMillis < config.proConnectMinimumDurationBetweenDiscoveryCalls.toMillis => IO.pure( Error .NotEnoughElapsedTimeBetweenDiscoveryCalls( @@ -247,8 +247,8 @@ class AgentConnectService @Inject() ( case _ => (IO.blocking( eventService.logNoRequest( - EventType.AgentConnectUpdateProviderConfiguration, - "Tentative de mise à jour de la provider configuration AgentConnect" + EventType.ProConnectUpdateProviderConfiguration, + "Tentative de mise à jour de la provider configuration ProConnect" ) ) >> fetchDiscoveryMetadata.value) .flatMap( @@ -273,7 +273,7 @@ class AgentConnectService @Inject() ( // Spec: If the Issuer value contains a path component, any terminating / MUST be removed before appending /.well-known/openid-configuration IO.pure( ws.url( - config.agentConnectIssuerUri.stripSuffix("/") + "/.well-known/openid-configuration" + config.proConnectIssuerUri.stripSuffix("/") + "/.well-known/openid-configuration" ).get() ) ).attempt @@ -293,14 +293,14 @@ class AgentConnectService @Inject() ( // TODO: check endpoints are https:// // Spec: The issuer value returned MUST be identical to the Issuer URL that was used as the prefix to /.well-known/openid-configuration to retrieve the configuration information. // See also "OpenID Connect Discovery 7.2. Impersonation Attacks https://openid.net/specs/openid-connect-discovery-1_0.html#Impersonation" - if (metadata.issuer === config.agentConnectIssuerUri) + if (metadata.issuer === config.proConnectIssuerUri) // Spec: Communication with the Token Endpoint MUST utilize TLS. // OpenID Connect 3.1.3. Token Endpoint https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint metadata.asRight else Error .ProviderConfigurationInvalidIssuer( - wantedIssuer = config.agentConnectIssuerUri, + wantedIssuer = config.proConnectIssuerUri, providedIssuer = metadata.issuer ) .asLeft @@ -372,9 +372,9 @@ class AgentConnectService @Inject() ( */ val STATE_SIZE_BYTES = 24 - val sessionStateKey = "agent-connect_state" - val sessionNonceKey = "agent-connect_nonce" - val sessionIdTokenKey = "agent-connect_idtoken" + val sessionStateKey = "pro-connect_state" + val sessionNonceKey = "pro-connect_nonce" + val sessionIdTokenKey = "pro-connect_idtoken" def resetSessionKeys(result: Result)(implicit request: RequestHeader): Result = result.removingFromSession(sessionStateKey, sessionNonceKey) @@ -383,7 +383,7 @@ class AgentConnectService @Inject() ( * https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest OpenID Connect * - OpenID Connect 15.5.2. Nonce Implementation Notes * https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes - * - https://github.com/numerique-gouv/agentconnect-documentation/blob/main/doc_fs/implementation_technique.md#2-faire-pointer-le-bouton-ac-vers-le-authorization_endpoint + * - https://github.com/numerique-gouv/proconnect-documentation/blob/main/doc_fs/implementation_technique.md#2-faire-pointer-le-bouton-proconnect-vers-le-authorization_endpoint */ def authenticationRequestUrl: EitherT[IO, Error, (String, (Result, RequestHeader) => Result)] = discoveryMetadata.subflatMap { metadata => @@ -401,7 +401,7 @@ class AgentConnectService @Inject() ( "email" -> Json.obj("essential" -> true), "siret" -> JsNull, ) - // ID token claims: https://github.com/numerique-gouv/agentconnect-documentation/blob/main/doc_fs/scope-claims.md#les-donn%C3%A9es-sur-lauthentification + // ID token claims: https://github.com/numerique-gouv/proconnect-documentation/blob/main/doc_fs/scope-claims.md#les-donn%C3%A9es-sur-lauthentification ) ) @@ -413,8 +413,8 @@ class AgentConnectService @Inject() ( // Multiple scope values MAY be used by creating a space-delimited, case-sensitive list of ASCII scope values. "scope" -> "openid given_name usual_name email siret", "response_type" -> "code", - "client_id" -> config.agentConnectClientId, - "redirect_uri" -> config.agentConnectRedirectUri, + "client_id" -> config.proConnectClientId, + "redirect_uri" -> config.proConnectRedirectUri, "state" -> state, // 2. OpenID Connect parameters @@ -436,10 +436,10 @@ class AgentConnectService @Inject() ( } } - /** https://github.com/numerique-gouv/agentconnect-documentation/blob/main/doc_fs/implementation_technique.md#34-stockage-du-id_token + /** https://github.com/numerique-gouv/proconnect-documentation/blob/main/doc_fs/implementation_technique.md#34-stockage-du-id_token * * Stocker le id_token dans la session du navigateur. Cette valeur sera utilisée plus tard, lors - * de la déconnexion auprès du serveur AgentConnect. + * de la déconnexion auprès du serveur ProConnect. */ def handleAuthenticationResponse( request: Request[_] @@ -529,7 +529,7 @@ class AgentConnectService @Inject() ( } /** Relevant docs: - * - https://github.com/numerique-gouv/agentconnect-documentation/blob/main/doc_fs/implementation_technique.md#32-g%C3%A9n%C3%A9ration-du-token + * - https://github.com/numerique-gouv/proconnect-documentation/blob/main/doc_fs/implementation_technique.md#32-g%C3%A9n%C3%A9ration-du-token * - OpenID Connect 3.1.3. Token Endpoint * https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint * - RFC6749 3.2. Token Endpoint https://www.rfc-editor.org/rfc/rfc6749.html#section-3.2 @@ -553,9 +553,9 @@ class AgentConnectService @Inject() ( Map( "grant_type" -> "authorization_code", "code" -> code.code, - "redirect_uri" -> config.agentConnectRedirectUri, - "client_id" -> config.agentConnectClientId, - "client_secret" -> config.agentConnectClientSecret, + "redirect_uri" -> config.proConnectRedirectUri, + "client_id" -> config.proConnectClientId, + "client_secret" -> config.proConnectClientSecret, ) ) ) @@ -607,7 +607,7 @@ class AgentConnectService @Inject() ( // Spec: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim. .requireIssuer(metadata.provider.issuer) // Spec: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client. - .requireAudience(config.agentConnectClientId) + .requireAudience(config.proConnectClientId) // Note: jjwt validates "exp" by default // - https://github.com/jwtk/jjwt/blob/5812f63a76084914c5b653025bd3b84048389223/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java#L677 // - https://github.com/jwtk/jjwt/blob/5812f63a76084914c5b653025bd3b84048389223/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java#L48 @@ -661,7 +661,7 @@ class AgentConnectService @Inject() ( .requireSubject(idTokenSubjectClaim) // Spec: If signed, the UserInfo Response MUST contain the Claims iss (issuer) and aud (audience) as members. The iss value MUST be the OP's Issuer Identifier URL. The aud value MUST be or include the RP's Client ID value. .requireIssuer(metadata.provider.issuer) - .requireAudience(config.agentConnectClientId) + .requireAudience(config.proConnectClientId) ).subflatMap { signedToken => Either.catchNonFatal( ( @@ -789,7 +789,7 @@ class AgentConnectService @Inject() ( .subflatMap { jws => val algorithm: Option[String] = Option(jws.getHeader).flatMap(header => Option(header.getAlgorithm)) - if (algorithm === Some(config.agentConnectSigningAlgorithm)) + if (algorithm === Some(config.proConnectSigningAlgorithm)) jws.asRight else Error.InvalidJwsAlgorithm(algorithm).asLeft diff --git a/app/services/UserService.scala b/app/services/UserService.scala index 00baa538..57c3d9c3 100644 --- a/app/services/UserService.scala +++ b/app/services/UserService.scala @@ -12,7 +12,7 @@ import java.sql.Connection import java.time.Instant import java.util.UUID import javax.inject.{Inject, Singleton} -import models.{AgentConnectClaims, Error, EventType, Organisation, User, UserSession} +import models.{Error, EventType, Organisation, ProConnectClaims, User, UserSession} import models.dataModels.UserRow import modules.AppConfig import org.postgresql.util.PSQLException @@ -663,7 +663,7 @@ class UserService @Inject() ( Column .of[String] .mapResult { - case "agent_connect" => UserSession.LoginType.AgentConnect.asRight + case "pro_connect" => UserSession.LoginType.ProConnect.asRight case "insecure_demo_key" => UserSession.LoginType.InsecureDemoKey.asRight case "magic_link" => UserSession.LoginType.MagicLink.asRight case "password" => UserSession.LoginType.Password.asRight @@ -740,7 +740,7 @@ class UserService @Inject() ( ) private def stringifyLoginType(loginType: UserSession.LoginType): String = loginType match { - case UserSession.LoginType.AgentConnect => "agent_connect" + case UserSession.LoginType.ProConnect => "pro_connect" case UserSession.LoginType.InsecureDemoKey => "insecure_demo_key" case UserSession.LoginType.MagicLink => "magic_link" case UserSession.LoginType.Password => "password" @@ -884,11 +884,11 @@ class UserService @Inject() ( ) // - // AgentConnect + // ProConnect // - val (agentConnectClaimsParser, agentConnectClaimsTableFields) = - Macros.parserWithFields[AgentConnectClaims]( + val (proConnectClaimsParser, proConnectClaimsTableFields) = + Macros.parserWithFields[ProConnectClaims]( "subject", "email", "given_name", @@ -900,14 +900,14 @@ class UserService @Inject() ( "user_id", ) - val agentConnectClaimsFieldsInSelect: String = - agentConnectClaimsTableFields.mkString(", ") + val proConnectClaimsFieldsInSelect: String = + proConnectClaimsTableFields.mkString(", ") - def saveAgentConnectClaims(claims: AgentConnectClaims): IO[Either[Error, Unit]] = + def saveProConnectClaims(claims: ProConnectClaims): IO[Either[Error, Unit]] = IO.blocking { val _ = db.withConnection { implicit connection => SQL""" - INSERT INTO agent_connect_claims ( + INSERT INTO pro_connect_claims ( subject, email, given_name, @@ -942,19 +942,19 @@ class UserService @Inject() ( .map( _.left.map(error => Error.SqlException( - EventType.AgentConnectClaimsSaveError, - s"Impossible de sauvegarder les claims AgentConnect [subject: ${claims.subject}]", + EventType.ProConnectClaimsSaveError, + s"Impossible de sauvegarder les claims ProConnect [subject: ${claims.subject}]", error, none ) ) ) - def linkUserToAgentConnectClaims(userId: UUID, subject: String): IO[Either[Error, Unit]] = + def linkUserToProConnectClaims(userId: UUID, subject: String): IO[Either[Error, Unit]] = IO.blocking { val _ = db.withConnection { implicit connection => SQL""" - UPDATE agent_connect_claims + UPDATE pro_connect_claims SET user_id = $userId::uuid WHERE subject = $subject """.executeUpdate() @@ -963,7 +963,7 @@ class UserService @Inject() ( .map( _.left.map(error => Error.SqlException( - EventType.AgentConnectClaimsSaveError, + EventType.ProConnectClaimsSaveError, s"Impossible de lier l'utilisateur $userId au claims de subject $subject", error, none diff --git a/app/views/login.scala b/app/views/login.scala index c24be318..f18a0bcc 100644 --- a/app/views/login.scala +++ b/app/views/login.scala @@ -7,27 +7,27 @@ import scalatags.Text.all._ object login { def page( - featureAgentConnectEnabled: Boolean, - agentConnectErrorMessage: Option[(String, String)] = None + featureProConnectEnabled: Boolean, + proConnectErrorMessage: Option[(String, String)] = None ): Tag = views.main.publicLayout( pageName = "Connexion", - content = loginInner(featureAgentConnectEnabled, agentConnectErrorMessage), + content = loginInner(featureProConnectEnabled, proConnectErrorMessage), breadcrumbs = List(("Connexion", LoginController.loginPage.url)), additionalHeadTags = frag(), additionalFooterTags = frag() ) private def loginInner( - featureAgentConnectEnabled: Boolean, - agentConnectErrorMessage: Option[(String, String)] + featureProConnectEnabled: Boolean, + proConnectErrorMessage: Option[(String, String)] ): Tag = div(cls := "fr-grid-row fr-grid-row-gutters fr-grid-row--center")( div(cls := "fr-col-12 fr-col-md-8 fr-col-lg-6")( h1("Connexion à Administration+"), - featureAgentConnectEnabled.some + featureProConnectEnabled.some .filter(identity) - .map(_ => agentConnectLoginBlock(agentConnectErrorMessage)), + .map(_ => proConnectLoginBlock(proConnectErrorMessage)), div( a( href := "/", @@ -37,33 +37,33 @@ object login { ) ) - private def agentConnectLoginBlock(agentConnectErrorMessage: Option[(String, String)]): Frag = + private def proConnectLoginBlock(proConnectErrorMessage: Option[(String, String)]): Frag = frag( div(cls := "fr-mt-10v fr-mb-8v")( - h2("Se connecter avec AgentConnect"), - agentConnectErrorMessage.map { case (title, description) => + h2("Se connecter avec ProConnect"), + proConnectErrorMessage.map { case (title, description) => div(cls := "fr-my-4w fr-alert fr-alert--error")( h3(cls := "fr-alert__title")(title), p(description) ) }, // https://www.systeme-de-design.gouv.fr/composants-et-modeles/composants/bouton-franceconnect/ - // https://github.com/numerique-gouv/agentconnect-documentation/blob/main/doc_fs/bouton_agentconnect.md + // https://github.com/numerique-gouv/proconnect-documentation/blob/main/doc_fs/bouton_proconnect.md div(cls := "fr-connect-group")( - a(href := LoginController.agentConnectLoginRedirection.url, cls := "fr-connect")( + a(href := LoginController.proConnectLoginRedirection.url, cls := "fr-connect")( span(cls := "fr-connect__login")("S’identifier avec"), - span(cls := "fr-connect__brand")("AgentConnect") + span(cls := "fr-connect__brand")("ProConnect") ), p( a( - href := "https://agentconnect.gouv.fr/", + href := "https://www.proconnect.gouv.fr/", target := "_blank", rel := "noopener", - title := "Qu’est ce que AgentConnect ? - nouvelle fenêtre" - )("Qu’est ce que AgentConnect ?") + title := "Qu’est ce que ProConnect ? - nouvelle fenêtre" + )("Qu’est ce que ProConnect ?") ), p( - "La connexion avec AgentConnect est actuellement en phase de test. Il n’est pas possible de créer de nouveau compte, vous devez posséder un compte sur Administration+. Si vous n’en avez pas, vous pouvez demander au responsable de votre structure ou à votre responsable départemental d’en créer un avec l’adresse email que vous utilisez pour vous connecter avec AgentConnect." + "La connexion avec ProConnect est actuellement en phase de test. Il n’est pas possible de créer de nouveau compte, vous devez posséder un compte sur Administration+. Si vous n’en avez pas, vous pouvez demander au responsable de votre structure ou à votre responsable départemental d’en créer un avec l’adresse email que vous utilisez pour vous connecter avec ProConnect." ) ) ), diff --git a/conf/application.conf b/conf/application.conf index 95c71afe..f0fd82f4 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -74,8 +74,8 @@ app.passwordRecoveryTokenExpirationInMinutes = ${?PASSWORD_RECOVERY_TOKEN_EXPIRA app.filesPath = ${?FILES_PATH} app.filesExpirationInDays = 7 app.filesExpirationInDays = ${?FILES_EXPIRATION_IN_DAYS} -app.features.agentConnectEnabled = false -app.features.agentConnectEnabled = ${?FEATURE_AGENT_CONNECT_ENABLED} +app.features.proConnectEnabled = false +app.features.proConnectEnabled = ${?FEATURE_PRO_CONNECT_ENABLED} app.filesOvhS3AccessKey = ${?FILES_OVH_S3_ACCESS_KEY} app.filesOvhS3SecretKey = ${?FILES_OVH_S3_SECRET_KEY} app.filesOvhS3Endpoint = ${?FILES_OVH_S3_ENDPOINT} @@ -131,13 +131,13 @@ app.statistics.percentOfApplicationsByStatusUrl = ${?APP_STATISTICS_PERCENT_OF_A app.statistics.bottomChartsUrls = ${?APP_STATISTICS_BOTTOM_CHARTS_URLS} app.groupsWithDsfr = "" app.groupsWithDsfr = ${?APP_GROUPS_WITH_DSFR} -app.agentConnect.issuerUri = ${?APP_AGENT_CONNECT_ISSUER_URI} -app.agentConnect.minimumDurationBetweenDiscoveryCallsInSeconds = ${?APP_AGENT_CONNECT_MIN_DURATION_BETWEEN_DISCOVERY_SECONDS} -app.agentConnect.clientId = ${?APP_AGENT_CONNECT_CLIENT_ID} -app.agentConnect.clientSecret = ${?APP_AGENT_CONNECT_CLIENT_SECRET} -app.agentConnect.redirectUri = ${?APP_AGENT_CONNECT_REDIRECT_URI} -app.agentConnect.postLogoutRedirectUri = ${?APP_AGENT_CONNECT_POST_LOGOUT_REDIRECT_URI} -app.agentConnect.signingAlgorithm = ${?APP_AGENT_CONNECT_SIGNING_ALGORITHM} +app.proConnect.issuerUri = ${?APP_PRO_CONNECT_ISSUER_URI} +app.proConnect.minimumDurationBetweenDiscoveryCallsInSeconds = ${?APP_PRO_CONNECT_MIN_DURATION_BETWEEN_DISCOVERY_SECONDS} +app.proConnect.clientId = ${?APP_PRO_CONNECT_CLIENT_ID} +app.proConnect.clientSecret = ${?APP_PRO_CONNECT_CLIENT_SECRET} +app.proConnect.redirectUri = ${?APP_PRO_CONNECT_REDIRECT_URI} +app.proConnect.postLogoutRedirectUri = ${?APP_PRO_CONNECT_POST_LOGOUT_REDIRECT_URI} +app.proConnect.signingAlgorithm = ${?APP_PRO_CONNECT_SIGNING_ALGORITHM} ### Sentry diff --git a/conf/evolutions/default/78.sql b/conf/evolutions/default/78.sql index 16b42dce..7a0114fa 100644 --- a/conf/evolutions/default/78.sql +++ b/conf/evolutions/default/78.sql @@ -1,6 +1,6 @@ --- !Ups -CREATE TABLE agent_connect_claims( +CREATE TABLE pro_connect_claims( subject text NOT NULL, email text NOT NULL, given_name text, @@ -13,11 +13,11 @@ CREATE TABLE agent_connect_claims( user_id uuid ); -CREATE UNIQUE INDEX agent_connect_claims_subject_unique_idx ON agent_connect_claims (subject); -CREATE INDEX agent_connect_claims_lower_email_idx ON agent_connect_claims (lower(email)); +CREATE UNIQUE INDEX pro_connect_claims_subject_unique_idx ON pro_connect_claims (subject); +CREATE INDEX pro_connect_claims_lower_email_idx ON pro_connect_claims (lower(email)); --- !Downs -DROP TABLE agent_connect_claims; +DROP TABLE pro_connect_claims; diff --git a/conf/routes b/conf/routes index 1cc03a4b..70209641 100644 --- a/conf/routes +++ b/conf/routes @@ -68,8 +68,8 @@ POST /connexion/mot-de-passe/reinitialisation GET /login/disconnect controllers.LoginController.disconnect GET /validation-connexion controllers.LoginController.magicLinkAntiConsumptionPage GET /connexion controllers.LoginController.loginPage -GET /connexion/redirection-vers-agent-connect controllers.LoginController.agentConnectLoginRedirection -GET /agentconnect/login controllers.LoginController.agentConnectAuthenticationResponseCallback +GET /connexion/redirection-vers-pro-connect controllers.LoginController.proConnectLoginRedirection +GET /agentconnect/login controllers.LoginController.proConnectAuthenticationResponseCallback # Users diff --git a/public/stylesheets/aplus-dsfr.css b/public/stylesheets/aplus-dsfr.css index f3c52d1f..08a3c150 100644 --- a/public/stylesheets/aplus-dsfr.css +++ b/public/stylesheets/aplus-dsfr.css @@ -509,3 +509,17 @@ font-size: .75rem; color: #666; } + + +/****** ProConnect styles ******/ + +.fr-connect { + padding-left: 4.375em !important; +} + +.fr-connect:before { + width: 3.375em !important; + background-size: 3.375em 3em !important; + left: 0.625em !important; + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2254%22%20height%3D%2248%22%20viewBox%3D%220%200%2054%2048%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M34.992%2015.0979L19.998%206.42087L5.004%2015.1819C4.91144%2015.2346%204.83441%2015.3108%204.78069%2015.4028C4.72697%2015.4948%204.69845%2015.5993%204.698%2015.7059V32.6539C4.70386%2032.7592%204.73465%2032.8616%204.78786%2032.9527C4.84106%2033.0439%204.91514%2033.121%205.004%2033.1779L19.996%2041.7779L34.99%2033.0719C35.0789%2033.0151%2035.153%2032.9379%2035.2063%2032.8468C35.2595%2032.7557%2035.2902%2032.6532%2035.296%2032.5479V15.6259C35.2966%2015.5188%2035.2687%2015.4136%2035.2153%2015.3208C35.1619%2015.2281%2035.0848%2015.1511%2034.992%2015.0979Z%22%20fill%3D%22%23000091%22%2F%3E%0A%3Cpath%20d%3D%22M14.641%2015.5979L9.612%2024.2259L5.055%2015.0509L10.445%2011.9379L14.934%2015.0979L14.641%2015.5979ZM35.297%2032.5779V15.6199C35.297%2015.5134%2035.2686%2015.4088%2035.2149%2015.3169C35.1611%2015.225%2035.0838%2015.1491%2034.991%2015.0969L19.998%206.42087%22%20fill%3D%22%23FCC63A%22%2F%3E%0A%3Cpath%20d%3D%22M4.7%2032.5779L20%206.42087V41.7829L5.004%2033.1779C4.91514%2033.121%204.84106%2033.0439%204.78786%2032.9527C4.73465%2032.8616%204.70386%2032.7592%204.698%2032.6539V15.7059L4.7%2032.5779ZM29.369%2011.8429L34.827%2014.9979L30.338%2024.1479L24.951%2014.9119L29.369%2011.8429Z%22%20fill%3D%22%230063CB%22%2F%3E%0A%3Cpath%20d%3D%22M39.606%2012.3029L20.416%201.28287C20.2868%201.2185%2020.1444%201.185%2020%201.185C19.8556%201.185%2019.7132%201.2185%2019.584%201.28287L0.394%2012.3029C0.278488%2012.3798%200.182654%2012.4827%200.114219%2012.6034C0.0457831%2012.7242%200.00665952%2012.8593%200%2012.9979V34.9979C0.00588693%2035.1373%200.0446444%2035.2734%200.113114%2035.3951C0.181583%2035.5167%200.277826%2035.6205%200.394%2035.6979L19.583%2046.7179C19.7122%2046.7822%2019.8546%2046.8157%2019.999%2046.8157C20.1434%2046.8157%2020.2858%2046.7822%2020.415%2046.7179L39.606%2035.6979C39.7222%2035.6205%2039.8184%2035.5167%2039.8869%2035.3951C39.9554%2035.2734%2039.9941%2035.1373%2040%2034.9979V12.9979C39.9933%2012.8593%2039.9542%2012.7242%2039.8858%2012.6034C39.8173%2012.4827%2039.7215%2012.3798%2039.606%2012.3029ZM10.789%2030.0589H10.868C10.826%2030.0589%2010.789%2030.0659%2010.789%2030.1089C10.789%2030.2089%2010.94%2030.1089%2010.989%2030.2089C10.7514%2030.2154%2010.5257%2030.3144%2010.36%2030.4849C10.36%2030.5359%2010.46%2030.5359%2010.511%2030.5359C10.436%2030.6359%2010.285%2030.5859%2010.234%2030.6869C10.2611%2030.7139%2010.2961%2030.7314%2010.334%2030.7369C10.284%2030.7369%2010.234%2030.7369%2010.234%2030.7879V30.9389C10.108%2030.9389%2010.058%2031.0389%209.957%2031.0899C10.157%2031.2409%2010.284%2031.0899%2010.485%2031.0899C9.957%2031.2899%209.529%2031.5679%209.001%2031.7189C8.901%2031.7189%209.001%2031.8699%208.901%2031.8699C9.052%2031.9699%209.128%2031.8199%209.278%2031.8199C8.624%2032.1969%207.945%2032.5199%207.241%2032.9519C7.18676%2033.0059%207.15164%2033.0761%207.141%2033.1519H6.941C6.841%2033.2019%206.891%2033.3279%206.79%2033.4289C7.016%2033.5799%207.29%2033.2289%207.444%2033.4289C7.494%2033.4289%207.344%2033.4789%207.244%2033.4789C7.194%2033.4789%207.194%2033.5789%207.144%2033.5789H6.99C6.89%2033.6539%206.79%2033.7049%206.79%2033.8549C6.74664%2033.8469%206.70187%2033.8521%206.66155%2033.8699C6.62122%2033.8878%206.58723%2033.9174%206.564%2033.9549C7.63803%2033.9506%208.70269%2033.7549%209.708%2033.3769C10.4877%2032.9799%2011.1938%2032.4526%2011.796%2031.8179C11.8231%2031.8449%2011.8406%2031.88%2011.846%2031.9179C11.6989%2032.3544%2011.4166%2032.7326%2011.04%2032.9979C10.763%2033.1489%2010.562%2033.3749%2010.34%2033.4759C10.1917%2033.5588%2010.0487%2033.651%209.912%2033.7519C9.27972%2033.9486%208.63063%2034.0865%207.973%2034.1639L7.668%2034.2079C7.443%2034.2409%207.219%2034.2769%206.997%2034.3159L5.004%2033.1779C4.93108%2033.132%204.86805%2033.072%204.8186%2033.0014C4.76914%2032.9308%204.73427%2032.8511%204.716%2032.7669C4.74933%2032.749%204.78082%2032.7279%204.81%2032.7039C4.77901%2032.671%204.74011%2032.6465%204.697%2032.6329V31.9829C5.74493%2031.7965%206.76549%2031.4801%207.735%2031.0409C6.81772%2030.4034%205.78681%2029.9475%204.698%2029.6979V28.1829C5.25307%2028.2738%205.80084%2028.4049%206.337%2028.5749C6.74977%2028.7265%207.14585%2028.9202%207.519%2029.1529C7.66563%2029.2939%207.82573%2029.4201%207.997%2029.5299C8.11768%2029.5987%208.25253%2029.639%208.3912%2029.6477C8.52987%2029.6564%208.66869%2029.6332%208.797%2029.5799H9.127C9.84341%2029.4615%2010.5135%2029.1484%2011.064%2028.6749C11.064%2028.7249%2011.114%2028.7249%2011.164%2028.7249C11.0857%2029.1235%2010.941%2029.5061%2010.736%2029.8569C10.739%2029.9079%2010.688%2030.0089%2010.789%2030.0589ZM13.606%2033.6299C13.857%2033.5299%2014.006%2033.3529%2014.235%2033.2529C14.185%2033.3029%2014.185%2033.4039%2014.135%2033.4529C13.9474%2033.5701%2013.7707%2033.704%2013.607%2033.8529C13.0413%2034.3514%2012.5116%2034.8894%2012.022%2035.4629C11.77%2035.7629%2011.494%2036.0409%2011.222%2036.3179C11.1256%2036.408%2011.0219%2036.4899%2010.912%2036.5629L8.385%2035.1129C8.74443%2035.1432%209.1063%2035.1254%209.461%2035.0599C9.75543%2034.9773%2010.0416%2034.8679%2010.316%2034.7329V34.8329C11.016%2034.5559%2011.548%2033.9269%2012.253%2033.7009C12.278%2033.7009%2012.379%2033.8009%2012.479%2033.7509C12.6601%2033.5264%2012.8904%2033.3466%2013.152%2033.2252C13.4136%2033.1038%2013.6997%2033.0442%2013.988%2033.0509C13.988%2033.1009%2013.988%2033.1509%2014.038%2033.1509H14.063C13.912%2033.2769%2013.736%2033.4019%2013.563%2033.5279C13.506%2033.5799%2013.556%2033.6299%2013.606%2033.6299ZM4.698%2027.4659V27.2799C5.21615%2027.1444%205.75053%2027.0811%206.286%2027.0919C6.44434%2027.0667%206.60566%2027.0667%206.764%2027.0919C6.05844%2027.0912%205.35854%2027.2179%204.698%2027.4659ZM35.298%2032.5539C35.2922%2032.6592%2035.2615%2032.7617%2035.2083%2032.8528C35.155%2032.9439%2035.0809%2033.0211%2034.992%2033.0779L24.913%2038.9289C23.7558%2038.5977%2022.618%2038.2021%2021.505%2037.7439C21.3437%2037.3929%2021.256%2037.0127%2021.2474%2036.6266C21.2388%2036.2404%2021.3095%2035.8566%2021.455%2035.4989C21.5349%2035.1909%2021.6531%2034.8943%2021.807%2034.6159C21.832%2034.5909%2021.857%2034.5659%2021.857%2034.5399C21.8636%2034.5399%2021.87%2034.5372%2021.8747%2034.5326C21.8794%2034.5279%2021.882%2034.5215%2021.882%2034.5149C21.9931%2034.3204%2022.1191%2034.1348%2022.259%2033.9599L22.274%2033.9449L22.294%2033.9239L22.309%2033.9089C22.309%2033.8839%2022.334%2033.8589%2022.359%2033.8329C22.384%2033.7819%2022.434%2033.7569%2022.459%2033.7069C22.6346%2033.521%2022.8285%2033.3535%2023.038%2033.2069C23.2507%2033.1298%2023.4695%2033.0706%2023.692%2033.0299C24.503%2033.0896%2025.3095%2033.1991%2026.107%2033.3579C26.205%2033.3726%2026.2992%2033.4066%2026.384%2033.4579C26.6851%2033.5165%2026.9962%2033.4994%2027.289%2033.4079C27.4799%2033.373%2027.6587%2033.2897%2027.8084%2033.1662C27.9581%2033.0426%2028.0736%2032.8827%2028.144%2032.7019C28.232%2032.5407%2028.2823%2032.3616%2028.2909%2032.1781C28.2996%2031.9947%2028.2664%2031.8116%2028.194%2031.6429C28.016%2031.3669%2028.181%2031.2059%2028.375%2031.0529L28.443%2030.9979C28.5294%2030.9367%2028.6071%2030.864%2028.674%2030.7819C28.8%2030.5299%2028.574%2030.3819%2028.523%2030.1519C28.473%2030.0519%2028.297%2030.1019%2028.196%2029.9519C28.548%2029.8009%2029.051%2029.5229%2028.825%2029.0949C28.674%2028.8679%2028.448%2028.4649%2028.725%2028.2379C29.077%2028.0379%2029.58%2028.0869%2029.731%2027.7589C29.7804%2027.5682%2029.7794%2027.368%2029.7282%2027.1778C29.6771%2026.9876%2029.5774%2026.814%2029.439%2026.6739L29.364%2026.5659C29.289%2026.4589%2029.215%2026.3519%2029.153%2026.2459C28.9941%2025.9821%2028.8177%2025.7292%2028.625%2025.4889C28.405%2025.1774%2028.2275%2024.8381%2028.097%2024.4799C27.946%2024.1019%2028.147%2023.7739%2028.147%2023.3959C28.1618%2022.6677%2028.0512%2021.9425%2027.82%2021.2519C27.694%2020.8989%2027.644%2020.5209%2027.493%2020.1929C27.474%2019.9821%2027.3956%2019.781%2027.267%2019.6129C27.2422%2019.5618%2027.2292%2019.5057%2027.2292%2019.4489C27.2292%2019.3921%2027.2422%2019.336%2027.267%2019.2849C27.472%2019.1403%2027.6656%2018.9802%2027.846%2018.8059C27.9017%2018.6836%2027.9122%2018.5455%2027.8756%2018.4162C27.839%2018.2869%2027.7576%2018.1748%2027.646%2018.0999C27.319%2017.9489%2027.346%2018.4279%2027.118%2018.5289H26.967C26.917%2018.4029%2027.017%2018.3519%2027.118%2018.2519C27.118%2018.2019%2027.118%2018.1009%2027.068%2018.1009C26.868%2018.1009%2026.691%2018.0499%2026.64%2017.9499C26.158%2017.3492%2025.5113%2016.9023%2024.779%2016.6639C24.967%2016.7216%2025.1615%2016.7552%2025.358%2016.7639C25.6956%2016.8353%2026.0471%2016.8003%2026.364%2016.6639C26.591%2016.5879%2026.641%2016.1849%2026.741%2015.9579C26.7613%2015.848%2026.7583%2015.7351%2026.7323%2015.6264C26.7063%2015.5177%2026.6578%2015.4157%2026.59%2015.3269C26.3629%2014.9967%2026.0495%2014.7352%2025.684%2014.5709C25.508%2014.4949%2025.231%2014.3439%2025.005%2014.2179C24.928%2014.1637%2024.8434%2014.1212%2024.754%2014.0919C21.789%2012.6069%2015.685%2013.8919%2015.22%2014.0919H15.211C14.7825%2014.2161%2014.3648%2014.375%2013.962%2014.5669C13.4074%2014.7728%2012.9066%2015.1015%2012.497%2015.5284C12.0875%2015.9552%2011.7798%2016.4692%2011.597%2017.0319C11.026%2017.407%2010.5658%2017.928%2010.264%2018.5409C9.836%2019.3409%209.208%2020.0499%209.308%2020.9549C9.408%2021.7349%209.585%2022.4389%209.736%2023.2439C9.77894%2023.5159%209.84586%2023.7836%209.936%2024.0439C10.036%2024.3199%209.936%2024.6729%2010.087%2024.8989C10.162%2025.0499%2010.112%2025.2259%2010.314%2025.3269V25.5269C10.364%2025.5769%2010.364%2025.6269%2010.465%2025.6269V25.8269C10.8997%2026.2504%2011.2723%2026.7331%2011.572%2027.2609C11.672%2027.5369%2011.094%2027.4119%2010.872%2027.3109C10.4565%2027.0404%2010.0762%2026.7192%209.74%2026.3549C9.71248%2026.3817%209.69456%2026.4168%209.689%2026.4549C9.889%2026.8069%2010.595%2027.2349%2010.217%2027.4609C10.017%2027.5609%209.789%2027.3099%209.588%2027.5119C9.538%2027.5869%209.588%2027.6879%209.588%2027.7879C9.311%2027.5879%209.01%2027.6879%208.733%2027.5879C8.533%2027.5379%208.481%2027.1609%208.255%2027.1609C7.65851%2027.0153%207.05373%2026.9061%206.444%2026.8339C5.86788%2026.7475%205.28724%2026.6944%204.705%2026.6749V15.7059C4.70545%2015.5993%204.73397%2015.4948%204.78769%2015.4028C4.84141%2015.3108%204.91844%2015.2346%205.011%2015.1819L19.998%206.42087L34.992%2015.0979C35.0846%2015.1505%2035.1617%2015.2267%2035.2154%2015.3187C35.2692%2015.4107%2035.2977%2015.5153%2035.298%2015.6219V32.5539ZM27.344%2024.2929C27.3138%2024.3399%2027.2719%2024.3782%2027.2225%2024.4043C27.1731%2024.4304%2027.1178%2024.4434%2027.062%2024.4419C26.9618%2024.5263%2026.8676%2024.6175%2026.78%2024.7149C26.88%2024.7149%2026.78%2024.8639%2026.88%2024.8639C26.675%2025.0869%2026.957%2025.5579%2026.675%2025.6569C26.3059%2025.7558%2025.9171%2025.7558%2025.548%2025.6569C25.6029%2025.6451%2025.6589%2025.6397%2025.715%2025.6409H25.8C25.8627%2025.6484%2025.9263%2025.6404%2025.9851%2025.6173C26.0439%2025.5943%2026.0961%2025.557%2026.137%2025.5089V25.3089C26.137%2025.2589%2026.086%2025.2589%2026.037%2025.2589C26.0107%2025.287%2025.9752%2025.3047%2025.937%2025.3089C25.9345%2025.2639%2025.9185%2025.2207%2025.891%2025.185C25.8635%2025.1493%2025.8259%2025.1227%2025.783%2025.1089C25.6501%2025.1266%2025.5148%2025.1109%2025.3894%2025.0633C25.2641%2025.0156%2025.1526%2024.9375%2025.065%2024.8359C25.2006%2024.7707%2025.3542%2024.7531%2025.501%2024.7859C25.629%2024.7859%2025.578%2024.5629%2025.732%2024.4639H25.886C26.193%2024.0919%2026.757%2023.9929%2026.86%2023.6209C26.86%2023.5209%2026.578%2023.5209%2026.373%2023.4719C26.0989%2023.4383%2025.821%2023.4552%2025.553%2023.5219C25.1932%2023.5715%2024.8405%2023.6631%2024.502%2023.7949C24.7823%2023.5891%2025.094%2023.4301%2025.425%2023.3239C25.6574%2023.2344%2025.8978%2023.1674%2026.143%2023.1239L26.275%2023.0979L26.408%2023.0709C26.5894%2023.0167%2026.7826%2023.0167%2026.964%2023.0709C27.195%2023.1709%2027.579%2023.1709%2027.63%2023.3189C27.73%2023.5919%2027.476%2023.8639%2027.195%2024.0629C27.138%2024.1439%2027.344%2024.1979%2027.344%2024.2929Z%22%20fill%3D%22white%22%2F%3E%0A%3Crect%20x%3D%2224%22%20y%3D%221%22%20width%3D%2229.56%22%20height%3D%2213.302%22%20rx%3D%222%22%20fill%3D%22%23FCC63A%22%2F%3E%0A%3Cpath%20d%3D%22M26.562%2012.1676V3.31589H29.4831C30.4526%203.31589%2031.2155%203.55194%2031.7719%204.02403C32.3367%204.49612%2032.6191%205.14104%2032.6191%205.95877C32.6191%206.76807%2032.3367%207.40876%2031.7719%207.88085C31.2155%208.35295%2030.4526%208.58899%2029.4831%208.58899H28.3576V12.1676H26.562ZM29.559%204.84598H28.3576V7.05891H29.559C29.9383%207.05891%2030.2334%206.96196%2030.4441%206.76807C30.6633%206.57417%2030.7729%206.29597%2030.7729%205.93348C30.7729%205.59627%2030.6633%205.33072%2030.4441%205.13682C30.2334%204.94293%2029.9383%204.84598%2029.559%204.84598Z%22%20fill%3D%22%23161616%22%2F%3E%0A%3Cpath%20d%3D%22M34.2307%2012.1676V3.31589H36.9368C37.9063%203.31589%2038.6734%203.55194%2039.2383%204.02403C39.8031%204.49612%2040.0855%205.14104%2040.0855%205.95877C40.0855%206.48987%2039.959%206.95353%2039.7061%207.34975C39.4617%207.73754%2039.116%208.03681%2038.6692%208.24757L41.4512%2012.1676H39.3015L36.9494%208.58899H36.0263V12.1676H34.2307ZM37.038%204.84598H36.0263V7.05891H37.038C37.4173%207.05891%2037.7124%206.96196%2037.9231%206.76807C38.1339%206.57417%2038.2393%206.29597%2038.2393%205.93348C38.2393%205.59627%2038.1339%205.33072%2037.9231%205.13682C37.7124%204.94293%2037.4173%204.84598%2037.038%204.84598Z%22%20fill%3D%22%23161616%22%2F%3E%0A%3Cpath%20d%3D%22M46.5486%203.06299C47.2399%203.06299%2047.8722%203.18944%2048.4454%203.44235C49.0271%203.69525%2049.5245%204.03246%2049.9376%204.45397C50.3507%204.87548%2050.671%205.37287%2050.8986%205.94612C51.1262%206.51095%2051.24%207.10949%2051.24%207.74176C51.24%208.37402%2051.1262%208.97678%2050.8986%209.55004C50.671%2010.1149%2050.3507%2010.608%2049.9376%2011.0295C49.5245%2011.451%2049.0271%2011.7883%2048.4454%2012.0412C47.8722%2012.2941%2047.2399%2012.4205%2046.5486%2012.4205C45.8574%2012.4205%2045.2209%2012.2941%2044.6392%2012.0412C44.0575%2011.7883%2043.5601%2011.451%2043.147%2011.0295C42.734%2010.608%2042.4136%2010.1149%2042.186%209.55004C41.9584%208.97678%2041.8446%208.37402%2041.8446%207.74176C41.8446%207.10949%2041.9584%206.51095%2042.186%205.94612C42.4136%205.37287%2042.734%204.87548%2043.147%204.45397C43.5601%204.03246%2044.0575%203.69525%2044.6392%203.44235C45.2209%203.18944%2045.8574%203.06299%2046.5486%203.06299ZM46.5486%2010.7387C46.9617%2010.7387%2047.3411%2010.6628%2047.6867%2010.5111C48.0408%2010.3509%2048.34%2010.1402%2048.5845%209.87882C48.8374%209.60905%2049.0355%209.29292%2049.1789%208.93042C49.3222%208.55949%2049.3938%208.16327%2049.3938%207.74176C49.3938%207.32025%2049.3222%206.92824%2049.1789%206.56574C49.0355%206.19481%2048.8374%205.87868%2048.5845%205.61734C48.34%205.34758%2048.0408%205.13682%2047.6867%204.98508C47.3411%204.8249%2046.9617%204.74482%2046.5486%204.74482C46.1355%204.74482%2045.752%204.8249%2045.3979%204.98508C45.0438%205.13682%2044.7403%205.34758%2044.4874%205.61734C44.243%205.87868%2044.0491%206.19481%2043.9058%206.56574C43.7624%206.92824%2043.6908%207.32025%2043.6908%207.74176C43.6908%208.16327%2043.7624%208.55949%2043.9058%208.93042C44.0491%209.29292%2044.243%209.60905%2044.4874%209.87882C44.7403%2010.1402%2045.0438%2010.3509%2045.3979%2010.5111C45.752%2010.6628%2046.1355%2010.7387%2046.5486%2010.7387Z%22%20fill%3D%22%23161616%22%2F%3E%0A%3C%2Fsvg%3E") !important; +}