diff --git a/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala b/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala index dd0e8c1..5f4c4b1 100644 --- a/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala +++ b/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala @@ -53,6 +53,7 @@ given JsonCodec[UserRole] = validatedStringCodec(UserRole) given JsonCodec[Avatar] = validatedStringCodec(Avatar) + given JsonCodec[Claim] = DeriveJsonCodec.gen[Claim] given JsonCodec[BasicProfile] = DeriveJsonCodec.gen[BasicProfile] given JsonCodec[FileRef] = DeriveJsonCodec.gen[FileRef] @@ -79,6 +80,7 @@ given Schema[UserName] = Schema.string given Schema[Avatar] = Schema.string given Schema[Email] = Schema.string + given Schema[Claim] = Schema.derived[Claim] given Schema[BasicProfile] = Schema.derived[BasicProfile] given Schema[FileRef] = Schema.derived[FileRef] given Schema[Moment] = diff --git a/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala b/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala index dd0e8c1..5f4c4b1 100644 --- a/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala +++ b/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala @@ -53,6 +53,7 @@ given JsonCodec[UserRole] = validatedStringCodec(UserRole) given JsonCodec[Avatar] = validatedStringCodec(Avatar) + given JsonCodec[Claim] = DeriveJsonCodec.gen[Claim] given JsonCodec[BasicProfile] = DeriveJsonCodec.gen[BasicProfile] given JsonCodec[FileRef] = DeriveJsonCodec.gen[FileRef] @@ -79,6 +80,7 @@ given Schema[UserName] = Schema.string given Schema[Avatar] = Schema.string given Schema[Email] = Schema.string + given Schema[Claim] = Schema.derived[Claim] given Schema[BasicProfile] = Schema.derived[BasicProfile] given Schema[FileRef] = Schema.derived[FileRef] given Schema[Moment] = diff --git a/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala b/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala index 47ef47f..32956d5 100644 --- a/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala +++ b/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala @@ -1,14 +1,20 @@ package works.iterative.core package auth +enum Claim: + case StringClaim(name: String, value: String) + final case class BasicProfile( subjectId: UserId, userName: Option[UserName], email: Option[Email], avatar: Option[Avatar], - roles: Set[UserRole] + roles: Set[UserRole], + claims: Set[Claim] = Set.empty ) extends UserProfile: val handle: UserHandle = UserHandle(subjectId, userName) + def stringClaim(name: String): Option[String] = + claims.collectFirst { case Claim.StringClaim(n, v) if n == name => v } object BasicProfile: def apply(p: UserProfile): BasicProfile = p match diff --git a/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala b/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala index dd0e8c1..5f4c4b1 100644 --- a/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala +++ b/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala @@ -53,6 +53,7 @@ given JsonCodec[UserRole] = validatedStringCodec(UserRole) given JsonCodec[Avatar] = validatedStringCodec(Avatar) + given JsonCodec[Claim] = DeriveJsonCodec.gen[Claim] given JsonCodec[BasicProfile] = DeriveJsonCodec.gen[BasicProfile] given JsonCodec[FileRef] = DeriveJsonCodec.gen[FileRef] @@ -79,6 +80,7 @@ given Schema[UserName] = Schema.string given Schema[Avatar] = Schema.string given Schema[Email] = Schema.string + given Schema[Claim] = Schema.derived[Claim] given Schema[BasicProfile] = Schema.derived[BasicProfile] given Schema[FileRef] = Schema.derived[FileRef] given Schema[Moment] = diff --git a/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala b/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala index 47ef47f..32956d5 100644 --- a/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala +++ b/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala @@ -1,14 +1,20 @@ package works.iterative.core package auth +enum Claim: + case StringClaim(name: String, value: String) + final case class BasicProfile( subjectId: UserId, userName: Option[UserName], email: Option[Email], avatar: Option[Avatar], - roles: Set[UserRole] + roles: Set[UserRole], + claims: Set[Claim] = Set.empty ) extends UserProfile: val handle: UserHandle = UserHandle(subjectId, userName) + def stringClaim(name: String): Option[String] = + claims.collectFirst { case Claim.StringClaim(n, v) if n == name => v } object BasicProfile: def apply(p: UserProfile): BasicProfile = p match diff --git a/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala b/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala index 63e031c..b1481ae 100644 --- a/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala +++ b/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala @@ -17,11 +17,14 @@ import works.iterative.tapir.BaseUri import org.http4s.server.Router import org.http4s.server.websocket.WebSocketBuilder2 +import com.nimbusds.jwt.JWTClaimsSet +import works.iterative.core.auth.Claim class BlazeHttpServer( config: BlazeServerConfig, pac4jConfig: Pac4jSecurityConfig, - baseUri: BaseUri + baseUri: BaseUri, + gatherClaims: JWTClaimsSet => Set[Claim] = _ => Set.empty ) extends HttpServer: override def serve[Env](app: HttpApplication[Env]): URIO[Env, Nothing] = type AppTask[A] = RIO[Env, A] @@ -40,7 +43,7 @@ ) val pac4jSecurity = - Pac4jHttpSecurity[AppTask](pac4jConfig, contextBuilder) + Pac4jHttpSecurity[AppTask](pac4jConfig, contextBuilder, gatherClaims) def provideCurrentUser( routes: HttpRoutes[SecuredTask] @@ -100,12 +103,13 @@ } object BlazeHttpServer: - val layer - : RLayer[BlazeServerConfig & Pac4jSecurityConfig & BaseUri, HttpServer] = + def layer( + gatherClaims: JWTClaimsSet => Set[Claim] = _ => Set.empty + ): RLayer[BlazeServerConfig & Pac4jSecurityConfig & BaseUri, HttpServer] = ZLayer { for config <- ZIO.service[BlazeServerConfig] pac4jConfig <- ZIO.service[Pac4jSecurityConfig] baseUri <- ZIO.service[BaseUri] - yield BlazeHttpServer(config, pac4jConfig, baseUri) + yield BlazeHttpServer(config, pac4jConfig, baseUri, gatherClaims) } diff --git a/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala b/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala index dd0e8c1..5f4c4b1 100644 --- a/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala +++ b/codecs/src/main/scala/works/iterative/core/codecs/Codecs.scala @@ -53,6 +53,7 @@ given JsonCodec[UserRole] = validatedStringCodec(UserRole) given JsonCodec[Avatar] = validatedStringCodec(Avatar) + given JsonCodec[Claim] = DeriveJsonCodec.gen[Claim] given JsonCodec[BasicProfile] = DeriveJsonCodec.gen[BasicProfile] given JsonCodec[FileRef] = DeriveJsonCodec.gen[FileRef] @@ -79,6 +80,7 @@ given Schema[UserName] = Schema.string given Schema[Avatar] = Schema.string given Schema[Email] = Schema.string + given Schema[Claim] = Schema.derived[Claim] given Schema[BasicProfile] = Schema.derived[BasicProfile] given Schema[FileRef] = Schema.derived[FileRef] given Schema[Moment] = diff --git a/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala b/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala index 47ef47f..32956d5 100644 --- a/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala +++ b/core/shared/src/main/scala/works/iterative/core/auth/BasicProfile.scala @@ -1,14 +1,20 @@ package works.iterative.core package auth +enum Claim: + case StringClaim(name: String, value: String) + final case class BasicProfile( subjectId: UserId, userName: Option[UserName], email: Option[Email], avatar: Option[Avatar], - roles: Set[UserRole] + roles: Set[UserRole], + claims: Set[Claim] = Set.empty ) extends UserProfile: val handle: UserHandle = UserHandle(subjectId, userName) + def stringClaim(name: String): Option[String] = + claims.collectFirst { case Claim.StringClaim(n, v) if n == name => v } object BasicProfile: def apply(p: UserProfile): BasicProfile = p match diff --git a/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala b/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala index 63e031c..b1481ae 100644 --- a/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala +++ b/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala @@ -17,11 +17,14 @@ import works.iterative.tapir.BaseUri import org.http4s.server.Router import org.http4s.server.websocket.WebSocketBuilder2 +import com.nimbusds.jwt.JWTClaimsSet +import works.iterative.core.auth.Claim class BlazeHttpServer( config: BlazeServerConfig, pac4jConfig: Pac4jSecurityConfig, - baseUri: BaseUri + baseUri: BaseUri, + gatherClaims: JWTClaimsSet => Set[Claim] = _ => Set.empty ) extends HttpServer: override def serve[Env](app: HttpApplication[Env]): URIO[Env, Nothing] = type AppTask[A] = RIO[Env, A] @@ -40,7 +43,7 @@ ) val pac4jSecurity = - Pac4jHttpSecurity[AppTask](pac4jConfig, contextBuilder) + Pac4jHttpSecurity[AppTask](pac4jConfig, contextBuilder, gatherClaims) def provideCurrentUser( routes: HttpRoutes[SecuredTask] @@ -100,12 +103,13 @@ } object BlazeHttpServer: - val layer - : RLayer[BlazeServerConfig & Pac4jSecurityConfig & BaseUri, HttpServer] = + def layer( + gatherClaims: JWTClaimsSet => Set[Claim] = _ => Set.empty + ): RLayer[BlazeServerConfig & Pac4jSecurityConfig & BaseUri, HttpServer] = ZLayer { for config <- ZIO.service[BlazeServerConfig] pac4jConfig <- ZIO.service[Pac4jSecurityConfig] baseUri <- ZIO.service[BaseUri] - yield BlazeHttpServer(config, pac4jConfig, baseUri) + yield BlazeHttpServer(config, pac4jConfig, baseUri, gatherClaims) } diff --git a/server/http/src/main/scala/works/iterative/server/http/impl/pac4j/Pac4jHttpSecurity.scala b/server/http/src/main/scala/works/iterative/server/http/impl/pac4j/Pac4jHttpSecurity.scala index 26a5db6..177f8d0 100644 --- a/server/http/src/main/scala/works/iterative/server/http/impl/pac4j/Pac4jHttpSecurity.scala +++ b/server/http/src/main/scala/works/iterative/server/http/impl/pac4j/Pac4jHttpSecurity.scala @@ -18,13 +18,16 @@ import works.iterative.core.auth.UserRole import works.iterative.core.Email import works.iterative.core.Avatar -import works.iterative.core.auth.BasicProfile +import works.iterative.core.auth.* +import org.pac4j.oidc.profile.OidcProfile +import com.nimbusds.jwt.JWTClaimsSet trait HttpSecurity class Pac4jHttpSecurity[F[_] <: AnyRef: Sync]( config: Pac4jSecurityConfig, - contextBuilder: (Request[F], Config) => Http4sWebContext[F] + contextBuilder: (Request[F], Config) => Http4sWebContext[F], + gatherClaims: JWTClaimsSet => Set[Claim] = _ => Set.empty ) extends HttpSecurity: protected val dsl: Http4sDsl[F] = new Http4sDsl[F] {} import dsl.* @@ -84,7 +87,11 @@ Option(p.getRoles()) .map(_.asScala.toSet) .getOrElse(Set.empty) - .flatMap(UserRole(_).toOption) + .flatMap(UserRole(_).toOption), + p match + case o: OidcProfile => + gatherClaims(o.getIdToken().getJWTClaimsSet()) + case _ => Set.empty ) ) r.context match {