OATH provides an easy way for Rest API Applications to manipulate JWTs securely in complex systems.
libraryDependencies += "io.github.scala-jwt" %% "oath-core" % "0.0.0"
libraryDependencies += "io.github.scala-jwt" %% "oath-circe" % "0.0.0"
libraryDependencies += "io.github.scala-jwt" %% "oath-jsoniter-scala" % "0.0.0"JWS Algorithm Description HS256 HMAC256 HMAC with SHA-256 HS384 HMAC384 HMAC with SHA-384 HS512 HMAC512 HMAC with SHA-512 RS256 RSA256 RSASSA-PKCS1-v1_5 with SHA-256 RS384 RSA384 RSASSA-PKCS1-v1_5 with SHA-384 RS512 RSA512 RSASSA-PKCS1-v1_5 with SHA-512 ES256 ECDSA256 ECDSA with curve P-256 and SHA-256 ES384 ECDSA384 ECDSA with curve P-384 and SHA-384 ES512 ECDSA512 ECDSA with curve P-521 and SHA-512
Oath is an extension on top of JWT. Oath will allow you to create custom tokens from scala ADT Enum
associated with different properties and hide the boilerplate in configuration files. Oath macros are inspired from Enumeratum in order to collect the information needed for the custom Enum
.
The oath-core
depends on oath0/java-jwt library. Is inspired by akka-http-session & jwt-scala if you have already used those libraries you would probably find your self familiar with this API.
In a microservice architecture you could have more than on service issuing or verifying tokens. The library is being design to follow this principle by splitting the requirements to different APIs.
All registered claims documented in RFC-7519 are provided with optional values, therefore the library doesn't enforce you to use them.
final case class RegisteredClaims( iss: Option[String] = None, sub: Option[String] = None, aud: Seq[String] = Seq.empty, exp: Option[Instant] = None, nbf: Option[Instant] = None, iat: Option[Instant] = None, jti: Option[String] = None )
Claims is more than Registered Claims though. Therefore, if the business requirements requires extra claims to be able to authenticate & authorize the clients, the library provides an ADT
to describe each use case and the location for additional claims. There is extension methods created for convenience import io.oath.syntax.*
then you should be able to convert Any
to a JwtClaims
.
sealed trait JwtClaims object JwtClaims { final case class Claims(registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims final case class ClaimsH[+H](header: H, registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims final case class ClaimsP[+P](payload: P, registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims final case class ClaimsHP[+H, +P](header: H, payload: P, registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims }
The JWT (JSON Web Token) is described as a whole with the claims
& token
in the below data structure. The token
is in this form base64(header).base64(payload).signature
.
final case class Jwt[+C <: JwtClaims](claims: C, token: String)
Use only for issuing JWT Tokens. For asymmetric algorithms only private-key is required, see configuration.
import io.circe.generic.auto.* import io.oath.syntax.* import io.oath.circe.derive.* final case class Foo(name: String, age: Int) val config = IssuerConfig.loadOrThrow("token") // HMAC256 with "secret" as secret val issuer = new JwtIssuer(config) val foo = Foo("foo", 10) val maybeJwt: Either[IssueJwtError, Jwt[JwtClaims.ClaimsP[Foo]]] = issuer.issueJwt(foo.toClaimsP) // Right(Jwt(ClaimsP(Foo(foo,10),RegisteredClaims(None,None,List(),None,None,None,None)),eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiZm9vIiwiYWdlIjoxMH0.oeU3zySKPA-fowGQkl0WPDwyBhXJUEtobSjGQsDBXcs))
Use only for verifying JWT Tokens. For asymmetric algorithms only public-key is required, see configuration. In order for the verifier API to determine the location of the data in the token, the verifyJwt
function takes a JwtToken
. There is extension methods created for convenience import io.oath.syntax.*
then you should be able to convert any string to a JwtToken
.
sealed trait JwtToken { def token: String } object JwtToken { final case class Token(token: String) extends JwtToken // From registered claims final case class TokenH(token: String) extends JwtToken // From registered claims + header final case class TokenP(token: String) extends JwtToken // From registered claims + payload final case class TokenHP(token: String) extends JwtToken // From registered claims + header + payload }
import io.circe.generic.auto.* import io.oath.syntax.* import io.oath.circe.derive.* final case class Foo(name: String, age: Int) val config = VerifierConfig.loadOrThrow("token") // HMAC256 with "secret" as secret val verifier = new JwtVerifier(config) val token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiZm9vIiwiYWdlIjoxMH0.oeU3zySKPA-fowGQkl0WPDwyBhXJUEtobSjGQsDBXcs" val claims: Either[JwtVerifyError, JwtClaims.ClaimsP[Foo]] = verifier.verifyJwt[Foo](token.toTokenP) // Right(ClaimsP(Foo(foo,10),RegisteredClaims(None,None,List(),None,None,None,None)))
Used for verifying and issuing JWT Tokens, see configuration.
import io.circe.generic.auto.* import eu.timepit.refined.auto.* import io.oath.syntax.* import io.oath.circe.derive.* final case class Foo(name: String, age: Int) val config = ManagerConfig.loadOrThrow("token") val manager = new JwtManager(config) val foo = Foo("foo", 10) val jwt: Jwt[JwtClaims.ClaimsP[Foo]] = manager.issueJwt(foo.toClaimsP).toOption.get val claims: JwtClaims.ClaimsP[Foo] = manager.verifyJwt[Foo](jwt.token.toTokenP).toOption.getAdvanced Encryption Standard (AES)
Sensitive data in JWT Tokens might lead to an exposure of unwanted information (User data, Internal technologies, etc.). It's recommended to encrypt the data when is possible on the client side to prevent data leaks and been exposed to attacks. To enable encryption you must provide a secret
key to the configuration file.
encrypt { secret = "password" }
The library also provides ad-hoc claims manipulation with priority to the claims that have been provided by the code.
token { algorithm { name = "HMAC256" secret = "secret" } issuer { registered { issuer-claim = "issuer" subject-claim = "subject" } } }
import io.circe.generic.auto.* import io.oath.model.* import io.oath.syntax.* import io.oath.circe.derive.* final case class Foo(name: String, age: Int) val config = IssuerConfig.loadOrThrow("token") val issuer = new JwtIssuer(config) val foo = Foo("foo", 10) val adHocClaimsP = JwtClaims.ClaimsP(foo, RegisteredClaims.empty.copy(iss = Some("foo"))) val maybeJwt: Either[IssueJwtError, Jwt[JwtClaims.ClaimsP[Foo]]] = issuer.issueJwt(adHocClaimsP) // Right(Jwt(ClaimsP(Foo(foo,10),RegisteredClaims(Some(foo),Some(subject),List(),None,None,None,None)),eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiZm9vIiwiaXNzIjoiaXNzdWVyIiwiYWdlIjoxMH0.Dlow6pYmJ-5STSuEzL3WYnjpCrGYMKzadIwlOK_WBBc))
As described above we have 3 main components JwtIssuer
, JwtVerifier
and JwtManager
. The bellow example will demonstrate how to create one of this components for multiple tokens with different configuration using scala ADT
.
Those traits are necessary to retrieve the names for each Enum
custom value on compile time using macros.
trait OathEnum[A <: OathEnumEntry]
The Enum
token names will be converted by default from UPPER_CAMEL => LOWER_HYPHEN
which is going to be the name that the library is going to search in your local config file.
sealed trait OathExampleToken extends OathEnumEntry object OathExampleToken extends OathEnum[OathExampleToken] { case object AccessToken extends OathExampleToken // name in config access-token case object RefreshToken extends OathExampleToken // refresh-token case object ActivationEmailToken extends OathExampleToken // activation-email-token case object ForgotPasswordToken extends OathExampleToken // forgot-password-token override val tokenValues: Set[OathExampleToken] = findTokenEnumMembers // Use OathIssuer or OathVerifier to construct JwtIssuer's or JwtVerifier's val oathManager: OathManager[OathExampleToken] = OathManager.createOrFail(OathExampleToken) val AccessTokenManager: JwtManager[AccessToken.type] = oathManager.as(AccessToken) val RefreshTokenManager: JwtManager[RefreshToken.type] = oathManager.as(RefreshToken) val ActivationEmailTokenManager: JwtManager[ActivationEmailToken.type] = oathManager.as(ActivationEmailToken) val ForgotPasswordTokenManager: JwtManager[ForgotPasswordToken.type] = oathManager.as(ForgotPasswordToken) }
OR you can override a configName with:
sealed trait OathExampleToken extends OathEnumEntry object OathExampleToken extends OathEnum[OathExampleToken] { case object AccessToken extends OathExampleToken { override val configName: String = "access-session-token" // name in config access-session-token } ... }
OR you can override all configNames with:
sealed abstract class OathExampleToken(override val configName: String) extends OathEnumEntry object OathExampleToken extends OathEnum[OathExampleToken] { case object AccessToken extends OathExampleToken("access-session-token") // name in config access-session-token ... }
token.algorithm:
Key Type Description Requiredtoken.algorithm.name
String The Algorithm name (HMAC256, RSA256, etc.) ✅ token.algorithm.private-key-pem-path
String Private key pem file path ✅ Only for asymmetric algorithms and issuing tokens token.algorithm.public-key-pem-path
String Public key pem file path ✅ Only for asymmetric algorithms and verifying tokens token.algorithm.secret
String Secret signing key ✅ Only for symmetric algorithms
token.encrypt:
Key Type Description Requiredtoken.encrypt.secret
String Secret encryption key ❎
token.issuer:
Key Type Description Required Defaulttoken.issuer.registered.issuer-claim
String iss
claim value ❎ Null token.issuer.registered.subject-claim
String sub
claim value ❎ Null token.issuer.registered.audience-claims
List[String] aud
claim values ❎ Null token.issuer.registered.include-issued-at-claim
Boolean iat
claim auto-generated value ❎ false token.issuer.registered.include-jwt-id-claim
Boolean jti
claim auto-generated value ❎ false token.issuer.registered.expires-at-offset
Duration exp
claim adjust time with offset provided ❎ Null token.issuer.registered.not-before-offset
Duration nbf
claim adjust time with offset provided ❎ Null
token.verifier:
Key Type Description Required Defaulttoken.verifier.provided-with.issuer-claim
String Verify iss
claim contains the exact value ❎ Null token.verifier.provided-with.subject-claim
String Verify sub
claim contains the exact value ❎ Null token.verifier.provided-with.audience-claims
List[String] Verify aud
claim contains the exact values ❎ Null token.verifier.leeway-window.leeway
Duration Leeway window allow late JWTs with offset, checks [exp
, nbf
, iat
] ❎ Null token.verifier.leeway-window.issued-at
Duration Leeway window allow late JWTs with offset, checks [iat
] ❎ Null token.verifier.leeway-window.expires-at
Duration Leeway window allow late JWTs with offset, checks [exp
] ❎ Null token.verifier.leeway-window.not-before
Duration Leeway window allow late JWTs with offset, checks [nbf
] ❎ Null
token { algorithm { name = "RS256" private-key-pem-path = "src/test/secrets/rsa-private.pem" } // algorithm { // name = "HMAC256" // secret = "secret" When using HMAC single secret is required for both verifier and issuer // } encrypt { secret = "password" } issuer { registered { issuer-claim = "issuer" subject-claim = "subject" audience-claims = ["aud1", "aud2"] include-issued-at-claim = true include-jwt-id-claim = false expires-at-offset = 1 day not-before-offset = 1 minute } } }
token { algorithm { name = "RS256" public-key-pem-path = "src/test/secrets/rsa-public.pem" } // algorithm { // name = "HMAC256" // secret = "secret" When using HMAC single secret is required for both verifier and issuer // } encrypt { secret = "password" } verifier { provided-with { issuer-claim = "issuer" subject-claim = "subject" audience-claims = [] } leeway-window { issued-at = 4 minutes expires-at = 3 minutes not-before = 2 minutes } } }
token { algorithm { name = "RS256" private-key-pem-path = "src/test/secrets/rsa-private.pem" public-key-pem-path = "src/test/secrets/rsa-public.pem" } // algorithm { // name = "HMAC256" // secret = "secret" When using HMAC single secret is required for both verifier and issuer // } encrypt { secret = "password" } issuer { registered { issuer-claim = "issuer" subject-claim = "subject" audience-claims = ["aud1", "aud2"] include-issued-at-claim = true include-jwt-id-claim = false expires-at-offset = 1 day not-before-offset = 1 minute } } verifier { provided-with { issuer-claim = ${token.issuer.registered.issuer-claim} subject-claim = ${token.issuer.registered.subject-claim} audience-claims = ${token.issuer.registered.audience-claims} } leeway-window { issued-at = 4 minutes expires-at = 3 minutes not-before = 2 minutes } } }
oath { access-token { algorithm { name = "HS256" secret-key = "secret" } issuer { registered { issuer-claim = "access-token" subject-claim = "subject" audience-claims = ["aud1", "aud2"] include-issued-at-claim = true include-jwt-id-claim = true expires-at-offset = 15 minutes not-before-offset = 0 minute } } verifier { provided-with { issuer-claim = ${oath.access-token.issuer.registered.issuer-claim} subject-claim = ${oath.access-token.issuer.registered.subject-claim} audience-claims = ${oath.access-token.issuer.registered.audience-claims} } leeway-window { leeway = 1 minute issued-at = 1 minute expires-at = 1 minute not-before = 1 minute } } } refresh-token = ${oath.access-token} refresh-token { issuer { registered { issuer-claim = "refresh-token" expires-at-offset = 6 hours } } verifier { provided-with { issuer-claim = ${oath.refresh-token.issuer.registered.issuer-claim} } } } activation-email-token = ${oath.access-token} activation-email-token { issuer { registered { issuer-claim = "activation-email-token" expires-at-offset = 1 day audience-claims = [] } } verifier { provided-with { issuer-claim = ${oath.activation-email-token.issuer.registered.issuer-claim} audience-claims = [] } } } forgot-password-token = ${oath.access-token} forgot-password-token { issuer { registered { issuer-claim = "forgot-password-token" expires-at-offset = 2 hours audience-claims = [] } } verifier { provided-with { issuer-claim = ${oath.forgot-password-token.issuer.registered.issuer-claim} audience-claims = [] } } } }
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4