Showing preview only (322K chars total). Download the full file or copy to clipboard to get everything.
Repository: pauldijou/jwt-scala
Branch: main
Commit: bb5348679d4e
Files: 93
Total size: 293.7 KB
Directory structure:
gitextract_3pvve9sy/
├── .git-blame-ignore-revs
├── .github/
│ └── workflows/
│ ├── docs.yml
│ ├── release.yml
│ └── tests.yml
├── .gitignore
├── .scala-steward.conf
├── .scalafmt.conf
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build.sbt
├── core/
│ ├── jvm/
│ │ └── src/
│ │ └── test/
│ │ └── scala/
│ │ ├── JwtSpec.scala
│ │ └── JwtUtilsSpec.scala
│ └── shared/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ ├── Jwt.scala
│ │ ├── JwtAlgorithm.scala
│ │ ├── JwtArrayUtils.scala
│ │ ├── JwtBase64.scala
│ │ ├── JwtClaim.scala
│ │ ├── JwtCore.scala
│ │ ├── JwtException.scala
│ │ ├── JwtHeader.scala
│ │ ├── JwtOptions.scala
│ │ ├── JwtTime.scala
│ │ └── JwtUtils.scala
│ └── test/
│ └── scala/
│ ├── Fixture.scala
│ ├── JwtBase64Spec.scala
│ └── JwtClaimSpec.scala
├── docs/
│ └── src/
│ └── main/
│ ├── paradox/
│ │ ├── index.md
│ │ ├── jwt-argonaut.md
│ │ ├── jwt-circe.md
│ │ ├── jwt-core/
│ │ │ ├── index.md
│ │ │ ├── jwt-claim-private.md
│ │ │ ├── jwt-claim.md
│ │ │ ├── jwt-ecdsa.md
│ │ │ └── jwt-header.md
│ │ ├── jwt-json4s.md
│ │ ├── jwt-play-json.md
│ │ ├── jwt-play-jwt-session.md
│ │ ├── jwt-upickle.md
│ │ ├── jwt-zio-json.md
│ │ └── project/
│ │ └── build.properties
│ └── scala/
│ ├── JwtArgonautDoc.scala
│ ├── JwtCirceDoc.scala
│ ├── JwtJson4sDoc.scala
│ ├── JwtPlayJsonDoc.scala
│ ├── JwtPlayJwtSessionDoc.scala
│ ├── JwtUpickleDoc.scala
│ └── JwtZioDoc.scala
├── json/
│ ├── argonaut/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ └── JwtArgonaut.scala
│ │ └── test/
│ │ └── scala/
│ │ ├── ArgonautFixture.scala
│ │ └── JwtArgonautSpec.scala
│ ├── circe/
│ │ ├── jvm/
│ │ │ └── src/
│ │ │ └── test/
│ │ │ └── scala/
│ │ │ ├── CirceFixture.scala
│ │ │ └── JwtCirceSpec.scala
│ │ └── shared/
│ │ └── src/
│ │ └── main/
│ │ └── scala/
│ │ └── JwtCirce.scala
│ ├── common/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ └── JwtJsonCommon.scala
│ │ └── test/
│ │ └── scala/
│ │ ├── JsonCommonFixture.scala
│ │ └── JwtJsonCommonSpec.scala
│ ├── json4s-common/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ └── JwtJson4sCommon.scala
│ │ └── test/
│ │ └── scala/
│ │ └── Json4sCommonFixture.scala
│ ├── json4s-jackson/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ ├── JwtJson4sImplicits.scala
│ │ │ └── JwtJson4sJackson.scala
│ │ └── test/
│ │ └── scala/
│ │ ├── Json4sJacksonFixture.scala
│ │ └── Json4sJacksonSpec.scala
│ ├── json4s-native/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ ├── JwtJson4sImplicits.scala
│ │ │ └── JwtJson4sNative.scala
│ │ └── test/
│ │ └── scala/
│ │ ├── Json4sNativeFixture.scala
│ │ └── Json4sNativeSpec.scala
│ ├── play-json/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ ├── JwtJson.scala
│ │ │ └── JwtJsonImplicits.scala
│ │ └── test/
│ │ └── scala/
│ │ ├── JsonFixture.scala
│ │ └── JwtJsonSpec.scala
│ ├── upickle/
│ │ ├── jvm/
│ │ │ └── src/
│ │ │ └── test/
│ │ │ └── scala/
│ │ │ ├── JwtUpickleFixture.scala
│ │ │ └── JwtUpickleSpec.scala
│ │ └── shared/
│ │ └── src/
│ │ └── main/
│ │ └── scala/
│ │ ├── JwtUpickle.scala
│ │ └── JwtUpickleImplicits.scala
│ └── zio-json/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ └── JwtZIOJson.scala
│ └── test/
│ └── scala/
│ ├── JwtZIOJsonSpec.scala
│ └── ZIOJsonFixture.scala
├── play/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ ├── JwtPlayImplicits.scala
│ │ └── JwtSession.scala
│ └── test/
│ └── scala/
│ ├── JwtResultSpec.scala
│ ├── JwtSessionAsymetricSpec.scala
│ ├── JwtSessionCustomDifferentNameSpec.scala
│ ├── JwtSessionCustomSpec.scala
│ ├── JwtSessionSpec.scala
│ └── PlayFixture.scala
├── project/
│ ├── Libs.scala
│ ├── build.properties
│ └── plugins.sbt
└── scripts/
├── bump.sh
├── clean.sh
└── pu.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .git-blame-ignore-revs
================================================
# Scala Steward: Reformat with scalafmt 3.7.2
e8c410d04442fe9ac7aa50df34a398972a602cdc
# Scala Steward: Reformat with scalafmt 3.7.17
14d9145808edddb1d80975faf427882b2e081e03
# Scala Steward: Reformat with scalafmt 3.8.3
7013c976689533b3513842d28925048c75cb592e
# Scala Steward: Reformat with scalafmt 3.9.7
427d1611771af19de6fd3a56f78c60b0ea18e910
================================================
FILE: .github/workflows/docs.yml
================================================
name: Docs
on:
push:
tags: ["*"]
concurrency:
group: docs
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
cache: sbt
- uses: sbt/setup-sbt@v1
- name: "Get latest tag"
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
- name: Build
run: sbt 'set docs/version := "${{ steps.previoustag.outputs.tag }}".drop(1)' docs/makeSite
- name: setup git config
run: |
git config user.name "GitHub Actions Bot"
git config user.email "<>"
- name: Deploy
run: |
git checkout --orphan gh-pages
git add -f docs/target/site
git commit -m "Rebuild GitHub pages"
git filter-branch -f --prune-empty --subdirectory-filter docs/target/site
git push -f origin gh-pages
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
branches: [master, main]
tags: ["*"]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 8
cache: sbt
- uses: sbt/setup-sbt@v1
- run: sbt ci-release
env:
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
PGP_SECRET: ${{ secrets.PGP_SECRET }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
================================================
FILE: .github/workflows/tests.yml
================================================
name: CI
on:
pull_request:
branches: [master, main]
push:
branches: [master, main]
env:
SCALA212: 2.12.20
SCALA213: 2.13.14
SCALA3: 3.3.0
jobs:
linting:
runs-on: ubuntu-latest
steps:
- uses: "actions/checkout@v3"
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
cache: sbt
- uses: sbt/setup-sbt@v1
- name: Checking code formatting
run: sbt formatCheck
docs-check:
runs-on: ubuntu-latest
steps:
- uses: "actions/checkout@v3"
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
cache: sbt
- uses: sbt/setup-sbt@v1
- name: Check the documentation
run: sbt docs/makeSite
mima:
runs-on: ubuntu-latest
steps:
- uses: "actions/checkout@v3"
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
cache: sbt
- uses: sbt/setup-sbt@v1
- name: Report binary issues
run: "sbt ${{ matrix.project }}/mimaReportBinaryIssues"
strategy:
matrix:
project:
- coreJVM
- coreJS
- coreNative
- playJson
- playFramework
- circeJVM
- circeJS
- circeNative
- upickleJVM
- upickleJS
- upickleNative
- json4sNative
- json4sJackson
- argonaut
- zioJson
tests:
runs-on: ubuntu-latest
name: Tests ${{ matrix.project }} (${{ matrix.scala }})
steps:
- uses: "actions/checkout@v3"
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
cache: sbt
- uses: sbt/setup-sbt@v1
- name: Test
run: "sbt ++${{ matrix.scala }} ${{ matrix.project }}/test"
strategy:
matrix:
exclude:
- project: playFramework
scala: $SCALA212
project:
- coreJVM
- coreJS
- coreNative
- playJson
- playFramework
- circeJVM
- circeJS
- circeNative
- upickleJVM
- upickleJS
- upickleNative
- json4sNative
- json4sJackson
- argonaut
- zioJson
scala:
- $SCALA212
- $SCALA213
- $SCALA3
================================================
FILE: .gitignore
================================================
.history
target
project/project
project/target
.bloop/
.idea
.bsp
.metals/
.vscode/
metals.sbt
================================================
FILE: .scala-steward.conf
================================================
updates.ignore = [
{ groupId = "com.google.inject", artifactId = "guice" }
]
================================================
FILE: .scalafmt.conf
================================================
version=3.10.7
runner.dialect=scala213
maxColumn = 100
rewrite.rules = [Imports, AvoidInfix, SortModifiers, PreferCurlyFors]
rewrite.imports.sort = ascii
rewrite.imports.groups = [
["java\\..*", "javax\\..*", "scala\\..*"]
]
================================================
FILE: CHANGELOG.md
================================================
# Changelog
Note: this file is no longer updated, check the [releases tab](https://github.com/jwt-scala/jwt-scala/releases)
for details about each version.
## 6.0.0 (26/02/2021)
Important: the groupId changed from `fr.pauldijou` to `com.github.jwt-scala`,
so you need to update your dependencies:
```
libraryDependencies += "com.github.jwt-scala" %% "<artifact>" % "6.0.0"
```
- Upgrade Play to 2.8.7
- Upgrade Play Json to 2.9.2
- Upgrade uPickle to 1.2.3
- Upgrade Argonaut to 6.3.3
- Upgrade Bouncycastle to 1.68
- Drop support for Scala 2.11
## 5.0.0 (31/10/2020)
- Make `JwtException` a proper exception (thanks @tpolecat)
- Update SBT and Scala version (thanks @erwan)
- Improve string splitting performance (thanks @jfosback)
- **Breaking** (a little): `JwtSession` should always have an expiration now if you have set a `play.http.session.maxAge`. Before, a few ways to create the session would forget to add it.
- **Breaking** (also a little): calling `refreshJwtSession` on a Play Result will now truly refresh and set a session if there was one to begin with. Before, it would always set a session with an expiration even if there was nothing.
- **Breaking** (maybe, maybe not, unsure): renamed KeyFactory algorithm from "ECDSA" to "EC" to better comply with [Java Security Standard Algorithm Names](https://docs.oracle.com/en/java/javase/14/docs/specs/security/standard-names.html#keyfactory-algorithms), this might impact curve names, check [ParameterSpec Names](https://docs.oracle.com/en/java/javase/14/docs/specs/security/standard-names.html#parameterspec-names) if you are impacted.
## 4.3.0 (29/02/2020)
- Add support for asymmetric algorithms for Play framerwork (thanks @Bangalor)
- Upgrade Circe to 0.13.0 (thanks @howyp)
- Upgrade Play and play-json to 2.8.0
- Upgrade upickle to 0.9.5
## 4.2.0 (03/11/2019)
- No longer fail on unknown algorithm when `signature` is `false` on options (thanks @Baccata)
- Upgrade upickle to 0.8.0 (thanks @vic)
## 4.1.0 (22/09/2019)
- Upgrade to Circe 0.12.1 (thanks @erwan)
## 4.0.0 (26/08/2019)
This is not really a breaking change release but I did some small adjustements that might break in very specific cases so not taking chances.
- Support Scala 2.13 for Play framework.
- Revert Circe to 0.11.1. After consideration, it was probably a mistake to use a Release Candidate version, I should stick to official stable releases.
- Fix an issue in `Jwt` pure Scala implementation around regexp. Again, try not to use this one, mostly for tests and demos.
- Fix examples.
## 3.1.0 (30/06/2019)
- If claim.audience is only one item, it will be stringified as a simple string compared to an array if several values. (thanks @msinton)
## 3.0.1 (16/06/2019)
- Fix support for Java 8. (thanks @brakthehack)
- Improve support for Scala 2.13. (thanks @erwan)
## 3.0.0 (09/06/2019)
- Allow override of the system clock, remove jmockit from tests. (thanks @Ophirr33)
- `JwtHeader` and `JwtClaim` are no longer `case class` so that you can extend them. (thanks @fahman)
- Fix Play demo app. (thanks @ma3574)
- Remove dependency on Bouncycastle. (thanks @brakthehack)
- Deprecate (but also improved) the pure Scala implementation of `JwtCore`. It's very limited, non-performant and should not be used. I will keep it around for tests and if some people need it.
## 2.1.0 (24/02/2019)
- Upgrade to play-json 2.7.1 (thanks @etspaceman)
- Add Scala 2.13.0-M5 to cross compilation for projects supporting it (thanks @2m and @ennru)
- Move JwtHeader and JwtClaim to basic classes (thanks @fahman)
## 2.0.0 (13/02/2019)
- Upgrade to Play 2.7.0 (thanks @prakhunov)
- Upgrade to play-json 2.7.0 (thanks @etspaceman)
- Drop support for Java 6 and 7
## 1.1.0 (09/01/2019)
- Upgrade to uPickle 0.7.1 (thanks @edombowsky)
- Add support for Argonaut (thanks @isbodand)
## 1.0.0 (25/11/2018)
- Bump bouncyCastle version to fix [CVE-2018-1000613](cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000613) (thanks @djamelz)
- Also 1.0.0 for no reason except no feature was needed over the last months.
## 0.19.0 (20/10/2018)
**Breaking change**
This is actually a simple one but still... fixed a typo at `asymmetric` missing one `m`, just need to rename a few types to fix your code (thanks @DrPhil).
- Add support to `spray-json` (thanks @Slakah)
- Bump some versions (thanks @vhiairrassary)
## 0.18.0 (09/10/2018)
- Add support to `aud` being a simple string on uPickle (thanks @deterdw)
- Make all `parseHeader` and `parseClaim` methods public.
## 0.17.0 (29/07/2018)
- After consideration, release #84 , which mostly allow users to write custom parsers by extending jwt-scala ones. Doc page can be found here.
## 0.16.0 (05/03/2018)
- Adding Key ID property to JwtHeader as `kid` in JSON payload
## 0.15.0 (24/02/2018)
- Upgrade to uPickle 0.5.1
- Upgrade to Circe 0.9.1 (thanks @jan0sch)
## 0.14.1 (30/10/2017)
- Fix exception when `play.http.session.maxAge` is `null` in Play 2.6.x (thanks @austinpernell)
## 0.14.0 (07/07/2017)
- Add `play.http.session.jwtResponseName` to customize response header in Play (thanks @Isammoc)
- Fix code snippet style in docs
## 0.13.0 (08/06/2017)
- Upgrade to Circe 0.8.0 (thanks @dvic)
- Play 2.6 support (thanks @perotom)
- Bouncy Castle 1.57 (thanks @rwhitworth)
## 0.12.1 (29/03/2017)
- Support spaces in JSON for pure Scala JWT
## 0.12.0 (20/02/2017)
- **Breaking changes** I liked having all implicits directly inside the package object but it started to create problems. When generating the documentation, which depends on all projects, we had runtime errors while all tests were green, but they are ran on project at a time. Also, it means all implicits where always present on the scope which might not be the best option. So the idea is to move them from the package object to the `JwtXXX` object. For example, for Play Json:
```scala
// Before
// JwtJson.scala.
package pdi.jwt
object JwtJson extends JwtJsonCommon[JsObject] {
// stuff...
}
// package.scala
package pdi
package object jwt extends JwtJsonImplicits {}
// --------------------------------------------------------
// After
// JwtJson.scala.
package pdi.jwt
object JwtJson extends JwtJsonCommon[JsObject] with JwtJsonImplicits {
// stuff...
}
```
## 0.11.0 (19/02/2017)
- Drop Scala 2.10
- Play support is back
## 0.10.0 (02/02/2017)
- Support Scala 2.12.0
- Drop Play Framework support until it supports Scala 2.12
- Add uPickle support (thanks @alonsodomin)
- Update Play Json to 2.6.0-M1 for Scala 2.12 support
- Update Circe to 0.7.0
## 0.9.2 (10/11/2016)
- Support Circe 0.6.0 (thanks @TimothyKlim )
## 0.9.1 (10/11/2016)
- Support Json4s 3.5.0 (thanks @sanllanta)
## 0.9.0 (08/10/2016)
- Transformation of Signature to ASN.1 DER for ECDSA Algorithms (thanks @bestehle)
- Remove algorithm aliases to align with [JWA spec](https://tools.ietf.org/html/rfc7518#section-3.1)
## 0.8.1 (04/09/2016)
- Update to Circe 0.5.0
## 0.8.0 (05/07/2016)
- Update to Circe 0.4.1
- `audience` is now `Set[String]` rather than just `String` inside `Claim` according to JWT spec. API using `String` still available.
- Use `org.bouncycastle.util.Arrays.constantTimeAreEqual` to check signature rather than home made function.
- Remove Play Legacy since Play 2.5+ only supports Java 1.8+
## 0.7.1 (20/04/2016)
Add `leeway` support in `JwtOptions`
## 0.7.0 (17/03/2016)
Support for Circe 0.3.0
## 0.6.0 (09/03/2016)
Support for Play Framework 2.5.0
## 0.5.1 (05/03/2016)
Fix bug not-escaping quotation mark `"` when stringifying JSON.
## 0.5.0 (31/12/2015)
### Circe support
Thanks to @dwhitney , `JWT Scala` now has support for [Circe](https://github.com/travisbrown/circe). Check out [samples](http://pauldijou.fr/jwt-scala/samples/jwt-circe/) and [Scaladoc](http://pauldijou.fr/jwt-scala/api/latest/jwt-circe/).
### Disable validation
When decoding, `JWT Scala` also performs validation. If you need to decode an invalid token, you can now use a `JwtOptions` as the last argument of any decoding function to disable validation checks like expiration, notBefore and signature. Read the **Options** section of the [core sample](http://pauldijou.fr/jwt-scala/samples/jwt-core/) to know more.
### Fix null session in Play 2.4
Since 2.4, Play assign `null` as default value for some configuration keys which throw a `ConfigException.Null` in TypeSafe config lib. This should be fixed with the new configuration system at some point in the future. In the mean time, all calls reading the configuration will be wrapped in a try/catch to prevent that.
## 0.4.1 (30/09/2015)
Fix tricky bug inside all JSON libs not supporting correctly the `none` algorithm.
## 0.4.0 (24/07/2015)
Thanks a lot to @drbild for helping review the code around security vulnerabilities.
### Now on Maven
All the sub-projects are now released directly on Maven Central. Since Sonatype didn't accept `pdi` as the groupId, I had to change it to `com.pauldijou`. Sorry about that, you will need to quickly update your `build.sbt` (or whatever file contains your dependencies).
### Breaking changes
**Good news** Those changes don't impact the `jwt-play` lib, only low level APIs.
All decoding and validating methods with a `key: String` are now removed for security reasons. Please use their counterpart which now needs a 3rd argument corresponding to the list of algorithms that the token can be signed with. This list cannot mix HMAC and asymetric algorithms (like RSA or ECDSA). This is to prevent a server using RSA with a String key to receive a forged token signed with a HMAC algorithm and the RSA public key to be accepted using the same RSA public key as the HMAC secret key by default. You can learn more by reading [this article](https://www.timmclean.net/2015/03/31/jwt-algorithm-confusion.html).
```scala
// Before
val claim = Jwt.decode(token, key)
// After (knowing that you only expect a HMAC 256)
val claim = Jwt.decode(token, key, Seq(JwtAlgorithm.HS256))
// After (supporting all HMAC algorithms)
val claim = Jwt.decode(token, key, JwtAlgorithm.allHmac)
```
If you are using `SecretKey` or `PublicKey`, the list of algorithms is optional and will be automatically computed (using `JwtAlgorithm.allHmac` and `JwtAlgorithm.allAsymetric` respesctively) but feel free to provide you own list if you want to restrict the possible algorithms. More security never killed any web application.
Why not deprecate them? I considered doing that but I decided to enforce the security fix. I'm pretty sure that most people only use one HMAC algorithm with a String key and it will force them to edit their code but it should be a minor edit since you usually only decode tokens once or twice inside a code base. The fact that the project is still very new and at a `0.x` version played in the decision.
### Fixes
Fix a security vulnerability around timing attacks.
### Features
Add implicit class to convert `JwtHeader` and `JwtClaim` to `JsValue` or `JValue`. See [examples for Play JSON](http://pauldijou.fr/jwt-scala/samples/jwt-play-json/) or [examples for Json4s](http://pauldijou.fr/jwt-scala/samples/jwt-json4s/).
```scala
// Play JSON
JwtHeader(JwtAlgorithm.HS256).toJsValue
JwtClaim().by("me").to("you").about("something").issuedNow.startsNow.expiresIn(15).toJsValue
// Json4s
JwtHeader(JwtAlgorithm.HS256).toJValue
JwtClaim().by("me").to("you").about("something").issuedNow.startsNow.expiresIn(15).toJValue
```
## 0.2.1 (24/07/2015)
Same as `0.4.0` but targeting Play 2.3
## 0.3.0 (08/06/2015)
### Breaking changes
- move exceptions to their own package
- move algorithms to their own package
### Features
- support Play 2.4.0
## 0.2.0 (02/06/2015)
### Breaking changes
- removed all `Option` from API. Now, it's either nothing or a valid key. It shouldn't have a big impact since the majority of users were using valid keys already.
- when decoding a token to a `Tuple3`, the last part representing the signature is now a `String` rather than an `Option[String]`.
### New features
- full support for `SecretKey` for HMAC algorithms
- full support for `PrivateKey` and `PublicKey` for RSA and ECDSA algorithms
- Nearly all API now have 4 possible signatures (note: `JwtAsymetricAlgorithm` is either a RSA or a ECDSA algorithm)
- `method(...)`
- `method(..., key: String, algorithm: JwtAlgorithm)`
- `method(..., key: SecretKey, algorithm: JwtHmacAlgorithm)`
- `method(..., key: PrivateKey/PublicKey, algorithm: JwtAsymetricAlgorithm)`
Use `PrivateKey` when encoding and `PublicKey` when decoding or verifying.
### Bug fixes
- Some ECDSA algorithms were extending the wrong super-type
- `{"algo":"none"}` header was incorrectly supported
## 0.1.0 (18/05/2015)
No code change from 0.0.6, just more doc and tests.
## 0.0.6 (14/05/2015)
Add support for Json4s (both Native and Jackson implementations)
## 0.0.5 (13/05/2015)
We should be API ready. Just need more tests and scaladoc before production ready.
================================================
FILE: CONTRIBUTING.md
================================================
## Contributor Guide
For any bug, new feature, or documentation improvement,
the best way to start a conversation is by creating a new issue on github.
You're welcome to submit PRs right away without creating a ticket (issue) first, but be aware that there
is no guarantee your PR is going to be merged so your work might be for nothing.
## Tests
Continuous integration will run tests on your PR, needless to say it has to be green to be merged :).
To run the tests locally:
- Run all tests with `sbt testAll` (if `java.lang.LinkageError`, just re-run the command)
- Run a single project test, for example `sbt circeProject/test`
## Formatting
The project using [Scalafmt](https://scalameta.org/scalafmt/) for formatting.
Before submitting your PR you can format the code by running `sbt format`, but the best way is to configure your IDE/Editor
to pick up the Scalafmt config from the repo and format it automatically. It is supported at least by IntelliJ and VSCode.
## Documentation
To have a locally running doc website and test your documentation changes:
- `sbt ~docs/makeMicrosite`
- `cd docs/target/site`
- `jekyll serve -b /jwt-scala`
- Go to [http://localhost:4000/jwt-scala/](http://localhost:4000/jwt-scala/)
## Publishing
Create a release with a new tag on GitHub, and a new version with automatically be published to Sonatype by Github Actions with the corresponding version number.
Documentation (microsite + scaladoc) can be published with:
- `sbt publish-doc`
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
================================================
FILE: README.md
================================================
# JWT Scala
Scala support for JSON Web Token ([JWT](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token)).
Supports Java 8+, Scala 2.12, Scala 2.13 and Scala 3 (for json libraries that support it).
Dependency free.
Optional helpers for Play Framework, Play JSON, Json4s Native, Json4s Jackson, Circe, uPickle and Argonaut.
[Contributor's guide](https://github.com/jwt-scala/jwt-scala/blob/main/CONTRIBUTING.md)
## Usage
Detailed documentation is on the [Microsite](https://jwt-scala.github.io/jwt-scala).
JWT Scala is divided in several sub-projects each targeting a specific JSON library,
check the doc from the menu of the Microsite for installation and usage instructions.
## Algorithms
If you are using `String` key, please keep in mind that such keys need to be parsed. Rather than implementing a super complex parser, the one in JWT Scala is pretty simple and might not work for all use-cases (especially for ECDSA keys). In such case, consider using `SecretKey` or `PrivateKey` or `PublicKey` directly. It is way better for you. All API support all those types.
Check [ECDSA samples](https://jwt-scala.github.io/jwt-scala/jwt-core-jwt-ecdsa.html) for more infos.
| Name | Description |
| ----- | ------------------------------ |
| HMD5 | HMAC using MD5 algorithm |
| HS224 | HMAC using SHA-224 algorithm |
| HS256 | HMAC using SHA-256 algorithm |
| HS384 | HMAC using SHA-384 algorithm |
| HS512 | HMAC using SHA-512 algorithm |
| RS256 | RSASSA using SHA-256 algorithm |
| RS384 | RSASSA using SHA-384 algorithm |
| RS512 | RSASSA using SHA-512 algorithm |
| ES256 | ECDSA using SHA-256 algorithm |
| ES384 | ECDSA using SHA-384 algorithm |
| ES512 | ECDSA using SHA-512 algorithm |
| EdDSA | EdDSA signature algorithms |
## Security concerns
This lib doesn't want to impose anything, that's why, by default, a JWT claim is totally empty. That said, you should always add an `issuedAt` attribute to it, probably using `claim.issuedNow`.
The reason is that even HTTPS isn't perfect and having always the same chunk of data transfered can be of a big help to crack it. Generating a slightly different token at each request is way better even if it adds a bit of payload to the response.
If you are using a session timeout through the `expiration` attribute which is extended at each request, that's fine too. I can't find the article I read about that vulnerability but if someone has some resources about the topic, I would be glad to link them.
## License
This software is licensed under the Apache 2 license, quoted below.
Copyright 2021 JWT-Scala Contributors.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with the License. You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
================================================
FILE: build.sbt
================================================
import scala.io.Source
import scala.sys.process._
import com.jsuereth.sbtpgp.PgpKeys._
import sbt.Keys._
import sbt.Tests._
import sbt._
val previousVersion = "9.4.0"
val buildVersion = "9.4.1"
val scala212 = "2.12.20"
val scala213 = "2.13.16"
val scala3 = "3.3.7"
Global / onChangedBuildSource := ReloadOnSourceChanges
ThisBuild / versionScheme := Some("early-semver")
val projects = Seq(
"playJson",
"json4sNative",
"json4sJackson",
"zioJson",
"argonaut",
"playFramework"
)
val crossProjects = Seq(
"core",
"circe",
"upickle"
)
val allProjects = crossProjects.flatMap(p => Seq(s"${p}JVM", s"${p}JS", s"${p}Native")) ++ projects
addCommandAlias("publish-doc", "docs/makeMicrosite; docs/publishMicrosite")
addCommandAlias("testAll", allProjects.map(p => p + "/test").mkString(";", ";", ""))
addCommandAlias("format", "all scalafmtAll scalafmtSbt")
addCommandAlias("formatCheck", "all scalafmtCheckAll scalafmtSbtCheck")
lazy val cleanScript = taskKey[Unit]("Clean tmp files")
cleanScript := {
"./scripts/clean.sh" !
}
lazy val docsMappingsAPIDir: SettingKey[String] =
settingKey[String]("Name of subdirectory in site target directory for api docs")
val crossVersionAll = Seq(scala212, scala213, scala3)
val crossVersionNo212 = Seq(scala213, scala3)
val baseSettings = Seq(
organization := "com.github.jwt-scala",
ThisBuild / scalaVersion := scala213,
crossScalaVersions := crossVersionAll,
autoAPIMappings := true,
libraryDependencies ++= Seq(Libs.munit.value, Libs.munitScalacheck.value),
testFrameworks += new TestFramework("munit.Framework"),
mimaFailOnNoPrevious := false,
Test / aggregate := false,
Test / fork := true,
Test / parallelExecution := false,
Compile / doc / scalacOptions ~= (_.filterNot(_ == "-Xfatal-warnings")),
Compile / doc / scalacOptions ++= Seq(
"-no-link-warnings" // Suppresses problems with Scaladoc @throws links
),
scalacOptions ++= (CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, _)) => Seq("-Xsource:3")
case _ => Nil
}),
javacOptions ++= Seq("-source", "1.8", "-target", "1.8")
)
val publishSettings = Seq(
homepage := Some(url("https://jwt-scala.github.io/jwt-scala/")),
apiURL := Some(url("https://jwt-scala.github.io/jwt-scala/api/")),
Test / publishArtifact := false,
licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0")),
pomIncludeRepository := { _ => false },
scmInfo := Some(
ScmInfo(
url("https://github.com/jwt-scala/jwt-scala"),
"scm:git@github.com:jwt-scala/jwt-scala.git"
)
),
developers := List(
Developer(
id = "pdi",
name = "Paul Dijou",
email = "paul.dijou@gmail.com",
url = url("http://pauldijou.fr")
),
Developer(
id = "erwan",
name = "Erwan Loisant",
email = "erwan@loisant.com",
url = url("https://caffeinelab.net")
)
),
publishConfiguration := publishConfiguration.value.withOverwrite(true),
publishSignedConfiguration := publishSignedConfiguration.value.withOverwrite(true),
publishLocalConfiguration := publishLocalConfiguration.value.withOverwrite(true),
publishLocalSignedConfiguration := publishLocalSignedConfiguration.value.withOverwrite(true)
)
val noPublishSettings = Seq(
publish := (()),
publishLocal := (()),
publishArtifact := false
)
lazy val commonJsSettings = Seq(
Test / fork := false
)
// Normal published settings
val releaseSettings = baseSettings ++ publishSettings
// Local non-published projects
val localSettings = baseSettings ++ noPublishSettings
lazy val jwtScala = project
.in(file("."))
.settings(localSettings)
.settings(
name := "jwt-scala"
)
.aggregate(
json4sNative,
json4sJackson,
circe.jvm,
circe.js,
circe.native,
upickle.jvm,
upickle.js,
upickle.native,
zioJson,
playFramework,
argonaut
)
.dependsOn(
json4sNative,
json4sJackson,
circe.jvm,
circe.js,
circe.native,
upickle.jvm,
upickle.js,
upickle.native,
zioJson,
playFramework,
argonaut
)
.settings(crossScalaVersions := List())
lazy val docs = project
.in(file("docs"))
.enablePlugins(
SitePreviewPlugin,
SiteScaladocPlugin,
ScalaUnidocPlugin,
ParadoxSitePlugin,
ParadoxMaterialThemePlugin
)
.settings(name := "jwt-docs")
.settings(localSettings)
.settings(
libraryDependencies ++= Seq("org.playframework" %% "play-test" % Versions.play),
ScalaUnidoc / siteSubdirName := "api",
addMappingsToSiteDir(
ScalaUnidoc / packageDoc / mappings,
ScalaUnidoc / siteSubdirName
),
ScalaUnidoc / unidoc / unidocProjectFilter := inProjects(
core.jvm,
circe.jvm,
json4sNative,
upickle.jvm,
zioJson,
playJson,
playFramework,
argonaut,
zioJson
),
baseSettings,
publishArtifact := false,
Compile / paradoxMaterialTheme ~= (_.withRepository(
uri("https://github.com/jwt-scala/jwt-scala")
)),
packageSite / artifactPath := new java.io.File("target/artifact.zip")
)
.dependsOn(playFramework, json4sNative, circe.jvm, upickle.jvm, zioJson, argonaut)
lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Full)
.settings(releaseSettings)
.settings(name := "jwt-core", libraryDependencies ++= Seq(Libs.bouncyCastle))
.jsSettings(commonJsSettings)
.jsSettings(
libraryDependencies ++= Seq(
Libs.scalaJavaTime.value,
Libs.scalajsSecureRandom.value
)
)
.nativeSettings(
libraryDependencies ++= Seq(
Libs.scalaJavaTime.value
),
Test / fork := false
)
lazy val jsonCommon = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("json/common"))
.settings(releaseSettings)
.settings(
name := "jwt-json-common"
)
.jsSettings(commonJsSettings)
.nativeSettings(Test / fork := false)
.aggregate(core)
.dependsOn(core % "compile->compile;test->test")
lazy val playJson = project
.in(file("json/play-json"))
.settings(releaseSettings)
.settings(
name := "jwt-play-json",
libraryDependencies ++= Seq(Libs.playJson)
)
.aggregate(jsonCommon.jvm)
.dependsOn(jsonCommon.jvm % "compile->compile;test->test")
lazy val circe = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Full)
.in(file("json/circe"))
.settings(releaseSettings)
.settings(
name := "jwt-circe",
libraryDependencies ++= Seq(
Libs.circeCore.value,
Libs.circeJawn.value,
Libs.circeParse.value,
Libs.circeGeneric.value % "test"
)
)
.jsSettings(commonJsSettings)
.nativeSettings(Test / fork := false)
.aggregate(jsonCommon)
.dependsOn(jsonCommon % "compile->compile;test->test")
lazy val upickle = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Full)
.in(file("json/upickle"))
.settings(releaseSettings)
.settings(
name := "jwt-upickle",
libraryDependencies ++= Seq(Libs.upickle.value)
)
.jsSettings(commonJsSettings)
.nativeSettings(Test / fork := false)
.aggregate(jsonCommon)
.dependsOn(jsonCommon % "compile->compile;test->test")
lazy val zioJson = project
.in(file("json/zio-json"))
.settings(releaseSettings)
.settings(
name := "jwt-zio-json",
libraryDependencies ++= Seq(Libs.zioJson)
)
.aggregate(jsonCommon.jvm)
.dependsOn(jsonCommon.jvm % "compile->compile;test->test")
lazy val json4sCommon = project
.in(file("json/json4s-common"))
.settings(releaseSettings)
.settings(
name := "jwt-json4s-common",
libraryDependencies ++= Seq(Libs.json4sCore)
)
.aggregate(jsonCommon.jvm)
.dependsOn(jsonCommon.jvm % "compile->compile;test->test")
lazy val json4sNative = project
.in(file("json/json4s-native"))
.settings(releaseSettings)
.settings(
name := "jwt-json4s-native",
libraryDependencies ++= Seq(Libs.json4sNative)
)
.aggregate(json4sCommon)
.dependsOn(json4sCommon % "compile->compile;test->test")
lazy val json4sJackson = project
.in(file("json/json4s-jackson"))
.settings(releaseSettings)
.settings(
name := "jwt-json4s-jackson",
libraryDependencies ++= Seq(Libs.json4sJackson)
)
.aggregate(json4sCommon)
.dependsOn(json4sCommon % "compile->compile;test->test")
lazy val argonaut = project
.in(file("json/argonaut"))
.settings(releaseSettings)
.settings(
name := "jwt-argonaut",
libraryDependencies ++= Seq(Libs.argonaut)
)
.aggregate(jsonCommon.jvm)
.dependsOn(jsonCommon.jvm % "compile->compile;test->test")
def groupPlayTest(tests: Seq[TestDefinition], files: Seq[File]) = tests.map { t =>
val options = ForkOptions()
Group(t.name, Seq(t), SubProcess(options))
}
lazy val playFramework = project
.in(file("play"))
.settings(releaseSettings)
.settings(
name := "jwt-play",
crossScalaVersions := crossVersionNo212,
libraryDependencies ++= Seq(Libs.play, Libs.playTest, Libs.guice),
Test / testGrouping := groupPlayTest(
(Test / definedTests).value,
(Test / dependencyClasspath).value.files
)
)
.aggregate(playJson)
.dependsOn(playJson % "compile->compile;test->test")
================================================
FILE: core/jvm/src/test/scala/JwtSpec.scala
================================================
package pdi.jwt
import scala.util.{Success, Try}
import pdi.jwt.algorithms.*
import pdi.jwt.exceptions.*
class JwtSpec extends munit.FunSuite with Fixture {
val afterExpirationJwt: Jwt = Jwt(afterExpirationClock)
val beforeNotBeforeJwt: Jwt = Jwt(beforeNotBeforeClock)
val afterNotBeforeJwt: Jwt = Jwt(afterNotBeforeClock)
val validTimeJwt: Jwt = Jwt(validTimeClock)
def battleTestEncode(d: DataEntryBase, key: String, jwt: Jwt) = {
assertEquals(d.tokenEmpty, jwt.encode(claim))
assertEquals(d.token, jwt.encode(d.header, claim, key, d.algo))
assertEquals(d.token, jwt.encode(claim, key, d.algo))
assertEquals(d.tokenEmpty, jwt.encode(claimClass))
assertEquals(d.token, jwt.encode(claimClass, key, d.algo))
assertEquals(d.token, jwt.encode(d.headerClass, claimClass, key))
}
test("should parse JSON with spaces") {
assert(Jwt.isValid(tokenWithSpaces))
}
test("should decode subject with a dash") {
Jwt.decode(validTimeJwt.encode("""{"sub":"das-hed"""")) match {
case Success(jwt) => assertEquals(jwt.subject, Option("das-hed"))
case _ => fail("failed decoding token")
}
}
test("should decode subject with an underscore") {
Jwt.decode(validTimeJwt.encode("""{"sub":"das_hed"""")) match {
case Success(jwt) => assertEquals(jwt.subject, Option("das_hed"))
case _ => fail("failed decoding token")
}
}
test("should decode jti with dashes") {
val id = java.util.UUID.randomUUID().toString
Jwt.decode(validTimeJwt.encode(s"""{"jti":"$id"""")) match {
case Success(jwt) => assertEquals(jwt.jwtId, Option(id))
case _ => fail("failed decoding token")
}
}
test("should decode issuer with dashes") {
Jwt.decode(validTimeJwt.encode(s"""{"iss":"das-_hed"""")) match {
case Success(jwt) => assertEquals(jwt.issuer, Option("das-_hed"))
case _ => fail("failed decoding token")
}
}
test("should encode Hmac") {
data.foreach { d => battleTestEncode(d, secretKey, validTimeJwt) }
}
test("should encode RSA") {
dataRSA.foreach { d => battleTestEncode(d, privateKeyRSA, validTimeJwt) }
}
test("should encode EdDSA") {
dataEdDSA.foreach { d => battleTestEncode(d, privateKeyEd25519, validTimeJwt) }
}
test("should be symmetric") {
data.foreach { d =>
testTryAll(
validTimeJwt.decodeAll(
validTimeJwt.encode(d.header, claim, secretKey, d.algo),
secretKey,
JwtAlgorithm.allHmac()
),
(d.headerClass, claimClass, d.signature),
d.algo.fullName
)
}
}
test("should be symmetric (RSA)") {
dataRSA.foreach { d =>
testTryAllWithoutSignature(
validTimeJwt.decodeAll(
validTimeJwt.encode(d.header, claim, randomRSAKey.getPrivate, d.algo),
randomRSAKey.getPublic,
JwtAlgorithm.allRSA()
),
(d.headerClass, claimClass),
d.algo.fullName
)
testTryAllWithoutSignature(
validTimeJwt.decodeAll(
validTimeJwt.encode(d.header, claim, randomRSAKey.getPrivate, d.algo),
randomRSAKey.getPublic
),
(d.headerClass, claimClass),
d.algo.fullName
)
}
}
test("should be symmetric (ECDSA)") {
dataECDSA.foreach { d =>
testTryAllWithoutSignature(
validTimeJwt.decodeAll(
validTimeJwt.encode(d.header, claim, randomECKey.getPrivate, d.algo),
randomECKey.getPublic,
JwtAlgorithm.allECDSA()
),
(d.headerClass, claimClass),
d.algo.fullName
)
}
}
test("should be symmetric (EdDSA)") {
dataEdDSA.foreach { d =>
testTryAllWithoutSignature(
validTimeJwt.decodeAll(
validTimeJwt.encode(d.header, claim, randomEd25519Key.getPrivate, d.algo),
randomEd25519Key.getPublic,
JwtAlgorithm.allEdDSA()
),
(d.headerClass, claimClass),
d.algo.fullName
)
testTryAllWithoutSignature(
validTimeJwt.decodeAll(
validTimeJwt.encode(d.header, claim, randomEd25519Key.getPrivate, d.algo),
randomEd25519Key.getPublic
),
(d.headerClass, claimClass),
d.algo.fullName
)
}
}
test("should decodeRawAll") {
data.foreach { d =>
assertEquals(
validTimeJwt.decodeRawAll(d.token, secretKey, JwtAlgorithm.allHmac()),
Success((d.header, claim, d.signature)),
d.algo.fullName
)
assertEquals(
validTimeJwt.decodeRawAll(d.token, secretKeyOf(d.algo)),
Success((d.header, claim, d.signature)),
d.algo.fullName
)
assertEquals(
validTimeJwt.decodeRawAll(d.token, secretKeyOf(d.algo), JwtAlgorithm.allHmac()),
Success((d.header, claim, d.signature)),
d.algo.fullName
)
}
}
test("should decodeRaw") {
data.foreach { d =>
assertEquals(
validTimeJwt.decodeRaw(d.token, secretKey, JwtAlgorithm.allHmac()),
Success((claim)),
d.algo.fullName
)
assertEquals(
validTimeJwt.decodeRaw(d.token, secretKeyOf(d.algo)),
Success((claim)),
d.algo.fullName
)
assertEquals(
validTimeJwt.decodeRaw(d.token, secretKeyOf(d.algo), JwtAlgorithm.allHmac()),
Success((claim)),
d.algo.fullName
)
}
}
test("should decodeAll") {
data.foreach { d =>
testTryAll(
validTimeJwt.decodeAll(d.token, secretKey, JwtAlgorithm.allHmac()),
(d.headerClass, claimClass, d.signature),
d.algo.fullName
)
testTryAll(
validTimeJwt.decodeAll(d.token, secretKeyOf(d.algo)),
(d.headerClass, claimClass, d.signature),
d.algo.fullName
)
}
}
test("should decode") {
data.foreach { d =>
assertEquals(
validTimeJwt.decode(d.token, secretKey, JwtAlgorithm.allHmac()).get,
claimClass,
d.algo.fullName
)
assertEquals(
validTimeJwt.decode(d.token, secretKeyOf(d.algo)).get,
claimClass,
d.algo.fullName
)
}
}
test("should validate correct tokens") {
data.foreach { d =>
assertEquals(
(),
validTimeJwt.validate(d.token, secretKey, JwtAlgorithm.allHmac()),
d.algo.fullName
)
assert(validTimeJwt.isValid(d.token, secretKey, JwtAlgorithm.allHmac()), d.algo.fullName)
assertEquals((), validTimeJwt.validate(d.token, secretKeyOf(d.algo)), d.algo.fullName)
assert(validTimeJwt.isValid(d.token, secretKeyOf(d.algo)), d.algo.fullName)
}
dataRSA.foreach { d =>
assertEquals(
(),
validTimeJwt.validate(d.token, publicKeyRSA, JwtAlgorithm.allRSA()),
d.algo.fullName
)
assert(validTimeJwt.isValid(d.token, publicKeyRSA, JwtAlgorithm.allRSA()), d.algo.fullName)
}
dataEdDSA.foreach { d =>
assertEquals(
(),
validTimeJwt.validate(d.token, publicKeyEd25519, JwtAlgorithm.allEdDSA()),
d.algo.fullName
)
assert(
validTimeJwt.isValid(d.token, publicKeyEd25519, JwtAlgorithm.allEdDSA()),
d.algo.fullName
)
}
}
def oneLine(key: String) = key.replaceAll("\r\n", " ").replaceAll("\n", " ")
test("should validate using RSA keys converted to single line") {
val pubKey = oneLine(publicKeyRSA)
dataRSA.foreach { d =>
assertEquals(
(),
validTimeJwt.validate(d.token, pubKey, JwtAlgorithm.allRSA()),
d.algo.fullName
)
}
}
test("should validate ECDSA from other implementations") {
val publicKey =
"MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQASisgweVL1tAtIvfmpoqvdXF8sPKTV9YTKNxBwkdkm+/auh4pR8TbaIfsEzcsGUVv61DFNFXb0ozJfurQ59G2XcgAn3vROlSSnpbIvuhKrzL5jwWDTaYa5tVF1Zjwia/5HUhKBkcPuWGXg05nMjWhZfCuEetzMLoGcHmtvabugFrqsAg="
val verifier = (token: String) => {
assert(Jwt.isValid(token, publicKey, Seq(JwtAlgorithm.ES512)))
}
// Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1
verifier(
"eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ"
)
// Test verification for token created using https://github.com/jwt/ruby-jwt/tree/v1.5.4
verifier(
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJ0ZXN0IjoidGVzdCJ9.AV26tERbSEwcoDGshneZmhokg-tAKUk0uQBoHBohveEd51D5f6EIs6cskkgwtfzs4qAGfx2rYxqQXr7LTXCNquKiAJNkTIKVddbPfped3_TQtmHZTmMNiqmWjiFj7Y9eTPMMRRu26w4gD1a8EQcBF-7UGgeH4L_1CwHJWAXGbtu7uMUn"
)
}
test("should invalidate WTF tokens") {
val tokens = Seq("1", "abcde", "", "a.b.c.d")
tokens.foreach { token =>
assert(Try(Jwt.validate(token, secretKey, JwtAlgorithm.allHmac())).isFailure)
assert(!Jwt.isValid(token, secretKey, JwtAlgorithm.allHmac()), token)
}
}
test("should invalidate non-base64 tokens") {
val tokens = Seq("a.b", "a.b.c", "1.2.3", "abcde.azer.azer", "aze$.azer.azer")
tokens.foreach { token =>
assert(Try(Jwt.validate(token, secretKey, JwtAlgorithm.allHmac())).isFailure)
assert(!Jwt.isValid(token, secretKey, JwtAlgorithm.allHmac()), token)
}
}
test("should invalidate expired tokens") {
data.foreach { d =>
assert(Try(afterExpirationJwt.validate(d.token, secretKey, JwtAlgorithm.allHmac())).isFailure)
assert(
!afterExpirationJwt.isValid(d.token, secretKey, JwtAlgorithm.allHmac()),
d.algo.fullName
)
assert(Try(afterExpirationJwt.validate(d.token, secretKeyOf(d.algo))).isFailure)
assert(!afterExpirationJwt.isValid(d.token, secretKeyOf(d.algo)), d.algo.fullName)
}
dataRSA.foreach { d =>
assert(
Try(afterExpirationJwt.validate(d.token, publicKeyRSA, JwtAlgorithm.allRSA())).isFailure
)
assert(
!afterExpirationJwt.isValid(d.token, publicKeyRSA, JwtAlgorithm.allRSA()),
d.algo.fullName
)
}
dataEdDSA.foreach { d =>
assert(
Try(
afterExpirationJwt.validate(d.token, publicKeyEd25519, JwtAlgorithm.allEdDSA())
).isFailure
)
assert(
!afterExpirationJwt.isValid(d.token, publicKeyEd25519, JwtAlgorithm.allEdDSA()),
d.algo.fullName
)
}
}
test("should validate expired tokens with leeway") {
val options = JwtOptions(leeway = 60)
data.foreach { d =>
afterExpirationJwt.validate(d.token, secretKey, JwtAlgorithm.allHmac(), options)
assert(
afterExpirationJwt.isValid(d.token, secretKey, JwtAlgorithm.allHmac(), options),
d.algo.fullName
)
afterExpirationJwt.validate(d.token, secretKeyOf(d.algo), options)
assert(afterExpirationJwt.isValid(d.token, secretKeyOf(d.algo), options), d.algo.fullName)
}
dataRSA.foreach { d =>
afterExpirationJwt.validate(d.token, publicKeyRSA, JwtAlgorithm.allRSA(), options)
assert(
afterExpirationJwt.isValid(d.token, publicKeyRSA, JwtAlgorithm.allRSA(), options),
d.algo.fullName
)
}
dataEdDSA.foreach { d =>
afterExpirationJwt.validate(d.token, publicKeyEd25519, JwtAlgorithm.allEdDSA(), options)
assert(
afterExpirationJwt.isValid(d.token, publicKeyEd25519, JwtAlgorithm.allEdDSA(), options),
d.algo.fullName
)
}
}
test("should invalidate early tokens") {
data.foreach { d =>
val claimNotBefore = claimClass.startsAt(notBefore)
val token = beforeNotBeforeJwt.encode(claimNotBefore, secretKey, d.algo)
assert(Try(beforeNotBeforeJwt.validate(token, secretKey, JwtAlgorithm.allHmac())).isFailure)
assert(!beforeNotBeforeJwt.isValid(token, secretKey, JwtAlgorithm.allHmac()), d.algo.fullName)
assert(Try(beforeNotBeforeJwt.validate(token, secretKeyOf(d.algo))).isFailure)
assert(!beforeNotBeforeJwt.isValid(token, secretKeyOf(d.algo)), d.algo.fullName)
}
dataRSA.foreach { d =>
val claimNotBefore = claimClass.startsAt(notBefore)
val token = beforeNotBeforeJwt.encode(claimNotBefore, privateKeyRSA, d.algo)
assert(Try(beforeNotBeforeJwt.validate(token, publicKeyRSA, JwtAlgorithm.allRSA())).isFailure)
assert(
!beforeNotBeforeJwt.isValid(token, publicKeyRSA, JwtAlgorithm.allRSA()),
d.algo.fullName
)
}
dataEdDSA.foreach { d =>
val claimNotBefore = claimClass.startsAt(notBefore)
val token = beforeNotBeforeJwt.encode(claimNotBefore, privateKeyEd25519, d.algo)
assert(
Try(beforeNotBeforeJwt.validate(token, publicKeyEd25519, JwtAlgorithm.allEdDSA())).isFailure
)
assert(
!beforeNotBeforeJwt.isValid(token, publicKeyEd25519, JwtAlgorithm.allEdDSA()),
d.algo.fullName
)
}
}
test("should validate early tokens with leeway") {
val options = JwtOptions(leeway = 60)
data.foreach { d =>
val claimNotBefore = claimClass.startsAt(notBefore)
val token = beforeNotBeforeJwt.encode(claimNotBefore, secretKey, d.algo)
beforeNotBeforeJwt.validate(token, secretKey, JwtAlgorithm.allHmac(), options)
assert(
beforeNotBeforeJwt.isValid(token, secretKey, JwtAlgorithm.allHmac(), options),
d.algo.fullName
)
beforeNotBeforeJwt.validate(token, secretKeyOf(d.algo), options)
assert(beforeNotBeforeJwt.isValid(token, secretKeyOf(d.algo), options), d.algo.fullName)
}
dataRSA.foreach { d =>
val claimNotBefore = claimClass.startsAt(notBefore)
val token = beforeNotBeforeJwt.encode(claimNotBefore, privateKeyRSA, d.algo)
assert(Try(beforeNotBeforeJwt.validate(token, publicKeyRSA, JwtAlgorithm.allRSA())).isFailure)
assert(
!beforeNotBeforeJwt.isValid(token, publicKeyRSA, JwtAlgorithm.allRSA()),
d.algo.fullName
)
}
dataEdDSA.foreach { d =>
val claimNotBefore = claimClass.startsAt(notBefore)
val token = beforeNotBeforeJwt.encode(claimNotBefore, privateKeyEd25519, d.algo)
assert(
Try(beforeNotBeforeJwt.validate(token, publicKeyEd25519, JwtAlgorithm.allEdDSA())).isFailure
)
assert(
!beforeNotBeforeJwt.isValid(token, publicKeyEd25519, JwtAlgorithm.allEdDSA()),
d.algo.fullName
)
}
}
test("should invalidate wrong keys") {
data.foreach { d =>
assert(
Try(
validTimeJwt.validate(d.token, "wrong key", JwtAlgorithm.allHmac())
).isFailure
)
assert(!validTimeJwt.isValid(d.token, "wrong key", JwtAlgorithm.allHmac()), d.algo.fullName)
}
dataRSA.foreach { d =>
assert(!validTimeJwt.isValid(d.token, "wrong key", JwtAlgorithm.allRSA()), d.algo.fullName)
}
dataEdDSA.foreach { d =>
assert(!validTimeJwt.isValid(d.token, "wrong key", JwtAlgorithm.allEdDSA()), d.algo.fullName)
}
}
test("should fail on non-exposed algorithms") {
data.foreach { d =>
assert(
Try(
validTimeJwt.validate(d.token, secretKey, Seq.empty[JwtHmacAlgorithm])
).isFailure
)
assert(
!validTimeJwt.isValid(d.token, secretKey, Seq.empty[JwtHmacAlgorithm]),
d.algo.fullName
)
}
data.foreach { d =>
assert(Try(validTimeJwt.validate(d.token, secretKey, JwtAlgorithm.allRSA())).isFailure)
assert(!validTimeJwt.isValid(d.token, secretKey, JwtAlgorithm.allRSA()), d.algo.fullName)
}
dataRSA.foreach { d =>
assert(
Try(
validTimeJwt.validate(d.token, publicKeyRSA, JwtAlgorithm.allHmac())
).isFailure
)
assert(!validTimeJwt.isValid(d.token, publicKeyRSA, JwtAlgorithm.allHmac()), d.algo.fullName)
}
}
test("should invalidate wrong algos") {
val token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJXVEYifQ.e30"
assert(Jwt.decode(token).isFailure)
intercept[JwtNonSupportedAlgorithm] { Jwt.decode(token).get }
}
test("should decode tokens with unknown algos depending on options") {
val token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJXVEYifQ.e30"
val decoded = Jwt.decode(token, options = JwtOptions(signature = false))
assert(decoded.isSuccess)
}
test("should skip expiration validation depending on options") {
val options = JwtOptions(expiration = false)
data.foreach { d =>
afterExpirationJwt.validate(d.token, secretKey, JwtAlgorithm.allHmac(), options)
assert(
afterExpirationJwt.isValid(d.token, secretKey, JwtAlgorithm.allHmac(), options),
d.algo.fullName
)
afterExpirationJwt.validate(d.token, secretKeyOf(d.algo), options)
assert(afterExpirationJwt.isValid(d.token, secretKeyOf(d.algo), options), d.algo.fullName)
}
dataRSA.foreach { d =>
afterExpirationJwt.validate(d.token, publicKeyRSA, JwtAlgorithm.allRSA(), options)
assert(
afterExpirationJwt.isValid(d.token, publicKeyRSA, JwtAlgorithm.allRSA(), options),
d.algo.fullName
)
}
dataEdDSA.foreach { d =>
afterExpirationJwt.validate(d.token, publicKeyEd25519, JwtAlgorithm.allEdDSA(), options)
assert(
afterExpirationJwt.isValid(d.token, publicKeyEd25519, JwtAlgorithm.allEdDSA(), options),
d.algo.fullName
)
}
}
test("should skip notBefore validation depending on options") {
val options = JwtOptions(notBefore = false)
data.foreach { d =>
val claimNotBefore = claimClass.startsAt(notBefore)
val token = beforeNotBeforeJwt.encode(claimNotBefore, secretKey, d.algo)
beforeNotBeforeJwt.validate(token, secretKey, JwtAlgorithm.allHmac(), options)
assert(
beforeNotBeforeJwt.isValid(token, secretKey, JwtAlgorithm.allHmac(), options),
d.algo.fullName
)
beforeNotBeforeJwt.validate(token, secretKeyOf(d.algo), options)
assert(beforeNotBeforeJwt.isValid(token, secretKeyOf(d.algo), options), d.algo.fullName)
}
dataRSA.foreach { d =>
val claimNotBefore = claimClass.startsAt(notBefore)
val token = beforeNotBeforeJwt.encode(claimNotBefore, privateKeyRSA, d.algo)
beforeNotBeforeJwt.validate(token, publicKeyRSA, JwtAlgorithm.allRSA(), options)
assert(
beforeNotBeforeJwt.isValid(token, publicKeyRSA, JwtAlgorithm.allRSA(), options),
d.algo.fullName
)
}
dataEdDSA.foreach { d =>
val claimNotBefore = claimClass.startsAt(notBefore)
val token = beforeNotBeforeJwt.encode(claimNotBefore, privateKeyEd25519, d.algo)
beforeNotBeforeJwt.validate(token, publicKeyEd25519, JwtAlgorithm.allEdDSA(), options)
assert(
beforeNotBeforeJwt.isValid(token, publicKeyEd25519, JwtAlgorithm.allEdDSA(), options),
d.algo.fullName
)
}
}
test("should skip signature validation depending on options") {
val options = JwtOptions(signature = false)
data.foreach { d =>
validTimeJwt.validate(d.token, "wrong key", JwtAlgorithm.allHmac(), options)
assert(
validTimeJwt.isValid(d.token, "wrong key", JwtAlgorithm.allHmac(), options),
d.algo.fullName
)
}
dataRSA.foreach { d =>
assert(
validTimeJwt.isValid(d.token, "wrong key", JwtAlgorithm.allRSA(), options),
d.algo.fullName
)
}
dataEdDSA.foreach { d =>
assert(
validTimeJwt.isValid(d.token, "wrong key", JwtAlgorithm.allEdDSA(), options),
d.algo.fullName
)
}
}
def testTryAll(
t: Try[(JwtHeader, JwtClaim, String)],
exp: (JwtHeader, JwtClaim, String),
clue: String
) = {
assert(t.isSuccess, clue)
val (h1, c1, s1) = t.get
val (h2, c2, s2) = exp
assertEquals(h1, h2)
assertEquals(c1, c2)
assertEquals(s1, s2)
}
def testTryAllWithoutSignature(
t: Try[(JwtHeader, JwtClaim, String)],
exp: (JwtHeader, JwtClaim, String),
clue: String
) = {
assert(t.isSuccess, clue)
val (h1, c1, _) = t.get
val (h2, c2, _) = exp
assertEquals(h1, h2)
assertEquals(c1, c2)
}
def testTryAllWithoutSignature(
t: Try[(JwtHeader, JwtClaim, String)],
exp: (JwtHeader, JwtClaim),
clue: String
) = {
assert(t.isSuccess, clue)
val (h1, c1, _) = t.get
val (h2, c2) = exp
assertEquals(h1, h2)
assertEquals(c1, c2)
}
}
================================================
FILE: core/jvm/src/test/scala/JwtUtilsSpec.scala
================================================
package pdi.jwt
import java.security.spec.ECGenParameterSpec
import java.security.{KeyPairGenerator, SecureRandom}
import org.scalacheck.Gen
import org.scalacheck.Prop.*
import pdi.jwt.exceptions.JwtSignatureFormatException
case class TestObject(value: String) {
override def toString(): String = this.value
}
class JwtUtilsSpec extends munit.ScalaCheckSuite with Fixture {
val ENCODING = JwtUtils.ENCODING
test("hashToJson should transform a seq of tuples to a valid JSON") {
val values: Seq[(String, Seq[(String, Any)])] = Seq(
"""{"a":"b","c":1,"d":true,"e":2,"f":3.4,"g":5.6}""" -> Seq(
"a" -> "b",
"c" -> 1,
"d" -> true,
"e" -> 2L,
"f" -> 3.4f,
"g" -> 5.6
),
"{}" -> Seq(),
"""{"a\"b":"a\"b","c\"d":"c\"d","e\"f":["e\"f","e\"f"]}""" -> Seq(
"""a"b""" -> """a"b""",
"""c"d""" -> TestObject("""c"d"""),
"""e"f""" -> Seq("""e"f""", TestObject("""e"f"""))
)
)
values.zipWithIndex.foreach { case (value, index) =>
assertEquals(value._1, JwtUtils.hashToJson(value._2), "at index " + index)
}
}
test("mergeJson should correctly merge 2 JSONs") {
val values: Seq[(String, String, Seq[String])] = Seq(
("{}", "{}", Seq("{}")),
("""{"a":1}""", """{"a":1}""", Seq("")),
("""{"a":1}""", """{"a":1}""", Seq("{}")),
("""{"a":1}""", """{}""", Seq("""{"a":1}""")),
("""{"a":1}""", "", Seq("""{"a":1}""")),
("""{"a":1,"b":2}""", """{"a":1}""", Seq("""{"b":2}""")),
("""{"a":1,"b":2,"c":"d"}""", """{"a":1}""", Seq("""{"b":2}""", """{"c":"d"}"""))
)
values.zipWithIndex.foreach { case (value, index) =>
assertEquals(value._1, JwtUtils.mergeJson(value._2, value._3: _*), "at index " + index)
}
}
test("Claim.toJson should correctly encode a Claim to JSON") {
val claim = JwtClaim(
issuer = Some(""),
audience = Some(Set("")),
subject = Some("da1b3852-6827-11e9-a923-1681be663d3e"),
expiration = Some(1597914901),
issuedAt = Some(1566378901),
content = "{\"a\":\"da1b3852-6827-11e9-a923-1681be663d3e\",\"b\":123.34}"
)
val jsonClaim =
"""{"iss":"","sub":"da1b3852-6827-11e9-a923-1681be663d3e","aud":"","exp":1597914901,"iat":1566378901,"a":"da1b3852-6827-11e9-a923-1681be663d3e","b":123.34}"""
assertEquals(jsonClaim, claim.toJson)
}
test("transcodeSignatureToDER should throw JwtValidationException if signature is too long") {
val signature = JwtUtils.bytify(
"AU6-jw28DX1QMY0Ar8CTcnIAc0WKGe3nNVHkE7ayHSxvOLxE5YQSiZtbPn3y-vDHoQCOMId4rPdIJhD_NOUqnH_rAKA5w9ZlhtW0GwgpvOg1_5oLWnWXQvPjJjC5YsLqEssoMITtOmfkBsQMgLAF_LElaaCWhkJkOCtcZmroUW_b5CXB"
)
interceptMessage[JwtSignatureFormatException]("Invalid ECDSA signature format") {
JwtUtils.transcodeSignatureToDER(signature ++ signature)
}
}
test("transcodeSignatureToDER should transocde empty signature") {
val signature: Array[Byte] = Array[Byte](0)
JwtUtils.transcodeSignatureToDER(signature)
}
test("transcodeSignatureToConcat should throw JwtValidationException if length incorrect") {
val signature = JwtUtils.bytify(
"MIEAAGg3OVb/ZeX12cYrhK3c07TsMKo7Kc6SiqW++4CAZWCX72DkZPGTdCv2duqlupsnZL53hiG3rfdOLj8drndCU+KHGrn5EotCATdMSLCXJSMMJoHMM/ZPG+QOHHPlOWnAvpC1v4lJb32WxMFNz1VAIWrl9Aa6RPG1GcjCTScKjvEE"
)
interceptMessage[JwtSignatureFormatException]("Invalid ECDSA signature format") {
JwtUtils.transcodeSignatureToConcat(signature, 132)
}
}
test(
"transcodeSignatureToConcat should throw JwtValidationException if signature is incorrect "
) {
val signature = JwtUtils.bytify(
"MIGBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
)
interceptMessage[JwtSignatureFormatException]("Invalid ECDSA signature format") {
JwtUtils.transcodeSignatureToConcat(signature, 132)
}
}
test(
"transcodeSignatureToConcat should throw JwtValidationException if signature is incorrect 2"
) {
val signature = JwtUtils.bytify(
"MIGBAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
)
interceptMessage[JwtSignatureFormatException]("Invalid ECDSA signature format") {
JwtUtils.transcodeSignatureToConcat(signature, 132)
}
}
test("transcodeSignatureToConcat and transcodeSignatureToDER should be symmetric") {
val signature = JwtUtils.bytify(
"AbxLPbA3dm9V0jt6c_ahf8PYioFvnryTe3odgolhcgwBUl4ifpwUBJ--GgiXC8vms45c8vI40ZSdkm5NoNn1wTHOAfkepNy-RRKHmBzAoWrWmBIb76yPa0lsjdAPEAXcbGfaQV8pKq7W10dpB2B-KeJxVonMuCLJHPuqsUl9S7CfASu2"
)
val dER: Array[Byte] = JwtUtils.transcodeSignatureToDER(signature)
val result = JwtUtils.transcodeSignatureToConcat(
dER,
JwtUtils.getSignatureByteArrayLength(JwtAlgorithm.ES512)
)
assertArrayEquals(signature, result)
}
test(
"transcodeSignatureToConcat and transcodeSignatureToDER should be symmetric for generated tokens"
) {
val ecGenSpec = new ECGenParameterSpec(ecCurveName)
val generatorEC = KeyPairGenerator.getInstance(JwtUtils.ECDSA)
generatorEC.initialize(ecGenSpec, new SecureRandom())
val randomECKey = generatorEC.generateKeyPair()
val header = """{"typ":"JWT","alg":"ES512"}"""
val claim = """{"test":"t"}"""
val signature = Jwt(validTimeClock)
.encode(header, claim, randomECKey.getPrivate, JwtAlgorithm.ES512)
.split("\\.")(2)
assertEquals(
signature,
JwtUtils.stringify(
JwtUtils.transcodeSignatureToConcat(
JwtUtils.transcodeSignatureToDER(JwtUtils.bytify(signature)),
JwtUtils.getSignatureByteArrayLength(JwtAlgorithm.ES512)
)
)
)
}
test("splitString should do nothing") {
forAll(Gen.asciiStr.suchThat(s => s.nonEmpty && !s.contains('a'))) { (value: String) =>
assertArrayEquals(
JwtUtils.splitString(value, 'a'),
Array(value)
)
}
}
test("splitString should split once") {
assertArrayEquals(JwtUtils.splitString("qwertyAzxcvb", 'A'), Array("qwerty", "zxcvb"))
}
test("splitString should split a token") {
assertArrayEquals(
JwtUtils.splitString("header.claim.signature", '.'),
Array("header", "claim", "signature")
)
}
test("splitString should split a token without signature") {
assertArrayEquals(JwtUtils.splitString("header.claim", '.'), Array("header", "claim"))
}
test("splitString should split a token with an empty signature") {
assertArrayEquals(JwtUtils.splitString("header.claim.", '.'), Array("header", "claim"))
}
test("splitString should split a token with an empty header") {
assertArrayEquals(JwtUtils.splitString(".claim.", '.'), Array("", "claim"))
}
test("splitString should be the same as normal split") {
var token = "header.claim.signature"
assertArrayEquals(token.split("\\."), JwtUtils.splitString(token, '.'))
token = "header.claim."
assertArrayEquals(token.split("\\."), JwtUtils.splitString(token, '.'))
token = "header.claim"
assertArrayEquals(token.split("\\."), JwtUtils.splitString(token, '.'))
token = ".claim.signature"
assertArrayEquals(token.split("\\."), JwtUtils.splitString(token, '.'))
token = ".claim."
assertArrayEquals(token.split("\\."), JwtUtils.splitString(token, '.'))
token = "1"
assertArrayEquals(token.split("\\."), JwtUtils.splitString(token, '.'))
token = "a.b.c.d"
assertArrayEquals(token.split("\\."), JwtUtils.splitString(token, '.'))
}
private def assertArrayEquals[A](arr1: Array[A], arr2: Array[A]): Unit = {
assertEquals(arr1.toSeq, arr2.toSeq)
}
}
================================================
FILE: core/shared/src/main/scala/Jwt.scala
================================================
package pdi.jwt
import java.time.Clock
import scala.util.matching.Regex
/** Test implementation of [[JwtCore]] using only Strings. Most of the time, you should use a lib
* implementing JSON and shouldn't be using this object. But just in case you need pure Scala
* support, here it is.
*
* To see a full list of samples, check the
* [[https://jwt-scala.github.io/jwt-scala/jwt-core-jwt.html online documentation]].
*
* '''Warning''': since there is no JSON support in Scala, this object doesn't have any way to
* parse a JSON string as an AST, so it only uses regex with all the limitations it implies. Try
* not to use keys like `exp` and `nbf` in sub-objects of the claim. For example, if you try to use
* the following claim: `{"user":{"exp":1},"exp":1300819380}`, it should be correct but it will
* fail because the regex extracting the expiration will return `1` instead of `1300819380`. Sorry
* about that.
*/
object Jwt extends Jwt(Clock.systemUTC) {
def apply(clock: Clock) = new Jwt(clock)
}
class Jwt(override val clock: Clock) extends JwtCore[JwtHeader, JwtClaim] {
private val extractAlgorithmRegex = "\"alg\" *: *\"([a-zA-Z0-9]+)\"".r
protected def extractAlgorithm(header: String): Option[JwtAlgorithm] =
(extractAlgorithmRegex.findFirstMatchIn(header)).map(_.group(1)).flatMap {
case "none" => None
case name: String => Some(JwtAlgorithm.fromString(name))
}
private val extractIssuerRegex = "\"iss\" *: *\"([\\-a-zA-Z0-9_]*)\"".r
protected def extractIssuer(claim: String): Option[String] =
(extractIssuerRegex.findFirstMatchIn(claim)).map(_.group(1))
private val extractSubjectRegex = "\"sub\" *: *\"([\\-a-zA-Z0-9_]*)\"".r
protected def extractSubject(claim: String): Option[String] =
(extractSubjectRegex.findFirstMatchIn(claim)).map(_.group(1))
private val extractExpirationRegex = "\"exp\" *: *([0-9]+)".r
protected def extractExpiration(claim: String): Option[Long] =
(extractExpirationRegex.findFirstMatchIn(claim)).map(_.group(1)).map(_.toLong)
private val extractNotBeforeRegex = "\"nbf\" *: *([0-9]+)".r
protected def extractNotBefore(claim: String): Option[Long] =
(extractNotBeforeRegex.findFirstMatchIn(claim)).map(_.group(1)).map(_.toLong)
private val extractIssuedAtRegex = "\"iat\" *: *([0-9]+)".r
protected def extractIssuedAt(claim: String): Option[Long] =
(extractIssuedAtRegex.findFirstMatchIn(claim)).map(_.group(1)).map(_.toLong)
private val extractJwtIdRegex = "\"jti\" *: *\"([\\-a-zA-Z0-9_]*)\"".r
protected def extractJwtId(claim: String): Option[String] =
(extractJwtIdRegex.findFirstMatchIn(claim)).map(_.group(1))
private val clearStartRegex = "\\{ *,".r
protected def clearStart(json: String): String =
clearStartRegex.replaceFirstIn(json, "{")
private val clearMiddleRegex = ", *(?=,)".r
protected def clearMiddle(json: String): String =
clearMiddleRegex.replaceAllIn(json, "")
private val clearEndRegex = ", *\\}".r
protected def clearEnd(json: String): String =
clearEndRegex.replaceFirstIn(json, "}")
protected def clearRegex(json: String, regex: Regex): String =
regex.replaceFirstIn(json, "")
protected def clearAll(json: String): String = {
val dirtyJson = List(
extractIssuerRegex,
extractSubjectRegex,
extractExpirationRegex,
extractNotBeforeRegex,
extractIssuedAtRegex,
extractJwtIdRegex
).foldLeft(json)(clearRegex)
clearStart(clearEnd(clearMiddle(dirtyJson)))
}
protected def headerToJson(header: JwtHeader): String = header.toJson
protected def claimToJson(claim: JwtClaim): String = claim.toJson
protected def extractAlgorithm(header: JwtHeader): Option[JwtAlgorithm] = header.algorithm
protected def extractExpiration(claim: JwtClaim): Option[Long] = claim.expiration
protected def extractNotBefore(claim: JwtClaim): Option[Long] = claim.notBefore
protected def parseHeader(header: String): JwtHeader = JwtHeader(extractAlgorithm(header))
protected def parseClaim(claim: String): JwtClaim =
JwtClaim(
content = clearAll(claim),
issuer = extractIssuer(claim),
subject = extractSubject(claim),
expiration = extractExpiration(claim),
notBefore = extractNotBefore(claim),
issuedAt = extractIssuedAt(claim),
jwtId = extractJwtId(claim)
)
}
================================================
FILE: core/shared/src/main/scala/JwtAlgorithm.scala
================================================
package pdi.jwt
import scala.annotation.nowarn
import pdi.jwt.algorithms.JwtUnknownAlgorithm
sealed trait JwtAlgorithm {
def name: String
def fullName: String
}
package algorithms {
sealed trait JwtAsymmetricAlgorithm extends JwtAlgorithm
sealed trait JwtHmacAlgorithm extends JwtAlgorithm
sealed trait JwtRSAAlgorithm extends JwtAsymmetricAlgorithm
sealed trait JwtECDSAAlgorithm extends JwtAsymmetricAlgorithm
sealed trait JwtEdDSAAlgorithm extends JwtAsymmetricAlgorithm
final case class JwtUnknownAlgorithm(name: String) extends JwtAlgorithm {
def fullName: String = name
}
}
object JwtAlgorithm {
/** Deserialize an algorithm from its string equivalent. Only real algorithms supported, if you
* need to support "none", use "optionFromString".
*
* @return
* the actual instance of the algorithm
* @param algo
* the name of the algorithm (e.g. HS256 or HmacSHA256)
* @throws JwtNonSupportedAlgorithm
* in case the string doesn't match any known algorithm
*/
@nowarn("cat=deprecation")
def fromString(algo: String): JwtAlgorithm = algo match {
case "HMD5" => HMD5
case "HS224" => HS224
case "HS256" => HS256
case "HS384" => HS384
case "HS512" => HS512
case "RS256" => RS256
case "RS384" => RS384
case "RS512" => RS512
case "ES256" => ES256
case "ES384" => ES384
case "ES512" => ES512
case "EdDSA" => EdDSA
case "Ed25519" => Ed25519
case other => JwtUnknownAlgorithm(other)
// Missing PS256 PS384 PS512
}
/** Deserialize an algorithm from its string equivalent. If it's the special "none" algorithm,
* return None, else, return Some with the corresponding algorithm inside.
*
* @return
* the actual instance of the algorithm
* @param algo
* the name of the algorithm (e.g. none, HS256 or HmacSHA256)
*/
def optionFromString(algo: String): Option[JwtAlgorithm] =
Option(algo).filterNot(_ == "none").map(fromString)
def allHmac(): Seq[algorithms.JwtHmacAlgorithm] = Seq(HMD5, HS224, HS256, HS384, HS512)
@nowarn("cat=deprecation")
def allAsymmetric(): Seq[algorithms.JwtAsymmetricAlgorithm] =
Seq(RS256, RS384, RS512, ES256, ES384, ES512, EdDSA, Ed25519)
def allRSA(): Seq[algorithms.JwtRSAAlgorithm] = Seq(RS256, RS384, RS512)
def allECDSA(): Seq[algorithms.JwtECDSAAlgorithm] = Seq(ES256, ES384, ES512)
@nowarn("cat=deprecation")
def allEdDSA(): Seq[algorithms.JwtEdDSAAlgorithm] = Seq(EdDSA, Ed25519)
case object HMD5 extends algorithms.JwtHmacAlgorithm {
def name = "HMD5"; def fullName = "HmacMD5"
}
case object HS224 extends algorithms.JwtHmacAlgorithm {
def name = "HS224"; def fullName = "HmacSHA224"
}
case object HS256 extends algorithms.JwtHmacAlgorithm {
def name = "HS256"; def fullName = "HmacSHA256"
}
case object HS384 extends algorithms.JwtHmacAlgorithm {
def name = "HS384"; def fullName = "HmacSHA384"
}
case object HS512 extends algorithms.JwtHmacAlgorithm {
def name = "HS512"; def fullName = "HmacSHA512"
}
case object RS256 extends algorithms.JwtRSAAlgorithm {
def name = "RS256"; def fullName = "SHA256withRSA"
}
case object RS384 extends algorithms.JwtRSAAlgorithm {
def name = "RS384"; def fullName = "SHA384withRSA"
}
case object RS512 extends algorithms.JwtRSAAlgorithm {
def name = "RS512"; def fullName = "SHA512withRSA"
}
case object ES256 extends algorithms.JwtECDSAAlgorithm {
def name = "ES256"; def fullName = "SHA256withECDSA"
}
case object ES384 extends algorithms.JwtECDSAAlgorithm {
def name = "ES384"; def fullName = "SHA384withECDSA"
}
case object ES512 extends algorithms.JwtECDSAAlgorithm {
def name = "ES512"; def fullName = "SHA512withECDSA"
}
case object EdDSA extends algorithms.JwtEdDSAAlgorithm {
def name = "EdDSA"; def fullName = "EdDSA"
}
@deprecated("Ed25519 is not a standard Json Web Algorithm name. Use EdDSA instead.", "9.3.0")
case object Ed25519 extends algorithms.JwtEdDSAAlgorithm {
def name = "Ed25519"; def fullName = "Ed25519"
}
}
================================================
FILE: core/shared/src/main/scala/JwtArrayUtils.scala
================================================
package pdi.jwt
object JwtArrayUtils {
/** A constant time equals comparison - does not terminate early if test will fail. For best
* results always pass the expected value as the first parameter.
*
* Ported from BouncyCastle to remove the need for a runtime dependency.
* https://github.com/bcgit/bc-java/blob/290df7b4edfc77b32d55d0a329bf15ef5b98733b/core/src/main/java/org/bouncycastle/util/Arrays.java#L136-L172
*
* @param expected
* first array
* @param supplied
* second array
* @return
* true if arrays equal, false otherwise.
*/
def constantTimeAreEqual(expected: Array[Byte], supplied: Array[Byte]): Boolean =
if (expected == supplied) true
else if (expected == null || supplied == null) false
else if (expected.length != supplied.length)
!JwtArrayUtils.constantTimeAreEqual(expected, expected)
else {
var nonEqual = 0
(0 until expected.length).foreach(i => nonEqual |= (expected(i) ^ supplied(i)))
nonEqual == 0
}
}
================================================
FILE: core/shared/src/main/scala/JwtBase64.scala
================================================
package pdi.jwt
object JwtBase64 {
private lazy val encoder = java.util.Base64.getUrlEncoder()
private lazy val decoder = java.util.Base64.getUrlDecoder()
private lazy val decoderNonSafe = java.util.Base64.getDecoder()
def encode(value: Array[Byte]): Array[Byte] = encoder.encode(value)
def decode(value: Array[Byte]): Array[Byte] = decoder.decode(value)
def encode(value: String): Array[Byte] = encode(JwtUtils.bytify(value))
def decode(value: String): Array[Byte] = decoder.decode(value)
// Since the complement character "=" is optional,
// we can remove it to save some bits in the HTTP header
def encodeString(value: Array[Byte]): String = encoder.encodeToString(value).replaceAll("=", "")
def decodeString(value: Array[Byte]): String = JwtUtils.stringify(decode(value))
def encodeString(value: String): String = encodeString(JwtUtils.bytify(value))
def decodeString(value: String): String = decodeString(JwtUtils.bytify(value))
def decodeNonSafe(value: Array[Byte]): Array[Byte] = decoderNonSafe.decode(value)
def decodeNonSafe(value: String): Array[Byte] = decoderNonSafe.decode(value)
}
================================================
FILE: core/shared/src/main/scala/JwtClaim.scala
================================================
package pdi.jwt
import java.time.Clock
object JwtClaim {
def apply(
content: String = "{}",
issuer: Option[String] = None,
subject: Option[String] = None,
audience: Option[Set[String]] = None,
expiration: Option[Long] = None,
notBefore: Option[Long] = None,
issuedAt: Option[Long] = None,
jwtId: Option[String] = None
) = new JwtClaim(content, issuer, subject, audience, expiration, notBefore, issuedAt, jwtId)
}
class JwtClaim(
val content: String,
val issuer: Option[String],
val subject: Option[String],
val audience: Option[Set[String]],
val expiration: Option[Long],
val notBefore: Option[Long],
val issuedAt: Option[Long],
val jwtId: Option[String]
) {
def toJson: String = JwtUtils.mergeJson(
JwtUtils.hashToJson(
Seq(
"iss" -> issuer,
"sub" -> subject,
"aud" -> audience.map(set => if (set.size == 1) set.head else set),
"exp" -> expiration,
"nbf" -> notBefore,
"iat" -> issuedAt,
"jti" -> jwtId
).collect { case (key, Some(value)) =>
key -> value
}
),
content
)
def +(json: String): JwtClaim = {
JwtClaim(
JwtUtils.mergeJson(this.content, json),
issuer,
subject,
audience,
expiration,
notBefore,
issuedAt,
jwtId
)
}
def +(key: String, value: Any): JwtClaim = {
JwtClaim(
JwtUtils.mergeJson(this.content, JwtUtils.hashToJson(Seq(key -> value))),
issuer,
subject,
audience,
expiration,
notBefore,
issuedAt,
jwtId
)
}
// Ok, it's Any, but just use "primitive" types
// It will not work with classes or case classes since, you know,
// there is no way to serialize them to JSON out of the box.
def ++[T <: Any](fields: (String, T)*): JwtClaim = {
JwtClaim(
JwtUtils.mergeJson(this.content, JwtUtils.hashToJson(fields)),
issuer,
subject,
audience,
expiration,
notBefore,
issuedAt,
jwtId
)
}
def by(issuer: String): JwtClaim = {
JwtClaim(content, Option(issuer), subject, audience, expiration, notBefore, issuedAt, jwtId)
}
// content should be a valid stringified JSON
def withContent(content: String): JwtClaim = {
JwtClaim(content, issuer, subject, audience, expiration, notBefore, issuedAt, jwtId)
}
def to(audience: String): JwtClaim = {
JwtClaim(
content,
issuer,
subject,
Option(Set(audience)),
expiration,
notBefore,
issuedAt,
jwtId
)
}
def to(audience: Set[String]): JwtClaim = {
JwtClaim(content, issuer, subject, Option(audience), expiration, notBefore, issuedAt, jwtId)
}
def about(subject: String): JwtClaim = {
JwtClaim(content, issuer, Option(subject), audience, expiration, notBefore, issuedAt, jwtId)
}
def withId(id: String): JwtClaim = {
JwtClaim(content, issuer, subject, audience, expiration, notBefore, issuedAt, Option(id))
}
def expiresAt(seconds: Long): JwtClaim =
JwtClaim(content, issuer, subject, audience, Option(seconds), notBefore, issuedAt, jwtId)
def expiresIn(seconds: Long)(implicit clock: Clock): JwtClaim = expiresAt(
JwtTime.nowSeconds + seconds
)
def expiresNow(implicit clock: Clock): JwtClaim = expiresAt(JwtTime.nowSeconds)
def startsAt(seconds: Long): JwtClaim =
JwtClaim(content, issuer, subject, audience, expiration, Option(seconds), issuedAt, jwtId)
def startsIn(seconds: Long)(implicit clock: Clock): JwtClaim = startsAt(
JwtTime.nowSeconds + seconds
)
def startsNow(implicit clock: Clock): JwtClaim = startsAt(JwtTime.nowSeconds)
def issuedAt(seconds: Long): JwtClaim =
JwtClaim(content, issuer, subject, audience, expiration, notBefore, Option(seconds), jwtId)
def issuedIn(seconds: Long)(implicit clock: Clock): JwtClaim = issuedAt(
JwtTime.nowSeconds + seconds
)
def issuedNow(implicit clock: Clock): JwtClaim = issuedAt(JwtTime.nowSeconds)
def isValid(issuer: String, audience: String)(implicit clock: Clock): Boolean =
this.audience.exists(_ contains audience) && this.isValid(issuer)
def isValid(issuer: String)(implicit clock: Clock): Boolean =
this.issuer.contains(issuer) && this.isValid
def isValid(implicit clock: Clock): Boolean =
JwtTime.nowIsBetweenSeconds(this.notBefore, this.expiration)
// equality code
def canEqual(other: Any): Boolean = other.isInstanceOf[JwtClaim]
override def equals(other: Any): Boolean = other match {
case that: JwtClaim =>
(that.canEqual(this)) &&
content == that.content &&
issuer == that.issuer &&
subject == that.subject &&
audience == that.audience &&
expiration == that.expiration &&
notBefore == that.notBefore &&
issuedAt == that.issuedAt &&
jwtId == that.jwtId
case _ => false
}
override def hashCode(): Int = {
val state = Seq(content, issuer, subject, audience, expiration, notBefore, issuedAt, jwtId)
state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
}
override def toString: String =
s"JwtClaim($content, $issuer, $subject, $audience, $expiration, $notBefore, $issuedAt, $jwtId)"
}
================================================
FILE: core/shared/src/main/scala/JwtCore.scala
================================================
package pdi.jwt
import java.security.{Key, PrivateKey, PublicKey}
import java.time.Clock
import javax.crypto.SecretKey
import scala.util.Try
import pdi.jwt.algorithms.*
import pdi.jwt.exceptions.*
/** Provide the main logic around Base64 encoding / decoding and signature using the correct
* algorithm. '''H''' and '''C''' types are respesctively the header type and the claim type. For
* the core project, they will be String but you are free to extend this trait using other types
* like JsObject or anything else.
*
* Please, check implementations, like [[Jwt]], for code samples.
*
* @tparam H
* the type of the extracted header from a JSON Web Token
* @tparam C
* the type of the extracted claim from a JSON Web Token
*
* @define token
* a JSON Web Token as a Base64 url-safe encoded String which can be used inside an HTTP header
* @define headerString
* a valid stringified JSON representing the header of the token
* @define claimString
* a valid stringified JSON representing the claim of the token
* @define key
* the key that will be used to check the token signature
* @define algo
* the algorithm to sign the token
* @define algos
* a list of possible algorithms that the token can use. See
* [[https://jwt-scala.github.io/jwt-scala/#security-concerns Security concerns]] for more infos.
*/
trait JwtCore[H, C] {
implicit private[jwt] def clock: Clock
// Abstract methods
protected def parseHeader(header: String): H
protected def parseClaim(claim: String): C
protected def extractAlgorithm(header: H): Option[JwtAlgorithm]
protected def extractExpiration(claim: C): Option[Long]
protected def extractNotBefore(claim: C): Option[Long]
def encode(header: String, claim: String): String = {
JwtBase64.encodeString(header) + "." + JwtBase64.encodeString(claim) + "."
}
/** Encode a JSON Web Token from its different parts. Both the header and the claim will be
* encoded to Base64 url-safe, then a signature will be eventually generated from it if you did
* pass a key and an algorithm, and finally, those three parts will be merged as a single string,
* using dots as separator.
*
* @return
* $token
* @param header
* $headerString
* @param claim
* $claimString
* @param key
* $key
* @param algorithm
* $algo
*/
def encode(header: String, claim: String, key: String, algorithm: JwtAlgorithm): String = {
val data = JwtBase64.encodeString(header) + "." + JwtBase64.encodeString(claim)
data + "." + JwtBase64.encodeString(JwtUtils.sign(data, key, algorithm))
}
def encode(header: String, claim: String, key: SecretKey, algorithm: JwtHmacAlgorithm): String = {
val data = JwtBase64.encodeString(header) + "." + JwtBase64.encodeString(claim)
data + "." + JwtBase64.encodeString(JwtUtils.sign(data, key, algorithm))
}
def encode(
header: String,
claim: String,
key: PrivateKey,
algorithm: JwtAsymmetricAlgorithm
): String = {
val data = JwtBase64.encodeString(header) + "." + JwtBase64.encodeString(claim)
data + "." + JwtBase64.encodeString(JwtUtils.sign(data, key, algorithm))
}
/** An alias to `encode` which will provide an automatically generated header.
*
* @return
* $token
* @param claim
* $claimString
*/
def encode(claim: String): String = encode(JwtHeader().toJson, claim)
/** An alias to `encode` which will provide an automatically generated header and allowing you to
* get rid of Option for the key and the algorithm.
*
* @return
* $token
* @param claim
* $claimString
* @param key
* $key
* @param algorithm
* $algo
*/
def encode(claim: String, key: String, algorithm: JwtAlgorithm): String =
encode(JwtHeader(algorithm).toJson, claim, key, algorithm)
/** An alias to `encode` which will provide an automatically generated header and allowing you to
* get rid of Option for the key and the algorithm.
*
* @return
* $token
* @param claim
* $claimString
* @param key
* $key
* @param algorithm
* $algo
*/
def encode(claim: String, key: SecretKey, algorithm: JwtHmacAlgorithm): String =
encode(JwtHeader(algorithm).toJson, claim, key, algorithm)
/** An alias to `encode` which will provide an automatically generated header and allowing you to
* get rid of Option for the key and the algorithm.
*
* @return
* $token
* @param claim
* $claimString
* @param key
* $key
* @param algorithm
* $algo
*/
def encode(claim: String, key: PrivateKey, algorithm: JwtAsymmetricAlgorithm): String =
encode(JwtHeader(algorithm).toJson, claim, key, algorithm)
/** An alias to `encode` which will provide an automatically generated header and setting both key
* and algorithm to None.
*
* @return
* $token
* @param claim
* the claim of the JSON Web Token
*/
def encode(claim: JwtClaim): String = encode(claim.toJson)
/** An alias to `encode` which will provide an automatically generated header and use the claim as
* a case class.
*
* @return
* $token
* @param claim
* the claim of the JSON Web Token
* @param key
* $key
* @param algorithm
* $algo
*/
def encode(claim: JwtClaim, key: String, algorithm: JwtAlgorithm): String =
encode(claim.toJson, key, algorithm)
/** An alias to `encode` which will provide an automatically generated header and use the claim as
* a case class.
*
* @return
* $token
* @param claim
* the claim of the JSON Web Token
* @param key
* $key
* @param algorithm
* $algo
*/
def encode(claim: JwtClaim, key: SecretKey, algorithm: JwtHmacAlgorithm): String =
encode(claim.toJson, key, algorithm)
/** An alias to `encode` which will provide an automatically generated header and use the claim as
* a case class.
*
* @return
* $token
* @param claim
* the claim of the JSON Web Token
* @param key
* $key
* @param algorithm
* $algo
*/
def encode(claim: JwtClaim, key: PrivateKey, algorithm: JwtAsymmetricAlgorithm): String =
encode(claim.toJson, key, algorithm)
/** An alias to `encode` if you want to use case classes for the header and the claim rather than
* strings, they will just be stringified to JSON format.
*
* @return
* $token
* @param header
* the header to stringify as a JSON before encoding the token
* @param claim
* the claim to stringify as a JSON before encoding the token
*/
def encode(header: JwtHeader, claim: JwtClaim): String = header.algorithm match {
case None => encode(header.toJson, claim.toJson)
case _ => throw new JwtNonEmptyAlgorithmException()
}
/** An alias of `encode` if you only want to pass a string as the key, the algorithm will be
* deduced from the header.
*
* @return
* $token
* @param header
* the header to stringify as a JSON before encoding the token
* @param claim
* the claim to stringify as a JSON before encoding the token
* @param key
* the secret key to use to sign the token (note that the algorithm will be deduced from the
* header)
*/
def encode(header: JwtHeader, claim: JwtClaim, key: String): String = header.algorithm match {
case Some(algo: JwtAlgorithm) => encode(header.toJson, claim.toJson, key, algo)
case _ => throw new JwtEmptyAlgorithmException()
}
/** An alias of `encode` if you only want to pass a string as the key, the algorithm will be
* deduced from the header.
*
* @return
* $token
* @param header
* the header to stringify as a JSON before encoding the token
* @param claim
* the claim to stringify as a JSON before encoding the token
* @param key
* the secret key to use to sign the token (note that the algorithm will be deduced from the
* header)
*/
def encode(header: JwtHeader, claim: JwtClaim, key: Key): String = (header.algorithm, key) match {
case (Some(algo: JwtHmacAlgorithm), k: SecretKey) =>
encode(header.toJson, claim.toJson, k, algo)
case (Some(algo: JwtAsymmetricAlgorithm), k: PrivateKey) =>
encode(header.toJson, claim.toJson, k, algo)
case _ =>
throw new JwtValidationException(
"The key type doesn't match the algorithm type. It's either a SecretKey and a HMAC algorithm or a PrivateKey and a RSA or ECDSA algorithm. And an algorithm is required of course."
)
}
/** @return
* a tuple of (header64, header, claim64, claim, signature or empty string if none)
* @throws JwtLengthException
* if there is not 2 or 3 parts in the token
*/
private def splitToken(token: String): (String, String, String, String, String) = {
val parts = JwtUtils.splitString(token, '.')
val signature = parts.length match {
case 2 => ""
case 3 => parts(2)
case _ =>
throw new JwtLengthException(
s"Expected token [$token] to be composed of 2 or 3 parts separated by dots."
)
}
(
parts(0),
JwtBase64.decodeString(parts(0)),
parts(1),
JwtBase64.decodeString(parts(1)),
signature
)
}
/** Will try to decode a JSON Web Token to raw strings
*
* @return
* if successful, a tuple of 3 strings, the header, the claim and the signature
* @param token
* $token
*/
def decodeRawAll(token: String, options: JwtOptions): Try[(String, String, String)] = Try {
val (_, header, _, claim, signature) = splitToken(token)
validate(parseHeader(header), parseClaim(claim), signature, options)
(header, claim, signature)
}
def decodeRawAll(token: String): Try[(String, String, String)] =
decodeRawAll(token, JwtOptions.DEFAULT)
/** Will try to decode a JSON Web Token to raw strings using a HMAC algorithm
*
* @return
* if successful, a tuple of 3 strings, the header, the claim and the signature
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeRawAll(
token: String,
key: String,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Try[(String, String, String)] = Try {
val (header64, header, claim64, claim, signature) = splitToken(token)
validate(
header64,
parseHeader(header),
claim64,
parseClaim(claim),
signature,
key,
algorithms,
options
)
(header, claim, signature)
}
def decodeRawAll(
token: String,
key: String,
algorithms: Seq[JwtHmacAlgorithm]
): Try[(String, String, String)] =
decodeRawAll(token, key, algorithms, JwtOptions.DEFAULT)
/** Will try to decode a JSON Web Token to raw strings using an asymmetric algorithm
*
* @return
* if successful, a tuple of 3 strings, the header, the claim and the signature
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeRawAll(
token: String,
key: String,
algorithms: => Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Try[(String, String, String)] = Try {
val (header64, header, claim64, claim, signature) = splitToken(token)
validate(
header64,
parseHeader(header),
claim64,
parseClaim(claim),
signature,
key,
algorithms,
options
)
(header, claim, signature)
}
def decodeRawAll(
token: String,
key: String,
algorithms: => Seq[JwtAsymmetricAlgorithm]
): Try[(String, String, String)] =
decodeRawAll(token, key, algorithms, JwtOptions.DEFAULT)
/** Will try to decode a JSON Web Token to raw strings using a HMAC algorithm
*
* @return
* if successful, a tuple of 3 strings, the header, the claim and the signature
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeRawAll(
token: String,
key: SecretKey,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Try[(String, String, String)] = Try {
val (header64, header, claim64, claim, signature) = splitToken(token)
validate(
header64,
parseHeader(header),
claim64,
parseClaim(claim),
signature,
key,
algorithms,
options
)
(header, claim, signature)
}
def decodeRawAll(
token: String,
key: SecretKey,
algorithms: Seq[JwtHmacAlgorithm]
): Try[(String, String, String)] =
decodeRawAll(token, key, algorithms, JwtOptions.DEFAULT)
def decodeRawAll(
token: String,
key: SecretKey,
options: JwtOptions
): Try[(String, String, String)] =
decodeRawAll(token, key, JwtAlgorithm.allHmac(), options)
def decodeRawAll(token: String, key: SecretKey): Try[(String, String, String)] =
decodeRawAll(token, key, JwtOptions.DEFAULT)
/** Will try to decode a JSON Web Token to raw strings using an asymmetric algorithm
*
* @return
* if successful, a tuple of 3 strings, the header, the claim and the signature
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeRawAll(
token: String,
key: PublicKey,
algorithms: Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Try[(String, String, String)] = Try {
val (header64, header, claim64, claim, signature) = splitToken(token)
validate(
header64,
parseHeader(header),
claim64,
parseClaim(claim),
signature,
key,
algorithms,
options
)
(header, claim, signature)
}
def decodeRawAll(
token: String,
key: PublicKey,
algorithms: Seq[JwtAsymmetricAlgorithm]
): Try[(String, String, String)] =
decodeRawAll(token, key, algorithms, JwtOptions.DEFAULT)
def decodeRawAll(
token: String,
key: PublicKey,
options: JwtOptions
): Try[(String, String, String)] =
decodeRawAll(token, key, JwtAlgorithm.allAsymmetric(), options)
def decodeRawAll(token: String, key: PublicKey): Try[(String, String, String)] =
decodeRawAll(token, key, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but only return the claim (you only care about the claim most of the
* time)
*
* @return
* if successful, a string representing the JSON version of the claim
* @param token
* $token
*/
def decodeRaw(token: String, options: JwtOptions): Try[String] =
decodeRawAll(token, options).map(_._2)
def decodeRaw(token: String): Try[String] = decodeRaw(token, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but only return the claim (you only care about the claim most of the
* time)
*
* @return
* if successful, a string representing the JSON version of the claim
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeRaw(
token: String,
key: String,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Try[String] =
decodeRawAll(token, key, algorithms, options).map(_._2)
def decodeRaw(token: String, key: String, algorithms: Seq[JwtHmacAlgorithm]): Try[String] =
decodeRaw(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but only return the claim (you only care about the claim most of the
* time)
*
* @return
* if successful, a string representing the JSON version of the claim
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeRaw(
token: String,
key: String,
algorithms: => Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Try[String] =
decodeRawAll(token, key, algorithms, options).map(_._2)
def decodeRaw(
token: String,
key: String,
algorithms: => Seq[JwtAsymmetricAlgorithm]
): Try[String] =
decodeRaw(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but only return the claim (you only care about the claim most of the
* time)
*
* @return
* if successful, a string representing the JSON version of the claim
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeRaw(
token: String,
key: SecretKey,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Try[String] =
decodeRawAll(token, key, algorithms, options).map(_._2)
def decodeRaw(token: String, key: SecretKey, algorithms: Seq[JwtHmacAlgorithm]): Try[String] =
decodeRaw(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but only return the claim (you only care about the claim most of the
* time)
*
* @return
* if successful, a string representing the JSON version of the claim
* @param token
* $token
* @param key
* $key
*/
def decodeRaw(token: String, key: SecretKey, options: JwtOptions): Try[String] =
decodeRaw(token, key, JwtAlgorithm.allHmac(), options)
def decodeRaw(token: String, key: SecretKey): Try[String] =
decodeRaw(token, key, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but only return the claim (you only care about the claim most of the
* time)
*
* @return
* if successful, a string representing the JSON version of the claim
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeRaw(
token: String,
key: PublicKey,
algorithms: Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Try[String] =
decodeRawAll(token, key, algorithms, options).map(_._2)
def decodeRaw(
token: String,
key: PublicKey,
algorithms: Seq[JwtAsymmetricAlgorithm]
): Try[String] =
decodeRaw(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but only return the claim (you only care about the claim most of the
* time)
*
* @return
* if successful, a string representing the JSON version of the claim
* @param token
* $token
* @param key
* $key
*/
def decodeRaw(token: String, key: PublicKey, options: JwtOptions): Try[String] =
decodeRaw(token, key, JwtAlgorithm.allAsymmetric(), options)
def decodeRaw(token: String, key: PublicKey): Try[String] =
decodeRaw(token, key, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but return the real header and claim types
*
* @return
* if successful, a tuple representing the header, the claim and eventually the signature
* @param token
* $token
*/
def decodeAll(token: String, options: JwtOptions): Try[(H, C, String)] = Try {
val (_, header, _, claim, signature) = splitToken(token)
val (h, c) = (parseHeader(header), parseClaim(claim))
validate(h, c, signature, options)
(h, c, signature)
}
def decodeAll(token: String): Try[(H, C, String)] = decodeAll(token, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but return the real header and claim types
*
* @return
* if successful, a tuple representing the header, the claim and eventually the signature
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeAll(
token: String,
key: String,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Try[(H, C, String)] = Try {
val (header64, header, claim64, claim, signature) = splitToken(token)
val (h, c) = (parseHeader(header), parseClaim(claim))
validate(header64, h, claim64, c, signature, key, algorithms, options)
(h, c, signature)
}
def decodeAll(
token: String,
key: String,
algorithms: Seq[JwtHmacAlgorithm]
): Try[(H, C, String)] =
decodeAll(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but return the real header and claim types
*
* @return
* if successful, a tuple representing the header, the claim and eventually the signature
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeAll(
token: String,
key: String,
algorithms: => Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Try[(H, C, String)] = Try {
val (header64, header, claim64, claim, signature) = splitToken(token)
val (h, c) = (parseHeader(header), parseClaim(claim))
validate(header64, h, claim64, c, signature, key, algorithms, options)
(h, c, signature)
}
def decodeAll(
token: String,
key: String,
algorithms: => Seq[JwtAsymmetricAlgorithm]
): Try[(H, C, String)] =
decodeAll(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but return the real header and claim types
*
* @return
* if successful, a tuple representing the header, the claim and eventually the signature
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeAll(
token: String,
key: SecretKey,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Try[(H, C, String)] = Try {
val (header64, header, claim64, claim, signature) = splitToken(token)
val (h, c) = (parseHeader(header), parseClaim(claim))
validate(header64, h, claim64, c, signature, key, algorithms, options)
(h, c, signature)
}
def decodeAll(
token: String,
key: SecretKey,
algorithms: Seq[JwtHmacAlgorithm]
): Try[(H, C, String)] =
decodeAll(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but return the real header and claim types
*
* @return
* if successful, a tuple representing the header, the claim and eventually the signature
* @param token
* $token
* @param key
* $key
*/
def decodeAll(token: String, key: SecretKey, options: JwtOptions): Try[(H, C, String)] =
decodeAll(token, key, JwtAlgorithm.allHmac(), options)
def decodeAll(token: String, key: SecretKey): Try[(H, C, String)] =
decodeAll(token, key, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but return the real header and claim types
*
* @return
* if successful, a tuple representing the header, the claim and eventually the signature
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decodeAll(
token: String,
key: PublicKey,
algorithms: Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Try[(H, C, String)] = Try {
val (header64, header, claim64, claim, signature) = splitToken(token)
val (h, c) = (parseHeader(header), parseClaim(claim))
validate(header64, h, claim64, c, signature, key, algorithms, options)
(h, c, signature)
}
def decodeAll(
token: String,
key: PublicKey,
algorithms: Seq[JwtAsymmetricAlgorithm]
): Try[(H, C, String)] =
decodeAll(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeRawAll` but return the real header and claim types
*
* @return
* if successful, a tuple representing the header, the claim and eventually the signature
* @param token
* $token
* @param key
* $key
*/
def decodeAll(token: String, key: PublicKey, options: JwtOptions): Try[(H, C, String)] =
decodeAll(token, key, JwtAlgorithm.allAsymmetric(), options)
def decodeAll(token: String, key: PublicKey): Try[(H, C, String)] =
decodeAll(token, key, JwtOptions.DEFAULT)
/** Same as `decodeAll` but only return the claim
*
* @return
* if successful, the claim of the token in its correct type
* @param token
* $token
*/
def decode(token: String, options: JwtOptions): Try[C] = decodeAll(token, options).map(_._2)
def decode(token: String): Try[C] = decode(token, JwtOptions.DEFAULT)
/** Same as `decodeAll` but only return the claim
*
* @return
* if successful, the claim of the token in its correct type
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decode(
token: String,
key: String,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Try[C] =
decodeAll(token, key, algorithms, options).map(_._2)
def decode(token: String, key: String, algorithms: Seq[JwtHmacAlgorithm]): Try[C] =
decode(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeAll` but only return the claim
*
* @return
* if successful, the claim of the token in its correct type
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decode(
token: String,
key: String,
algorithms: => Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Try[C] =
decodeAll(token, key, algorithms, options).map(_._2)
def decode(token: String, key: String, algorithms: => Seq[JwtAsymmetricAlgorithm]): Try[C] =
decode(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeAll` but only return the claim
*
* @return
* if successful, the claim of the token in its correct type
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decode(
token: String,
key: SecretKey,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Try[C] =
decodeAll(token, key, algorithms, options).map(_._2)
def decode(token: String, key: SecretKey, algorithms: Seq[JwtHmacAlgorithm]): Try[C] =
decode(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeAll` but only return the claim
*
* @return
* if successful, the claim of the token in its correct type
* @param token
* $token
* @param key
* $key
*/
def decode(token: String, key: SecretKey, options: JwtOptions): Try[C] =
decode(token, key, JwtAlgorithm.allHmac(), options)
def decode(token: String, key: SecretKey): Try[C] = decode(token, key, JwtOptions.DEFAULT)
/** Same as `decodeAll` but only return the claim
*
* @return
* if successful, the claim of the token in its correct type
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def decode(
token: String,
key: PublicKey,
algorithms: Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Try[C] =
decodeAll(token, key, algorithms, options).map(_._2)
def decode(token: String, key: PublicKey, algorithms: Seq[JwtAsymmetricAlgorithm]): Try[C] =
decode(token, key, algorithms, JwtOptions.DEFAULT)
/** Same as `decodeAll` but only return the claim
*
* @return
* if successful, the claim of the token in its correct type
* @param token
* $token
* @param key
* $key
*/
def decode(token: String, key: PublicKey, options: JwtOptions): Try[C] =
decode(token, key, JwtAlgorithm.allAsymmetric(), options)
def decode(token: String, key: PublicKey): Try[C] = decode(token, key, JwtOptions.DEFAULT)
// Validate
protected def validateTiming(claim: C, options: JwtOptions): Try[Unit] = {
val maybeExpiration: Option[Long] =
if (options.expiration) extractExpiration(claim) else None
val maybeNotBefore: Option[Long] =
if (options.notBefore) extractNotBefore(claim) else None
JwtTime.validateNowIsBetweenSeconds(
maybeNotBefore.map(_ - options.leeway),
maybeExpiration.map(_ + options.leeway)
)
}
// Validate if an algorithm is inside the authorized range
protected def validateHmacAlgorithm(
algorithm: JwtHmacAlgorithm,
algorithms: Seq[JwtHmacAlgorithm]
): Boolean = algorithms.contains(algorithm)
// Validate if an algorithm is inside the authorized range
protected def validateAsymmetricAlgorithm(
algorithm: JwtAsymmetricAlgorithm,
algorithms: Seq[JwtAsymmetricAlgorithm]
): Boolean = algorithms.contains(algorithm)
// Validation when no key and no algorithm (or unknown)
protected def validate(header: H, claim: C, signature: String, options: JwtOptions) = {
if (options.signature) {
if (!signature.isEmpty) {
throw new JwtNonEmptySignatureException()
}
extractAlgorithm(header).foreach {
case JwtUnknownAlgorithm(name) => throw new JwtNonSupportedAlgorithm(name)
case _ => throw new JwtNonEmptyAlgorithmException()
}
}
validateTiming(claim, options).get
}
// Validation when both key and algorithm
protected def validate(
header64: String,
header: H,
claim64: String,
claim: C,
signature: String,
options: JwtOptions,
verify: (Array[Byte], Array[Byte], JwtAlgorithm) => Boolean
): Unit = {
if (options.signature) {
val maybeAlgo = extractAlgorithm(header)
if (options.signature && signature.isEmpty) {
throw new JwtEmptySignatureException()
} else if (maybeAlgo.isEmpty) {
throw new JwtEmptyAlgorithmException()
} else if (
!verify(
JwtUtils.bytify(header64 + "." + claim64),
JwtBase64.decode(signature),
maybeAlgo.get
)
) {
throw new JwtValidationException("Invalid signature for this token or wrong algorithm.")
}
}
validateTiming(claim, options).get
}
// Generic validation on String Key for HMAC algorithms
protected def validate(
header64: String,
header: H,
claim64: String,
claim: C,
signature: String,
key: String,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Unit = validate(
header64,
header,
claim64,
claim,
signature,
options,
(data: Array[Byte], signature: Array[Byte], algorithm: JwtAlgorithm) =>
algorithm match {
case algo: JwtHmacAlgorithm =>
validateHmacAlgorithm(algo, algorithms) && JwtUtils.verify(data, signature, key, algo)
case _ => false
}
)
// Generic validation on String Key for asymmetric algorithms
protected def validate(
header64: String,
header: H,
claim64: String,
claim: C,
signature: String,
key: String,
algorithms: => Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Unit = validate(
header64,
header,
claim64,
claim,
signature,
options,
(data: Array[Byte], signature: Array[Byte], algorithm: JwtAlgorithm) =>
algorithm match {
case algo: JwtAsymmetricAlgorithm =>
validateAsymmetricAlgorithm(algo, algorithms) && JwtUtils.verify(
data,
signature,
key,
algo
)
case _ => false
}
)
// Validation for HMAC algorithm using a SecretKey
protected def validate(
header64: String,
header: H,
claim64: String,
claim: C,
signature: String,
key: SecretKey,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Unit = validate(
header64,
header,
claim64,
claim,
signature,
options,
(data: Array[Byte], signature: Array[Byte], algorithm: JwtAlgorithm) =>
algorithm match {
case algo: JwtHmacAlgorithm =>
validateHmacAlgorithm(algo, algorithms) && JwtUtils.verify(data, signature, key, algo)
case _ => false
}
)
// Validation for RSA and ECDSA algorithms using PublicKey
protected def validate(
header64: String,
header: H,
claim64: String,
claim: C,
signature: String,
key: PublicKey,
algorithms: Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Unit = validate(
header64,
header,
claim64,
claim,
signature,
options,
(data: Array[Byte], signature: Array[Byte], algorithm: JwtAlgorithm) =>
algorithm match {
case algo: JwtAsymmetricAlgorithm =>
validateAsymmetricAlgorithm(algo, algorithms) && JwtUtils.verify(
data,
signature,
key,
algo
)
case _ => false
}
)
/** Valid a token: doesn't return anything but will thrown exceptions if there are any errors.
*
* @param token
* $token
* @throws JwtValidationException
* default validation exception
* @throws JwtLengthException
* the number of parts separated by dots is wrong
* @throws JwtNotBeforeException
* the token isn't valid yet because its `notBefore` attribute is in the future
* @throws JwtExpirationException
* the token isn't valid anymore because its `expiration` attribute is in the past
* @throws IllegalArgumentException
* couldn't decode the token since it's not a valid base64 string
*/
def validate(token: String, options: JwtOptions): Unit = {
val (_, header, _, claim, signature) = splitToken(token)
validate(parseHeader(header), parseClaim(claim), signature, options)
}
def validate(token: String): Unit = validate(token, JwtOptions.DEFAULT)
/** An alias of `validate` in case you want to directly pass a string key for HMAC algorithms.
*
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
* @throws JwtValidationException
* default validation exception
* @throws JwtLengthException
* the number of parts separated by dots is wrong
* @throws JwtNotBeforeException
* the token isn't valid yet because its `notBefore` attribute is in the future
* @throws JwtExpirationException
* the token isn't valid anymore because its `expiration` attribute is in the past
* @throws IllegalArgumentException
* couldn't decode the token since it's not a valid base64 string
*/
def validate(
token: String,
key: String,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Unit = {
val (header64, header, claim64, claim, signature) = splitToken(token)
validate(
header64,
parseHeader(header),
claim64,
parseClaim(claim),
signature,
key,
algorithms,
options
)
}
def validate(token: String, key: String, algorithms: Seq[JwtHmacAlgorithm]): Unit =
validate(token, key, algorithms, JwtOptions.DEFAULT)
/** An alias of `validate` in case you want to directly pass a string key for asymmetric
* algorithms.
*
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
* @throws JwtValidationException
* default validation exception
* @throws JwtLengthException
* the number of parts separated by dots is wrong
* @throws JwtNotBeforeException
* the token isn't valid yet because its `notBefore` attribute is in the future
* @throws JwtExpirationException
* the token isn't valid anymore because its `expiration` attribute is in the past
* @throws IllegalArgumentException
* couldn't decode the token since it's not a valid base64 string
*/
def validate(
token: String,
key: String,
algorithms: => Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Unit = {
val (header64, header, claim64, claim, signature) = splitToken(token)
validate(
header64,
parseHeader(header),
claim64,
parseClaim(claim),
signature,
key,
algorithms,
options
)
}
def validate(token: String, key: String, algorithms: => Seq[JwtAsymmetricAlgorithm]): Unit =
validate(token, key, algorithms, JwtOptions.DEFAULT)
/** An alias of `validate` in case you want to directly pass a string key.
*
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
* @throws JwtValidationException
* default validation exception
* @throws JwtLengthException
* the number of parts separated by dots is wrong
* @throws JwtNotBeforeException
* the token isn't valid yet because its `notBefore` attribute is in the future
* @throws JwtExpirationException
* the token isn't valid anymore because its `expiration` attribute is in the past
* @throws IllegalArgumentException
* couldn't decode the token since it's not a valid base64 string
*/
def validate(
token: String,
key: SecretKey,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Unit = {
val (header64, header, claim64, claim, signature) = splitToken(token)
validate(
header64,
parseHeader(header),
claim64,
parseClaim(claim),
signature,
key,
algorithms,
options
)
}
def validate(token: String, key: SecretKey, algorithms: Seq[JwtHmacAlgorithm]): Unit =
validate(token, key, algorithms, JwtOptions.DEFAULT)
def validate(token: String, key: SecretKey, options: JwtOptions): Unit =
validate(token, key, JwtAlgorithm.allHmac(), options)
def validate(token: String, key: SecretKey): Unit = validate(token, key, JwtOptions.DEFAULT)
/** An alias of `validate` in case you want to directly pass a string key.
*
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
* @throws JwtValidationException
* default validation exception
* @throws JwtLengthException
* the number of parts separated by dots is wrong
* @throws JwtNotBeforeException
* the token isn't valid yet because its `notBefore` attribute is in the future
* @throws JwtExpirationException
* the token isn't valid anymore because its `expiration` attribute is in the past
* @throws IllegalArgumentException
* couldn't decode the token since it's not a valid base64 string
*/
def validate(
token: String,
key: PublicKey,
algorithms: Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Unit = {
val (header64, header, claim64, claim, signature) = splitToken(token)
validate(
header64,
parseHeader(header),
claim64,
parseClaim(claim),
signature,
key,
algorithms,
options
)
}
def validate(token: String, key: PublicKey, algorithms: Seq[JwtAsymmetricAlgorithm]): Unit =
validate(token, key, algorithms, JwtOptions.DEFAULT)
def validate(token: String, key: PublicKey, options: JwtOptions): Unit =
validate(token, key, JwtAlgorithm.allAsymmetric(), options)
def validate(token: String, key: PublicKey): Unit = validate(token, key, JwtOptions.DEFAULT)
/** Test if a token is valid. Doesn't throw any exception.
*
* @return
* a boolean value indicating if the token is valid or not
* @param token
* $token
*/
def isValid(token: String, options: JwtOptions): Boolean = Try(validate(token, options)).isSuccess
def isValid(token: String): Boolean = isValid(token, JwtOptions.DEFAULT)
/** An alias for `isValid` if you want to directly pass a string as the key for HMAC algorithms
*
* @return
* a boolean value indicating if the token is valid or not
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def isValid(
token: String,
key: String,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Boolean = Try(validate(token, key, algorithms, options)).isSuccess
def isValid(token: String, key: String, algorithms: Seq[JwtHmacAlgorithm]): Boolean =
isValid(token, key, algorithms, JwtOptions.DEFAULT)
/** An alias for `isValid` if you want to directly pass a string as the key for asymmetric
* algorithms
*
* @return
* a boolean value indicating if the token is valid or not
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def isValid(
token: String,
key: String,
algorithms: => Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Boolean = Try(validate(token, key, algorithms, options)).isSuccess
def isValid(token: String, key: String, algorithms: => Seq[JwtAsymmetricAlgorithm]): Boolean =
isValid(token, key, algorithms, JwtOptions.DEFAULT)
/** An alias for `isValid` if you want to directly pass a string as the key
*
* @return
* a boolean value indicating if the token is valid or not
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def isValid(
token: String,
key: SecretKey,
algorithms: Seq[JwtHmacAlgorithm],
options: JwtOptions
): Boolean = Try(validate(token, key, algorithms, options)).isSuccess
def isValid(token: String, key: SecretKey, algorithms: Seq[JwtHmacAlgorithm]): Boolean =
isValid(token, key, algorithms, JwtOptions.DEFAULT)
def isValid(token: String, key: SecretKey, options: JwtOptions): Boolean =
isValid(token, key, JwtAlgorithm.allHmac(), options)
def isValid(token: String, key: SecretKey): Boolean = isValid(token, key, JwtOptions.DEFAULT)
/** An alias for `isValid` if you want to directly pass a string as the key
*
* @return
* a boolean value indicating if the token is valid or not
* @param token
* $token
* @param key
* $key
* @param algorithms
* $algos
*/
def isValid(
token: String,
key: PublicKey,
algorithms: Seq[JwtAsymmetricAlgorithm],
options: JwtOptions
): Boolean = Try(validate(token, key, algorithms, options)).isSuccess
def isValid(token: String, key: PublicKey, algorithms: Seq[JwtAsymmetricAlgorithm]): Boolean =
isValid(token, key, algorithms, JwtOptions.DEFAULT)
def isValid(token: String, key: PublicKey, options: JwtOptions): Boolean =
isValid(token, key, JwtAlgorithm.allAsymmetric(), options)
def isValid(token: String, key: PublicKey): Boolean = isValid(token, key, JwtOptions.DEFAULT)
}
================================================
FILE: core/shared/src/main/scala/JwtException.scala
================================================
package pdi.jwt.exceptions
import pdi.jwt.JwtTime
sealed abstract class JwtException(message: String) extends RuntimeException(message)
class JwtLengthException(message: String) extends JwtException(message)
class JwtValidationException(message: String) extends JwtException(message)
class JwtSignatureFormatException(message: String) extends JwtException(message)
class JwtEmptySignatureException()
extends JwtException(
"No signature found inside the token while trying to verify it with a key."
)
class JwtNonEmptySignatureException()
extends JwtException(
"Non-empty signature found inside the token while trying to verify without a key."
)
class JwtEmptyAlgorithmException()
extends JwtException(
"No algorithm found inside the token header while having a key to sign or verify it."
)
class JwtNonEmptyAlgorithmException()
extends JwtException(
"Algorithm found inside the token header while trying to sign or verify without a key."
)
class JwtExpirationException(expiration: Long)
extends JwtException("The token is expired since " + JwtTime.format(expiration))
class JwtNotBeforeException(notBefore: Long)
extends JwtException("The token will only be valid after " + JwtTime.format(notBefore))
class JwtNonSupportedAlgorithm(algo: String)
extends JwtException(s"The algorithm [$algo] is not currently supported.")
class JwtNonSupportedCurve(curve: String)
extends JwtException(s"The curve [$curve] is not currently supported.")
class JwtNonStringException(val key: String)
extends JwtException(s"During JSON parsing, expected a String for key [$key]") {
@deprecated("Use key instead", since = "9.0.1")
def getKey = key
}
object JwtNonStringException {
def unapply(e: JwtNonStringException) = Some(e.key)
}
class JwtNonStringSetOrStringException(val key: String)
extends JwtException(s"During JSON parsing, expected a Set[String] or String for key [$key]") {
@deprecated("Use key instead", since = "9.0.1")
def getKey = key
}
class JwtNonNumberException(val key: String)
extends JwtException(s"During JSON parsing, expected a Number for key [$key]") {
@deprecated("Use key instead", since = "9.0.1")
def getKey = key
}
object JwtNonNumberException {
def unapply(e: JwtNonNumberException) = Some(e.key)
}
================================================
FILE: core/shared/src/main/scala/JwtHeader.scala
================================================
package pdi.jwt
object JwtHeader {
val DEFAULT_TYPE = "JWT"
def apply(
algorithm: Option[JwtAlgorithm] = None,
typ: Option[String] = None,
contentType: Option[String] = None,
keyId: Option[String] = None
) = new JwtHeader(algorithm, typ, contentType, keyId)
def apply(algorithm: Option[JwtAlgorithm]): JwtHeader = algorithm match {
case Some(algo) => JwtHeader(algo)
case _ => new JwtHeader(None, None, None, None)
}
def apply(algorithm: JwtAlgorithm): JwtHeader =
new JwtHeader(Option(algorithm), Option(DEFAULT_TYPE), None, None)
def apply(algorithm: JwtAlgorithm, typ: String): JwtHeader =
new JwtHeader(Option(algorithm), Option(typ), None, None)
def apply(algorithm: JwtAlgorithm, typ: String, contentType: String): JwtHeader =
new JwtHeader(Option(algorithm), Option(typ), Option(contentType), None)
def apply(algorithm: JwtAlgorithm, typ: String, contentType: String, keyId: String): JwtHeader =
new JwtHeader(Option(algorithm), Option(typ), Option(contentType), Option(keyId))
}
class JwtHeader(
val algorithm: Option[JwtAlgorithm],
val typ: Option[String],
val contentType: Option[String],
val keyId: Option[String]
) {
def toJson: String = JwtUtils.hashToJson(
Seq(
"typ" -> typ,
"alg" -> algorithm.map(_.name).orElse(Option("none")),
"cty" -> contentType,
"kid" -> keyId
).collect { case (key, Some(value)) =>
(key -> value)
}
)
/** Assign the type to the header */
def withType(typ: String): JwtHeader = {
JwtHeader(algorithm, Option(typ), contentType, keyId)
}
/** Assign the default type `JWT` to the header */
def withType: JwtHeader = this.withType(JwtHeader.DEFAULT_TYPE)
/** Assign a key id to the header */
def withKeyId(keyId: String): JwtHeader = {
JwtHeader(algorithm, typ, contentType, Option(keyId))
}
// equality code
def canEqual(other: Any): Boolean = other.isInstanceOf[JwtHeader]
override def equals(other: Any): Boolean = other match {
case that: JwtHeader =>
(that.canEqual(this)) &&
algorithm == that.algorithm &&
typ == that.typ &&
contentType == that.contentType &&
keyId == that.keyId
case _ => false
}
override def hashCode(): Int = {
val state = Seq(algorithm, typ, contentType, keyId)
state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
}
override def toString: String = s"JwtHeader($algorithm, $typ, $contentType, $keyId)"
}
================================================
FILE: core/shared/src/main/scala/JwtOptions.scala
================================================
package pdi.jwt
case class JwtOptions(
signature: Boolean = true,
expiration: Boolean = true,
notBefore: Boolean = true,
leeway: Long = 0 // in seconds
)
object JwtOptions {
val DEFAULT = new JwtOptions()
}
================================================
FILE: core/shared/src/main/scala/JwtTime.scala
================================================
package pdi.jwt
import java.time.{Clock, Instant}
import scala.util.{Failure, Success, Try}
import pdi.jwt.exceptions.{JwtExpirationException, JwtNotBeforeException}
/** Util object to handle time operations */
object JwtTime {
/** Returns the number of millis since the 01.01.1970
*
* @return
* the number of millis since the 01.01.1970
*/
def now(implicit clock: Clock): Long = clock.instant().toEpochMilli
/** Returns the number of seconds since the 01.01.1970
*
* @return
* the number of seconds since the 01.01.1970
*/
def nowSeconds(implicit clock: Clock): Long = this.now / 1000
def format(time: Long): String = Instant.ofEpochMilli(time).toString
/** Test if the current time is between the two prams
*
* @return
* the result of the test
* @param start
* if set, the instant that must be before now (in millis)
* @param end
* if set, the instant that must be after now (in millis)
*/
def nowIsBetween(start: Option[Long], end: Option[Long])(implicit clock: Clock): Boolean =
validateNowIsBetween(start, end).isSuccess
/** Same as `nowIsBetween` but using seconds rather than millis.
*
* @param start
* if set, the instant that must be before now (in seconds)
* @param end
* if set, the instant that must be after now (in seconds)
*/
def nowIsBetweenSeconds(start: Option[Long], end: Option[Long])(implicit clock: Clock): Boolean =
nowIsBetween(start.map(_ * 1000), end.map(_ * 1000))
/** Test if the current time is between the two params and throw an exception if we don't have
* `start` <= now < `end`
*
* @param start
* if set, the instant that must be before now (in millis)
* @param end
* if set, the instant that must be after now (in millis)
* @return
* Failure(JwtNotBeforeException) if `start` > now, Failure(JwtExpirationException) if now >=
* `end`
*/
def validateNowIsBetween(start: Option[Long], end: Option[Long])(implicit
clock: Clock
): Try[Unit] = {
val timeNow = now
(start, end) match {
case (Some(s), _) if s > timeNow => Failure(new JwtNotBeforeException(s))
case (_, Some(e)) if e <= timeNow => Failure(new JwtExpirationException(e))
case _ => Success(())
}
}
/** Same as `validateNowIsBetween` but using seconds rather than millis.
*
* @param start
* if set, the instant that must be before now (in seconds)
* @param end
* if set, the instant that must be after now (in seconds)
* @return
* Failure(JwtNotBeforeException) if `start` > now, Failure(JwtExpirationException) if now >
* `end`
*/
def validateNowIsBetweenSeconds(start: Option[Long], end: Option[Long])(implicit
clock: Clock
): Try[Unit] =
validateNowIsBetween(start.map(_ * 1000), end.map(_ * 1000))
}
================================================
FILE: core/shared/src/main/scala/JwtUtils.scala
================================================
package pdi.jwt
import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec}
import java.security.{KeyFactory, PrivateKey, PublicKey, Signature}
import javax.crypto.spec.SecretKeySpec
import javax.crypto.{Mac, SecretKey}
import pdi.jwt.JwtAlgorithm.{ES256, ES384, ES512}
import pdi.jwt.algorithms.*
import pdi.jwt.exceptions.{JwtNonSupportedAlgorithm, JwtSignatureFormatException}
object JwtUtils {
val ENCODING = "UTF-8"
val RSA = "RSA"
val ECDSA = "EC"
val EdDSA = "EdDSA"
/** Convert an array of bytes to its corresponding string using the default encoding.
*
* @return
* the final string
* @param arr
* the array of bytes to transform
*/
def stringify(arr: Array[Byte]): String = new String(arr, ENCODING)
/** Convert a string to its corresponding array of bytes using the default encoding.
*
* @return
* the final array of bytes
* @param str
* the string to convert
*/
def bytify(str: String): Array[Byte] = str.getBytes(ENCODING)
private def escape(value: String): String = value.replaceAll("\"", "\\\\\"")
/** Convert a sequence to a JSON array
*/
def seqToJson(seq: Seq[Any]): String = seq
.map {
case value: String => "\"" + escape(value) + "\""
case value: Boolean => if (value) "true" else "false"
case value: Double => value.toString
case value: Short => value.toString
case value: Float => value.toString
case value: Long => value.toString
case value: Int => value.toString
case value: BigDecimal => value.toString
case value: BigInt => value.toString
case (key: String, value) => hashToJson(Seq(key -> value))
case value: Any => "\"" + escape(value.toString) + "\""
}
.mkString("[", ",", "]")
/** Convert a sequence of tuples to a JSON object
*/
def hashToJson(hash: Seq[(String, Any)]): String = hash
.map {
case (key, value: String) => "\"" + escape(key) + "\":\"" + escape(value) + "\""
case (key, value: Boolean) => "\"" + escape(key) + "\":" + (if (value) "true" else "false")
case (key, value: Double) => "\"" + escape(key) + "\":" + value.toString
case (key, value: Short) => "\"" + escape(key) + "\":" + value.toString
case (key, value: Float) => "\"" + escape(key) + "\":" + value.toString
case (key, value: Long) => "\"" + escape(key) + "\":" + value.toString
case (key, value: Int) => "\"" + escape(key) + "\":" + value.toString
case (key, value: BigDecimal) => "\"" + escape(key) + "\":" + value.toString
case (key, value: BigInt) => "\"" + escape(key) + "\":" + value.toString
case (key, (vKey: String, vValue)) =>
"\"" + escape(key) + "\":" + hashToJson(Seq(vKey -> vValue))
case (key, value: Seq[Any]) => "\"" + escape(key) + "\":" + seqToJson(value)
case (key, value: Set[_]) => "\"" + escape(key) + "\":" + seqToJson(value.toSeq)
case (key, value: Any) => "\"" + escape(key) + "\":\"" + escape(value.toString) + "\""
}
.mkString("{", ",", "}")
/** Merge multiple JSON strings to a unique one
*/
def mergeJson(json: String, jsonSeq: String*): String = {
val initJson = json.trim match {
case "" => ""
case value => value.drop(1).dropRight(1)
}
"{" + jsonSeq.map(_.trim).fold(initJson) {
case (j1, result) if j1.length < 5 => result.drop(1).dropRight(1)
case (result, j2) if j2.length < 7 => result
case (j1, j2) => j1 + "," + j2.drop(1).dropRight(1)
} + "}"
}
private def parseKey(key: String): Array[Byte] = JwtBase64.decodeNonSafe(
key.replaceAll("-----BEGIN ([^-]*)-----|-----END ([^-]*)-----|\\s*", "")
)
private def parsePrivateKey(key: String, keyAlgo: String) = {
val spec = new PKCS8EncodedKeySpec(parseKey(key))
KeyFactory.getInstance(keyAlgo).generatePrivate(spec)
}
private def parsePublicKey(key: String, keyAlgo: String): PublicKey = {
val spec = new X509EncodedKeySpec(parseKey(key))
KeyFactory.getInstance(keyAlgo).generatePublic(spec)
}
/** Generate the signature for a given data using the key and HMAC algorithm provided.
*/
def sign(data: Array[Byte], key: SecretKey, algorithm: JwtHmacAlgorithm): Array[Byte] = {
val mac = Mac.getInstance(algorithm.fullName)
mac.init(key)
mac.doFinal(data)
}
def sign(data: String, key: SecretKey, algorithm: JwtHmacAlgorithm): Array[Byte] =
sign(bytify(data), key, algorithm)
/** Generate the signature for a given data using the key and RSA or ECDSA algorithm provided.
*/
def sign(data: Array[Byte], key: PrivateKey, algorithm: JwtAsymmetricAlgorithm): Array[Byte] = {
val signer = Signature.getInstance(algorithm.fullName)
signer.initSign(key)
signer.update(data)
algorithm match {
case _: JwtRSAAlgorithm => signer.sign
case algorithm: JwtECDSAAlgorithm =>
transcodeSignatureToConcat(signer.sign, getSignatureByteArrayLength(algorithm))
case _: JwtEdDSAAlgorithm => signer.sign
}
}
def sign(data: String, key: PrivateKey, algorithm: JwtAsymmetricAlgorithm): Array[Byte] =
sign(bytify(data), key, algorithm)
/** Will try to sign some given data by parsing the provided key, if parsing fail, please consider
* retrieving the SecretKey or the PrivateKey on your side and then use another "sign" method.
*/
def sign(data: Array[Byte], key: String, algorithm: JwtAlgorithm): Array[Byte] =
algorithm match {
case algo: JwtHmacAlgorithm => sign(data, new SecretKeySpec(bytify(key), algo.fullName), algo)
case algo: JwtRSAAlgorithm => sign(data, parsePrivateKey(key, RSA), algo)
case algo: JwtECDSAAlgorithm => sign(data, parsePrivateKey(key, ECDSA), algo)
case algo: JwtEdDSAAlgorithm => sign(data, parsePrivateKey(key, EdDSA), algo)
case algo: JwtUnknownAlgorithm => throw new JwtNonSupportedAlgorithm(algo.fullName)
}
/** Alias to `sign` using a String data which will be converted to an array of bytes.
*/
def sign(data: String, key: String, algorithm: JwtAlgorithm): Array[Byte] =
sign(bytify(data), key, algorithm)
/** Check if a signature is valid for a given data using the key and the HMAC algorithm provided.
*/
def verify(
data: Array[Byte],
signature: Array[Byte],
key: SecretKey,
algorithm: JwtHmacAlgorithm
): Boolean = {
JwtArrayUtils.constantTimeAreEqual(sign(data, key, algorithm), signature)
}
/** Check if a signature is valid for a given data using the key and the RSA or ECDSA algorithm
* provided.
*/
def verify(
data: Array[Byte],
signature: Array[Byte],
key: PublicKey,
algorithm: JwtAsymmetricAlgorithm
): Boolean = {
val signer = Signature.getInstance(algorithm.fullName)
signer.initVerify(key)
signer.update(data)
algorithm match {
case _: JwtRSAAlgorithm => signer.verify(signature)
case _: JwtECDSAAlgorithm => signer.verify(transcodeSignatureToDER(signature))
case _: JwtEdDSAAlgorithm => signer.verify(signature)
}
}
/** Will try to check if a signature is valid for a given data by parsing the provided key, if
* parsing fail, please consider retrieving the SecretKey or the PublicKey on your side and then
* use another "verify" method.
*/
def verify(
data: Array[Byte],
signature: Array[Byte],
key: String,
algorithm: JwtAlgorithm
): Boolean = algorithm match {
case algo: JwtHmacAlgorithm =>
verify(data, signature, new SecretKeySpec(bytify(key), algo.fullName), algo)
case algo: JwtRSAAlgorithm => verify(data, signature, parsePublicKey(key, RSA), algo)
case algo: JwtECDSAAlgorithm => verify(data, signature, parsePublicKey(key, ECDSA), algo)
case algo: JwtEdDSAAlgorithm => verify(data, signature, parsePublicKey(key, EdDSA), algo)
case algo: JwtUnknownAlgorithm => throw new JwtNonSupportedAlgorithm(algo.fullName)
}
/** Alias for `verify`
*/
def verify(data: String, signature: String, key: String, algorithm: JwtAlgorithm): Boolean =
verify(bytify(data), bytify(signature), key, algorithm)
/** Returns the expected signature byte array length (R + S parts) for the specified ECDSA
* algorithm.
*
* @param algorithm
* The ECDSA algorithm. Must be supported and not {@code null} .
* @return
* The expected byte array length for the signature.
*/
def getSignatureByteArrayLength(algorithm: JwtECDSAAlgorithm): Int = algorithm match {
case ES256 => 64
case ES384 => 96
case ES512 => 132
}
/** Transcodes the JCA ASN.1/DER-encoded signature into the concatenated R + S format expected by
* ECDSA JWS.
*
* @param derSignature
* The ASN1./DER-encoded. Must not be {@code null} .
* @param outputLength
* The expected length of the ECDSA JWS signature.
* @return
* The ECDSA JWS encoded signature.
* @throws JwtSignatureFormatException
* If the ASN.1/DER signature format is invalid.
*/
@throws[JwtSignatureFormatException]
def transcodeSignatureToConcat(derSignature: Array[Byte], outputLength: Int): Array[Byte] = {
if (derSignature.length < 8 || derSignature(0) != 48)
throw new JwtSignatureFormatException("Invalid ECDSA signature format")
val offset: Int = derSignature(1) match {
case s if s > 0 => 2
case s if s == 0x81.toByte => 3
case _ => throw new JwtSignatureFormatException("Invalid ECDSA signature format")
}
val rLength: Byte = derSignature(offset + 1)
var i = rLength.toInt
while ((i > 0) && (derSignature((offset + 2 + rLength) - i) == 0)) {
i -= 1
}
val sLength: Byte = derSignature(offset + 2 + rLength + 1)
var j = sLength.toInt
while ((j > 0) && (derSignature((offset + 2 + rLength + 2 + sLength) - j) == 0)) {
j -= 1
}
val rawLen: Int = Math.max(Math.max(i, j), outputLength / 2)
if (
(derSignature(offset - 1) & 0xff) != derSignature.length - offset
|| (derSignature(offset - 1) & 0xff) != 2 + rLength + 2 + sLength
|| derSignature(offset) != 2 || derSignature(offset + 2 + rLength) != 2
)
throw new JwtSignatureFormatException("Invalid ECDSA signature format")
val concatSignature: Array[Byte] = new Array[Byte](2 * rawLen)
System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i)
System.arraycopy(
derSignature,
(offset + 2 + rLength + 2 + sLength) - j,
concatSignature,
2 * rawLen - j,
j
)
concatSignature
}
/** Transcodes the ECDSA JWS signature into ASN.1/DER format for use by the JCA verifier.
*
* @param signature
* The JWS signature, consisting of the concatenated R and S values. Must not be {@code null} .
* @return
* The ASN.1/DER encoded signature.
* @throws JwtSignatureFormatException
* If the ECDSA JWS signature format is invalid.
*/
@throws[JwtSignatureFormatException]
def transcodeSignatureToDER(signature: Array[Byte]): Array[Byte] = {
var (r, s) = signature.splitAt(signature.length / 2)
r = r.dropWhile(_ == 0)
if (r.length > 0 && r(0) < 0)
r +:= 0.toByte
s = s.dropWhile(_ == 0)
if (s.length > 0 && s(0) < 0)
s +:= 0.toByte
val signatureLength = 2 + r.length + 2 + s.length
if (signatureLength > 255)
throw new JwtSignatureFormatException("Invalid ECDSA signature format")
val signatureDER = scala.collection.mutable.ListBuffer.empty[Byte]
signatureDER += 48
if (signatureLength >= 128)
signatureDER += 0x81.toByte
signatureDER += signatureLength.toByte
signatureDER ++= (Seq(2.toByte, r.length.toByte) ++ r)
signatureDER ++= (Seq(2.toByte, s.length.toByte) ++ s)
signatureDER.toArray
}
def splitString(input: String, separator: Char): Array[String] = {
val builder = scala.collection.mutable.ArrayBuffer.empty[String]
var lastIndex = 0
var index = input.indexOf(separator.toInt, lastIndex)
while (index != -1) {
builder += input.substring(lastIndex, index)
lastIndex = index + 1
index = input.indexOf(separator.toInt, lastIndex)
}
// Add the remainder
if (lastIndex < input.length) {
builder += input.substring(lastIndex, input.length)
}
builder.toArray
}
}
================================================
FILE: core/shared/src/test/scala/Fixture.scala
================================================
package pdi.jwt
import java.security.spec.*
import java.security.{KeyFactory, KeyPairGenerator, SecureRandom, Security}
import java.time.{Clock, Instant, ZoneOffset}
import javax.crypto.spec.SecretKeySpec
import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.jce.spec.ECNamedCurveSpec
import pdi.jwt.algorithms.{JwtECDSAAlgorithm, JwtHmacAlgorithm, JwtRSAAlgorithm}
trait DataEntryBase {
def algo: JwtAlgorithm
def header: String
def headerClass: JwtHeader
def header64: String
def signature: String
def token: String
def tokenUnsigned: String
def tokenEmpty: String
}
case class DataEntry[A <: JwtAlgorithm](
algo: A,
header: String,
headerClass: JwtHeader,
header64: String,
signature: String,
token: String = "",
tokenUnsigned: String = "",
tokenEmpty: String = ""
) extends DataEntryBase
trait ClockFixture {
val expiration: Long = 1300819380
val expirationMillis: Long = expiration * 1000
val beforeExpirationMillis: Long = expirationMillis - 1
val afterExpirationMillis: Long = expirationMillis + 1
def fixedUTC(millis: Long): Clock = Clock.fixed(Instant.ofEpochMilli(millis), ZoneOffset.UTC)
val afterExpirationClock: Clock = fixedUTC(afterExpirationMillis)
val notBefore: Long = 1300819320
val notBeforeMillis: Long = notBefore * 1000
val beforeNotBeforeMillis: Long = notBeforeMillis - 1
val afterNotBeforeMillis: Long = notBeforeMillis + 1
val beforeNotBeforeClock: Clock = fixedUTC(beforeNotBeforeMillis)
val afterNotBeforeClock: Clock = fixedUTC(afterNotBeforeMillis)
val validTime: Long = (expiration + notBefore) / 2
val validTimeMillis: Long = validTime * 1000
val validTimeClock: Clock = fixedUTC(validTimeMillis)
val ecCurveName = "secp521r1"
}
trait Fixture extends ClockFixture {
// Bouncycastle is not included by default. Add it for each test.
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider())
()
}
val Ed25519 = "Ed25519"
val secretKey =
"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
val secretKeyBytes = JwtUtils.bytify(secretKey)
def secretKeyOf(algo: JwtAlgorithm) = new SecretKeySpec(secretKeyBytes, algo.fullName)
val claim = s"""{"iss":"joe","exp":$expiration,"http://example.com/is_root":true}"""
val claimClass = JwtClaim(
"""{"http://example.com/is_root":true}""",
issuer = Option("joe"),
expiration = Option(expiration)
)
val claim64 =
"eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
val headerEmpty = """{"alg":"none"}"""
val headerClassEmpty = JwtHeader()
val header64Empty = "eyJhbGciOiJub25lIn0"
val tokenEmpty = header64Empty + "." + claim64 + "."
val headerWithSpaces = """{"alg" : "none"}"""
val claimWithSpaces = """{"nbf" :0 , "foo" : "bar" , "exp": 32086368000}"""
val tokenWithSpaces =
JwtBase64.encodeString(headerWithSpaces) + "." + JwtBase64.encodeString(claimWithSpaces) + "."
val publicKeyRSA = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvzoCEC2rpSpJQaWZbUml
sDNwp83Jr4fi6KmBWIwnj1MZ6CUQ7rBasuLI8AcfX5/10scSfQNCsTLV2tMKQaHu
vyrVfwY0dINk+nkqB74QcT2oCCH9XduJjDuwWA4xLqAKuF96FsIes52opEM50W7/
W7DZCKXkC8fFPFj6QF5ZzApDw2Qsu3yMRmr7/W9uWeaTwfPx24YdY7Ah+fdLy3KN
40vXv9c4xiSafVvnx9BwYL7H1Q8NiK9LGEN6+JSWfgckQCs6UUBOXSZdreNN9zbQ
Cwyzee7bOJqXUDAuLcFARzPw1EsZAyjVtGCKIQ0/btqK+jFunT2NBC8RItanDZpp
tQIDAQAB
-----END PUBLIC KEY-----"""
val privateKeyRSA = """-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAvzoCEC2rpSpJQaWZbUmlsDNwp83Jr4fi6KmBWIwnj1MZ6CUQ
7rBasuLI8AcfX5/10scSfQNCsTLV2tMKQaHuvyrVfwY0dINk+nkqB74QcT2oCCH9
XduJjDuwWA4xLqAKuF96FsIes52opEM50W7/W7DZCKXkC8fFPFj6QF5ZzApDw2Qs
u3yMRmr7/W9uWeaTwfPx24YdY7Ah+fdLy3KN40vXv9c4xiSafVvnx9BwYL7H1Q8N
iK9LGEN6+JSWfgckQCs6UUBOXSZdreNN9zbQCwyzee7bOJqXUDAuLcFARzPw1EsZ
AyjVtGCKIQ0/btqK+jFunT2NBC8RItanDZpptQIDAQABAoIBAQCsssO4Pra8hFMC
gX7tr0x+tAYy1ewmpW8stiDFilYT33YPLKJ9HjHbSms0MwqHftwwTm8JDc/GXmW6
qUui+I64gQOtIzpuW1fvyUtHEMSisI83QRMkF6fCSQm6jJ6oQAtOdZO6R/gYOPNb
3gayeS8PbMilQcSRSwp6tNTVGyC33p43uUUKAKHnpvAwUSc61aVOtw2wkD062XzM
hJjYpHm65i4V31AzXo8HF42NrAtZ8K/AuQZne5F/6F4QFVlMKzUoHkSUnTp60XZx
X77GuyDeDmCgSc2J7xvR5o6VpjsHMo3ek0gJk5ZBnTgkHvnpbULCRxTmDfjeVPue
v3NN2TBFAoGBAPxbqNEsXPOckGTvG3tUOAAkrK1hfW3TwvrW/7YXg1/6aNV4sklc
vqn/40kCK0v9xJIv9FM/l0Nq+CMWcrb4sjLeGwHAa8ASfk6hKHbeiTFamA6FBkvQ
//7GP5khD+y62RlWi9PmwJY21lEkn2mP99THxqvZjQiAVNiqlYdwiIc7AoGBAMH8
f2Ay7Egc2KYRYU2qwa5E/Cljn/9sdvUnWM+gOzUXpc5sBi+/SUUQT8y/rY4AUVW6
YaK7chG9YokZQq7ZwTCsYxTfxHK2pnG/tXjOxLFQKBwppQfJcFSRLbw0lMbQoZBk
S+zb0ufZzxc2fJfXE+XeJxmKs0TS9ltQuJiSqCPPAoGBALEc84K7DBG+FGmCl1sb
ZKJVGwwknA90zCeYtadrIT0/VkxchWSPvxE5Ep+u8gxHcqrXFTdILjWW4chefOyF
5ytkTrgQAI+xawxsdyXWUZtd5dJq8lxLtx9srD4gwjh3et8ZqtFx5kCHBCu29Fr2
PA4OmBUMfrs0tlfKgV+pT2j5AoGBAKnA0Z5XMZlxVM0OTH3wvYhI6fk2Kx8TxY2G
nxsh9m3hgcD/mvJRjEaZnZto6PFoqcRBU4taSNnpRr7+kfH8sCht0k7D+l8AIutL
ffx3xHv9zvvGHZqQ1nHKkaEuyjqo+5kli6N8QjWNzsFbdvBQ0CLJoqGhVHsXuWnz
W3Z4cBbVAoGAEtnwY1OJM7+R2u1CW0tTjqDlYU2hUNa9t1AbhyGdI2arYp+p+umA
b5VoYLNsdvZhqjVFTrYNEuhTJFYCF7jAiZLYvYm0C99BqcJnJPl7JjWynoNHNKw3
9f6PIOE1rAmPE8Cfz/GFF5115ZKVlq+2BY8EKNxbCIy2d/vMEvisnXI=
-----END RSA PRIVATE KEY-----"""
val generatorRSA = KeyPairGenerator.getInstance(JwtUtils.RSA)
generatorRSA.initialize(1024)
val randomRSAKey = generatorRSA.generateKeyPair()
val ecGenSpec = new ECGenParameterSpec(ecCurveName)
val generatorEC = KeyPairGenerator.getInstance(JwtUtils.ECDSA)
generatorEC.initialize(ecGenSpec, new SecureRandom())
val randomECKey = generatorEC.generateKeyPair()
val S = BigInt(
"1ed498eedf499e5dd12b1ab94ee03d1a722eaca3ed890630c8b25f1015dd4ec5630a02ddb603f3248a3b87c88637e147ecc7a6e2a1c2f9ff1103be74e5d42def37d",
16
)
val X = BigInt(
"16528ac15dc4c8e0559fad628ac3ffbf5c7cfefe12d50a97c7d088cc10b408d4ab03ac0d543bde862699a74925c1f2fe7c247c00fddc1442099dfa0671fc032e10a",
16
)
val Y = BigInt(
"b7f22b3c1322beef766cadd1a5f0363840195b7be10d9a518802d8d528e03bc164c9588c5e63f1473d05195510676008b6808508539367d2893e1aa4b7cb9f9dab",
16
)
val curveParams = ECNamedCurveTable.getParameterSpec(ecCurveName)
val curveSpec: ECParameterSpec = new ECNamedCurveSpec(
ecCurveName,
curveParams.getCurve(),
curveParams.getG(),
curveParams.getN(),
curveParams.getH()
)
val privateSpec = new ECPrivateKeySpec(S.underlying(), curveSpec)
val publicSpec = new ECPublicKeySpec(new ECPoint(X.underlying(), Y.underlying()), curveSpec)
val privateKeyEC = KeyFactory.getInstance(JwtUtils.ECDSA).generatePrivate(privateSpec)
val publicKeyEC = KeyFactory.getInstance(JwtUtils.ECDSA).generatePublic(publicSpec)
def setToken[A <: JwtAlgorithm](entry: DataEntry[A]): DataEntry[A] = {
entry.copy(
token = Seq(entry.header64, claim64, entry.signature).mkString("."),
tokenUnsigned = Seq(entry.header64, claim64, "").mkString("."),
tokenEmpty = Seq(header64Empty, claim64, "").mkString(".")
)
}
val privateKeyEd25519 = "MC4CAQAwBQYDK2VwBCIEIHf3EQMqRKbBYOEjmrRm6Zu5hIYombr3DoWaRjZqK7uv"
val publicKeyEd25519 = "MCowBQYDK2VwAyEAMGx9f797iAEdcI/QULMQFxgnt3ANZAqlTHavvAf3nD4="
val generatorEd25519 = KeyPairGenerator.getInstance(Ed25519)
val randomEd25519Key = generatorEd25519.generateKeyPair()
val data = Seq(
DataEntry[JwtHmacAlgorithm](
JwtAlgorithm.HMD5,
"""{"typ":"JWT","alg":"HMD5"}""",
JwtHeader(JwtAlgorithm.HMD5, "JWT"),
"eyJ0eXAiOiJKV1QiLCJhbGciOiJITUQ1In0",
"BVRxj65Lk3DXIug2IosRvw"
),
DataEntry[JwtHmacAlgorithm](
JwtAlgorithm.HS256,
"""{"typ":"JWT","alg":"HS256"}""",
JwtHeader(JwtAlgorithm.HS256, "JWT"),
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9",
"IPSERPZc5wyxrZ4Yiq7l31wFk_qaDY5YrnfLjIC0Lmc"
),
DataEntry[JwtHmacAlgorithm](
JwtAlgorithm.HS384,
"""{"typ":"JWT","alg":"HS384"}""",
JwtHeader(JwtAlgorithm.HS384, "JWT"),
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9",
"tCjCk4PefnNV6E_PByT5xumMVm6KAt_asxP8DXwcDnwsldVJi_Y7SfTVJzvyuGBY"
),
DataEntry[JwtHmacAlgorithm](
JwtAlgorithm.HS512,
"""{"typ":"JWT","alg":"HS512"}""",
JwtHeader(JwtAlgorithm.HS512, "JWT"),
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9",
"ngZsdQj8p2wvUAo8xCbJPwganGPnG5UnLkg7VrE6NgmQdV16UITjlBajZxcai_U5PjQdeN-yJtyA5kxf8O5BOQ"
)
).map(setToken)
val dataRSA = Seq(
DataEntry[JwtRSAAlgorithm](
JwtAlgorithm.RS256,
"""{"typ":"JWT","alg":"RS256"}""",
JwtHeader(JwtAlgorithm.RS256, "JWT"),
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9",
"h943pccw-3rOGJW_RlURooRk7SawDZcQiyP9iziq_LUKHtZME_UMGATeVpoc1aoGK0SlWPlVgV1HaB9fNEyziRYPi7i2K_l9XysRHhdo-luCL_D2rNK4Kv_034knQdC_pZPQ4vMviLDqHVL7w0edG-5-96fzFiP3jwV7FIz7r86fvtNgmKw8cH-cSZfEbj_vgWXT_bE_MHcCE0g4UBiXvTUbd9FpkiTugM6Lr9SXLiFKUtAraOxaKKeZ0VSLMTATK8M2PqLq4I0NnJMaZpcIp1pP9DFz07GomTpMP49Ag4CGzutFIUXz-J277OYDrLjfIT7jDnQIYuzrwE3vatwp2g"
),
DataEntry[JwtRSAAlgorithm](
JwtAlgorithm.RS384,
"""{"typ":"JWT","alg":"RS384"}""",
JwtHeader(JwtAlgorithm.RS384, "JWT"),
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9",
"jpusk3t1NPdT7VaZLB6mO3_L4R59gSbgRM866HVZzN6qkH3vYy9y91eMs6YQZLXgg1nBi1ZY8pb4R9G_on4Xsenh-K7odRCHX-XzVbzAtnljMMChdqKp7zTAlAWF03ZrFyv91kxAQeyQSkwxDP4vP70SCLtt3_kevAzon5fE1L1DD1TNySe52TDCofd2RUPFhWzsfdAPvo_Qj1s_zG-DThHSMXXMY9GOtugyJjbDCDrl8uGeF_0XQm-wBuYQ_EGw0S9TsoI_8dggmeEyv8XwT2XKB20fKOc298GNWJ6q6E01hI0EjmWKXEtTyLG0edAF-QrNkXtkz-yX9WJmjmyVfA"
),
DataEntry[JwtRSAAlgorithm](
JwtAlgorithm.RS512,
"""{"typ":"JWT","alg":"RS512"}""",
JwtHeader(JwtAlgorithm.RS512, "JWT"),
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9",
"UJLq3LjgGgxQHpIHYMc48mCW1GrqwNT4sVF7IpyT6Vtbk0b_TcZ-MoWzkdYnP4-V0D8fl7kxJlLooXDWVso25UQMC66t35pAjFsQHvz7WGn1MQf5F2IOVeS2T_Qg0ckfhykw-jqXgOCrtgI-8lq_A0W8lATLoWjaQSosZxH7oYk6XJY3v5gi3reurAsrbqRCi6Gc87MdB_Yl29acAMr2_G3hun6h_VJckemOsBudLf8kGj_3lCSCY8TLncJYTLB9ZAtWhS92LpKRwPGS2CED2sQcHbq4BK10yJh-YrLrUnhCibBNMVWt1EyFf2obqSl-4Qllv4_WRnCOE4HLrosIYQ"
)
).map(setToken)
val dataECDSA = Seq(
DataEntry[JwtECDSAAlgorithm](
JwtAlgorithm.ES256,
"""{"typ":"JWT","alg":"ES256"}""",
JwtHeader(JwtAlgorithm.ES256, "JWT"),
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9",
"MIGIAkIBFmPwOO2eBdtkCko3pjjJs5Wpdi2GBhRywwptlosRQVlQxmT95uOoKE9BUjqVdyjd8o9TcNHHqM6ayPmQml0aTYICQgGDYkPc5EUfJ1F9VFvbPW1bIpX_sZ3XwyXIeL_4jt7BeKmB_LPorgeO-agmx4UdqMyCG1-Y31m8cJEPNm7h5x5V-Q"
),
DataEntry[JwtECDSAAlgorithm](
JwtAlgorithm.ES384,
"""{"typ":"JWT","alg":"ES384"}""",
JwtHeader(JwtAlgorithm.ES384, "JWT"),
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzM4NCJ9",
"MEUCIQC5trx72Z6QKUKxoK_DIX9S3X5QOJBu9tC3f5i6C_1gRQIgOYnA7NoLI3CNVLbibqAwQHSyU44f-yLYGn0YaJvReMA"
),
DataEntry[JwtECDSAAlgorithm](
JwtAlgorithm.ES512,
"""{"typ":"JWT","alg":"ES512"}""",
JwtHeader(JwtAlgorithm.ES512, "JWT"),
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9",
"MEUCICcluU9j5N40Mcr_Mo5_r5KVexcgrXH0LMVC_k1EPswPAiEA-8W2vz2bVZCzPv-S6CNDlbxNktEkOtTAg0XXiZ0ghLk"
)
).map(setToken)
val dataEdDSA = Seq(
DataEntry(
JwtAlgorithm.EdDSA,
"""{"typ":"JWT","alg":"EdDSA"}""",
JwtHeader(JwtAlgorithm.EdDSA, "JWT"),
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9",
"I4phqhsuywTyv0Fb12v0X-ILw8tFdDlDExRTsBUYMB2yjo340KXC8L_QfUyO7-8NoMzO5k4rHPkxq8cC2xu8CQ"
)
).map(setToken)
}
================================================
FILE: core/shared/src/test/scala/JwtBase64Spec.scala
================================================
package pdi.jwt
class JwtBase64Spec extends munit.FunSuite {
val eol = System.getProperty("line.separator")
val values: Seq[(String, String)] = Seq(
("", ""),
("a", "YQ"),
("1", "MQ"),
(
"""{"alg": "algo"}.{"user": 1, "admin": true, "value": "foo"}""",
"eyJhbGciOiAiYWxnbyJ9LnsidXNlciI6IDEsICJhZG1pbiI6IHRydWUsICJ2YWx1ZSI6ICJmb28ifQ"
),
(
"azeklZJEKL,93l,zae:km838{az:e}lekr[l874:e]aze",
"YXpla2xaSkVLTCw5M2wsemFlOmttODM4e2F6OmV9bGVrcltsODc0OmVdYXpl"
),
(
"""azeqsdwxcrtyfghvbnuyiopjhkml1234567890&é'(-è_çà)=$£ù%*µ,?;.:/!+-*/§äâêëûüîïÂÄÊËÎÏÜÛÔÖZRTYPQSDFGHJKLMWXCVBN<>#{}[]|`\^@¤""",
"YXplcXNkd3hjcnR5ZmdodmJudXlpb3BqaGttbDEyMzQ1Njc4OTAmw6knKC3DqF_Dp8OgKT0kwqPDuSUqwrUsPzsuOi8hKy0qL8Knw6TDosOqw6vDu8O8w67Dr8OCw4TDisOLw47Dj8Ocw5vDlMOWWlJUWVBRU0RGR0hKS0xNV1hDVkJOPD4je31bXXxgXF5AwqQ"
)
)
test("should encode string") {
values.foreach { value =>
assertEquals(value._2, JwtBase64.encodeString(value._1))
}
}
test("should decode strings") {
values.foreach { value =>
assertEquals(value._1, JwtBase64.decodeString(value._2))
}
}
test("should be symmetrical") {
values.foreach { value =>
assertEquals(value._1, JwtBase64.decodeString(JwtBase64.encodeString(value._1)))
}
values.foreach { value =>
assertEquals(value._2, JwtBase64.encodeString(JwtBase64.decodeString(value._2)))
}
}
test("should throw when invalid string") {
val vals = Seq("a", "abcde", "*", "aze$")
vals.foreach { v =>
intercept[IllegalArgumentException] { JwtBase64.decode(v) }
}
}
}
================================================
FILE: core/shared/src/test/scala/JwtClaimSpec.scala
================================================
package pdi.jwt
import java.time.{Clock, Instant, ZoneOffset}
import munit.ScalaCheckSuite
import org.scalacheck.Prop.*
class JwtClaimSpec extends ScalaCheckSuite {
val fakeNowSeconds = 1615411490L
implicit val clock: Clock = Clock.fixed(Instant.ofEpochSecond(fakeNowSeconds), ZoneOffset.UTC)
val claim = JwtClaim()
test("JwtClaim.+ should add a json") {
forAll { (value: Long) =>
val result = claim + s"""{"foo": $value}"""
assertEquals(result.content, s"""{"foo": $value}""")
}
}
test("JwtClaim.+ should add a key/value") {
forAll { (value: Long) =>
val result = claim + ("foo", value)
assertEquals(result.content, s"""{"foo":$value}""")
}
}
test("JwtClaim.++ should add a key/value") {
forAll { (value: Long) =>
val result = claim ++ ("foo" -> value)
assertEquals(result.content, s"""{"foo":$value}""")
}
}
test("JwtClaim.expireIn should set the expiration time") {
forAll { (delta: Long) =>
val result = claim.expiresIn(delta)
assertEquals(result.expiration, Some(fakeNowSeconds + delta))
}
}
test("JwtClaim.expireNow should set the expiration time") {
val result = claim.expiresNow
assertEquals(result.expiration, Some(fakeNowSeconds))
}
test("JwtClaim.expireAt should set the expiration time") {
forAll { (epoch: Long) =>
val result = claim.expiresAt(epoch)
assertEquals(result.expiration, Some(epoch))
}
}
test("JwtClaim.startIn should set the notBefore") {
forAll { (delta: Long) =>
val result = claim.startsIn(delta)
assertEquals(result.notBefore, Some(fakeNowSeconds + delta))
}
}
test("JwtClaim.startAt should set the notBefore") {
forAll { (epoch: Long) =>
val result = claim.startsAt(epoch)
assertEquals(result.notBefore, Some(epoch))
}
}
test("JwtClaim.startNow should set the notBefore") {
val result = claim.startsNow
assertEquals(result.notBefore, Some(fakeNowSeconds))
}
test("JwtClaim.issuedIn should set the issuedAt") {
forAll { (delta: Long) =>
val result = claim.issuedIn(delta)
assertEquals(result.issuedAt, Some(fakeNowSeconds + delta))
}
}
test("JwtClaim.issuedAt should set the issuedAt") {
forAll { (epoch: Long) =>
val result = claim.issuedAt(epoch)
assertEquals(result.issuedAt, Some(epoch))
}
}
test("JwtClaim.issuedNow should set the issuedAt") {
val result = claim.issuedNow
assertEquals(result.issuedAt, Some(fakeNowSeconds))
}
}
================================================
FILE: docs/src/main/paradox/index.md
================================================
# JWT Scala
@@@ index
- [Native](jwt-core/index.md)
- [Argonaut](jwt-argonaut.md)
- [Circe](jwt-circe.md)
- [Json4S](jwt-json4s.md)
- [Play Json](jwt-play-json.md)
- [Play Framework](jwt-play-jwt-session.md)
- [upickle](jwt-upickle.md)
- [ZIO Json](jwt-zio-json.md)
@@@
Scala support for JSON Web Token ([JWT](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token)).
Supports Java 8+, Scala 2.12, Scala 2.13 and Scala 3 (for json libraries that support it).
Dependency free.
Optional helpers for Play Framework, Play JSON, Json4s Native, Json4s Jackson, Circe, uPickle and Argonaut.
## Usage
JWT Scala is divided in several sub-projects each targeting a specific JSON library,
check the doc from the menu for installation and usage instructions.
## Algorithms
If you are using `String` key, please keep in mind that such keys need to be parsed. Rather than implementing a super complex parser, the one in JWT Scala is pretty simple and might not work for all use-cases (especially for ECDSA keys). In such case, consider using `SecretKey` or `PrivateKey` or `PublicKey` directly. It is way better for you. All API support all those types.
Check @ref:[ECDSA samples](jwt-core/jwt-ecdsa.md) for more infos.
| Name | Description |
| ----- | ------------------------------ |
| HMD5 | HMAC using MD5 algorithm |
| HS224 | HMAC using SHA-224 algorithm |
| HS256 | HMAC using SHA-256 algorithm |
| HS384 | HMAC using SHA-384 algorithm |
| HS512 | HMAC using SHA-512 algorithm |
| RS256 | RSASSA using SHA-256 algorithm |
| RS384 | RSASSA using SHA-384 algorithm |
| RS512 | RSASSA using SHA-512 algorithm |
| ES256 | ECDSA using SHA-256 algorithm |
| ES384 | ECDSA using SHA-384 algorithm |
| ES512 | ECDSA using SHA-512 algorithm |
| EdDSA | EdDSA signature algorithms |
## Security concerns
This lib doesn't want to impose anything, that's why, by default, a JWT claim is totally empty. That said, you should always add an `issuedAt` attribute to it, probably using `claim.issuedNow`.
The reason is that even HTTPS isn't perfect and having always the same chunk of data transfered can be of a big help to crack it. Generating a slightly different token at each request is way better even if it adds a bit of payload to the response.
If you are using a session timeout through the `expiration` attribute which is extended at each request, that's fine too. I can't find the article I read about that vulnerability but if someone has some resources about the topic, I would be glad to link them.
## License
This software is licensed under the Apache 2 license, quoted below.
Copyright 2021 JWT-Scala Contributors.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with the License. You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
================================================
FILE: docs/src/main/paradox/jwt-argonaut.md
================================================
## Argonaut
- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtArgonaut$.html)
@@@vars
```scala
libraryDependencies += "com.github.jwt-scala" %% "jwt-argonaut" % "$project.version$"
```
@@@
### Basic usage
@@snip [JwtArgonautDoc.scala](/docs/src/main/scala/JwtArgonautDoc.scala) { #example }
### Encoding
@@snip [JwtArgonautDoc.scala](/docs/src/main/scala/JwtArgonautDoc.scala) { #encoding }
### Decoding
@@snip [JwtArgonautDoc.scala](/docs/src/main/scala/JwtArgonautDoc.scala) { #decoding }
================================================
FILE: docs/src/main/paradox/jwt-circe.md
================================================
## Circe
- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtCirce$.html)
@@@vars
```scala
libraryDependencies += "com.github.jwt-scala" %% "jwt-circe" % "$project.version$"
```
@@@
### Basic usage
@@snip [JwtCirceDoc.scala](/docs/src/main/scala/JwtCirceDoc.scala) { #example }
### Encoding
@@snip [JwtCirceDoc.scala](/docs/src/main/scala/JwtCirceDoc.scala) { #encoding }
### Decoding
@@snip [JwtCirceDoc.scala](/docs/src/main/scala/JwtCirceDoc.scala) { #decoding }
================================================
FILE: docs/src/main/paradox/jwt-core/index.md
================================================
@@@ index
- [Claim](jwt-claim.md)
- [Claim Private](jwt-claim-private.md)
- [Header](jwt-header.md)
- [ECDSA](jwt-ecdsa.md)
@@@
This module doesn't use any dependency, it is useful if you don't have any Json library in your project.
It is based a naive parsing of Json strings, and doesn't support any custom parameter in the Claim so if you need any custom parameter, or if you're already using one of the supported Json libraries, consider using that instead.
## Native
- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/Jwt$.html)
@@@vars
```scala
libraryDependencies += "com.github.jwt-scala" %% "jwt-core" % "$project.version$"
```
@@@
### Basic usage
```scala mdoc:reset
import java.time.Clock
import pdi.jwt.{Jwt, JwtAlgorithm, JwtHeader, JwtClaim, JwtOptions}
implicit val clock: Clock = Clock.systemUTC
val token = Jwt.encode("""{"user":1}""", "secretKey", JwtAlgorithm.HS256)
Jwt.decodeRawAll(token, "secretKey", Seq(JwtAlgorithm.HS256))
Jwt.decodeRawAll(token, "wrongKey", Seq(JwtAlgorithm.HS256))
```
### Encoding
```scala mdoc
// Encode from string, header automatically generated
Jwt.encode("""{"user":1}""", "secretKey", JwtAlgorithm.HS384)
// Encode from case class, header automatically generated
// Set that the token has been issued now and expires in 10 seconds
Jwt.encode(JwtClaim({"""{"user":1}"""}).issuedNow.expiresIn(10), "secretKey", JwtAlgorithm.HS512)
// You can encode without signing it
Jwt.encode("""{"user":1}""")
// You can specify a string header but also need to specify the algorithm just to be sure
// This is not really typesafe, so please use it with care
Jwt.encode("""{"typ":"JWT","alg":"HS256"}""", """{"user":1}""", "key", JwtAlgorithm.HS256)
// If using a case class header, no need to repeat the algorithm
// This is way better than the previous one
Jwt.encode(JwtHeader(JwtAlgorithm.HS256), JwtClaim("""{"user":1}"""), "key")
```
### Decoding
In JWT Scala, espcially when using raw strings which are not typesafe at all, there are a lot of possible errors. This is why nearly all `decode` functions will return a `Try` rather than directly the expected result. In case of failure, the wrapped exception should tell you what went wrong.
Take note that nearly all decoding methods (including those from helper libs) support either a `String` key, or a `PrivateKey` with a Hmac algorithm or a `PublicKey` with a RSA or ECDSA algorithm.
```scala mdoc
// Decode all parts of the token as string
Jwt.decodeRawAll(token, "secretKey", JwtAlgorithm.allHmac())
// Decode only the claim as a string
Jwt.decodeRaw(token, "secretKey", Seq(JwtAlgorithm.HS256))
// Decode all parts and cast them as a better type if possible.
// Since the implementation in JWT Core only use string, it is the same as decodeRawAll
// But check the result in JWT Play JSON to see the difference
Jwt.decodeAll(token, "secretKey", Seq(JwtAlgorithm.HS256))
// Same as before, but only the claim
// (you should start to see a pattern in the naming convention of the functions)
Jwt.decode(token, "secretKey", Seq(JwtAlgorithm.HS256))
// Failure because the token is not a token at all
Jwt.decode("Hey there!")
// Failure if not Base64 encoded
Jwt.decode("a.b.c")
// Failure in case we use the wrong key
Jwt.decode(token, "wrongKey", Seq(JwtAlgorithm.HS256))
// Failure if the token only starts in 5 seconds
Jwt.decode(Jwt.encode(JwtClaim().startsIn(5)))
```
### Validating
If you only want to check if a token is valid without decoding it. You have two options: `validate` functions that will throw the exceptions we saw in the decoding section, so you know what went wrong, or `isValid` functions that will return a boolean in case you don't care about the actual error and don't want to bother with catching exception.
```scala mdoc:crash
// All good
Jwt.validate(token, "secretKey", Seq(JwtAlgorithm.HS256))
Jwt.isValid(token, "secretKey", Seq(JwtAlgorithm.HS256))
// Wrong key here
Jwt.validate(token, "wrongKey", Seq(JwtAlgorithm.HS256))
Jwt.isValid(token, "wrongKey", Seq(JwtAlgorithm.HS256))
// No key for unsigned token => ok
Jwt.validate(Jwt.encode("{}"))
Jwt.isValid(Jwt.encode("{}"))
// No key while the token is actually signed => wrong
Jwt.validate(token)
Jwt.isValid(token)
// The token hasn't started yet!
Jwt.validate(Jwt.encode(JwtClaim().startsIn(5)))
Jwt.isValid(Jwt.encode(JwtClaim().startsIn(5)))
// This is no token
Jwt.validate("a.b.c")
Jwt.isValid("a.b.c")
```
### Using a custom clock
For testing, it can sometimes be useful to use a fake clock that will always return a fixed time. It can be done by instanciating
`Jwt` instead of using the object (based on the system clock):
```scala mdoc
import java.time.{Clock, Instant, ZoneId}
val startTime = Clock.fixed(Instant.ofEpochSecond(0), ZoneId.of("UTC"))
val endTime = Clock.fixed(Instant.ofEpochSecond(5), ZoneId.of("UTC"))
val customJwt = Jwt(endTime)
val claim = JwtClaim().issuedNow(startTime).expiresIn(10)(startTime)
val encoded = customJwt.encode(claim, "key", JwtAlgorithm.HS256)
customJwt.decode(encoded, "key", JwtAlgorithm.allHmac())
```
### Options
All validating and decoding methods support a final optional argument as a `JwtOptions` which allow you to disable validation checks. This is useful if you need to access data from an expired token for example. You can disable `expiration`, `notBefore` and `signature` checks. Be warned that if you disable the last one, you have no guarantee that the user didn't change the content of the token.
```scala mdoc
val expiredToken = Jwt.encode(JwtClaim().by("me").expiresIn(-1))
// Fail since the token is expired
Jwt.isValid(expiredToken)
Jwt.decode(expiredToken)
// Let's disable expiration check
Jwt.isValid(expiredToken, JwtOptions(expiration = false))
Jwt.decode(expiredToken, JwtOptions(expiration = false))
```
You can also specify a leeway, in seconds, to account for clock skew.
```scala mdoc
// Allow 30sec leeway
Jwt.isValid(expiredToken, JwtOptions(leeway = 30))
Jwt.decode(expiredToken, JwtOptions(leeway = 30))
```
================================================
FILE: docs/src/main/paradox/jwt-core/jwt-claim-private.md
================================================
## Jwt Reserved Claims and Private Claims
A common use-case of Jwt-Scala (and JWT at large) is developing so-called "public" or "private" claims (and or header params). These are functionally no different than "reserved" claims/header params, other than that they have no standard definition and may only be distinguished within your network or niche industry. "issuer", "subject", "audience" etc. are all examples of reserved claims, whereas "user" is a fairly common example of a non-reserved claim.
Given that there may be many of these public/private claims, rather than parsing them yourself separate from how reserved claims are parsed (see the *JwtClaim Class*), you can simply compose `JwtClaim` with your own custom claims that extend from the `JwtReservedClaim` trait.
Here is an example where reserved headers, along with a private "user" claim, is used:
```scala mdoc:reset
import pdi.jwt.{Jwt, JwtHeader, JwtClaim, JwtUtils, JwtJson4sParser}
import java.time.Clock
// define your network-specific claims, and compose them with the usual reservedClaims
case class JwtPrivateClaim(user: Option[String] = None, reservedClaims: JwtClaim = JwtClaim()) {
// merge your json definition along with the reserved claims too
def toJson: String = JwtUtils.mergeJson(JwtUtils.hashToJson(Seq(
"user" -> user,
).collect {
case (key, Some(value)) => (key -> value)
}), reservedClaims.toJson)
}
// create a parser with claim type set to the one you just defined
// notice that the default `JwtHeader` class was used since we're only interested in overriding with a custom private claims type in this example
object JwtJson4sPrivate extends JwtJson4sParser[JwtHeader, JwtPrivateClaim] {
override implicit val clock = Clock.systemUTC
override protected def parseClaim(claim: String): JwtPrivateClaim = {
val claimJson = super.parse(claim)
val jwtReservedClaim: JwtClaim = super.readClaim(claimJson)
val content = super.parse(jwtReservedClaim.content)
JwtPrivateClaim(super.extractString(content, "user"), jwtReservedClaim.withContent("{}"))
}
// here is the only boilerplate (but if you chose to also specify a custom header type then you would make use of this)
override protected def parseHeader(header: String): JwtHeader = super.readHeader(parse(header))
// marginal boilerplate to ensure consistency with isValid checks now that your nesting reserved claims into your custom private claims
override protected def extractExpiration(claim: JwtPrivateClaim): Option[Long] = claim.reservedClaims.expiration
override protected def extractNotBefore(claim: JwtPrivateClaim): Option[Long] = claim.reservedClaims.notBefore
}
```
You can then use the same decodeAll method as you would before, now with your fully objectified claims:
```scala mdoc
import scala.util.Try
// this example chose to use JwtJson4s, but any Json implementation would work the same
val token: String = Jwt.encode("""{"user":"someone", "iss": "me"}""");
val decoded: Try[(JwtHeader, JwtPrivateClaim, String)] = JwtJson4sPrivate.decodeAll(token)
```
================================================
FILE: docs/src/main/paradox/jwt-core/jwt-claim.md
================================================
## JwtClaim Class
```scala mdoc:reset
import java.time.Clock
import pdi.jwt.JwtClaim
JwtClaim()
implicit val clock: Clock = Clock.systemUTC
// Specify the content as JSON string
// (don't use var in your code if possible, this is just to ease the sample)
var claim = JwtClaim("""{"user":1}""")
// Append new content
claim = claim + """{"key1":"value1"}"""
claim = claim + ("key2", true)
claim = claim ++ (("key3", 3), ("key4", Seq(1, 2)), ("key5", ("key5.1", "Subkey")))
// Stringify as JSON
claim.toJson
// Manipulate basic attributes
// Set the issuer
claim = claim.by("Me")
// Set the audience
claim = claim.to("You")
// Set the subject
claim = claim.about("Something")
// Set the id
claim = claim.withId("42")
// Set the expiration
// In 10 seconds from now
claim = claim.expiresIn(5)
// At a specific timestamp (in seconds)
claim.expiresAt(1431520421)
// Right now! (the token is directly invalid...)
claim.expiresNow
// Set the beginning of the token (aka the "not before" attribute)
// 5 seconds ago
claim.startsIn(-5)
// At a specific timestamp (in seconds)
claim.startsAt(1431520421)
// Right now!
claim = claim.startsNow
// Set the date when the token was created
// (you should always use claim.issuedNow, but I let you do otherwise if needed)
// 5 seconds ago
claim.issuedIn(-5)
// At a specific timestamp (in seconds)
claim.issuedAt(1431520421)
// Right now!
claim = claim.issuedNow
// We can test if the claim is valid => testing if the current time is between "not before" and "expiration"
claim.isValid
// Also test the issuer and audience
claim.isValid("Me", "You")
// Let's stringify the final version
claim.toJson
```
================================================
FILE: docs/src/main/paradox/jwt-core/jwt-ecdsa.md
================================================
## Jwt with ECDSA algorithms
### With generated keys
#### Generation
```scala
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.spec.{ECPrivateKeySpec, ECPublicKeySpec, ECGenParameterSpec, ECParameterSpec, ECPoint}
import java.security.{SecureRandom, KeyFactory, KeyPairGenerator, Security}
import pdi.jwt.{Jwt, JwtAlgorithm}
// We specify the curve we want to use
val ecGenSpec = new ECGenParameterSpec("P-521")
// We are going to use a ECDSA algorithm
// and the Bouncy Castle provider
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider())
}
val generatorEC = KeyPairGenerator.getInstance("ECDSA", "BC")
generatorEC.initialize(ecGenSpec, new SecureRandom())
// Generate a pair of keys, one private for encoding
// and one public for decoding
val ecKey = generatorEC.generateKeyPair()
```
#### Usage
```scala
val token = Jwt.encode("""{"user":1}""", ecKey.getPrivate, JwtAlgorithm.ES512)
Jwt.decode(token, ecKey.getPublic, JwtAlgorithm.allECDSA)
```
### With saved keys
Let's say you already have your keys, it means you know the **S** param for the private key and both **(X, Y)** for the public key. So we will first recreate the keys from those params and then use them just as we did for the previously generated keys.
#### Creation
```scala
import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.spec.ECNamedCurveSpec
// Our saved params
val S = BigInt("1ed498eedf499e5dd12b1ab94ee03d1a722eaca3ed890630c8b25f1015dd4ec5630a02ddb603f3248a3b87c88637e147ecc7a6e2a1c2f9ff1103be74e5d42def37d", 16)
val X = BigInt("16528ac15dc4c8e0559fad628ac3ffbf5c7cfefe12d50a97c7d088cc10b408d4ab03ac0d543bde862699a74925c1f2fe7c247c00fddc1442099dfa0671fc032e10a", 16)
val Y = BigInt("b7f22b3c1322beef766cadd1a5f0363840195b7be10d9a518802d8d528e03bc164c9588c5e63f1473d05195510676008b6808508539367d2893e1aa4b7cb9f9dab", 16)
// Here we are using the P-521 curve but you need to change it
// to your own curve
val curveParams = ECNamedCurveTable.getParameterSpec("P-521")
val curveSpec: ECParameterSpec = new ECNamedCurveSpec( "P-521", curveParams.getCurve(), curveParams.getG(), curveParams.getN(), curveParams.getH());
val privateSpec = new ECPrivateKeySpec(S.underlying(), curveSpec)
val publicSpec = new ECPublicKeySpec(new ECPoint(X.underlying(), Y.underlying()), curveSpec)
val privateKeyEC = KeyFactory.getInstance("ECDSA", "BC").generatePrivate(privateSpec)
val publicKeyEC = KeyFactory.getInstance("ECDSA", "BC").generatePublic(publicSpec)
```
#### Usage
```scala
val token = Jwt.encode("""{"user":1}""", privateKeyEC, JwtAlgorithm.ES512)
Jwt.decode(token, publicKeyEC, Seq(JwtAlgorithm.ES512))
// Wrong key...
Jwt.decode(token, ecKey.getPublic, Seq(JwtAlgorithm.ES512))
```
================================================
FILE: docs/src/main/paradox/jwt-core/jwt-header.md
================================================
## JwtHeader Case Class
```scala mdoc:reset
import pdi.jwt.{JwtHeader, JwtAlgorithm}
JwtHeader()
JwtHeader(JwtAlgorithm.HS256)
JwtHeader(JwtAlgorithm.HS256, "JWT")
// You can stringify it to JSON
JwtHeader(JwtAlgorithm.HS256, "JWT").toJson
// You can assign the default type (but it would have be done automatically anyway)
JwtHeader(JwtAlgorithm.HS256).withType
```
================================================
FILE: docs/src/main/paradox/jwt-json4s.md
================================================
## Json4s
- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtJson4s$.html)
@@@vars
```scala
libraryDependencies += "com.github.jwt-scala" %% "jwt-json4s" % "$project.version$"
```
@@@
### Basic usage
@@snip [JwtJson4sDoc.scala](/docs/src/main/scala/JwtJson4sDoc.scala) { #example }
### Encoding
@@snip [JwtJson4sDoc.scala](/docs/src/main/scala/JwtJson4sDoc.scala) { #encode }
### Decoding
@@snip [JwtJson4sDoc.scala](/docs/src/main/scala/JwtJson4sDoc.scala) { #decode }
================================================
FILE: docs/src/main/paradox/jwt-play-json.md
================================================
## Play Json
- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtJson$.html)
@@@vars
```scala
libraryDependencies += "com.github.jwt-scala" %% "jwt-play-json" % "$project.version$"
```
@@@
### Basic usage
@@snip [JwtPlayJsonDoc.scala](/docs/src/main/scala/JwtPlayJsonDoc.scala) { #example }
### Encoding
@@snip [JwtPlayJsonDoc.scala](/docs/src/main/scala/JwtPlayJsonDoc.scala) { #encode }
### Decoding
@@snip [JwtPlayJsonDoc.scala](/docs/src/main/scala/JwtPlayJsonDoc.scala) { #decode }
### Formating
The project provides implicit reader and writer for both `JwtHeader` and `JwtClaim`
@@snip [JwtPlayJsonDoc.scala](/docs/src/main/scala/JwtPlayJsonDoc.scala) { #format }
================================================
FILE: docs/src/main/paradox/jwt-play-jwt-session.md
================================================
## JwtSession case class
@@@vars
```scala
libraryDependencies += "com.github.jwt-scala" %% "jwt-play" % "$project.version$"
```
@@@
Provides an API similar to the Play [Session](https://www.playframework.com/documentation/2.3.x/api/scala/index.html#play.api.mvc.Session) but using `JsValue` rather than `String` as values. It also separates `headerData` from `claimData` rather than having only one `data`.
### Basic usage
@@snip [JwtPlayJwtSessionDoc.scala](/docs/src/main/scala/JwtPlayJwtSessionDoc.scala) { #example }
### Using implicits
If you have implicit `Reads` and/or `Writes`, you can access and/or add data directly as case class or object.
@@snip [JwtPlayJwtSessionDoc.scala](/docs/src/main/scala/JwtPlayJwtSessionDoc.scala) { #implicits }
## Play RequestHeader
You can extract a `JwtSession` from a `RequestHeader`.
@@snip [JwtPlayJwtSessionDoc.scala](/docs/src/main/scala/JwtPlayJwtSessionDoc.scala) { #requestheader }
## Play Result
There are also implicit helpers around `Result` to help you manipulate the session inside it.
@@snip [JwtPlayJwtSessionDoc.scala](/docs/src/main/scala/JwtPlayJwtSessionDoc.scala) { #result }
## Play configuration
### Secret key
`play.http.secret.key`
> Default: none
The secret key is used to secure cryptographics functions. We are using the same key to sign Json Web Tokens so you don't need to worry about it.
### Private key
`play.http.session.privateKey`
> Default: none
The PKCS8 format private key is used to sign JWT session. If `play.http.session.privateKey` is missing `play.http.secret.key` used instead.
### Public key
`play.http.session.publicKey`
> Default: none
The X.509 format public key is used to verify JWT session signed with private key `play.http.session.privateKey`
### Session timeout
`play.http.session.maxAge`
> Default: none
Just like for the cookie session, you can use this key to specify the duration, in milliseconds or using the duration syntax (for example 30m or 1h), after which the user should be logout, which mean the token will no longer be valid. It means you need to refresh the expiration date at each request
### Signature algorithm
`play.http.session.algorithm`
> Default: HS256
>
> Supported: HMD5, HS1, HS224, HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512
You can specify which algorithm you want to use, among the supported ones, in order to create the signature which will assure you that nobody can actually change the token. You should probably stick with the default one or use HmacSHA512 for maximum security.
### Header name
`play.http.session.jwtName`
> Default: Authorization
You can change the name of the header in which the token should be stored. It will be used for both requests and responses.
### Response header name
`play.http.session.jwtResponseName`
> Default: none
If you need to have a different header for request and response, you can override the response header using this key.
### Token prefix
`play.http.session.tokenPrefix`
> Default: "Bearer "
Authorization header should have a prefix before the token, like "Basic" for example. For a JWT token, it should be "Bearer" (which is the default value) but you can freely change or remove it (using an empty string). The token prefix will be directly prepend before the token, so be sure to put any necessary whitespaces in it.
================================================
FILE: docs/src/main/paradox/jwt-upickle.md
================================================
## upickle
- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtUpickle$.html)
@@@vars
```scala
libraryDependencies += "com.github.jwt-scala" %% "jwt-upickle" % "$project.version$"
```
@@@
### Basic usage
@@snip [JwtUpickleDoc.scala](/docs/src/main/scala/JwtUpickleDoc.scala) { #example }
### Encoding
@@snip [JwtUpickleDoc.scala](/docs/src/main/scala/JwtUpickleDoc.scala) { #encoding }
### Decoding
@@snip [JwtUpickleDoc.scala](/docs/src/main/scala/JwtUpickleDoc.scala) { #decoding }
================================================
FILE: docs/src/main/paradox/jwt-zio-json.md
================================================
## ZIO Json
- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtZioJson$.html)
@@@vars
```scala
libraryDependencies += "com.github.jwt-scala" %% "jwt-zio-json" % "$project.version$"
```
@@@
### Basic usage
@@snip [JwtZioDoc.scala](/docs/src/main/scala/JwtZioDoc.scala) { #example }
### Encoding
@@snip [JwtZioDoc.scala](/docs/src/main/scala/JwtZioDoc.scala) { #encoding }
### Decoding
@@snip [JwtZioDoc.scala](/docs/src/main/scala/JwtZioDoc.scala) { #decoding }
================================================
FILE: docs/src/main/paradox/project/build.properties
================================================
sbt.version=1.5.0
================================================
FILE: docs/src/main/scala/JwtArgonautDoc.scala
================================================
package pdi.jwt.docs
object JwtArgonautDoc {
// #example
import java.time.Instant
import scala.util.Try
import argonaut.Json
import pdi.jwt.{JwtAlgorithm, JwtArgonaut, JwtClaim}
val claim = JwtClaim(
expiration = Some(Instant.now().plusSeconds(157784760).getEpochSecond),
issuedAt = Some(Instant.now.getEpochSecond)
)
val key = "secretKey"
val alg = JwtAlgorithm.HS512
val token = JwtArgonaut.encode(claim, key, alg)
val decodedJson: Try[Json] = JwtArgonaut.decodeJson(token, key, Seq(alg))
val decodedClaim: Try[JwtClaim] = JwtArgonaut.decode(token, key, Seq(alg))
// #example
}
object JwtArgonautDocEncoding {
// #encoding
import java.time.Instant
import argonaut.Parse
import pdi.jwt.{JwtAlgorithm, JwtArgonaut}
val key = "secretKey"
val alg = JwtAlgorithm.HS512
val jsonClaim = Parse.parseOption(s"""{"expires":${Instant.now().getEpochSecond}}""").get
val jsonHeader = Parse.parseOption("""{"typ":"JWT","alg":"HS512"}""").get
val token1: String = JwtArgonaut.encode(jsonClaim)
val token2: String = JwtArgonaut.encode(jsonClaim, key, alg)
val token3: String = JwtArgonaut.encode(jsonHeader, jsonClaim, key)
// #encoding
}
object JwtArgonautDocDecoding {
import java.time.Instant
// #decoding
import scala.util.Try
import argonaut.Json
import pdi.jwt.{JwtAlgorithm, JwtArgonaut, JwtClaim, JwtHeader}
val claim = JwtClaim(
expiration = Some(Instant.now.plusSeconds(157784760).getEpochSecond),
issuedAt = Some(Instant.now.getEpochSecond)
)
val key = "secretKey"
val alg = JwtAlgorithm.HS512
val token = JwtArgonaut.encode(claim, key, alg)
val decodedJsonClaim: Try[Json] = JwtArgonaut.decodeJson(token, key, Seq(alg))
val decodedJson: Try[(Json, Json, String)] = JwtArgonaut.decodeJsonAll(token, key, Seq(alg))
val decodedClaim: Try[JwtClaim] = JwtArgonaut.decode(token, key, Seq(alg))
val decodedToken: Try[(JwtHeader, JwtClaim, String)] = JwtArgonaut.decodeAll(token, key, Seq(alg))
// #decoding
}
================================================
FILE: docs/src/main/scala/JwtCirceDoc.scala
================================================
package pdi.jwt.docs
import scala.annotation.nowarn
@nowarn
object CirceExample {
// #example
import java.time.Instant
import pdi.jwt.{JwtAlgorithm, JwtCirce, JwtClaim}
val claim = JwtClaim(
expiration = Some(Instant.now.plusSeconds(157784760).getEpochSecond),
issuedAt = Some(Instant.now.getEpochSecond)
)
val key = "secretKey"
val algo = JwtAlgorithm.HS256
val token = JwtCirce.encode(claim, key, algo)
JwtCirce.decodeJson(token, key, Seq(JwtAlgorithm.HS256))
JwtCirce.decode(token, key, Seq(JwtAlgorithm.HS256))
// #example
}
@nowarn
object CirceEncoding {
// #encoding
import java.time.Instant
import io.circe._
import jawn.{parse => jawnParse}
import pdi.jwt.{JwtAlgorithm, JwtCirce}
val key = "secretKey"
val algo = JwtAlgorithm.HS256
val Right(claimJson) = jawnParse(s"""{"expires":${Instant.now.getEpochSecond}}""")
val Right(header) = jawnParse("""{"typ":"JWT","alg":"HS256"}""")
// From just the claim to all possible attributes
JwtCirce.encode(claimJson)
JwtCirce.encode(claimJson, key, algo)
JwtCirce.encode(header, claimJson, key)
// #encoding
}
@nowarn
object CirceDecoding {
// #decoding
import java.time.Instant
import pdi.jwt.{JwtAlgorithm, JwtCirce, JwtClaim}
val claim = JwtClaim(
expiration = Some(Instant.now.plusSeconds(157784760).getEpochSecond),
issuedAt = Some(Instant.now.getEpochSecond)
)
val key = "secretKey"
val algo = JwtAlgorithm.HS256
val token = JwtCirce.encode(claim, key, algo)
// You can decode to JsObject
JwtCirce.decodeJson(token, key, Seq(JwtAlgorithm.HS256))
JwtCirce.decodeJsonAll(token, key, Seq(JwtAlgorithm.HS256))
// Or to case classes
JwtCirce.decode(token, key, Seq(JwtAlgorithm.HS256))
JwtCirce.decodeAll(token, key, Seq(JwtAlgorithm.HS256))
// #decoding
}
================================================
FILE: docs/src/main/scala/JwtJson4sDoc.scala
================================================
package pdi.jwt.docs
import scala.annotation.nowarn
@nowarn
object JwtJson4sDoc {
// #example
import org.json4s.JsonDSL.WithBigDecimal._
import org.json4s._
import pdi.jwt.{JwtAlgorithm, JwtJson4s}
val claim = JObject(("user", 1), ("nbf", 1431520421))
val key = "secretKey"
val algo = JwtAlgorithm.HS256
JwtJson4s.encode(claim)
val token = JwtJson4s.encode(claim, key, algo)
JwtJson4s.decodeJson(token, key, Seq(JwtAlgorithm.HS256))
JwtJson4s.decode(token, key, Seq(JwtAlgorithm.HS256))
// #example
// #encode
val header = JObject(("typ", "JWT"), ("alg", "HS256"))
JwtJson4s.encode(claim)
JwtJson4s.encode(claim, key, algo)
JwtJson4s.encode(header, claim, key)
// #encode
// #decode
// You can decode to JsObject
JwtJson4s.decodeJson(token, key, Seq(JwtAlgorithm.HS256))
JwtJson4s.decodeJsonAll(token, key, Seq(JwtAlgorithm.HS256))
// Or to case classes
JwtJson4s.decode(token, key, Seq(JwtAlgorithm.HS256))
JwtJson4s.decodeAll(token, key, Seq(JwtAlgorithm.HS256))
// #decode
}
================================================
FILE: docs/src/main/scala/JwtPlayJsonDoc.scala
================================================
package pdi.jwt.docs
import scala.annotation.nowarn
@nowarn
object JwtPlayJsonDoc {
// #example
import java.time.Clock
import pdi.jwt._
import play.api.libs.js
gitextract_3pvve9sy/
├── .git-blame-ignore-revs
├── .github/
│ └── workflows/
│ ├── docs.yml
│ ├── release.yml
│ └── tests.yml
├── .gitignore
├── .scala-steward.conf
├── .scalafmt.conf
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build.sbt
├── core/
│ ├── jvm/
│ │ └── src/
│ │ └── test/
│ │ └── scala/
│ │ ├── JwtSpec.scala
│ │ └── JwtUtilsSpec.scala
│ └── shared/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ ├── Jwt.scala
│ │ ├── JwtAlgorithm.scala
│ │ ├── JwtArrayUtils.scala
│ │ ├── JwtBase64.scala
│ │ ├── JwtClaim.scala
│ │ ├── JwtCore.scala
│ │ ├── JwtException.scala
│ │ ├── JwtHeader.scala
│ │ ├── JwtOptions.scala
│ │ ├── JwtTime.scala
│ │ └── JwtUtils.scala
│ └── test/
│ └── scala/
│ ├── Fixture.scala
│ ├── JwtBase64Spec.scala
│ └── JwtClaimSpec.scala
├── docs/
│ └── src/
│ └── main/
│ ├── paradox/
│ │ ├── index.md
│ │ ├── jwt-argonaut.md
│ │ ├── jwt-circe.md
│ │ ├── jwt-core/
│ │ │ ├── index.md
│ │ │ ├── jwt-claim-private.md
│ │ │ ├── jwt-claim.md
│ │ │ ├── jwt-ecdsa.md
│ │ │ └── jwt-header.md
│ │ ├── jwt-json4s.md
│ │ ├── jwt-play-json.md
│ │ ├── jwt-play-jwt-session.md
│ │ ├── jwt-upickle.md
│ │ ├── jwt-zio-json.md
│ │ └── project/
│ │ └── build.properties
│ └── scala/
│ ├── JwtArgonautDoc.scala
│ ├── JwtCirceDoc.scala
│ ├── JwtJson4sDoc.scala
│ ├── JwtPlayJsonDoc.scala
│ ├── JwtPlayJwtSessionDoc.scala
│ ├── JwtUpickleDoc.scala
│ └── JwtZioDoc.scala
├── json/
│ ├── argonaut/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ └── JwtArgonaut.scala
│ │ └── test/
│ │ └── scala/
│ │ ├── ArgonautFixture.scala
│ │ └── JwtArgonautSpec.scala
│ ├── circe/
│ │ ├── jvm/
│ │ │ └── src/
│ │ │ └── test/
│ │ │ └── scala/
│ │ │ ├── CirceFixture.scala
│ │ │ └── JwtCirceSpec.scala
│ │ └── shared/
│ │ └── src/
│ │ └── main/
│ │ └── scala/
│ │ └── JwtCirce.scala
│ ├── common/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ └── JwtJsonCommon.scala
│ │ └── test/
│ │ └── scala/
│ │ ├── JsonCommonFixture.scala
│ │ └── JwtJsonCommonSpec.scala
│ ├── json4s-common/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ └── JwtJson4sCommon.scala
│ │ └── test/
│ │ └── scala/
│ │ └── Json4sCommonFixture.scala
│ ├── json4s-jackson/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ ├── JwtJson4sImplicits.scala
│ │ │ └── JwtJson4sJackson.scala
│ │ └── test/
│ │ └── scala/
│ │ ├── Json4sJacksonFixture.scala
│ │ └── Json4sJacksonSpec.scala
│ ├── json4s-native/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ ├── JwtJson4sImplicits.scala
│ │ │ └── JwtJson4sNative.scala
│ │ └── test/
│ │ └── scala/
│ │ ├── Json4sNativeFixture.scala
│ │ └── Json4sNativeSpec.scala
│ ├── play-json/
│ │ └── src/
│ │ ├── main/
│ │ │ └── scala/
│ │ │ ├── JwtJson.scala
│ │ │ └── JwtJsonImplicits.scala
│ │ └── test/
│ │ └── scala/
│ │ ├── JsonFixture.scala
│ │ └── JwtJsonSpec.scala
│ ├── upickle/
│ │ ├── jvm/
│ │ │ └── src/
│ │ │ └── test/
│ │ │ └── scala/
│ │ │ ├── JwtUpickleFixture.scala
│ │ │ └── JwtUpickleSpec.scala
│ │ └── shared/
│ │ └── src/
│ │ └── main/
│ │ └── scala/
│ │ ├── JwtUpickle.scala
│ │ └── JwtUpickleImplicits.scala
│ └── zio-json/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ └── JwtZIOJson.scala
│ └── test/
│ └── scala/
│ ├── JwtZIOJsonSpec.scala
│ └── ZIOJsonFixture.scala
├── play/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ ├── JwtPlayImplicits.scala
│ │ └── JwtSession.scala
│ └── test/
│ └── scala/
│ ├── JwtResultSpec.scala
│ ├── JwtSessionAsymetricSpec.scala
│ ├── JwtSessionCustomDifferentNameSpec.scala
│ ├── JwtSessionCustomSpec.scala
│ ├── JwtSessionSpec.scala
│ └── PlayFixture.scala
├── project/
│ ├── Libs.scala
│ ├── build.properties
│ └── plugins.sbt
└── scripts/
├── bump.sh
├── clean.sh
└── pu.sh
Condensed preview — 93 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (320K chars).
[
{
"path": ".git-blame-ignore-revs",
"chars": 352,
"preview": "# Scala Steward: Reformat with scalafmt 3.7.2\ne8c410d04442fe9ac7aa50df34a398972a602cdc\n\n# Scala Steward: Reformat with s"
},
{
"path": ".github/workflows/docs.yml",
"chars": 1012,
"preview": "name: Docs\non:\n push:\n tags: [\"*\"]\nconcurrency:\n group: docs\n\njobs:\n publish:\n runs-on: ubuntu-latest\n steps"
},
{
"path": ".github/workflows/release.yml",
"chars": 628,
"preview": "name: Release\non:\n push:\n branches: [master, main]\n tags: [\"*\"]\njobs:\n publish:\n runs-on: ubuntu-latest\n s"
},
{
"path": ".github/workflows/tests.yml",
"chars": 2454,
"preview": "name: CI\n\non:\n pull_request:\n branches: [master, main]\n push:\n branches: [master, main]\n\nenv:\n SCALA212: 2.12.2"
},
{
"path": ".gitignore",
"chars": 94,
"preview": ".history\ntarget\nproject/project\nproject/target\n.bloop/\n.idea\n.bsp\n.metals/\n.vscode/\nmetals.sbt"
},
{
"path": ".scala-steward.conf",
"chars": 79,
"preview": "updates.ignore = [\n { groupId = \"com.google.inject\", artifactId = \"guice\" }\n]\n"
},
{
"path": ".scalafmt.conf",
"chars": 229,
"preview": "version=3.10.7\nrunner.dialect=scala213\n\nmaxColumn = 100\n\nrewrite.rules = [Imports, AvoidInfix, SortModifiers, PreferCurl"
},
{
"path": "CHANGELOG.md",
"chars": 12951,
"preview": "# Changelog\n\nNote: this file is no longer updated, check the [releases tab](https://github.com/jwt-scala/jwt-scala/relea"
},
{
"path": "CONTRIBUTING.md",
"chars": 1493,
"preview": "## Contributor Guide\n\nFor any bug, new feature, or documentation improvement,\nthe best way to start a conversation is by"
},
{
"path": "LICENSE",
"chars": 10174,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 3204,
"preview": "# JWT Scala\n\nScala support for JSON Web Token ([JWT](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token)).\nSuppo"
},
{
"path": "build.sbt",
"chars": 9232,
"preview": "import scala.io.Source\nimport scala.sys.process._\n\nimport com.jsuereth.sbtpgp.PgpKeys._\nimport sbt.Keys._\nimport sbt.Tes"
},
{
"path": "core/jvm/src/test/scala/JwtSpec.scala",
"chars": 20443,
"preview": "package pdi.jwt\n\nimport scala.util.{Success, Try}\n\nimport pdi.jwt.algorithms.*\nimport pdi.jwt.exceptions.*\n\nclass JwtSpe"
},
{
"path": "core/jvm/src/test/scala/JwtUtilsSpec.scala",
"chars": 7869,
"preview": "package pdi.jwt\n\nimport java.security.spec.ECGenParameterSpec\nimport java.security.{KeyPairGenerator, SecureRandom}\n\nimp"
},
{
"path": "core/shared/src/main/scala/Jwt.scala",
"chars": 4366,
"preview": "package pdi.jwt\n\nimport java.time.Clock\nimport scala.util.matching.Regex\n\n/** Test implementation of [[JwtCore]] using o"
},
{
"path": "core/shared/src/main/scala/JwtAlgorithm.scala",
"chars": 4133,
"preview": "package pdi.jwt\n\nimport scala.annotation.nowarn\n\nimport pdi.jwt.algorithms.JwtUnknownAlgorithm\n\nsealed trait JwtAlgorith"
},
{
"path": "core/shared/src/main/scala/JwtArrayUtils.scala",
"chars": 1029,
"preview": "package pdi.jwt\n\nobject JwtArrayUtils {\n\n /** A constant time equals comparison - does not terminate early if test will"
},
{
"path": "core/shared/src/main/scala/JwtBase64.scala",
"chars": 1131,
"preview": "package pdi.jwt\n\nobject JwtBase64 {\n private lazy val encoder = java.util.Base64.getUrlEncoder()\n private lazy val dec"
},
{
"path": "core/shared/src/main/scala/JwtClaim.scala",
"chars": 5250,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nobject JwtClaim {\n def apply(\n content: String = \"{}\",\n issuer: Opti"
},
{
"path": "core/shared/src/main/scala/JwtCore.scala",
"chars": 42392,
"preview": "package pdi.jwt\n\nimport java.security.{Key, PrivateKey, PublicKey}\nimport java.time.Clock\nimport javax.crypto.SecretKey\n"
},
{
"path": "core/shared/src/main/scala/JwtException.scala",
"chars": 2328,
"preview": "package pdi.jwt.exceptions\n\nimport pdi.jwt.JwtTime\n\nsealed abstract class JwtException(message: String) extends RuntimeE"
},
{
"path": "core/shared/src/main/scala/JwtHeader.scala",
"chars": 2506,
"preview": "package pdi.jwt\n\nobject JwtHeader {\n val DEFAULT_TYPE = \"JWT\"\n\n def apply(\n algorithm: Option[JwtAlgorithm] = Non"
},
{
"path": "core/shared/src/main/scala/JwtOptions.scala",
"chars": 227,
"preview": "package pdi.jwt\n\ncase class JwtOptions(\n signature: Boolean = true,\n expiration: Boolean = true,\n notBefore: Bo"
},
{
"path": "core/shared/src/main/scala/JwtTime.scala",
"chars": 2914,
"preview": "package pdi.jwt\n\nimport java.time.{Clock, Instant}\nimport scala.util.{Failure, Success, Try}\n\nimport pdi.jwt.exceptions."
},
{
"path": "core/shared/src/main/scala/JwtUtils.scala",
"chars": 12552,
"preview": "package pdi.jwt\n\nimport java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec}\nimport java.security.{KeyFactory, P"
},
{
"path": "core/shared/src/test/scala/Fixture.scala",
"chars": 11475,
"preview": "package pdi.jwt\n\nimport java.security.spec.*\nimport java.security.{KeyFactory, KeyPairGenerator, SecureRandom, Security}"
},
{
"path": "core/shared/src/test/scala/JwtBase64Spec.scala",
"chars": 1620,
"preview": "package pdi.jwt\n\nclass JwtBase64Spec extends munit.FunSuite {\n val eol = System.getProperty(\"line.separator\")\n\n val va"
},
{
"path": "core/shared/src/test/scala/JwtClaimSpec.scala",
"chars": 2538,
"preview": "package pdi.jwt\n\nimport java.time.{Clock, Instant, ZoneOffset}\n\nimport munit.ScalaCheckSuite\nimport org.scalacheck.Prop."
},
{
"path": "docs/src/main/paradox/index.md",
"chars": 3238,
"preview": "# JWT Scala\n\n@@@ index\n\n- [Native](jwt-core/index.md)\n- [Argonaut](jwt-argonaut.md)\n- [Circe](jwt-circe.md)\n- [Json4S](j"
},
{
"path": "docs/src/main/paradox/jwt-argonaut.md",
"chars": 526,
"preview": "## Argonaut\n\n- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtArgonaut$.html)\n\n@@@vars\n\n```sca"
},
{
"path": "docs/src/main/paradox/jwt-circe.md",
"chars": 499,
"preview": "## Circe\n\n- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtCirce$.html)\n\n@@@vars\n\n```scala\nlib"
},
{
"path": "docs/src/main/paradox/jwt-core/index.md",
"chars": 6042,
"preview": "@@@ index\n\n- [Claim](jwt-claim.md)\n- [Claim Private](jwt-claim-private.md)\n- [Header](jwt-header.md)\n- [ECDSA](jwt-ecdsa"
},
{
"path": "docs/src/main/paradox/jwt-core/jwt-claim-private.md",
"chars": 3081,
"preview": "## Jwt Reserved Claims and Private Claims\n\nA common use-case of Jwt-Scala (and JWT at large) is developing so-called \"pu"
},
{
"path": "docs/src/main/paradox/jwt-core/jwt-claim.md",
"chars": 1653,
"preview": "## JwtClaim Class\n\n```scala mdoc:reset\nimport java.time.Clock\nimport pdi.jwt.JwtClaim\n\nJwtClaim()\n\nimplicit val clock: C"
},
{
"path": "docs/src/main/paradox/jwt-core/jwt-ecdsa.md",
"chars": 2768,
"preview": "## Jwt with ECDSA algorithms\n\n### With generated keys\n\n#### Generation\n\n```scala\nimport org.bouncycastle.jce.provider.Bo"
},
{
"path": "docs/src/main/paradox/jwt-core/jwt-header.md",
"chars": 371,
"preview": "## JwtHeader Case Class\n\n```scala mdoc:reset\nimport pdi.jwt.{JwtHeader, JwtAlgorithm}\n\nJwtHeader()\nJwtHeader(JwtAlgorith"
},
{
"path": "docs/src/main/paradox/jwt-json4s.md",
"chars": 504,
"preview": "## Json4s\n\n- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtJson4s$.html)\n\n@@@vars\n\n```scala\nl"
},
{
"path": "docs/src/main/paradox/jwt-play-json.md",
"chars": 706,
"preview": "## Play Json\n\n- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtJson$.html)\n\n@@@vars\n\n```scala\n"
},
{
"path": "docs/src/main/paradox/jwt-play-jwt-session.md",
"chars": 3362,
"preview": "## JwtSession case class\n\n@@@vars\n\n```scala\nlibraryDependencies += \"com.github.jwt-scala\" %% \"jwt-play\" % \"$project.vers"
},
{
"path": "docs/src/main/paradox/jwt-upickle.md",
"chars": 517,
"preview": "## upickle\n\n- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtUpickle$.html)\n\n@@@vars\n\n```scala"
},
{
"path": "docs/src/main/paradox/jwt-zio-json.md",
"chars": 495,
"preview": "## ZIO Json\n\n- [API Documentation](https://jwt-scala.github.io/jwt-scala/api/pdi/jwt/JwtZioJson$.html)\n\n@@@vars\n\n```scal"
},
{
"path": "docs/src/main/paradox/project/build.properties",
"chars": 18,
"preview": "sbt.version=1.5.0\n"
},
{
"path": "docs/src/main/scala/JwtArgonautDoc.scala",
"chars": 2013,
"preview": "package pdi.jwt.docs\n\nobject JwtArgonautDoc {\n\n // #example\n import java.time.Instant\n import scala.util.Try\n\n impor"
},
{
"path": "docs/src/main/scala/JwtCirceDoc.scala",
"chars": 1819,
"preview": "package pdi.jwt.docs\n\nimport scala.annotation.nowarn\n\n@nowarn\nobject CirceExample {\n\n // #example\n import java.time.In"
},
{
"path": "docs/src/main/scala/JwtJson4sDoc.scala",
"chars": 1040,
"preview": "package pdi.jwt.docs\n\nimport scala.annotation.nowarn\n\n@nowarn\nobject JwtJson4sDoc {\n // #example\n import org.json4s.Js"
},
{
"path": "docs/src/main/scala/JwtPlayJsonDoc.scala",
"chars": 1505,
"preview": "package pdi.jwt.docs\n\nimport scala.annotation.nowarn\n\n@nowarn\nobject JwtPlayJsonDoc {\n // #example\n import java.time.C"
},
{
"path": "docs/src/main/scala/JwtPlayJwtSessionDoc.scala",
"chars": 3685,
"preview": "package pdi.jwt.docs\n\nimport scala.annotation.nowarn\n\n@nowarn\nobject JwtPlayJwtSessionDoc {\n // #example\n import java."
},
{
"path": "docs/src/main/scala/JwtUpickleDoc.scala",
"chars": 1822,
"preview": "package pdi.jwt.docs\n\nimport scala.annotation.nowarn\n\n@nowarn\nobject JwtUpickleDoc {\n // #example\n import java.time.In"
},
{
"path": "docs/src/main/scala/JwtZioDoc.scala",
"chars": 1916,
"preview": "package pdi.jwt.docs\n\nimport scala.annotation.nowarn\n\n@nowarn\nobject JwtZioDoc {\n // #example\n import java.time.Instan"
},
{
"path": "json/argonaut/src/main/scala/JwtArgonaut.scala",
"chars": 2722,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport argonaut.*\nimport argonaut.Argonaut.*\n\ntrait JwtArgonautParser[H, C] ext"
},
{
"path": "json/argonaut/src/test/scala/ArgonautFixture.scala",
"chars": 804,
"preview": "package pdi.jwt\n\nimport argonaut.*\n\ncase class JsonDataEntry(\n algo: JwtAlgorithm,\n header: String,\n headerClas"
},
{
"path": "json/argonaut/src/test/scala/JwtArgonautSpec.scala",
"chars": 256,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport argonaut.Json\n\nclass JwtArgonautSpec extends JwtJsonCommonSpec[Json] wit"
},
{
"path": "json/circe/jvm/src/test/scala/CirceFixture.scala",
"chars": 919,
"preview": "package pdi.jwt\n\nimport io.circe.*\nimport io.circe.jawn.{parse => jawnParse}\n\ncase class JsonDataEntry(\n algo: JwtAlg"
},
{
"path": "json/circe/jvm/src/test/scala/JwtCirceSpec.scala",
"chars": 204,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport io.circe.*\n\nclass JwtCirceSpec extends JwtJsonCommonSpec[Json] with Circ"
},
{
"path": "json/circe/shared/src/main/scala/JwtCirce.scala",
"chars": 2233,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport io.circe.*\nimport io.circe.jawn.{parse => jawnParse}\nimport io.circe.syn"
},
{
"path": "json/common/src/main/scala/JwtJsonCommon.scala",
"chars": 7112,
"preview": "package pdi.jwt\n\nimport java.security.{Key, PrivateKey, PublicKey}\nimport javax.crypto.SecretKey\nimport scala.util.Try\n\n"
},
{
"path": "json/common/src/test/scala/JsonCommonFixture.scala",
"chars": 363,
"preview": "package pdi.jwt\n\ntrait JsonDataEntryTrait[J] extends DataEntryBase {\n def headerJson: J\n}\n\ntrait JsonCommonFixture[J] e"
},
{
"path": "json/common/src/test/scala/JwtJsonCommonSpec.scala",
"chars": 5282,
"preview": "package pdi.jwt\n\nimport java.time.Clock\nimport scala.util.Success\n\nabstract class JwtJsonCommonSpec[J] extends munit.Fun"
},
{
"path": "json/json4s-common/src/main/scala/JwtJson4sCommon.scala",
"chars": 3472,
"preview": "package pdi.jwt\n\nimport org.json4s.*\nimport pdi.jwt.exceptions.{\n JwtNonNumberException,\n JwtNonStringException,\n Jwt"
},
{
"path": "json/json4s-common/src/test/scala/Json4sCommonFixture.scala",
"chars": 1196,
"preview": "package pdi.jwt\n\nimport org.json4s.*\n\ncase class JsonDataEntry(\n algo: JwtAlgorithm,\n header: String,\n headerCl"
},
{
"path": "json/json4s-jackson/src/main/scala/JwtJson4sImplicits.scala",
"chars": 298,
"preview": "package pdi.jwt\n\nimport org.json4s.JValue\n\ntrait JwtJson4sImplicits {\n implicit class RichJwtClaim(claim: JwtClaim) {\n "
},
{
"path": "json/json4s-jackson/src/main/scala/JwtJson4sJackson.scala",
"chars": 1074,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport org.json4s.*\nimport org.json4s.jackson.JsonMethods.*\nimport org.json4s.j"
},
{
"path": "json/json4s-jackson/src/test/scala/Json4sJacksonFixture.scala",
"chars": 193,
"preview": "package pdi.jwt\n\nimport org.json4s.*\nimport org.json4s.jackson.JsonMethods.*\n\ntrait Json4sJacksonFixture extends Json4sC"
},
{
"path": "json/json4s-jackson/src/test/scala/Json4sJacksonSpec.scala",
"chars": 1388,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport org.json4s.*\nimport org.json4s.JsonDSL.*\n\nclass JwtJson4sJacksonSpec ext"
},
{
"path": "json/json4s-native/src/main/scala/JwtJson4sImplicits.scala",
"chars": 298,
"preview": "package pdi.jwt\n\nimport org.json4s.JValue\n\ntrait JwtJson4sImplicits {\n implicit class RichJwtClaim(claim: JwtClaim) {\n "
},
{
"path": "json/json4s-native/src/main/scala/JwtJson4sNative.scala",
"chars": 1071,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport org.json4s.*\nimport org.json4s.native.JsonMethods.*\nimport org.json4s.na"
},
{
"path": "json/json4s-native/src/test/scala/Json4sNativeFixture.scala",
"chars": 191,
"preview": "package pdi.jwt\n\nimport org.json4s.*\nimport org.json4s.native.JsonMethods.*\n\ntrait Json4sNativeFixture extends Json4sCom"
},
{
"path": "json/json4s-native/src/test/scala/Json4sNativeSpec.scala",
"chars": 1370,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport org.json4s.*\nimport org.json4s.JsonDSL.*\n\nclass JwtJson4sNativeSpec exte"
},
{
"path": "json/play-json/src/main/scala/JwtJson.scala",
"chars": 2147,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport pdi.jwt.exceptions.{JwtNonNumberException, JwtNonStringException, JwtVal"
},
{
"path": "json/play-json/src/main/scala/JwtJsonImplicits.scala",
"chars": 3971,
"preview": "package pdi.jwt\n\nimport pdi.jwt.exceptions.{\n JwtNonNumberException,\n JwtNonStringException,\n JwtNonStringSetOrString"
},
{
"path": "json/play-json/src/test/scala/JsonFixture.scala",
"chars": 971,
"preview": "package pdi.jwt\n\nimport play.api.libs.json.JsObject\n\ncase class JsonDataEntry(\n algo: JwtAlgorithm,\n header: Strin"
},
{
"path": "json/play-json/src/test/scala/JwtJsonSpec.scala",
"chars": 3615,
"preview": "package pdi.jwt\n\nimport java.time.Clock\nimport scala.util.Failure\nimport scala.util.Success\n\nimport pdi.jwt.exceptions.J"
},
{
"path": "json/upickle/jvm/src/test/scala/JwtUpickleFixture.scala",
"chars": 838,
"preview": "package pdi.jwt\n\ncase class JsonDataEntry(\n algo: JwtAlgorithm,\n header: String,\n headerClass: JwtHeader,\n h"
},
{
"path": "json/upickle/jvm/src/test/scala/JwtUpickleSpec.scala",
"chars": 194,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nclass JwtUpickleSpec extends JwtJsonCommonSpec[ujson.Value] with JwtUpickleFixt"
},
{
"path": "json/upickle/shared/src/main/scala/JwtUpickle.scala",
"chars": 1133,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport upickle.default.*\n\n/** Implementation of `JwtCore` using `Js.Value` from"
},
{
"path": "json/upickle/shared/src/main/scala/JwtUpickleImplicits.scala",
"chars": 2082,
"preview": "package pdi.jwt\n\nimport pdi.jwt.exceptions.JwtNonStringSetOrStringException\nimport upickle.default.*\n\ntrait JwtUpickleIm"
},
{
"path": "json/zio-json/src/main/scala/JwtZIOJson.scala",
"chars": 2440,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport zio.json.*\nimport zio.json.ast.*\nimport zio.json.ast.JsonCursor.*\n\ntrait"
},
{
"path": "json/zio-json/src/test/scala/JwtZIOJsonSpec.scala",
"chars": 219,
"preview": "package pdi.jwt\n\nimport java.time.Clock\n\nimport zio.json.ast.Json\n\nclass JwtZIOJsonSpec extends JwtJsonCommonSpec[Json] "
},
{
"path": "json/zio-json/src/test/scala/ZIOJsonFixture.scala",
"chars": 908,
"preview": "package pdi.jwt\n\nimport zio.json._\nimport zio.json.ast.Json\n\ncase class JsonDataEntry(\n algo: JwtAlgorithm,\n heade"
},
{
"path": "play/src/main/scala/JwtPlayImplicits.scala",
"chars": 6022,
"preview": "package pdi.jwt\n\nimport java.time.Clock\nimport javax.inject.Inject\n\nimport play.api.Configuration\nimport play.api.libs.j"
},
{
"path": "play/src/main/scala/JwtSession.scala",
"chars": 8401,
"preview": "package pdi.jwt\n\nimport java.time.Clock\nimport javax.inject.Inject\nimport scala.concurrent.duration.Duration\n\nimport pdi"
},
{
"path": "play/src/test/scala/JwtResultSpec.scala",
"chars": 3103,
"preview": "package pdi.jwt\n\nimport org.apache.pekko.stream.Materializer\nimport play.api.Configuration\nimport play.api.inject.guice."
},
{
"path": "play/src/test/scala/JwtSessionAsymetricSpec.scala",
"chars": 6589,
"preview": "package pdi.jwt\nimport java.time.{Clock, Duration}\n\nimport org.apache.pekko.stream.Materializer\nimport play.api.Configur"
},
{
"path": "play/src/test/scala/JwtSessionCustomDifferentNameSpec.scala",
"chars": 6089,
"preview": "package pdi.jwt\n\nimport java.time.{Clock, Duration}\n\nimport org.apache.pekko.stream.Materializer\nimport play.api.Configu"
},
{
"path": "play/src/test/scala/JwtSessionCustomSpec.scala",
"chars": 6435,
"preview": "package pdi.jwt\n\nimport java.time.{Clock, Duration}\n\nimport org.apache.pekko.stream.Materializer\nimport play.api.Configu"
},
{
"path": "play/src/test/scala/JwtSessionSpec.scala",
"chars": 6391,
"preview": "package pdi.jwt\n\nimport scala.concurrent.duration.Duration\n\nimport org.apache.pekko.stream.Materializer\nimport play.api."
},
{
"path": "play/src/test/scala/PlayFixture.scala",
"chars": 2626,
"preview": "package pdi.jwt\n\nimport java.time.Clock\nimport scala.concurrent.Future\n\nimport org.apache.pekko.stream.Materializer\nimpo"
},
{
"path": "project/Libs.scala",
"chars": 2173,
"preview": "import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._\nimport sbt._\n\nobject Versions {\n val munit = \""
},
{
"path": "project/build.properties",
"chars": 19,
"preview": "sbt.version=1.12.9\n"
},
{
"path": "project/plugins.sbt",
"chars": 1028,
"preview": "// Documentation\n\naddSbtPlugin(\"com.github.sbt\" % \"sbt-site-paradox\" % \"1.7.0\")\n\naddSbtPlugin(\"com.github.sbt\" % \"sbt-un"
},
{
"path": "scripts/bump.sh",
"chars": 352,
"preview": "#!/bin/bash\n\nOLDVERSION=$1\nVERSION=$2\n\necho \"Stating publish process for JWT Scala from $OLDVERSION to $VERSION ...\"\n\nec"
},
{
"path": "scripts/clean.sh",
"chars": 40,
"preview": "find . -name \"*.tmp\" -exec rm -rf {} \\;\n"
},
{
"path": "scripts/pu.sh",
"chars": 263,
"preview": "#!/bin/bash\n\nVERSION=$1\n\necho \"Pushing to GitHub\"\ngit add .\ngit commit -m \"Release v$VERSION\"\ngit tag -a v$VERSION -m \"R"
}
]
About this extraction
This page contains the full source code of the pauldijou/jwt-scala GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 93 files (293.7 KB), approximately 85.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.