Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First Draft for JWT Best Practices Doc #1182

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ news.xml
package-lock.json
yarn.lock
venv
/.idea
/.kdev4/CheatSheetSeries.kdev4
/CheatSheetSeries.kdev4
158 changes: 103 additions & 55 deletions Index.md

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions assets/JWTCSA/0-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
search:
exclude: true
---

# Verification

## `None` Examples

### Java

<!-- --8<-- [start:java] -->
```
// HMAC key - Block serialization and storage as String in JVM memory
private transient byte[] keyHMAC = ...;

...

//Create a verification context for the token requesting
//explicitly the use of the HMAC-256 hashing algorithm
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)).build();

//Verify the token, if the verification fail then a exception is thrown
DecodedJWT decodedToken = verifier.verify(token);
```
<!-- --8<-- [end:java] -->

### Python:pyjwt

<!-- --8<-- [start:pyjwt] -->
```
try:
pyjwt.decode(encoded, key, algorithms=["HS256","ES256"])
except Exception as error:
# handle exception here
raise error
else:
continue
```
<!-- --8<-- [end:pyjwt] -->

### NodeJS:Jose

<!-- --8<-- [start:jose] -->
```
const { payload, protectedHeader } = await jose.jwtVerify(jwt, secret, {
algorithms: "HS256"
})

console.log(protectedHeader)
console.log(payload)
```
<!-- --8<-- [end:jose] -->
38 changes: 38 additions & 0 deletions assets/JWTCSA/1-jwks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
search:
exclude: true
---

# JWKS

## Generating a JWKS Key

### Python:jwcrypto

<!-- --8<-- [start:jwcrypto] -->

```python
import uuid
from jwcrypto import jwk, jwt

unique_kid = uuid.uuid4()

this_nodes_jwk = jwk.JWK.generate(kty="RSA", size=4096, kid=str(unique_kid))

public_key = this_nodes_jwk.export_public()

# Publish to Datastore in Multinode system

this_token = jwt.JWT(header={"alg": "RS256"},
claims={"name": "example_system"}
)

this_token.make_signed_token(this_nodes_jwk)

# For validation on jwt.io as an example
encoded_token = this_token.serialize()
public_key = this_node_jwk.export_public()
private_key = this_node_jwk.export_private()
```

<!-- --8<-- [end:jwcrypto] -->
Binary file added assets/JWTCSA/jwks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions assets/JWTCSA/jwks.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@startuml

() request as "Request to \n"".well-known/jwks.json"""

node api1 as "API 1"
node api2 as "API 2"
node api3 as "API 3"
node api4 as "API 4"
database redis as "Redis (Or other DB)"

api1 --> redis : Pub (S)
api2 --> redis : Pub (S)
api3 --> redis : Pub (S)
api4 --> redis : Pub (S)

request --> api4 : ""{"keys":[\n\t\t{"kid": "kidnode1"...},\n\t\t{"kid": "kidnode2"...},\n\t\t{"kid": "kidnode3"...},\n\t\t{"kid": "kidnode4"...}\n]}""
api4 <-- redis : Read all Public Keys

@enduml
227 changes: 227 additions & 0 deletions cheatsheets/JWT_Cheat_Sheet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# JWT Cheat Sheet

## Introduction

Many applications use **JSON Web Tokens** (JWT) to allow the client to indicate its identity for further exchange after
authentication and to securely transmit data.

From [JWT.IO](https://jwt.io/introduction):

> JSON Web Token (JWT) is an open standard ([RFC 7519](https://tools.ietf.org/html/rfc7519)) that defines a compact and
> self-contained way for securely transmitting information between parties as a JSON object. This information can be
> verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the **HMAC** algorithm)
> or a public/private key pair using **RSA** or **ECDSA**.

JSON Web Token is used to carry information related to the identity and characteristics (claims) of a client. This
information should signed by the server in order for it to detect whether it was tampered with after sending it to the
client. This will prevent an attacker from changing the identity or any characteristics (for example, changing the role
from simple user to admin or change the client login).

This token is created during authentication (is provided in case of successful authentication) and is verified by the
server before any processing. It is used by an application to allow a client to present a token representing the user's
"identity card" to the server and allow the server to verify the validity and integrity of the token in a secure way,
all of this in a stateless and portable approach (portable in the way that client and server technologies can be
different including also the transport channel even if HTTP is the most often used).


### Verification

Your application, absolutely should verify that the tokens it recieves are valid. You can do this with a symmetric scheme, where
a secret between all parties out of band. For more on how to manage secrets please refer to the [secrets management cheat sheet](Secrets_Management_Cheat_Sheet.md). Or you can use an asymmetric key to signe your tokens. you have the option of signing your
tokens with a centrally signed certificate but, a system [Json Web Key Sets](https://datatracker.ietf.org/doc/html/rfc7517) a
reasonably well supported system that integrates well with most JWT libraries and allows you the benefits of asymmetric encryption
while being able to take advantage of modern TLS solutions that prevent your application from needing access to the same certificate
that's encrypting traffic in transit. There are concerns on how to do this with multi-node systems; but in general use of a data
store like redis can be used to keep track of the largely ephemeral keys you would create.

![JWKS Multi-Node](../assets/JWTCSA/jwks.png)

### JWKS Startup Examples

=== "Python Example (`jwcrypto`)"
--8<-- "JWTCSA/1-jwks.md:jwcrypto"

## Token Structure

Token structure example taken from [JWT.IO](https://jwt.io/#debugger):

`[Base64(HEADER)].[Base64(PAYLOAD)].[Base64(SIGNATURE)]`

```text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
```

Chunk 1: **Header**

```json
{
"alg": "HS256",
"typ": "JWT"
}
```

Chunk 2: **Payload**

```json
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
```

Chunk 3: **Signature**

```javascript
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), KEY )
```

And example of this jwt can be found [here](https://jwt.io/) on JWT.io signed with a symmetric key called `test` (obviously
weak).

## Objective

This cheatsheet provides tips to prevent common security issues when using JSON Web Tokens (JWT).

## Consideration about Using JWT

Even if a JWT token is "easy" to use and allow to expose services (mostly REST style) in a stateless way, it's not the
solution that fits for all applications because it comes with some caveats, like for example the question of the
storage of the token (tackled in this cheatsheet) and others.

If your application does not need to be fully stateless, you can consider using traditional session system provided by
all web frameworks and follow the advice from the dedicated [session management cheat sheet](Session_Management_Cheat_Sheet.md).
Especially for authenticating users, the use of tools like Oauth2 or OIDC backed by SAML has become an industry best
practice, as it allows one SAML implementation/Server to integrate a large number of authentication best practices.
Some of those tools use JWT's under the hood.

## Issues

### None Hashing Algorithm

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None is not an "hashing" algorithm. It is an "authentication algorithm".


#### Symptom

This attack, described [here](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/), occurs
when an attacker alters the token and changes the hashing algorithm to indicate, through the *none* keyword, that the
integrity of the token has already been verified. As explained in the link above *some libraries treated tokens signed
with the none algorithm as a valid token with a verified signature*, so an attacker can alter the token claims and
the modified token will still be trusted by the application.

#### How to Prevent

First, use a JWT library that is not exposed to this vulnerability, or use a library that allows you to specify which
algorithms are considered acceptable and do not include (or explicitly exclude) the `none` algorithm. For example in
[`pyjwt.decode`](https://pyjwt.readthedocs.io/en/stable/api.html#jwt.decode) (python) you can explicitly state which
signature(s) is acceptable. Most libraries have fixed this issue, but as a rule, if you're using a symmetric key you
should explicitly set which libraries are allowed.

Second (and not shown) is to use an asymmetric key to sign your jwts. In general, it should be seen as a best practice
to use an asymetrc key to sign jwts rather than a symmetric key.

#### Implementation Example Symmetric Key

=== "Java Example"
--8<-- "JWTCSA/0-verification.md:java"

=== "Python Example (`pyjwt`)"
--8<-- "JWTCSA/0-verification.md:pyjwt"

=== "JavaScript Example (`jose`)"
--8<-- "JWTCSA/0-verification.md:jose"

### Token Sidejacking

#### Symptom

This attack occurs when a token has been intercepted/stolen by an attacker and they use it to gain access to the system
using targeted user identity. In part, this attack really can't be mitigated "in platform" as JWTs are generically stateless
by design.

#### How to Prevent

If you're using a jwt as a session token in the context of a webapp you should follow the
[Session Management Cheat Sheet](Session Management Cheat Sheet). But a more generic fix might be to use JWTs in conjunction
with OIDC/OAUTH2 and utilize the replay protections those meta libraries provide. However, if you're looking to implement
a similar solution you can utilize a `nonce`.

Additionally, depending on your application and security designs; it might make sense to have your JWT tokens valid for
short periods of time; especially in a "service to service" context where the service can regenerate a token at any time.
There may be a performance concern associated with signing a jwt token on each request; but modern CPUs should be able to
generate tokens without much consideration for all but the most abnormal workloads. This wouldn't eliminate the problem
of token sidejacking, but it would limit the amount of time a successful sidejacking could be utilized.

/// details | Stateful Considerations
type: warning

Using a nonce in a multi-node environment will require state to be shared between each node. Generally this is done with
a database table or a tool like [redis](https://redis.io/). If you're application is designed to be stateless, or if it's
a microservices type architecture this approach may not be ideal, or might be somewhat difficult to utilize.
///

So in this example you're server would return a jwt with the `nonce` claim (along with the normal `nbf`,
`iat`, `exp` time based claims).

/// details | ToDo: Nonce Recommendation
type: ToDo

Find the "right way" to implement a nonce. Finding conflicting information about how it should work (just server verify,
just client verify, both verify); protections for simultaneous API calls etc...
///

### No Built-In Token Revocation by the User

#### Symptom

This problem is inherent to JWT because a token only becomes invalid when it expires. The
[`jti`](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7) component of the specification is supposed to allow for the
ability for token revocation on the server side. Additionally, if a public certificate is used and the library supports it,
a signing certificate should be able to be revoked and that should invalidate the jwt token(s) signed with it.

#### How to Prevent

/// details | Stateful Considerations
type: warning

Using a `jti` in a multi-node environment will require token IDs to be shared between each node. Generally this is done
with a database table or a tool like [redis](https://redis.io/). If you're application is designed to be stateless, or
if it's a microservices type architecture this approach may not be ideal, or might be somewhat difficult to utilize.
///

Use a `jti`.

Use a publicly signed certificate to sign jwts and check for certificate revocation on validation.

### Validate Common Claims

So you have a REST or similar api, it accepts JWTs. How to you validate both A, that the jwt is who it says it's from and B, that
the things in the JWT are nominally "correct". There's a couple of options..

We'd like to see the following claims be included in our standard:

| Claim | Name | Example |
|:------|:-----|:-------------------|
| `iss` | Issuer Claim | `requestingapplication.example.com` |
| `sub` | Subject Claim | * `[email protected]`<br>* `system`<br>* `[email protected]`|
| `aud` | Audience Claim | `targetapplication.example.com` |
| `exp` | Expiration Time Claim | unix timestamp |
| `nbf` | Not Before Claim | unix timestamp |
| `iat` | Issued At Claim | unix timestamp |

### Implement Buisness Logic post Validation

TODO

Discuss a moving target towards OIDC claims.

## Further Reading

- [{JWT}.{Attack}.Playbook](https://github.com/ticarpi/jwt_tool/wiki) - A project documents the known attacks and potential security vulnerabilities and misconfigurations of JSON Web Tokens.
- [JWT Best Practices Internet Draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-jwt-bcp/)
- [JWT.io Discussion Forum](https://community.auth0.com/c/jwt/8) (Hosted by [Auth0](https://auth0.com/))
- [JWT Overview on Wikipedia](https://en.wikipedia.org/wiki/JSON_Web_Token)
- [OpenID](https://openid.net/) - A larger framework to provide ways to connect JWT based authentication with other authentication
systems.
- [JSON Web Encryption (JWE) RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516) - A JWT like specification that includes
payload encryption.
13 changes: 10 additions & 3 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ copyright: ©Copyright <script>document.write(new Date().getFullYear())</script>

#Config
docs_dir: cheatsheets/
google_analytics:
- !!python/object/apply:os.getenv ["WORKFLOW_GOOGLE_ANALYTICS_KEY", "none"]
- auto
extra:
analytics:
provider: google
property: !!python/object/apply:os.getenv ["WORKFLOW_GOOGLE_ANALYTICS_KEY", "none"]
use_directory_urls: false
plugins:
- search:
Expand Down Expand Up @@ -56,6 +57,12 @@ markdown_extensions:
- pymdownx.highlight
- pymdownx.superfences # Required by Pygments
- pymdownx.inlinehilite
- pymdownx.blocks.details
- pymdownx.snippets:
base_path:
../assets/
- pymdownx.tabbed:
alternate_style: true
- pymdownx.emoji:
emoji_index: !!python/name:pymdownx.emoji.twemoji
emoji_generator: !!python/name:pymdownx.emoji.to_svg
Expand Down
Loading