diff --git a/docs/custom_big_font.css b/docs/custom_big_font.css new file mode 100644 index 0000000..02007ca --- /dev/null +++ b/docs/custom_big_font.css @@ -0,0 +1,11 @@ + + .highlight { + background-color: yellow; + } + .highlight-line { + background-color: yellow; + display: block; + //padding: 2px 4px; +} + +.src-python { font-size: 140%; } diff --git a/docs/slides_big_font.html b/docs/slides_big_font.html new file mode 100644 index 0000000..fd0f58a --- /dev/null +++ b/docs/slides_big_font.html @@ -0,0 +1,4811 @@ + + + + +Tips, Tricks, and Reasons for JSON Web Tokens (JWTs) + + + + + + + + + + + +
+
+

Tips, Tricks, and Reasons for JSON Web Tokens (JWTs)

+

Emin Martinian

+
+ +
+
+

JWT: JSON Web Token

+

+Used for authentication/authorization such as: +

+ + + + + + +
+
+
+
+

Why JWTs?

+

+Imagine app with many features + servers + engineers: +

+ +
    +
  • Load balance, payments, profiles, PII, DB
  • +
  • Local/remote/international workers + consultants
  • +
  • How to manage security? +
      +
    • Can't give everyone access to sensitive info
    • + +
  • + +
+
+
+
+
+

Authentication vs Validation

+ + + +
+

+

+
+
+
+
+
+

JWT: Authentication Request

+ + + + +
+

+

+
+
+
+
+
+

JWT: Authentication Response

+ +
+

+

+
+
+
+
+
+

JWT: Application Request

+ + + +
+

+

+
+
+
+
+
+

Separate Auth From Validation

+

+Auth Server has secrets; needs security + maintenance +

+ +
    +
  • App Server(s) needs public keys; low security
  • +
  • Easy to deploy App Server(s); e.g., serverless
  • +
  • Lower security for App Server(s), logs, debug, etc.
  • + +
+
+
+
+
+

What do JWTs look like?

+

+Base64 encoded header.payload.signature: +

+ +
+ +
HEADER:     { "alg": "EdDSA", "typ": "JWT" }
+
+
+ +
+ +
PAYLOAD:    {"sub": "a", "name": "arbitrary data", "iat": 1 }
+
+
+ +
+ +
SIGNATURE:  SU6aXJ0YbH7Vg1jROpQfvnhn98Rt9zBeS7-c5O9jH-L
+            L5mQqMMFq61eZjf0tLLqExm-dckRUNa3-qT7R2SKmCw
+            
+
+
+ +
+ +
ENCODED JWT:   eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9
+               .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9
+               .SU6aXJ0YbH7Vg1jROpQfvnhn98Rt9zBeS7-c5O9jH-L
+                L5mQqMMFq61eZjf0tLLqExm-dckRUNa3-qT7R2SKmCw
+
+
+ + +

+Signed using EdDSA with secret key: +

+ +
+ +
MC4CAQAwBQYDK2VwBCIEIC+D6rD2YbXtV0ccR3smoR0ynhVuyyqvplFLbQWDdAtn
+
+
+ + + +
+
+
+
+

Main JWT Fields

+
    +
  • sub: Subject (username, email, etc.)
  • +
  • iat: Issued at (useful for checking freshness)
  • +
  • exp: Expiry (useful for managing life-cycle )
  • +
  • nbf: Not before (useful for managing life-cycle )
  • + +
+
+
+
+
+

Python/Flask Example

+
    +
  • Easy to verify/decode using libraries (e.g., pyjwt) +
      +
    • can compose checks using decorators:
    • + +
  • + +
+ +
+
+ +
@app.route('/support/urgent')  # built-in flask decorator
+@requires_jwt                  # custom decorator to validate JWT
+@jwt_claims(['paid_support'])  # ensures token is for premium user
+@jwt_iat(datetime.timedelta(hours=24))  # ensure recent token
+def support_urgent():
+    ... # process ending support request
+
+
+
+
+
+
+
+

Python/Flask Example

+
    +
  • Easy to verify/decode using libraries (e.g., pyjwt) +
      +
    • can compose checks using decorators:
    • + +
  • + +
+ + + +
+
+ +
@app.route('/support/urgent')  # built-in flask decorator
+@requires_jwt                  # custom decorator to validate JWT
+@jwt_claims(['paid_support'])  # ensures token is for premium user
+@jwt_iat(datetime.timedelta(hours=24))  # ensure recent token
+def support_urgent():
+    ... # process ending support request
+
+
+
+
+
+
+
+

Python/Flask Example

+
    +
  • Easy to verify/decode using libraries (e.g., pyjwt) +
      +
    • can compose checks using decorators:
    • + +
  • + +
+ + + +
+
+ +
@app.route('/support/urgent')  # built-in flask decorator
+@requires_jwt                  # custom decorator to validate JWT
+@jwt_claims(['paid_support'])  # ensures token is for premium user
+@jwt_iat(datetime.timedelta(hours=24))  # ensure recent token
+def support_urgent():
+    ... # process ending support request
+
+
+
+
+
+
+
+

Python/Flask Example

+
    +
  • Easy to verify/decode using libraries (e.g., pyjwt) +
      +
    • can compose checks using decorators:
    • + +
  • + +
+ + + +
+
+ +
@app.route('/support/urgent')  # built-in flask decorator
+@requires_jwt                  # custom decorator to validate JWT
+@jwt_claims(['paid_support'])  # ensures token is for premium user
+@jwt_iat(datetime.timedelta(hours=24))  # ensure recent token
+def support_urgent():
+    ... # process ending support request
+
+
+
+
+
+
+
+

Python/Flask Example

+
    +
  • Easy to verify/decode using libraries (e.g., pyjwt) +
      +
    • can compose checks using decorators:
    • + +
  • + +
+ + + +
+
+ +
@app.route('/support/urgent')  # built-in flask decorator
+@requires_jwt                  # custom decorator to validate JWT
+@jwt_claims(['paid_support'])  # ensures token is for premium user
+@jwt_iat(datetime.timedelta(hours=24))  # ensure recent token
+def support_urgent():
+    ... # process ending support request
+
+
+
+
+
+
+
+

Example of @requires_jwt

+
+
+ +
def requires_jwt(func):
+    def decorated(*args, **kwargs):        
+        token = request.headers.get("Authorization").split(" ")[1]
+        if not token:
+            return 'missing token', 401  # if no token return error   
+        try:
+            g.decoded_jwt = jwt.decode(token, algorithms=['EdDSA'],
+                key=current_app.config['JWT_KEY'])  # public key
+            return func(*args, **kwargs)
+        except Exception as problem:
+            return f'{problem=}', 401 # return 401 or other error code
+    return decorated
+
+
+
+
+
+
+
+

Example of @jwt_claims

+
+
+ +
def jwt_claims(claims_list: typing.Sequence[str]):
+    def make_decorator(func):
+        @wraps(func)
+        def decorated(*args, **kwargs):        
+            missing = [c for c in claims_list
+                       if not g.decoded_jwt.get(c)]
+            if missing:
+                return f'Missing claims: {missing}', 401
+            return func(*args, **kwargs)
+        return decorated
+    return make_decorator
+
+
+
+
+
+
+
+

Example Use Case: Proxy

+
    +
  • Auth Server grants JWT letting Alice to act for Bob
  • +
  • claims: {"sub": "Alice", "proxy": "Bob"}
  • +
  • Alice sends request to act for Bob
  • + +
+
+
+
+
+

Example Use Case: Proxy

+
    +
  • Auth Server grants JWT letting Alice to act for Bob
  • +
  • claims: {"sub": "Alice", "proxy": "Bob"}
  • +
  • Alice sends request to act for Bob
  • + +
+ +
+
+ +
@APP.route("/issue")
+@requires_jwt
+def issue():
+    "Example route to create an issue."
+    user = g.decoded_jwt.get('proxy', g.decoded_jwt.get('sub'))
+    msg = f'Created issue assigned to {user}.'
+    # ... Create the actual issue here
+
+
+
+    return msg
+
+
+
+
+
+
+
+

Example Use Case: Proxy

+
    +
  • Auth Server grants JWT letting Alice to act for Bob
  • +
  • claims: {"sub": "Alice", "proxy": "Bob"}
  • +
  • Alice sends request to act for Bob
  • + +
+ +
+
+ +
@APP.route("/issue")
+@requires_jwt
+def issue():
+    "Example route to create an issue."
+    user = g.decoded_jwt.get('proxy', g.decoded_jwt.get('sub'))
+    msg = f'Created issue assigned to {user}.'
+    # ... Create the actual issue here
+    real_user = g.decoded_jwt['sub']
+    if real_user != user:
+        msg += f'\n{real_user} acted on behalf of {user}'
+    return msg
+
+
+
+
+
+
+
+

Caveats

+
    +
  • Beware using header fields to check signature +
      +
    • don't trust alg field or limit possibilities +
        +
      • e.g., algorithms=['EdDSA']
      • + +
    • +
    • be careful with kid, jku, jwk, etc.
    • + +
  • +
  • Don't simulate sessions with JWTs +
      +
    • Use access/refresh tokens to solve logout/revocation
    • + +
  • + +
+
+
+
+
+

Example JKU Header Attack

+
    +
  • Header can provide URL for key (useful): +
      +
    • {alg: "EdDSA", jku: "https://good.com/pk.json"}
    • + +
  • +
  • Attacker can replace JKU with their own key: +
      +
    • {alg: "EdDSA", jku: "https://bad.com/pk.json"}
    • + +
  • +
  • Don't trust header (validate against whitelist)
  • + +
+
+
+
+
+

Example ALG Header Attack

+
    +
  • Header can provide URL for key (useful): +
      +
    • {alg: "EdDSA", jku: "https://good.com/pk.json"}
    • + +
  • +
  • Attacker can replace ALG with symmetric version: +
      +
    • {alg: "HS256", jku: "https://good.com/pk.json"}
    • + +
  • +
  • Don't trust header (validate against whitelist)
  • + +
+
+
+
+
+

Revocation via Access/Refresh

+
    +
  • Problem: Can't cancel or logout a JWT
  • +
  • Solution: Refresh/Access token +
      +
    • "refresh token" with long expiry
    • +
    • used to get access token w/o credential check
    • +
    • "access token" with short expiry
    • +
    • can be used to access services
    • + +
  • + +
+ + + +
+
+
+
+

Get Refresh Token

+ +
+

+

+
+
+
+
+
+

Get Access Token

+ +
+

+

+
+
+
+
+
+

Use Access Token

+ +
+

+

+
+
+
+
+
+

Revocation

+ +
+

+

+
+
+
+
+
+

Separate validation from parsing

+ + +
    +
  • Can use middleware to verify signature
  • +
  • +e.g., NGINX can verify before passing to app server +

    + +
    +

    +

    +
  • +
  • See example.conf in nginx directory on github.com/aocks/ox_jwt
  • + +
+
+
+
+
+

Summary and next steps

+ + + +
+
+
+
+ + + + + + + + diff --git a/docs/slides_big_font.org b/docs/slides_big_font.org new file mode 100644 index 0000000..ba2d2d2 --- /dev/null +++ b/docs/slides_big_font.org @@ -0,0 +1,1086 @@ + + + +#+COMMENT: using timestamp:nil suppresses "created at" in title +#+COMMENT: using num:nil prevents slide titles being numbered +#+OPTIONS: timestamp:nil num:nil toc:nil ^:{} reveal_single_file:t + +#+COMMENT: For generating presentation slides, we do not want to +#+COMMENT: export CodeTool slides. But for archived slides, we +#+COMMENT: may want to remove that from EXCLUDE_TAGS to include it. +#+EXCLUDE_TAGS: noexport CodeTool + +#+REVEAL_REVEAL_JS_VERSION: 4 +#+COMMENT: #+REVEAL_ROOT: https://cdn.jsdelivr.net/npm/reveal.js@4.0.0/ +#+REVEAL_ROOT: reveal +#+REVEAL_PLUGINS: (notes) +#+REVEAL_THEME: solarized +#+REVEAL_INIT_OPTIONS: fragments:true, transition:'fade', margin: 0.0001, width:'100%' + +#+REVEAL_EXTRA_SCRIPT_SRC: custom_highlight.js +#+REVEAL_EXTRA_CSS: custom_big_font.css + +#+COMMENT: Use `s` to engage speaker mode + +#+TITLE: Tips, Tricks, and Reasons for JSON Web Tokens (JWTs) +#+AUTHOR: Emin Martinian + +#+COMMENT: THe following setup is needed for the highlighting +#+COMMENT: in the custom_highlight.js file to work. See comments +#+COMMENT: there and uses of `highlightSlide` below for usage. +#+BEGIN_EXPORT html + +#+END_EXPORT + + +* Macro Example :noexport: + +This example defines a code block called =example-code-block= and then +uses noweb syntax to reference it in later slides and the next with +slight tweaks. + +#+name: example-code-block +#+BEGIN_SRC python :exports none :results value +@app.route('/support/urgent') # built-in flask decorator +@requires_jwt # custom decorator to validate JWT +@jwt_claims(['paid_support']) # ensures token is for premium user +@jwt_iat(datetime.timedelta(hours=24)) # ensure recent token +def support_urgent(): + ... # process ending support request +#+END_SRC + +* Macro Example :noexport: + +- First reference + +#+BEGIN_SRC python :noweb yes :exports code +# here it is +<> +#+END_SRC + +* Reusing macro :noexport: + :PROPERTIES: + :CUSTOM_ID: reusing-macro + :END: + +- Second reference with highlight + +#+COMMENT: setup highlighting +#+html: + + +#+BEGIN_SRC python :noweb yes :exports code +# another version +<> +#+END_SRC + + + +* Fancy Fragment :noexport: + +#+MACRO: fb @@html:
@@ +#+MACRO: fe @@html:
@@ + +{{{fb(1)}}} + - Point A +#+begin_src +Code for point A +#+end_src +{{{fe}}} +{{{fb(2)}}} + - Point B +#+begin_src +Code for point B +#+end_src +{{{fe}}} +{{{fb(3)}}} + - Point C +#+begin_src bash +Code for point C +#+end_src +{{{fe}}} + +* Fancy Table :noexport: + +| @@html:
A
@@ | @@html:
B
@@ | +|--------------------------------------------------------------------+---| +| @@html:
C
@@ | @@html:
D
@@ | + +* Embedded graphviz :noexport: + +Could use graphviz online at https://rsms.me/graphviz/ to visualize +the diagram as you are create it or below to generate it using +javascript. Harder to manage width in org-reveal, so we just stick to +locally generated images. + +#+begin_export html + + +
+#+END_export + +* Mermaid.js :noexport: + +Could also use mermaid.js (and [[https://www.mermaidchart.com/play][online editor]]) to create + embed +diagram. Pretty cool, but more limited layout options than graphviz. + +* JWT: JSON Web Token + +Used for authentication/authorization such as: + + +- front-end client to access back-end or API server +- compact, [[https://datatracker.ietf.org/doc/html/rfc7519][standardized]], secured, customizable +- "state-less" alternative to cookies/sessions +- slides/examples: https://github.com/aocks/ox_jwt + + +#+BEGIN_NOTES +- standard: [[https://datatracker.ietf.org/doc/html/rfc7519][RFC 7519]] +#+END_NOTES + +* Why JWTs? + +Imagine app with many features + servers + engineers: + +#+ATTR_REVEAL: :frag (appear appear appear) +- Load balance, payments, profiles, PII, DB +- Local/remote/international workers + consultants +- How to manage security? + - Can't give everyone access to sensitive info + + +* Authentication vs Validation +#+BEGIN_NOTES +- Auth server manages passwords, takes credit cards, etc. +- Must be secure and in sync; hard to load balance +- Cannot let just any employee have access +#+END_NOTES + +#+name: jwt-auth-vs-app-start +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-start.jpg :eval never-export +digraph auth_system { + node [fontsize="30"]; + edge [fontsize="30"]; + + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server\n", shape=box]; + hidden [style=invis]; + AppServer [label="App Server(s)", shape=box]; + } + subgraph bottom { + rank=same; + Client [label="Client", shape=box]; + } + + AuthServer -> hidden [color=none,xlabel="\nAuthentication requires\nsecret keys (high security)",fontsize="20"]; + AppServer -> hidden [color=none,xlabel="\nValidation can use\n public key (less security)",fontsize="20"]; + + // Define connections + AuthServer -> Client [label="JWT", constraint=false, splines=ortho, style=invis]; + Client -> AuthServer [label="Authenticate\n(e.g., login\nor OAuth or\ncredit card)", constraint=false, splines=ortho,style=invis]; + Client -> AppServer [label="Request Service\nusing JWT", constraint=false, splines=ortho,style=invis]; + + // Define hidden edges to force layout + AuthServer -> hidden [style=invis]; + hidden -> AppServer [style=invis]; + hidden -> Client [style=invis]; +} + +#+end_src + +#+ATTR_HTML: :width 90% +#+RESULTS: jwt-auth-vs-app-start +[[file:images/jwt-auth-vs-app-start.jpg]] + +* JWT: Authentication Request + +#+BEGIN_NOTES +Managing the authentication server is more complicated. +- Can't allow just anyone to access/maintain/deploy (has secrets) +- Must maintain state (e.g., current user password) so hard to load balance +#+END_NOTES + + +#+name: jwt-auth-vs-app-auth +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-auth.jpg :eval never-export + +digraph auth_system { + node [fontsize="30"]; + edge [fontsize="30"]; + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server(s)", shape=box]; + } + + subgraph bottom { + rank=same; + Client [label="Client", shape=box]; + } + + // Define connections + AuthServer -> Client [label="JWT", constraint=false, splines=ortho, style=invis]; + Client -> AuthServer [label="Authenticate\n(e.g., login\nor OAuth or\ncredit card)", constraint=false, splines=ortho]; + Client -> AppServer [label="Request Service\nusing JWT", constraint=false, splines=ortho,style=invis]; + + // Define hidden edges to force layout + AuthServer -> hidden [style=invis]; + hidden -> AppServer [style=invis]; + hidden -> Client [style=invis]; +} + +#+end_src + +#+ATTR_HTML: :width 90% +#+RESULTS: jwt-auth-vs-app-auth +[[file:images/jwt-auth-vs-app-auth.jpg]] + + +* JWT: Authentication Response + +#+name: jwt-auth-vs-app-auth-response +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-auth-response.jpg :eval never-export + +digraph auth_system { + node [fontsize="30"]; + edge [fontsize="30"]; + + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server(s)", shape=box]; + } + + subgraph bottom { + rank=same; + Client [label="Client", shape=box]; + } + + AppServer -> hidden [style=invis]; + + // Define connections + Client -> AuthServer [label=" JWT contains:\nheader\nclaims\nsignature", color=none,constraint=false, splines=ortho]; + AuthServer -> Client [label="JWT", constraint=false, splines=ortho]; +// Client -> AuthServer [label="Authenticate\n(e.g., login\nor OAuth)", constraint=false, splines=ortho,style=invis]; + Client -> AppServer [label="Request Service\nusing JWT", constraint=false, splines=ortho,style=invis]; + + // Define hidden edges to force layout + AuthServer -> hidden [style=invis]; + hidden -> AppServer [style=invis]; + hidden -> Client [style=invis]; +} + +#+end_src + +#+ATTR_HTML: :width 90% +#+RESULTS: jwt-auth-vs-app-auth-response +[[file:images/jwt-auth-vs-app-auth-response.jpg]] + + +* JWT: Application Request + +#+BEGIN_NOTES +- App Server validates JWT with public key (lower security needs) +- No DB/state/sync/update; can be serverless +- Checks JWT for rights + provides service + + +#+END_NOTES + +#+name: jwt-auth-vs-app-request-app +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-request-app.jpg :eval never-export + +digraph auth_system { + node [fontsize="30"]; + edge [fontsize="30"]; + + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server(s)", shape=box]; + } + + subgraph bottom { + rank=same; + Client [label="Client", shape=box]; + } + + // Define connections + Client -> AuthServer [label="App Server\nvalidates\nJWT with\npublic key", color=none,constraint=false, splines=ortho]; +// Client -> AuthServer [label="Authenticate\n(e.g., login\nor OAuth)", constraint=false, splines=ortho,style=invis]; + Client -> AppServer [label="Send JWT to\nRequest Service", constraint=false, splines=ortho]; + + // Define hidden edges to force layout + AuthServer -> hidden [style=invis]; + hidden -> AppServer [style=invis]; + hidden -> Client [style=invis]; +} + +#+end_src + +#+ATTR_HTML: :width 90% +#+RESULTS: jwt-auth-vs-app-request-app +[[file:images/jwt-auth-vs-app-request-app.jpg]] + + + + + +* Separate Auth From Validation + +Auth Server has **secrets**; needs **security** + maintenance + +#+ATTR_REVEAL: :frag (appear appear) +- App Server(s) needs public keys; low security +- Easy to deploy App Server(s); e.g., serverless +- Lower security for App Server(s), logs, debug, etc. + + + +* What do JWTs look like? + +Base64 encoded header.payload.signature: + +#+ATTR_REVEAL: :frag appear :frag_idx 1 +#+BEGIN_src shell +HEADER: { "alg": "EdDSA", "typ": "JWT" } +#+END_src + +#+ATTR_REVEAL: :frag appear :frag_idx 2 +#+BEGIN_src shell +PAYLOAD: {"sub": "a", "name": "arbitrary data", "iat": 1 } +#+END_src + +#+ATTR_REVEAL: :frag appear :frag_idx 3 +#+BEGIN_src shell +SIGNATURE: SU6aXJ0YbH7Vg1jROpQfvnhn98Rt9zBeS7-c5O9jH-L + L5mQqMMFq61eZjf0tLLqExm-dckRUNa3-qT7R2SKmCw + +#+END_src + +#+ATTR_REVEAL: :frag appear :frag_idx 4 +#+BEGIN_src shell +ENCODED JWT: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 + .SU6aXJ0YbH7Vg1jROpQfvnhn98Rt9zBeS7-c5O9jH-L + L5mQqMMFq61eZjf0tLLqExm-dckRUNa3-qT7R2SKmCw +#+END_src + + +#+ATTR_REVEAL: :frag appear :frag_idx 5 +Signed using EdDSA with secret key: + +#+ATTR_REVEAL: :frag appear :frag_idx 5 +#+BEGIN_src python +MC4CAQAwBQYDK2VwBCIEIC+D6rD2YbXtV0ccR3smoR0ynhVuyyqvplFLbQWDdAtn +#+END_src + + +#+BEGIN_NOTES +- Use https://jwt.io/#debugger-io to verify/validate/decode +- You can put arbitrary data in the payload: + - indicate username, roles, rights, restrictions, payments +#+END_NOTES + +#+COMMENT: May want to set sub-nodes (with **) below to :noexport: +#+COMMENT: to simplify navigation when doing presentation + +** Secret Key :CodeTool: + +#+BEGIN_NOTES +- We use EdDSA because it is secure, short, and deterministic. +- You could use ESA256, but beware that uses a nonce and is non-deterministic. +#+END_NOTES + +#+name: create-keys +#+BEGIN_SRC python :session jwt_example :exports code :python ~/code/ox_jwt/venv_ox_jwt/bin/python3 :eval never-export +import base64, jwt # pip install 'pyjwt[crypto]' +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.backends import default_backend + +secret_key = base64.b64encode( # How to generate new key + ed25519.Ed25519PrivateKey.generate().private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption()) +).decode('utf8') + +secret_key = ( # We hard code secret key so you can verify results + 'MC4CAQAwBQYDK2VwBCIEIC+D6rD2YbXtV0ccR3smoR0ynhVuyyqvplFLbQWDdAtn' +) +#+END_SRC + +#+RESULTS: create-keys + + +** Public Key :CodeTool: + +#+name: get-public-key +#+BEGIN_SRC python :session jwt_example :exports both :results output :eval never-export +sk = serialization.load_der_private_key( # de-serialize encoded key + base64.b64decode(secret_key),backend=default_backend(), + password=None) + +pk = sk.public_key() +public_key = pk.public_bytes( # serialize + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo +).decode('utf8') + +print(public_key) +#+END_SRC + + +#+RESULTS: get-public-key +: -----BEGIN PUBLIC KEY----- +: MCowBQYDK2VwAyEAUVLjZWAVK5ZE1ewI5QBdr0Nig1Qkx3kl5zHIADvw0M8= +: -----END PUBLIC KEY----- + + + +** Encoding Example JWT :CodeTool: + +#+NAME: encoded-jwt +#+BEGIN_SRC python :session jwt_example :exports both :results output :eval never-export +import textwrap # just for display + +example_jwt = jwt.encode( + headers={'typ':'JWT', 'alg':'EdDSA'}, + payload={'sub': 'a', 'name': 'b', 'iat': 1}, + key=sk) # this is the JWT that would be used +print(textwrap.indent(textwrap.fill( # format for + '\n.'.join(example_jwt.split('.')), # nice display + width=44, replace_whitespace=False), prefix=' ')) +#+END_SRC + +Encoded JWT: +#+RESULTS: encoded-jwt +: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9 +: .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 +: .SU6aXJ0YbH7Vg1jROpQfvnhn98Rt9zBeS7-c5O9jH- +: LL5mQqMMFq61eZjf0tLLqExm-dckRUNa3-qT7R2SKmCw + +** Decoding Example JWT :CodeTool: + +#+NAME: decoded-jwt +#+BEGIN_SRC python :session jwt_example :exports both :results output :eval never-export +decoded_jwt = jwt.decode(example_jwt, algorithms=['EdDSA'], key=pk) +print(decoded_jwt) +#+END_SRC + +Decoded Payload from JWT: +#+RESULTS: decoded-jwt +: {'sub': 'a', 'name': 'b', 'iat': 1} + +* Main JWT Fields + +- *sub*: Subject (username, email, etc.) +- *iat*: Issued at (useful for checking freshness) +- *exp*: Expiry (useful for managing life-cycle ) +- *nbf*: Not before (useful for managing life-cycle ) + +* Python/Flask Example + :PROPERTIES: + :CUSTOM_ID: python-flask-example + :END: + +- Easy to verify/decode using libraries (e.g., =pyjwt=) + - can compose checks using decorators: + +#+COMMENT: Define a named source block so we can reuse it +#+COMMENT: later using noweb. +#+REVEAL_HTML:
+#+name: example-flask-route +#+BEGIN_SRC python :exports code +@app.route('/support/urgent') # built-in flask decorator +@requires_jwt # custom decorator to validate JWT +@jwt_claims(['paid_support']) # ensures token is for premium user +@jwt_iat(datetime.timedelta(hours=24)) # ensure recent token +def support_urgent(): + ... # process ending support request +#+END_SRC +#+REVEAL_HTML:
+ +* Python/Flask Example + :PROPERTIES: + :CUSTOM_ID: python-flask-example-h-1 + :END: + +- Easy to verify/decode using libraries (e.g., =pyjwt=) + - can compose checks using decorators: + +#+COMMENT: Indicate that we want to highlight line 1 in the slide with +#+COMMENT: the given sectionId (based on CUSTOM_ID org property of slide). +#+html: + +#+COMMENT: Now reproduce the previous defined source block using +#+COMMENT: noweb syntax so we have an exact copy but with highlighting. +#+REVEAL_HTML:
+#+BEGIN_SRC python :noweb yes :exports code +<> +#+END_SRC +#+REVEAL_HTML:
+ +* Python/Flask Example + :PROPERTIES: + :CUSTOM_ID: python-flask-example-h-2 + :END: + +- Easy to verify/decode using libraries (e.g., =pyjwt=) + - can compose checks using decorators: + +#+html: + +#+REVEAL_HTML:
+#+BEGIN_SRC python :noweb yes :exports code +<> +#+END_SRC +#+REVEAL_HTML:
+ +* Python/Flask Example + :PROPERTIES: + :CUSTOM_ID: python-flask-example-h-3 + :END: + +- Easy to verify/decode using libraries (e.g., =pyjwt=) + - can compose checks using decorators: + +#+html: + +#+REVEAL_HTML:
+#+BEGIN_SRC python :noweb yes :exports code +<> +#+END_SRC +#+REVEAL_HTML:
+ +* Python/Flask Example + :PROPERTIES: + :CUSTOM_ID: python-flask-example-h-4 + :END: + +- Easy to verify/decode using libraries (e.g., =pyjwt=) + - can compose checks using decorators: + +#+html: + +#+REVEAL_HTML:
+#+BEGIN_SRC python :noweb yes :exports code +<> +#+END_SRC +#+REVEAL_HTML:
+ +** Starting Flask :CodeTool: + +#+name: start-flask +#+BEGIN_SRC python :session jwt_example :exports code :python ~/code/ox_jwt/venv_ox_jwt/bin/python3 :eval never-export +import os +import sys +import subprocess + +os.chdir(os.path.expanduser('~/code/ox_jwt/src/ox_jwt')) +my_env = os.environ.copy() +my_env['FLASK_JWT_KEY'] = public_key.split('\n')[1] +my_env['FLASK_JWT_ALGS'] = 'EdDSA,ES256' +proc = subprocess.Popen([sys.executable, 'app.py'], env=my_env) +# Use proc.kill() to shutdown server + +#+END_SRC + +#+RESULTS: start-flask + + +* Example of =@requires_jwt= + +#+REVEAL_HTML:
+#+ATTR_REVEAL: :code_attribs data-line-numbers='4|5' +#+BEGIN_SRC python +def requires_jwt(func): + def decorated(*args, **kwargs): + token = request.headers.get("Authorization").split(" ")[1] + if not token: + return 'missing token', 401 # if no token return error + try: + g.decoded_jwt = jwt.decode(token, algorithms=['EdDSA'], + key=current_app.config['JWT_KEY']) # public key + return func(*args, **kwargs) + except Exception as problem: + return f'{problem=}', 401 # return 401 or other error code + return decorated +#+END_SRC +#+REVEAL_HTML:
+ +** Ensure Valid Token :CodeTool: + +#+name: ensure-valid-token +#+BEGIN_SRC python :session jwt_example :results output :exports both :python ~/code/ox_jwt/venv_ox_jwt/bin/python3 :eval never-export +import requests + +req = requests.get('http://127.0.0.1:5000/hello', headers={ + 'Authorization': f'Bearer {example_jwt}mybad'}) # bad token +print(f'Bad token response:\n code: {req.status_code}\n' + f' text: {req.text}\n') + +req = requests.get('http://127.0.0.1:5000/hello', headers={ + 'Authorization': f'Bearer {example_jwt}'}) +print(f'Good token response:\n code: {req.status_code}\n' + f' text: {req.text}\n') +#+END_SRC + +#+RESULTS: ensure-valid-token +: Bad token response: +: code: 401 +: text: problem=InvalidSignatureError('Signature verification failed') +: +: Good token response: +: code: 200 +: text: Hello World! + + + +* Example of =@jwt_claims= + +#+COMMENT: should we include or skip if tight on time? +#+COMMENT: or maybe have as backup slide + +#+REVEAL_HTML:
+#+BEGIN_SRC python +def jwt_claims(claims_list: typing.Sequence[str]): + def make_decorator(func): + @wraps(func) + def decorated(*args, **kwargs): + missing = [c for c in claims_list + if not g.decoded_jwt.get(c)] + if missing: + return f'Missing claims: {missing}', 401 + return func(*args, **kwargs) + return decorated + return make_decorator +#+END_SRC +#+REVEAL_HTML:
+ +** Ensure Claims (Bad Token) :CodeTool: + +#+name: ensure-valid-claims-bad-token +#+BEGIN_SRC python :session jwt_example :results output :exports both :python ~/code/ox_jwt/venv_ox_jwt/bin/python3 :eval never-export +import datetime, requests + +req = requests.get('http://127.0.0.1:5000/support/urgent', headers={ + 'Authorization': f'Bearer {example_jwt}'}) # bad token + +print(f'Bad token response:\n code: {req.status_code}\n' + f' text: {req.text}\n') +#+END_SRC + +#+RESULTS: ensure-valid-claims-bad-token +: Bad token response: +: code: 401 +: text: Missing claims: ['premium_user'] + +** Ensure Claims (Bad Claims) :CodeTool: + +#+name: ensure-valid-claims-bad-claim +#+BEGIN_SRC python :session jwt_example :results output :exports both :python ~/code/ox_jwt/venv_ox_jwt/bin/python3 :eval never-export + +premium_jwt = jwt.encode(headers={'typ':'JWT', 'alg':'EdDSA'}, + payload={'sub': 'a', 'premium_user': 'b', 'iat': 1}, key=sk) + +req = requests.get('http://127.0.0.1:5000/support/urgent', headers={ + 'Authorization': f'Bearer {premium_jwt}'}) + +print(f'Premium token response:\n code: {req.status_code}\n' + f' text: {req.text}\n') +#+END_SRC + +#+RESULTS: ensure-valid-claims-bad-claim +: Premium token response: +: code: 401 +: text: Token age 20193 days, 17:37:05.670865 not within 0:00:30 + +** Ensure Claims (Success) :CodeTool: + +#+name: ensure-valid-claims-good +#+BEGIN_SRC python :session jwt_example :results output :exports both :python ~/code/ox_jwt/venv_ox_jwt/bin/python3 :eval never-export +now = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() +recent_premium_jwt = jwt.encode(headers={'typ':'JWT', 'alg':'EdDSA'}, + payload={'sub': 'a', 'premium_user': 'b', 'iat': int(now)}, key=sk) + +req = requests.get('http://127.0.0.1:5000/support/urgent', headers={ + 'Authorization': f'Bearer {recent_premium_jwt}'}) + +print(f'Recent premium token response:\n code: {req.status_code}\n' + f' text: {req.text}\n') +#+END_SRC + +#+RESULTS: ensure-valid-claims-good +: Recent premium token response: +: code: 200 +: text: processing support request for user b + +* Example Use Case: Proxy + +#+ATTR_REVEAL: :frag (none appear appear) +- Auth Server grants JWT letting Alice to act for Bob +- claims: ={"sub": "Alice", "proxy": "Bob"}= +- Alice sends request to act for Bob + + +* Example Use Case: Proxy + + +- Auth Server grants JWT letting Alice to act for Bob +- claims: ={"sub": "Alice", "proxy": "Bob"}= +- Alice sends request to act for Bob + +#+REVEAL_HTML:
+#+BEGIN_SRC python +@APP.route("/issue") +@requires_jwt +def issue(): + "Example route to create an issue." + user = g.decoded_jwt.get('proxy', g.decoded_jwt.get('sub')) + msg = f'Created issue assigned to {user}.' + # ... Create the actual issue here + + + + return msg +#+END_SRC +#+REVEAL_HTML:
+ +* Example Use Case: Proxy + +- Auth Server grants JWT letting Alice to act for Bob +- claims: ={"sub": "Alice", "proxy": "Bob"}= +- Alice sends request to act for Bob + +#+REVEAL_HTML:
+#+BEGIN_SRC python +@APP.route("/issue") +@requires_jwt +def issue(): + "Example route to create an issue." + user = g.decoded_jwt.get('proxy', g.decoded_jwt.get('sub')) + msg = f'Created issue assigned to {user}.' + # ... Create the actual issue here + real_user = g.decoded_jwt['sub'] + if real_user != user: + msg += f'\n{real_user} acted on behalf of {user}' + return msg +#+END_SRC +#+REVEAL_HTML:
+ +** Python demo :CodeTool: +#+name: proxy-example +#+BEGIN_SRC python :session jwt_example :results output :exports both :python ~/code/ox_jwt/venv_ox_jwt/bin/python3 :eval never-export + +now = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() +proxy_example_jwt = jwt.encode(headers={'typ':'JWT', 'alg':'EdDSA'}, + payload={'sub': 'Alice', 'proxy': 'Bob'}, key=sk) + +req = requests.get('http://127.0.0.1:5000/issue', headers={ + 'Authorization': f'Bearer {proxy_example_jwt}'}) +print(req.text) + +#+END_SRC + +#+RESULTS: proxy-example +: 127.0.0.1 - - [24/Apr/2025 13:25:28] "GET /issue HTTP/1.1" 200 - +: Created issue assigned to Bob. +: Alice acted on behalf of Bob + + + + +* Caveats + +#+ATTR_REVEAL: :frag (appear appear) +- Beware using header fields to check signature + - don't trust =alg= field or limit possibilities + - e.g., ~algorithms=['EdDSA']~ + - be careful with =kid=, =jku=, =jwk=, etc. +- Don't simulate sessions with JWTs + - Use access/refresh tokens to solve logout/revocation + +* Example JKU Header Attack + +#+ATTR_REVEAL: :frag (none appear appear) +- Header can provide URL for key (useful): + - src_shell[:exports code]{{alg: "EdDSA", jku: "https://good.com/pk.json"}} +- Attacker can replace JKU with their own key: + - src_shell[:exports code]{{alg: "EdDSA", jku: "https://bad.com/pk.json"}} +- Don't trust header (validate against whitelist) + +* Example ALG Header Attack + +#+ATTR_REVEAL: :frag (none appear appear) +- Header can provide URL for key (useful): + - src_shell[:exports code]{{alg: "EdDSA", jku: "https://good.com/pk.json"}} +- Attacker can replace ALG with symmetric version: + - src_shell[:exports code]{{alg: "HS256", jku: "https://good.com/pk.json"}} +- Don't trust header (validate against whitelist) + + +* Revocation via Access/Refresh + :PROPERTIES: + :ID: b06374ea-7534-4153-b5e6-8e2aa62a24c5 + :END: + + +#+ATTR_REVEAL: :frag (none appear) +- Problem: Can't cancel or logout a JWT +- Solution: Refresh/Access token + - "refresh token" with long expiry + - used to get access token w/o credential check + - "access token" with short expiry + - can be used to access services + + +#+BEGIN_NOTES +On security events (role changes, credential changes, hacks), auth +server will invalidate refresh token + require new credential check. +#+END_NOTES + + +* Get Refresh Token + +#+name: jwt-get-refresh +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-get-refresh.jpg :eval never-export + +digraph auth_system { + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server(s)", shape=box]; + } + + subgraph bottom { + rank=same; + Client [label="Client", shape=box]; + } + + // Define connections + AuthServer -> Client [label="Get JWT\nRefresh Token\n(long lived)", constraint=false, splines=ortho]; + Client -> AuthServer [label="Authenticate\n(e.g., login\nor OAuth\nMFA, etc.)", constraint=false, splines=ortho]; + Client -> AppServer [label="Send JWT to\nRequest Service", constraint=false, splines=ortho, style=invis]; + + // Define hidden edges to force layout + AuthServer -> hidden [style=invis]; + hidden -> AppServer [style=invis]; + hidden -> Client [style=invis]; +} +#+END_SRC + +#+ATTR_HTML: :width 90% +#+RESULTS: jwt-get-refresh +[[file:images/jwt-get-refresh.jpg]] + + +* Get Access Token + +#+name: jwt-get-access +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-get-access.jpg :eval never-export + +digraph auth_system { + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server(s)", shape=box]; + } + + subgraph bottom { + rank=same; + Client [label="Client", shape=box]; + } + + // Define connections + AuthServer -> Client [label="Get JWT\nAccess Token\n(short lived)", constraint=false, splines=ortho]; + Client -> AuthServer [label="Send Refresh\nToken", constraint=false, splines=ortho]; + Client -> AppServer [label="Send JWT to\nRequest Service", constraint=false, splines=ortho, style=invis]; + + // Define hidden edges to force layout + AuthServer -> hidden [style=invis]; + hidden -> AppServer [style=invis]; + hidden -> Client [style=invis]; +} +#+END_SRC + +#+ATTR_HTML: :width 90% +#+RESULTS: jwt-get-access +[[file:images/jwt-get-access.jpg]] + + +* Use Access Token + +#+name: jwt-use-access +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-use-access.jpg :eval never-export + +digraph auth_system { + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server(s)", shape=box]; + } + + subgraph bottom { + rank=same; + Client [label="Client", shape=box]; + } + + // Define connections + AuthServer -> Client [label="Get JWT\nAccess Token\n(short lived)", constraint=false, splines=ortho, style=invis]; + Client -> AuthServer [label="Send Refresh\nToken", constraint=false, splines=ortho,style=invis]; + Client -> AppServer [label="Send JWT\nAccess Token\nto Request Service", constraint=false, splines=ortho]; + + // Define hidden edges to force layout + AuthServer -> hidden [style=invis]; + hidden -> AppServer [style=invis]; + hidden -> Client [style=invis]; +} +#+END_SRC + +#+ATTR_HTML: :width 90% +#+RESULTS: jwt-use-access +[[file:images/jwt-use-access.jpg]] + + +* Revocation + :PROPERTIES: + :ID: b85213ac-fd6e-4453-a250-141ea99156c6 + :END: + +#+name: jwt-revoke +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-revoke.jpg :eval never-export + +digraph auth_system { + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server(s)", shape=box]; + } + + subgraph bottom { + rank=same; + Client [label="Client", shape=box]; + } + + // Define connections + AuthServer -> Client [label="Cancel Refresh\nToken. Require\nFresh Login", constraint=false, splines=ortho, penwidth=0, dir=none]; + Client -> AuthServer [label="Logout/Cancel\n or Fraud\nDetected", constraint=false, splines=ortho, style=dashed]; + Client -> AppServer [label="Send JWT\nAccess Token\nto Request Service", constraint=false, splines=ortho, style=invis]; + + + // Define hidden edges to force layout + AuthServer -> hidden [style=invis]; + hidden -> AppServer [style=invis]; + hidden -> Client [style=invis]; +} +#+END_SRC + +#+ATTR_HTML: :width 90% +#+RESULTS: jwt-revoke +[[file:images/jwt-revoke.jpg]] + + +* Separate validation from parsing + +#+BEGIN_NOTES +We can go one step beyond separating authentication from validation +and separate validation from parsing. + +- aside: NGINX+JWTs can protect stand-alone sites +#+END_NOTES + +#+ATTR_REVEAL: :frag (none appear appear) +- Can use middleware to verify signature +- e.g., NGINX can verify before passing to app server + #+RESULTS: nginx-example + [[file:images/nginx-example.jpg]] +- See [[https://github.com/aocks/ox_jwt/blob/main/nginx/conf.d/example.conf#L44][example.conf]] in =nginx= directory on [[https://github.com/aocks/ox_jwt][github.com/aocks/ox_jwt]] + + +#+ATTR_HTML: :width 50% +#+name: nginx-example +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/nginx-example.jpg :eval never-export + +,#+BEGIN_SRC dot +digraph RequestFlow { + node [fontsize="30"]; + edge [fontsize="30"]; + + rankdir = LR; + request [label="request\nwith JWT"]; + request -> nginx; + + subgraph cluster_0 { + nginx [label="NGINX Server\nvalidates JWT\nefficiently"]; + label="Application Server"; + nginx -> "Flask server\nparses JWT claims"; + } +} +#+END_SRC + + +* Summary and next steps + +#+BEGIN_NOTES +If you are writing a small application, you can quickly and easily put +together a secure system using various JWT libraries. + +If you are doing a full enterprise authentication system, you may want +to go with an existing platform. Many of those use JWTs under the hood +so it's still useful to have a high level understanding of the basic diea. +#+END_NOTES + +#+ATTR_REVEAL: :frag (none none none appear appear appear) +- Distributed trust can enable many use cases +- JWTs = secure, efficient, standardized auth tool +- Python decorators = nice way to validate claims +- Libraries: + - [[https://pyjwt.readthedocs.io/en/stable/][pyjwt]], [[https://flask-jwt-extended.readthedocs.io/en/stable/][flask-jwt-extended]], [[https://django-rest-framework-simplejwt.readthedocs.io/en/latest/][djangorestframework-simplejwt]] +- Platforms: + - [[https://auth0.com][auth0]], [[https://supertokens.com/][supertokens]], [[https://docs.aws.amazon.com/cognito/][cognito]], [[https://www.keycloak.org/][keycloak]] +- Slides/examples: https://github.com/aocks/ox_jwt/ + + + + + +