Skip to content

Commit

Permalink
agentconnect => proconnect
Browse files Browse the repository at this point in the history
  • Loading branch information
niladic committed Oct 30, 2024
1 parent c92b059 commit aa98447
Show file tree
Hide file tree
Showing 15 changed files with 249 additions and 229 deletions.
2 changes: 1 addition & 1 deletion app/actions/LoginAction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ object LoginAction {
List(
Keys.Session.signupId,
Keys.Session.signupLoginExpiresAt,
Keys.Session.signupAgentConnectSubject
Keys.Session.signupProConnectSubject
)

}
Expand Down
232 changes: 116 additions & 116 deletions app/controllers/LoginController.scala

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions app/controllers/SignupController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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
)
Expand Down
18 changes: 9 additions & 9 deletions app/models/EventType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
2 changes: 1 addition & 1 deletion app/models/UserSession.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 20 additions & 20 deletions app/modules/AppConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")

}
2 changes: 1 addition & 1 deletion app/serializers/Keys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
*/
Expand All @@ -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

Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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 =>
Expand All @@ -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
)
)

Expand All @@ -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
Expand All @@ -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[_]
Expand Down Expand Up @@ -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
Expand All @@ -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,
)
)
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
(
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit aa98447

Please sign in to comment.