From 00f456127a210459c62a87fc62bc1b2c68effaa3 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Wed, 9 Apr 2025 17:23:51 -0400 Subject: [PATCH 01/30] initial verison of slides --- slides.org | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 slides.org diff --git a/slides.org b/slides.org new file mode 100644 index 0000000..375c73d --- /dev/null +++ b/slides.org @@ -0,0 +1,145 @@ + +#+BEGIN_SRC emacs-lisp :exports none +(require 'ox-reveal) + +;; Make sure to use version 4.0 and set REVEAL_REVEAL_JS_VERSION below +(setq org-reveal-root "https://cdn.jsdelivr.net/npm/reveal.js@4.0.0/") +(setq org-reveal-plugins '(notes)) +#+END_SRC + +#+COMMENT: using timestamp:nil suppresses "created at" in title +#+COMMENT: using num:nil prevents slide titles being numbered +#+OPTIONS: timestamp:nil num:nil + +#+REVEAL_REVEAL_JS_VERSION: 4 +#+REVEAL_ROOT: https://cdn.jsdelivr.net/npm/reveal.js@4.0.0/ +#+REVEAL_PLUGINS: (notes) +#+REVEAL_THEME: solarized + +#+COMMENT: Use `s` to engage speaker mode + +#+TITLE: Tips, Tricks, and Reasons for JSON Web Tokens (JWTs) +#+AUTHOR: Emin Martinian + +* 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 +- scalable, performant, distributed trust + + +#+BEGIN_NOTES +- standard: [[https://datatracker.ietf.org/doc/html/rfc7519][RFC 7519]] +#+END_NOTES + +* What do JWTs look like? + +Base64 encoded header.payload.signature: + +#+BEGIN_EXAMPLE +HEADER: { "alg": "HS256", "typ": "JWT" } +#+END_EXAMPLE + + +#+BEGIN_EXAMPLE +PAYLOAD: {"sub": "a", "name": "arbitrary data", "iat": 1 } +#+END_EXAMPLE + +#+BEGIN_EXAMPLE +SIGNATURE: (depends on signing algorithm) +#+END_EXAMPLE + +Signed using HS256 with secret=123: +#+BEGIN_EXAMPLE + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 + .k4P2aZc9d0yYjaEXlHwl0e1PhNtmN1gLD9gtZvA59f4 +#+END_EXAMPLE + +#+BEGIN_NOTES +- Use https://jwt.io/#debugger-io to verify/validate/decode +#+END_NOTES + +* alt + +#+name: jwt-auth-vs-app +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/alt-jwt-process.jpg + +digraph auth_system { + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server", shape=box]; + } + + subgraph bottom { + rank=same; + Client [label="Client", shape=box]; + } + + // Define connections + AuthServer -> Client [label="JWT", constraint=false, splines=ortho]; + Client -> AuthServer [label="Authenticate\n(e.g., login\nor OAuth)", constraint=false, splines=ortho]; + Client -> AppServer [label="Request Service\nusing JWT", constraint=false, splines=ortho]; + + // Define hidden edges to force layout + AuthServer -> hidden [style=invis]; + hidden -> AppServer [style=invis]; + hidden -> Client [style=invis]; +} + +#+end_src + +#+RESULTS: jwt-auth-vs-app +[[file:images/alt-jwt-process.jpg]] + +* fixme + +#+name: jwt-process +#+begin_src dot :cmdline -Kdot -Tpng :exports results :file images/jwt-process.png + +digraph jwt_process { + node[shape=box, style=filled]; + + client[label="Client"]; + auth_server[label="Auth Server (Token Issuer)"]; + jwt_structure[label="JWT Structure"]; + resource_server[label="Resource Server (Token Verifier)"]; + authentication_result[label="Authentication Result"]; + client_access[label="Client Access"]; + + client -> auth_server[label="Request Token"]; + auth_server -> jwt_structure[label="Generate JWT"]; + jwt_structure -> client[label="Return JWT to Client"]; + + client -> resource_server[label="Send JWT with Request"]; + resource_server -> authentication_result[label="Verify JWT"]; + authentication_result -> client_access[label="Grant/Deny Access"]; + + {rank=same; client; auth_server;} + {rank=same; resource_server; authentication_result;} + {rank=same; client_access;} +} +#+end_src + +#+RESULTS: jwt-process +[[file:images/jwt-process.png]] + +* Why JWTs? + +Separate checking identity from performing service: + +- Authentication server can check identity + - verify username+password/MFA/etc + issue JWT + - may require database check, locks, or other slow operations + +- App servers can just check JWT is valid + check roles in payload + - don't need DB or even hashed passwords (use asymmetric encryption) + - don't need state (e.g., horizontal scaling via load balancing) + - can be serverless + - don't need to be updated when password DB changes From 3ddbe5e7a97a0ca5e2a5dcf568badadac2d26e9a Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Wed, 9 Apr 2025 19:08:59 -0400 Subject: [PATCH 02/30] work in progress --- slides.org | 296 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 239 insertions(+), 57 deletions(-) diff --git a/slides.org b/slides.org index 375c73d..6a91226 100644 --- a/slides.org +++ b/slides.org @@ -15,12 +15,26 @@ #+REVEAL_ROOT: https://cdn.jsdelivr.net/npm/reveal.js@4.0.0/ #+REVEAL_PLUGINS: (notes) #+REVEAL_THEME: solarized +#+REVEAL_INIT_OPTIONS: fragments:true, transition:'fade' + #+COMMENT: Use `s` to engage speaker mode #+TITLE: Tips, Tricks, and Reasons for JSON Web Tokens (JWTs) #+AUTHOR: Emin Martinian +* Code Fragment Example :noexport: + +#+BEGIN_SRC python +print("This appears immediately") +#+END_SRC + +#+ATTR_REVEAL: :frag appear +#+BEGIN_SRC python +print("This appears after clicking") +#+END_SRC + + * JWT: JSON Web Token Used for authentication/authorization such as: @@ -35,38 +49,115 @@ Used for authentication/authorization such as: - standard: [[https://datatracker.ietf.org/doc/html/rfc7519][RFC 7519]] #+END_NOTES -* What do JWTs look like? +* Why JWTs? -Base64 encoded header.payload.signature: +#+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 -#+BEGIN_EXAMPLE -HEADER: { "alg": "HS256", "typ": "JWT" } -#+END_EXAMPLE +Separate authentication from validation/application: +- Authentication requires secret keys (high security) +- Validation can use public key (less security) +- Easier to manage secrets, keys, load, sync, etc. +#+name: jwt-auth-vs-app-start +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-start.jpg -#+BEGIN_EXAMPLE -PAYLOAD: {"sub": "a", "name": "arbitrary data", "iat": 1 } -#+END_EXAMPLE +digraph auth_system { + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server", shape=box]; + } -#+BEGIN_EXAMPLE -SIGNATURE: (depends on signing algorithm) -#+END_EXAMPLE + subgraph bottom { + rank=same; + Client [label="Client", shape=box]; + } -Signed using HS256 with secret=123: -#+BEGIN_EXAMPLE - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 - .k4P2aZc9d0yYjaEXlHwl0e1PhNtmN1gLD9gtZvA59f4 -#+END_EXAMPLE + // Define connections + AuthServer -> Client [label="JWT", constraint=false, splines=ortho, style=invis]; + Client -> AuthServer [label="Authenticate\n(e.g., login\nor OAuth\nor credit 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 + +#+RESULTS: jwt-auth-vs-app-start +[[file:images/jwt-auth-vs-app-start.jpg]] + + +* JWT: Authentication Request #+BEGIN_NOTES -- Use https://jwt.io/#debugger-io to verify/validate/decode +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 -* alt -#+name: jwt-auth-vs-app -#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/alt-jwt-process.jpg +Client authenticates to server: + +#+ATTR_REVEAL: :frag (appear appear) +- login with username/password/MFA +- may require database check, locks, other slow ops +- auth server must be secure + + +#+name: jwt-auth-vs-app-auth +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-auth.jpg + +digraph auth_system { + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server", 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 + +#+RESULTS: jwt-auth-vs-app-auth +[[file:images/jwt-auth-vs-app-auth.jpg]] + + +* JWT: Authentication Response + +Server responds with JWT: + +#+ATTR_REVEAL: :frag (appear appear) +- header describing JWT +- claims describing information/rights +- signature from Auth Server + +#+name: jwt-auth-vs-app-auth-response +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-auth-response.jpg digraph auth_system { // Define subgraphs @@ -84,8 +175,8 @@ digraph auth_system { // Define connections AuthServer -> Client [label="JWT", constraint=false, splines=ortho]; - Client -> AuthServer [label="Authenticate\n(e.g., login\nor OAuth)", constraint=false, splines=ortho]; - Client -> AppServer [label="Request Service\nusing 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]; @@ -95,51 +186,142 @@ digraph auth_system { #+end_src -#+RESULTS: jwt-auth-vs-app -[[file:images/alt-jwt-process.jpg]] +#+RESULTS: jwt-auth-vs-app-auth-response +[[file:images/jwt-auth-vs-app-auth-response.jpg]] + + +* JWT: Application Request -* fixme +#+BEGIN_NOTES +- Distributed Trust +- App Server can be load balanced or serverless +- App Server can be maintained with lower security requirements +#+END_NOTES -#+name: jwt-process -#+begin_src dot :cmdline -Kdot -Tpng :exports results :file images/jwt-process.png -digraph jwt_process { - node[shape=box, style=filled]; +Client sends JWT to App Server: - client[label="Client"]; - auth_server[label="Auth Server (Token Issuer)"]; - jwt_structure[label="JWT Structure"]; - resource_server[label="Resource Server (Token Verifier)"]; - authentication_result[label="Authentication Result"]; - client_access[label="Client Access"]; +#+ATTR_REVEAL: :frag (appear appear) +- App Server validates JWT with public key +- No DB/state/sync/update; can be serverless +- Checks JWT for rights + provides service - client -> auth_server[label="Request Token"]; - auth_server -> jwt_structure[label="Generate JWT"]; - jwt_structure -> client[label="Return JWT to Client"]; - client -> resource_server[label="Send JWT with Request"]; - resource_server -> authentication_result[label="Verify JWT"]; - authentication_result -> client_access[label="Grant/Deny Access"]; - {rank=same; client; auth_server;} - {rank=same; resource_server; authentication_result;} - {rank=same; client_access;} + +#+name: jwt-auth-vs-app-request-app +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-request-app.jpg + +digraph auth_system { + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server", 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)", 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 -#+RESULTS: jwt-process -[[file:images/jwt-process.png]] +#+RESULTS: jwt-auth-vs-app-request-app +[[file:images/jwt-auth-vs-app-request-app.jpg]] -* Why JWTs? -Separate checking identity from performing service: -- Authentication server can check identity - - verify username+password/MFA/etc + issue JWT - - may require database check, locks, or other slow operations -- App servers can just check JWT is valid + check roles in payload - - don't need DB or even hashed passwords (use asymmetric encryption) - - don't need state (e.g., horizontal scaling via load balancing) - - can be serverless - - don't need to be updated when password DB changes + +* Separate Auth From Validation + +Auth Server has **secrets**; needs **security** + maintenance + +#+ATTR_REVEAL: :frag (appear appear) +- App Server needs public keys; low security +- Easy to deploy App Server(s); e.g., serverless +- Lower security for App Server, logs, debug, etc. + +#+name: jwt-auth-vs-app-separate +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-separate.jpg + +digraph auth_system { + // Define subgraphs + subgraph top { + rank=same; + AuthServer [label="Auth Server", shape=box]; + hidden [style=invis]; + AppServer [label="App Server", 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)", constraint=false, splines=ortho,style=invis]; + 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 + +#+RESULTS: jwt-auth-vs-app-separate +[[file:images/jwt-auth-vs-app-separate.jpg]] + + + + + + +* What do JWTs look like? + +Base64 encoded header.payload.signature: + +#+ATTR_REVEAL: :frag appear +#+BEGIN_EXAMPLE +HEADER: { "alg": "HS256", "typ": "JWT" } +#+END_EXAMPLE + +#+ATTR_REVEAL: :frag appear +#+BEGIN_EXAMPLE +PAYLOAD: {"sub": "a", "name": "arbitrary data", "iat": 1 } +#+END_EXAMPLE + +#+ATTR_REVEAL: :frag appear +#+BEGIN_EXAMPLE +SIGNATURE: (depends on signing algorithm) +#+END_EXAMPLE + +Signed using HS256 with secret=123: +#+BEGIN_EXAMPLE + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 + .k4P2aZc9d0yYjaEXlHwl0e1PhNtmN1gLD9gtZvA59f4 +#+END_EXAMPLE + +#+BEGIN_NOTES +- Use https://jwt.io/#debugger-io to verify/validate/decode +#+END_NOTES + + + From 5d0f5bd9a4a5b8d9ca3a341a80625b46c05bffd0 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Wed, 9 Apr 2025 19:23:34 -0400 Subject: [PATCH 03/30] WIP --- slides.org | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/slides.org b/slides.org index 6a91226..0384204 100644 --- a/slides.org +++ b/slides.org @@ -297,27 +297,30 @@ digraph auth_system { Base64 encoded header.payload.signature: -#+ATTR_REVEAL: :frag appear -#+BEGIN_EXAMPLE +#+ATTR_REVEAL: :frag appear :frag_idx 1 +#+BEGIN_src shell HEADER: { "alg": "HS256", "typ": "JWT" } -#+END_EXAMPLE +#+END_src -#+ATTR_REVEAL: :frag appear -#+BEGIN_EXAMPLE +#+ATTR_REVEAL: :frag appear :frag_idx 2 +#+BEGIN_src shell PAYLOAD: {"sub": "a", "name": "arbitrary data", "iat": 1 } -#+END_EXAMPLE +#+END_src -#+ATTR_REVEAL: :frag appear -#+BEGIN_EXAMPLE +#+ATTR_REVEAL: :frag appear :frag_idx 3 +#+BEGIN_src shell SIGNATURE: (depends on signing algorithm) -#+END_EXAMPLE +#+END_src + +#+ATTR_REVEAL: :frag appear :frag_idx 4 +- Signed using HS256 with secret=123: -Signed using HS256 with secret=123: -#+BEGIN_EXAMPLE +#+ATTR_REVEAL: :frag appear :frag_idx 4 +#+BEGIN_src shell eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 .k4P2aZc9d0yYjaEXlHwl0e1PhNtmN1gLD9gtZvA59f4 -#+END_EXAMPLE +#+END_src #+BEGIN_NOTES - Use https://jwt.io/#debugger-io to verify/validate/decode From e636e300731a3bd929518cad6d39fede89780a88 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Wed, 9 Apr 2025 19:56:24 -0400 Subject: [PATCH 04/30] more WIP --- slides.org | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/slides.org b/slides.org index 0384204..e6cf2c9 100644 --- a/slides.org +++ b/slides.org @@ -326,5 +326,110 @@ SIGNATURE: (depends on signing algorithm) - Use https://jwt.io/#debugger-io to verify/validate/decode #+END_NOTES +* Python/Flask Example + +Easy to verify/decode using libraries (e.g., =pyjwt=) and compose +checks using decorators: + +#+BEGIN_SRC python +@app.route('/support/urgent') +@requires_jwt # validates JWT +@jwt_claims(['premium_user']) # ensures token is for premium user +@jwt_iat(datetime.timedelta(hours=24)) # ensure recent token +def support_urgent(): + ... # process ending support request +#+END_SRC + +* Example of =@requires_jwt= + +#+BEGIN_SRC python +def requires_jwt(func): + @wraps(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=['ES256'], + key=current_app.config['J_KEY']) + check_nbf_and_exp() # ensure active and not expired + return func(*args, **kwargs) + except Exception as problem: + return f'{problem=}', 401 # return 401 or other error code + return decorated +#+END_SRC + +* Example of =@jwt_claims= + +#+COMMENT: should we include or skip if tight on time? +#+COMMENT: or maybe have as backup slide + +#+BEGIN_SRC python +def jwt_claims(claims_list): + 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 + +* Separate validation from parsing + +Can use middleware to verify signature + +- slow/expensive for public keys or even secure symmetric keys +- e.g., NGINX can verify before passing to app server + +* Traps, Vulnerabilities, and Anti-Patterns + +#+ATTR_REVEAL: :frag (appear appear appear) +- Beware of using header fields in checking signature + - don't trust =alg= field or limit possibilities + - be careful with =kid=, =jku=, =jwk=, etc. +- Don't simulate sessions with JWTs +- Token revocation issue: access/refresh tokens + + +* Revocation via Access/Refresh Tokens + +After initial credential check (e.g., username/password or API +key/secret), Auth server provides: + +- "refresh token" with long expiry + - can be used to get access token without credential check +- "access token" with short expiry + - can be used to access services + +On security events (role changes, credential changes, hacks), auth +server will invalidate refresh token + require new credential check. + +* 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) +- 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://aws.amazon.com/cognito/][cognito]], [[https://www.keycloak.org/][keycloak]] + + + + + From b73459dfcbb88c6eef0cb98a932ccb34fc095776 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Thu, 10 Apr 2025 09:44:22 -0400 Subject: [PATCH 05/30] minor tweaks --- slides.org | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/slides.org b/slides.org index e6cf2c9..92f134d 100644 --- a/slides.org +++ b/slides.org @@ -334,7 +334,7 @@ checks using decorators: #+BEGIN_SRC python @app.route('/support/urgent') @requires_jwt # validates JWT -@jwt_claims(['premium_user']) # ensures token is for premium user +@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 @@ -369,7 +369,8 @@ def jwt_claims(claims_list): def make_decorator(func): @wraps(func) def decorated(*args, **kwargs): - missing = [c for c in claims_list if not g.decoded_jwt.get(c)] + 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) @@ -379,15 +380,23 @@ def jwt_claims(claims_list): * Separate validation from parsing -Can use middleware to verify signature +#+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 -- slow/expensive for public keys or even secure symmetric keys +- Validation can be slow for some keys +- Can use middleware to verify signature - e.g., NGINX can verify before passing to app server +#+COMMENT: FIXME: consider diagram of NGINX idea + * Traps, Vulnerabilities, and Anti-Patterns #+ATTR_REVEAL: :frag (appear appear appear) -- Beware of using header fields in checking signature +- Beware using header fields to check signature - don't trust =alg= field or limit possibilities - be careful with =kid=, =jku=, =jwk=, etc. - Don't simulate sessions with JWTs @@ -395,6 +404,12 @@ Can use middleware to verify signature * Revocation via Access/Refresh Tokens + :PROPERTIES: + :ID: b06374ea-7534-4153-b5e6-8e2aa62a24c5 + :END: + +#+COMMENT: FIXME: need more work here +#+COMMENT: FIXME: might want diagram here After initial credential check (e.g., username/password or API key/secret), Auth server provides: From a41200c2ee1ec9ebc116aa514dfaac377510a9af Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Thu, 10 Apr 2025 09:46:15 -0400 Subject: [PATCH 06/30] Moved slides.org into new docs directory --- slides.org => docs/slides.org | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename slides.org => docs/slides.org (100%) diff --git a/slides.org b/docs/slides.org similarity index 100% rename from slides.org rename to docs/slides.org From 0a2f949ac7e16c358d3cbd1edce1aad45b347d24 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Thu, 10 Apr 2025 09:47:09 -0400 Subject: [PATCH 07/30] Added images for slides --- docs/images/jwt-auth-vs-app-auth-response.jpg | Bin 0 -> 8412 bytes docs/images/jwt-auth-vs-app-auth.jpg | Bin 0 -> 11578 bytes docs/images/jwt-auth-vs-app-request-app.jpg | Bin 0 -> 9792 bytes docs/images/jwt-auth-vs-app-separate.jpg | Bin 0 -> 6981 bytes docs/images/jwt-auth-vs-app-start.jpg | Bin 0 -> 7257 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/jwt-auth-vs-app-auth-response.jpg create mode 100644 docs/images/jwt-auth-vs-app-auth.jpg create mode 100644 docs/images/jwt-auth-vs-app-request-app.jpg create mode 100644 docs/images/jwt-auth-vs-app-separate.jpg create mode 100644 docs/images/jwt-auth-vs-app-start.jpg diff --git a/docs/images/jwt-auth-vs-app-auth-response.jpg b/docs/images/jwt-auth-vs-app-auth-response.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cc82d2ee089b131fec6523ece0e1b35df0ba712b GIT binary patch literal 8412 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%0DVjiuvDFW! zc~>7lxTTV1;aEy=>?l~7i{kRqFo}7RAeBGMR3>eU2tL!T@a~$gCaSJ@)ne37a@b=2Ir-3dw-?3>3#kf8&kW8=*%*h8a9wmy)oQDx^*O{1WWwsXMJ z#?ux1fYGNeXzhwprVM2xw59A@ovPQ?>l*i%%DffE%{J+?$IJJ!d@lJgnWH;vGZO{j zMJlQsN0Wa|(f*XG{R^%NdK`CV3GN&C#F6o8j|Bt+!+N}li7A&*Uw0$!q564~wYii- zvyIybZYlYxCOw>Hh-~^a1Ke~;K)E_gx01;x{Hl_j`o1Zq`uMIb^Vs@XK|Yr7Ikfy7RArY&?!fpz3pMc^lrs9%(W|6@Ct$)$?z ziW0MNRgSH-7p8m(>LLtk3}&;fcZ+<{pYdI98V59qZ^g3}BK=h*I}8?H#Vnw_W%Q|( zvIzs{2F2O*s@Kc9jLE?JkhBKl+`S&$qktE(eHX=yv;Z1QvU`y^u>csf&s zp{mH2V05uxVuBqzA1ttF{lb1}Z1;fR2h|N5l{{!sMm7KR^K05OEnMt1m)p?w!VOrq z>JvbWCK0yhgw|TUaQ78+jeVh$htO_sL7-Nn@hNLUIDq}hlJ#dx+rQhcpwOz>?08+8 zW6j0vPuyO!k!)6(Ufe;2W?-GbMO*!v3_M~8+jOGj_C7ev_9$qvLjN*DCJlXClxN@W zC_TSf&h&G7xRlh?8inoH4-Psdo)1JILAz_A4*Akp_PmK8M8F`I^bm z`e0=5Zlia#3s*N!rrjwowVvi=GluRWOZu^#5;-)_D%PgA-U zquE&kNB|r(<7D8z2d?q^{Jyd{{cT^X#m|t=Mp*59uO$QhPn`18;6^0Y9#GbpPXUF; zyAh2fB~7A8Dp;rV?5n_3Y#5?{|XPJu)0r$g2FN#Z)ly!e|w`$}tmMxcTo!Q}KYGLYh*N`jvx1B4av<`8eZY?2@1 zQ2|!ysL9EXrn68{Z9xFi<#n<1)+sU&!-pj`xRQa595Y~P;U{I+pOt5++nWkNIq{&c zhjGv72B+Lq>U8W7?H%7PXE1k5C5lATYj!;^AHNF^=_L6|ZqQ8KiEqS*OsIB6M=GmB z;lZXERaV5XV~4))>)y9AS8F(peU}C<)D6xK<%W03ovOV2(bZnbI~TUytu!3k+(ST* zs6LdrEwyE#jCo&?Rm7H6LCs`fi4D)ux)$mrzQ6QCRuli>wuW zJ?Rzix-8DAWRNW(`YK!305m4w|@ky}ltk7vyyMyhs#XE8H1+<9Q<_~GDcFK=bM>d z)`)%YIJ~rWWR%yAlKru(DlGAcG6%EBvq7TxG`;^!YfMF?*d@~E;jeFXI(TE*yQ)-6 z&R!B^D6MK`IJMw3K?ty@!(S+ObHOib^=;DDVoDJHw<@N!rBqLL%e`!s+Z!uJL?xxc zhnHLX)Nck{Pri`m##`(};EOSo^g8q$UlqaT$ppBW84 zqvFfjN8lUPe~oud$X&Xm(f_$~Sj1laM8nDhmI~vTVAVE-HgnH3cyP5|DEAt|3{-@= zCQ0${!LlOAaC3U&!rrY>)YV|F-ZjRG7z+7Qorw#fp#|%3+AnM z7RX+cBy|iP-nQn+&8j5YfmKr~lYN!NmwAfBG+B#D-4* literal 0 HcmV?d00001 diff --git a/docs/images/jwt-auth-vs-app-auth.jpg b/docs/images/jwt-auth-vs-app-auth.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7649764dd99e33fcf7d11f3a4aa40bf331fbebf3 GIT binary patch literal 11578 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 literal 0 HcmV?d00001 diff --git a/docs/images/jwt-auth-vs-app-request-app.jpg b/docs/images/jwt-auth-vs-app-request-app.jpg new file mode 100644 index 0000000000000000000000000000000000000000..79ab6d2105fd3961a2ca2260a9481227ddc040d9 GIT binary patch literal 9792 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 ZuV3M|y+F Date: Thu, 10 Apr 2025 11:24:53 -0400 Subject: [PATCH 08/30] WIP --- docs/slides.org | 130 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 120 insertions(+), 10 deletions(-) diff --git a/docs/slides.org b/docs/slides.org index 92f134d..88ebdd8 100644 --- a/docs/slides.org +++ b/docs/slides.org @@ -51,6 +51,16 @@ Used for authentication/authorization such as: * 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 + + +* JWT Architecture #+BEGIN_NOTES - Auth server manages passwords, takes credit cards, etc. - Must be secure and in sync; hard to load balance @@ -108,9 +118,10 @@ Managing the authentication server is more complicated. Client authenticates to server: #+ATTR_REVEAL: :frag (appear appear) -- login with username/password/MFA -- may require database check, locks, other slow ops -- auth server must be secure +- Auth server must be secure +- Payment or Login with username/password/MFA +- May require database check, locks, other slow ops + #+name: jwt-auth-vs-app-auth @@ -309,23 +320,122 @@ PAYLOAD: {"sub": "a", "name": "arbitrary data", "iat": 1 } #+ATTR_REVEAL: :frag appear :frag_idx 3 #+BEGIN_src shell -SIGNATURE: (depends on signing algorithm) +SIGNATURE: aIBLdQuQ8z4F50Z4fsBRX21WZR7DUqTWa9GXDAN6-M3 + fsF56vwO9XviIeTz-n-0IcDRqM3nej83ueixpIvGs0g #+END_src -#+ATTR_REVEAL: :frag appear :frag_idx 4 -- Signed using HS256 with secret=123: - #+ATTR_REVEAL: :frag appear :frag_idx 4 #+BEGIN_src shell - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 - .k4P2aZc9d0yYjaEXlHwl0e1PhNtmN1gLD9gtZvA59f4 +ENCODED JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9 + .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 + .aIBLdQuQ8z4F50Z4fsBRX21WZR7DUqTWa9GXDAN6-M3 + fsF56vwO9XviIeTz-n-0IcDRqM3nej83ueixpIvGs0g #+END_src + +#+ATTR_REVEAL: :frag appear :frag_idx 5 +Signed using HS256 with secret: +#+REVEAL_HTML:
+#+ATTR_REVEAL: :frag appear :frag_idx 5 +#+BEGIN_src python + MHcCAQEEIOiPNn3IQok5k/pPLHSKW1G2rPkZCkED9RWA54oYS5L1oAoGCCqGSM49AwEHoUQDQgAEtuhKtU + Cz4bc79LV3sfVHNQ++ALIixWwbhAjnkRMp/MRpcHv97AtJIaSJW75/tC9PQEkPwVkurMP3O+eQhJ5Elw== +#+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 +** Example Key Details + +PUBLIC KEY: +#+BEGIN_SRC python :session jwt_example :exports code :python ~/code/ox_jwt/venv_ox_jwt/bin/python3 +public_key = ''' +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ4Au4Cb+KMMqarLlsBcv1+U4gBkv +gYu4S/SidhgeZtIVBo3z8ltvlz/QNTFoZJ70bQMu39EQY10ZW20448Mq/w== +-----END PUBLIC KEY----- +''' +#+END_SRC + + +SECRET KEY: +#+BEGIN_SRC python :session jwt_example :exports code +secret_key = ''' +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIHdYJrZXnSG7PA6f61OaPJJ7st8zNhVTCd6mnryb+ZqgoAoGCCqGSM49 +AwEHoUQDQgAEJ4Au4Cb+KMMqarLlsBcv1+U4gBkvgYu4S/SidhgeZtIVBo3z8ltv +lz/QNTFoZJ70bQMu39EQY10ZW20448Mq/w== +-----END EC PRIVATE KEY----- +''' +#+END_SRC + + +** Loading Keys + +#+BEGIN_SRC python :session jwt_example :exports code +import base64 +import jwt # pip install 'pyjwt[crypto]' +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +der_secret_key = ''.join(secret_key.split('\n')[2:-2]) +sk = serialization.load_der_private_key( + base64.b64decode(der_secret_key), + backend=default_backend(), password=None) + +der_public_key = ''.join(public_key.split('\n')[2:-2]) +pk = serialization.load_der_public_key( + base64.b64decode(der_public_key), backend=default_backend()) + + +#+END_SRC + +#+RESULTS: + +** Encoding Example JWT + +#+NAME: encoded-jwt +#+BEGIN_SRC python :session jwt_example :exports both :results output +import textwrap + +example_jwt = jwt.encode( + headers={'typ':'JWT', 'alg':'ES256'}, + payload={'sub': 'a', 'name': 'b', 'iat': 1}, + key=sk) +print(textwrap.fill('\n.'.join(example_jwt.split('.')), + width=46, replace_whitespace=False)) +#+END_SRC + +#+RESULTS: encoded-jwt +: eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9 +: .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 +: .TPrm2ijE9Rr0boAs2n5ltEMWQY0i5AKsJ-Ew_qb4Yb4Jf +: EKKKRgKnFoXep2AMyVNlJP4sDf1vvkuDq1Bm5-Ukg + + + +** Decoding Example JWT + +#+NAME: decoded-jwt +#+BEGIN_SRC python :session jwt_example :exports both :results output +decoded_jwt = jwt.decode(example_jwt, algorithms=['ES256'], key=pk) +print(decoded_jwt) +#+END_SRC + +#+RESULTS: decoded-jwt +: {'sub': 'a', 'name': 'b', 'iat': 1} + + + + + + + + * Python/Flask Example Easy to verify/decode using libraries (e.g., =pyjwt=) and compose From 35bede22c135956e1a9d168092bb601ba47279ee Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Thu, 10 Apr 2025 12:18:33 -0400 Subject: [PATCH 09/30] switch to EdDSA + provide python code --- docs/slides.org | 157 ++++++++++++++++++++++++------------------------ 1 file changed, 79 insertions(+), 78 deletions(-) diff --git a/docs/slides.org b/docs/slides.org index 88ebdd8..7aafe3f 100644 --- a/docs/slides.org +++ b/docs/slides.org @@ -1,11 +1,4 @@ -#+BEGIN_SRC emacs-lisp :exports none -(require 'ox-reveal) - -;; Make sure to use version 4.0 and set REVEAL_REVEAL_JS_VERSION below -(setq org-reveal-root "https://cdn.jsdelivr.net/npm/reveal.js@4.0.0/") -(setq org-reveal-plugins '(notes)) -#+END_SRC #+COMMENT: using timestamp:nil suppresses "created at" in title #+COMMENT: using num:nil prevents slide titles being numbered @@ -23,6 +16,19 @@ #+TITLE: Tips, Tricks, and Reasons for JSON Web Tokens (JWTs) #+AUTHOR: Emin Martinian + +* Basic Setup :noexport: + +#+BEGIN_SRC emacs-lisp :exports none +(require 'ox-reveal) + +;; Make sure to use version 4.0 and set REVEAL_REVEAL_JS_VERSION below +(setq org-reveal-root "https://cdn.jsdelivr.net/npm/reveal.js@4.0.0/") +(setq org-reveal-plugins '(notes)) +(setq org-export-babel-evaluate 't) +#+END_SRC + + * Code Fragment Example :noexport: #+BEGIN_SRC python @@ -73,7 +79,7 @@ Separate authentication from validation/application: - Easier to manage secrets, keys, load, sync, etc. #+name: jwt-auth-vs-app-start -#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-start.jpg +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-start.jpg :eval never-export digraph auth_system { // Define subgraphs @@ -125,7 +131,7 @@ Client authenticates to server: #+name: jwt-auth-vs-app-auth -#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-auth.jpg +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-auth.jpg :eval never-export digraph auth_system { // Define subgraphs @@ -168,7 +174,7 @@ Server responds with JWT: - signature from Auth Server #+name: jwt-auth-vs-app-auth-response -#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-auth-response.jpg +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-auth-response.jpg :eval never-export digraph auth_system { // Define subgraphs @@ -221,7 +227,7 @@ Client sends JWT to App Server: #+name: jwt-auth-vs-app-request-app -#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-request-app.jpg +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-request-app.jpg :eval never-export digraph auth_system { // Define subgraphs @@ -267,7 +273,7 @@ Auth Server has **secrets**; needs **security** + maintenance - Lower security for App Server, logs, debug, etc. #+name: jwt-auth-vs-app-separate -#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-separate.jpg +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-separate.jpg :eval never-export digraph auth_system { // Define subgraphs @@ -320,26 +326,26 @@ PAYLOAD: {"sub": "a", "name": "arbitrary data", "iat": 1 } #+ATTR_REVEAL: :frag appear :frag_idx 3 #+BEGIN_src shell -SIGNATURE: aIBLdQuQ8z4F50Z4fsBRX21WZR7DUqTWa9GXDAN6-M3 - fsF56vwO9XviIeTz-n-0IcDRqM3nej83ueixpIvGs0g +SIGNATURE: SU6aXJ0YbH7Vg1jROpQfvnhn98Rt9zBeS7-c5O9jH-L + L5mQqMMFq61eZjf0tLLqExm-dckRUNa3-qT7R2SKmCw + #+END_src #+ATTR_REVEAL: :frag appear :frag_idx 4 #+BEGIN_src shell -ENCODED JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9 - .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 - .aIBLdQuQ8z4F50Z4fsBRX21WZR7DUqTWa9GXDAN6-M3 - fsF56vwO9XviIeTz-n-0IcDRqM3nej83ueixpIvGs0g +ENCODED JWT: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 + .SU6aXJ0YbH7Vg1jROpQfvnhn98Rt9zBeS7-c5O9jH-L + L5mQqMMFq61eZjf0tLLqExm-dckRUNa3-qT7R2SKmCw #+END_src #+ATTR_REVEAL: :frag appear :frag_idx 5 -Signed using HS256 with secret: -#+REVEAL_HTML:
+Signed using EdDSA with secret key: + #+ATTR_REVEAL: :frag appear :frag_idx 5 #+BEGIN_src python - MHcCAQEEIOiPNn3IQok5k/pPLHSKW1G2rPkZCkED9RWA54oYS5L1oAoGCCqGSM49AwEHoUQDQgAEtuhKtU - Cz4bc79LV3sfVHNQ++ALIixWwbhAjnkRMp/MRpcHv97AtJIaSJW75/tC9PQEkPwVkurMP3O+eQhJ5Elw== +MC4CAQAwBQYDK2VwBCIEIC+D6rD2YbXtV0ccR3smoR0ynhVuyyqvplFLbQWDdAtn #+END_src @@ -349,93 +355,88 @@ Signed using HS256 with secret: - indicate username, roles, rights, restrictions, payments #+END_NOTES -** Example Key Details +** Secret Key -PUBLIC KEY: -#+BEGIN_SRC python :session jwt_example :exports code :python ~/code/ox_jwt/venv_ox_jwt/bin/python3 -public_key = ''' ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ4Au4Cb+KMMqarLlsBcv1+U4gBkv -gYu4S/SidhgeZtIVBo3z8ltvlz/QNTFoZJ70bQMu39EQY10ZW20448Mq/w== ------END PUBLIC KEY----- -''' -#+END_SRC +#+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: -#+BEGIN_SRC python :session jwt_example :exports code -secret_key = ''' ------BEGIN EC PRIVATE KEY----- -MHcCAQEEIHdYJrZXnSG7PA6f61OaPJJ7st8zNhVTCd6mnryb+ZqgoAoGCCqGSM49 -AwEHoUQDQgAEJ4Au4Cb+KMMqarLlsBcv1+U4gBkvgYu4S/SidhgeZtIVBo3z8ltv -lz/QNTFoZJ70bQMu39EQY10ZW20448Mq/w== ------END EC PRIVATE KEY----- -''' +secret_key = ( # We hard code secret key so you can verify results + 'MC4CAQAwBQYDK2VwBCIEIC+D6rD2YbXtV0ccR3smoR0ynhVuyyqvplFLbQWDdAtn' +) #+END_SRC +#+RESULTS: -** Loading Keys - -#+BEGIN_SRC python :session jwt_example :exports code -import base64 -import jwt # pip install 'pyjwt[crypto]' -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization +** Public Key + +#+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 -der_secret_key = ''.join(secret_key.split('\n')[2:-2]) -sk = serialization.load_der_private_key( - base64.b64decode(der_secret_key), - backend=default_backend(), password=None) -der_public_key = ''.join(public_key.split('\n')[2:-2]) -pk = serialization.load_der_public_key( - base64.b64decode(der_public_key), backend=default_backend()) +#+RESULTS: get-public-key +: -----BEGIN PUBLIC KEY----- +: MCowBQYDK2VwAyEAUVLjZWAVK5ZE1ewI5QBdr0Nig1Qkx3kl5zHIADvw0M8= +: -----END PUBLIC KEY----- -#+END_SRC - -#+RESULTS: ** Encoding Example JWT #+NAME: encoded-jwt -#+BEGIN_SRC python :session jwt_example :exports both :results output -import textwrap +#+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':'ES256'}, + headers={'typ':'JWT', 'alg':'EdDSA'}, payload={'sub': 'a', 'name': 'b', 'iat': 1}, - key=sk) -print(textwrap.fill('\n.'.join(example_jwt.split('.')), - width=46, replace_whitespace=False)) + 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 -: eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9 -: .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 -: .TPrm2ijE9Rr0boAs2n5ltEMWQY0i5AKsJ-Ew_qb4Yb4Jf -: EKKKRgKnFoXep2AMyVNlJP4sDf1vvkuDq1Bm5-Ukg - - +: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9 +: .eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9 +: .SU6aXJ0YbH7Vg1jROpQfvnhn98Rt9zBeS7-c5O9jH- +: LL5mQqMMFq61eZjf0tLLqExm-dckRUNa3-qT7R2SKmCw ** Decoding Example JWT #+NAME: decoded-jwt -#+BEGIN_SRC python :session jwt_example :exports both :results output -decoded_jwt = jwt.decode(example_jwt, algorithms=['ES256'], key=pk) +#+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} - - - - - - * Python/Flask Example Easy to verify/decode using libraries (e.g., =pyjwt=) and compose From 5f4431c5974c4a216038c073c66b1c2bf8d1fb2a Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 13:46:40 -0400 Subject: [PATCH 10/30] Provided simple flask app to use as demo --- src/ox_jwt/app.py | 95 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/ox_jwt/app.py diff --git a/src/ox_jwt/app.py b/src/ox_jwt/app.py new file mode 100644 index 0000000..7f91713 --- /dev/null +++ b/src/ox_jwt/app.py @@ -0,0 +1,95 @@ +"""Simple Flask app to demonstrate how JWTs work. + +See accompanying slides for more documentation. +""" + +import base64 +import datetime +from functools import wraps +import typing + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +import jwt +from flask import Flask, request, g, current_app + + +APP = Flask(__name__) # Update config from env vars like FLASK_* +APP.config.from_prefixed_env() # pylint: disable=no-member + + +def requires_jwt(func): + """Decorator to verify that valid jwt present in header. + +This decorator also puts the decoded JWT into g.decoded_jwt so +that other decorators (and general) functions can look at the +JWT info without having to redo validation. + """ + @wraps(func) + def decorated(*args, **kwargs): + token = request.headers.get("Authorization", "NA ").split(" ")[1] + # if no token return error + try: + key = serialization.load_der_public_key( + base64.b64decode(current_app.config['JWT_KEY']), + backend=default_backend()) + g.decoded_jwt = jwt.decode( + token, algorithms=current_app.config['JWT_ALGS'].split(','), + key=key) + return func(*args, **kwargs) + except Exception as problem: # pylint: disable=broad-except + return f'{problem=}', 401 # return 401 or other error code + return decorated + + +def jwt_claims(claims_list: typing.Sequence[str]): + "Decorator to verify jwt contains given sequence of strings." + 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 + + +def jwt_iat(within: datetime.timedelta): + "Decorator to check that iat in jwt is within given timedelta." + def make_decorator(func): + @wraps(func) + def decorated(*args, **kwargs): + iat = g.decoded_jwt.get('iat', None) + if iat is None: + return 'Missing iat', 401 + age = datetime.datetime.utcnow( + ) - datetime.datetime.utcfromtimestamp(iat) + if age <= within: + return func(*args, **kwargs) + return f'Token age {age} not within {within}', 401 + return decorated + return make_decorator + + +@APP.route("/hello") +@requires_jwt +def hello(): + "Example route which just verifies jwt present (no claim checks)." + return "Hello World!" + + +@APP.route('/support/urgent') +@requires_jwt +@jwt_claims(['premium_user']) +@jwt_iat(datetime.timedelta(seconds=30)) +def support_urgent(): + "Example route for urgent support request (checks iat and claims)." + return (f'processing support request for' + f' user {g.decoded_jwt["premium_user"]}') + + + +if __name__ == "__main__": + APP.run(debug=False) From c6d5565bce82c20fcf768acd08277e16060dbd6b Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 13:47:57 -0400 Subject: [PATCH 11/30] added __init__ --- src/ox_jwt/__init__.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/ox_jwt/__init__.py diff --git a/src/ox_jwt/__init__.py b/src/ox_jwt/__init__.py new file mode 100644 index 0000000..c17f6bb --- /dev/null +++ b/src/ox_jwt/__init__.py @@ -0,0 +1,2 @@ +"""Project to show how to use JWTs. +""" From 3106bec378ca4f0e04d01453abe851318b9bb25c Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 13:48:17 -0400 Subject: [PATCH 12/30] added pylint target --- makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/makefile b/makefile index 73ff66a..d22122e 100644 --- a/makefile +++ b/makefile @@ -7,7 +7,7 @@ .DEFAULT_GOAL := help # Indicate targets which are not files but commands -.PHONY: clean help test +.PHONY: clean help test pylint help: @echo " " @@ -18,6 +18,9 @@ help: @echo "help: Shows this help message." @echo " " +pylint: + pylint src/ox_jwt + test: $(MAKE) -C nginx test_ojwt_nginx From 09938933a18594f149a8e45dad0a4238fa04787a Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 16:21:33 -0400 Subject: [PATCH 13/30] Lots more improvements to slides --- docs/slides.org | 278 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 265 insertions(+), 13 deletions(-) diff --git a/docs/slides.org b/docs/slides.org index 7aafe3f..7c7024f 100644 --- a/docs/slides.org +++ b/docs/slides.org @@ -28,6 +28,9 @@ (setq org-export-babel-evaluate 't) #+END_SRC +#+RESULTS: +: t + * Code Fragment Example :noexport: @@ -170,7 +173,7 @@ Server responds with JWT: #+ATTR_REVEAL: :frag (appear appear) - header describing JWT -- claims describing information/rights +- claims describing info/rights (iat, nbf, exp, etc.) - signature from Auth Server #+name: jwt-auth-vs-app-auth-response @@ -357,6 +360,12 @@ MC4CAQAwBQYDK2VwBCIEIC+D6rD2YbXtV0ccR3smoR0ynhVuyyqvplFLbQWDdAtn ** Secret Key +#+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 @@ -375,7 +384,6 @@ secret_key = ( # We hard code secret key so you can verify results ) #+END_SRC -#+RESULTS: ** Public Key @@ -451,6 +459,25 @@ def support_urgent(): ... # process ending support request #+END_SRC +** Starting Flask + +#+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= #+BEGIN_SRC python @@ -463,13 +490,40 @@ def requires_jwt(func): try: g.decoded_jwt = jwt.decode(token, algorithms=['ES256'], key=current_app.config['J_KEY']) - check_nbf_and_exp() # ensure active and not expired return func(*args, **kwargs) except Exception as problem: return f'{problem=}', 401 # return 401 or other error code return decorated #+END_SRC +** Ensure Valid Token + +#+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? @@ -489,6 +543,65 @@ def jwt_claims(claims_list): return make_decorator #+END_SRC +** Ensure Claims (Bad Token) + +#+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) + +#+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) + +#+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 + + * Separate validation from parsing #+BEGIN_NOTES @@ -504,7 +617,7 @@ and separate validation from parsing. #+COMMENT: FIXME: consider diagram of NGINX idea -* Traps, Vulnerabilities, and Anti-Patterns +* Anti-Patterns #+ATTR_REVEAL: :frag (appear appear appear) - Beware using header fields to check signature @@ -514,24 +627,163 @@ and separate validation from parsing. - Token revocation issue: access/refresh tokens -* Revocation via Access/Refresh Tokens +* Revocation via Access/Refresh :PROPERTIES: :ID: b06374ea-7534-4153-b5e6-8e2aa62a24c5 :END: -#+COMMENT: FIXME: need more work here -#+COMMENT: FIXME: might want diagram here - -After initial credential check (e.g., username/password or API -key/secret), Auth server provides: -- "refresh token" with long expiry - - can be used to get access token without credential check -- "access token" with short expiry +#+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", 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)", 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 + +#+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", 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 + +#+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", 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 + +#+RESULTS: jwt-use-access +[[file:images/jwt-use-access.jpg]] + +* Revocation + +#+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", 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 + +#+RESULTS: jwt-revoke +[[file:images/jwt-revoke.jpg]] * Summary and next steps From 1da75a3cb4fb363105a9322a3e6136f7ad9c6741 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 16:21:47 -0400 Subject: [PATCH 14/30] Added images for slides --- docs/images/jwt-get-access.jpg | Bin 0 -> 13438 bytes docs/images/jwt-get-refresh.jpg | Bin 0 -> 15029 bytes docs/images/jwt-revoke.jpg | Bin 0 -> 13340 bytes docs/images/jwt-use-access.jpg | Bin 0 -> 11020 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/jwt-get-access.jpg create mode 100644 docs/images/jwt-get-refresh.jpg create mode 100644 docs/images/jwt-revoke.jpg create mode 100644 docs/images/jwt-use-access.jpg diff --git a/docs/images/jwt-get-access.jpg b/docs/images/jwt-get-access.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5112f10ad5c165f99c86b32143c85d95fc01282d GIT binary patch literal 13438 zcmeHt1yoe;y7z_wM!GwtmF{*3X^~PIk&>2j=n|wG1QewOqz35_32Bk;lun63VkBnn zc+S1&p6mJF`<-*ox4!k=weFtvteO4pJ@4Mn8^5Q1s2S8UK%$|dt^#0SU;vNN9{_~} z?kMRgD?HHFlV-8A;d6AewPW!T;umD$hI%?YwPR7&P-W53QC4N~k`%nf%VJ~u#1iWK zjKv*l>Fn^#7Y{WLC;>NcaKJd&H^5*pF76FH0t!L`e0&00GIC-HMmlCDMmh!t7IuCv z7FJ$11_o}4TfBloqN1YATvBq9!m|7#qQbvA!NA4ECBP@3Ata;`zR7S?_#Zw{?EvWw zU>f5Tgux16l45{JF;HCqGXP*?`{9PkZXJbd&GbtC{L1_*?S1;WP0 z!b0C2h`tVBkz$kG6jHz;*R=$*x>E>0PtLo+rdZWMsW*DWE@I^oii<}@O+!n^!O6va z>$a$vxP+vXw9;K=6;(BL4gCiX4GfKpO{~#_WoPf;==lul8H0qV>YW!UAD|f9ZvR>5Uc;DHirkAsjLVU9hD) zIjivV8x)Gkc~u>_Y$AF`lvW<2cvS47D;)4&s{Ki`zfZBy|CVNdQS9IKnga+x80g6Z zkpd9l{DM0_1n;k<9S)A>9IYdi{yeVQG;whFqJlw3lfnP4r6!q;EpwO5)U`+E^wrB? zyx{3iC;%Bo2{0d}AX1Spg!qL$;MBP{5J&I0{G@{hO9) zLuJ4t=YLFRCL;k_cPh#(95R~urZT)82=s-udG(&E^qHZ>a@4+~6=1om6F3Pz0^s9n0}u&-k?{<7GsK9pC9m{ixCG zVkcHcitye2O{zHww|604C(G{bxx`u)#uZo^9@y4TI&tgQF!ly}S4(@`9G3iDiPTvi&_ zQ#2=g#+sGGt7VsrJW7~D9koXM<@?_Z(m%@^HK))?F7($}#gQkxix>u&>XdyAa%?M? z5XJY$#1bjY`=;qAdNyhYDypKb(@12UDM%LkS$;G=lPnXTO5M(!j%pDRFDqNneBVjY z{Gfp@ZfHCHvL4UdhGyY_-Qz*rNiOld{Fcn`PIL8@mG*urh3nb{KXv9bJz-h|Rf)G4 zG+A#f+^3E1_v*nBl+Gd~8rPp}aVVeLWn!IRyxA~*NMgLOA8p=HbNewtkJlPjz!w!? zAcR;QrjLfel)}p38U6tjpkiidXjYVi0ys1up@0wO8OXTCTq0NucP{4NDMb3e@98Ej zC!_`>9J05B0#JZLOmj0(=38RCbLg1ijf-!7k0nIt@|Z+T_b^<{v1P3XKXBD%^6`gJ zG5Q+Ee7-wpv-^{*%_>C0m;DbLp~{lr13^Z$(u=mJgBGO~2zW0usMxnyY^cJ?#T)x0 zHg=|b3Y!i2adi?{k(gy&2V6Y*NNBC(G#mQNZ?T?~@D2|Dl9P%Qu;KlxPsotgGw)7`o|syWlQm`UCKRP5l@(ejgeUv-FKW zjOHb1?tBe$`svQ9GN#UEE%`vlH~~WvIC(cNHYj7{kkYgDH3evWb}ufG>ie?v2W)L{ zD8^N{6{78KhGEOt`nZ^T(O$iZ@%OBPAEeV5HU|0=Bm)Q1G3!tW-5#BJM4fA?!&I|( zRK?UF=ck5O*V&rBoHHB*4`Zx7PxVwh!{T(n<+_igR(XOccJ9!^30IxIS=U_REzB!9 zm8<(pCYur%s9PJ5ey~g;#v&jg;_>X3=Dol-LgYagy-c0_w+km+=5=2xt&3AeDYF3O zO3CB&k@onq$q?k~LP0Ia1UnXFYJuPO+a4af7DIUc#rZ{I?-zjMXS$ zH0@L2PN!zgrSRh7qQI*IrUPr9_^vq{SpF?TXRC&~NT2;K;#?bzFPR9N0y)Zc;&M7dTz-*b4U zxSDnYo$}GNsRIs4Vz2KMpDM92LWrZ^EcmW}y_|Z4(`?_|mjNIoAPhK7wp?=}pg2X5QbC}`T z`_nc|AB!%pvn0n%v6fNe_nK}^i37Sz{#L2&`Q68U-iWZdn_y&R88?h$?pEK`d!4~!mJ`6Kn_hH&yntg?V0SdkbkSxJt zuWlDzdF=n_tTZ=oh@ya>w@uEkL%*XfY4?9_O8>~b{^lV*`|-Jf>R55(vsxid!Aob- z1R4^Bb1-XD{NI4t!044+vAwkQ&YXufWtoV0Z*#h0Ve`J7mEm-#6#J=IJEC~yx;4wr z+GQn&=+V-<=64beJJ;_tgbQ$-qYE$h-`WuMvb*Q#<%h_kfSyN`&?BOXVjoZC#^wpA zPBhnRFQ)r~p6sdWDHH?%?p}M3*0W5Uuwo?2>O`v73`cXV=zd8(bsv@)=@vFJd}nLceAD$>N_SA{jjjdjW+D*ZIP^|9)Cln z>$E4&yvE+eZ&v$cQmb$H;@`v~F^;zG=;efs%F|{hM%HGT-bMDbeTWE@vC4fOiAQ~~ z#t`Slmf33&(Mp!6SwC($mWi+FiyWs@fQs4;t`U8O;S08Ru&>qgpnW%bx*Pdw6IBl} z6rS+CB`G49ao!os? z-ircYZrmUhhy42ITiFj{xF>4*zEeIRdY3w4$Qqztib#ulL{@*dnv1aFQQmRj)hmQ> zP7w9NsO!o+3Lw9q85x_V+?;fF!S32j^D*KT3K%dvyDa!1=k(zrUfu%$Xb05+p3N3v zqI*1(6O~IYrUuN%*Fj3v!P8=wRf)DBI4l7TYcS1jK}Wbxf&66cB9qH~8Oehb`;~0& z*? zQH4%^@$z|+!l8NgN62gdq{PgQ>0xcK|2+(ub{Uh5w!FcmFZA=0|yuk1R|D zvX|uFC@@ZPet*5ceP0dfzL@Qf;fAQbm+--eZwyY2{s9a&;53~yPZp< z|9fch{z{6f3cAKU`8$KC8mRA;`YP>-zt>Ut{!1Ol z^mDyov5lJB`FBjilTv$gVP}6msINswO*`0ss8w~-Wq4bMl;Ws!jfIHRU3*P;&RMoJ z8m7&5>6mtjaCB9C?g?vx6tf@kh;$;Eny7obm{@Fb%+m1kZ+$fo%K2$a-FBW+#{Bel zWyJnEoF5%T;s)i>^Kxj|pB)pQ3NqD@&aUr$Z)6a$q~d^fV!|OAO=Vy$SH%#Vp6S&W*<_dEA z*(Wp0igrmHo6U;~9Oo+z^3)6Nl(v&z?N0t-nD_xoIA>B)FC5_6drliiS z0igh1P(Tt2m{$QqzL)t;8+y7^W|dD%5RuX7zDpif))CfT=XrC)QCnFs09(3gy8qOo zzK_1#JvpiX1%&ia1ghr}4(_#e+Tc_fnA#-i2BYMFX5UCG`u0tl#Oy9^dn5`HJNspf&Hc zCQBw&ArL{ndmcpPRAGR`ZxcS)vefjXkDw-t;Pp;YV0jFDRAo3VLU_0PB}Tdy#1Kid}InuENUkF9B+p)aO>gDxa z3z0;_(W**xp#>tRBV}=Cs-|DOh$XEs+^C->e%{)~qiG8qQzHI+r4r8viNw!h>JWAD z*0h^(nRb+GT4cpYpeUN|Ew!tIj{8{cUUt=Aq!*S2mJWScUYIq(w^hP_c`J1JYFAjT z56o2pqIBvCqN#y%j|WW|YsIgc=UJ@IC+VPo>h>gMz&A3&xuD@lykS`;OMHq^`Ead) zzSH!$~M;QO6x=2Db9p7ePB8295;3GpzP@(W>pBgv!Ny1h7?D& z6#JIgcw2aRPEQr$ZTh92@f-73KeY$ZtLYFh4vKTsrnEmtPvOR9vO^$Y#yqP%N4ln` zIGgRw(2%{bVLk1!5aAZ;6&JEj%~5D9qxjU=X>H$Hil$Gx2sczk=ID-f8klRTdH3tl zS(f)ZdxK2q#=3-usmh1)QF%*(Qh-)(a0tu=38|3&3tX?akOkLpOkN3I1=}uC*E7}E zkiplsHH)t>o9EU8v`_$eW)mSE28pth0WmD6?%y(yNRg4nFNIf3#x~SkYQSzDw72D5SW}7jUEPD(KfF_%*t}&;*eW5_?p_N|ogfeK zG+2cXy}B?TT(3V*(sK2`&Yseydp)-nr;=w>Muzq*d!+h0a)~lymRp!hEi3{Q;P)E?K0J zJbswX$;Jiq+>ZecwqfKQNbZ)*%Wi{aSOMi(R=!5-qjFLmwL*O9`fo2^_TU16JSNPv z9sK#CXJb$`SI!2fHR@s137*NSpBVoiJ0a&fkMGHMep`IDP!d zm-;2yrY7UrFkN1}c_;*X`g0rL6U*?=v9Fn8SStdfwyB5KP4l8s#pXgI>3m6`AacCp?mVUoD+R8x>l}T52~9S!RE8jMfQ(9bq&bKTGd``&+#*qy-n<)#vIQXUpnuU$(LRB z1P%Tx#`+VO(#-B7mob|H&`N}wtd*z>zRB2H(-_6DTLvk8{1`*7PAn^P64io-Jb#N_ zF||{fR)O-G0H9hr+h5w^Y35*Idp#m>JKcv=Q%HsAKD9eU+#WQ|dzvMn)08}hFbw7v^>jo{KI*lBP> zqz|ZMj#Bci$9{i(-Lf{^TZ&D!3QJRb9DGXL^TgnRql76rzhf%lYOd}CvEbLb}34m%lGACbV5Fd1OG z%6F@_Qy`*T(b0R))|vSs)Xmi007^oV8$N`={-hA$|9)|obQc9UX-WH%?Rb{#G#v{> z$E(1RBNz{>dX%38=uABuAk^;|S=^=ipvlyP{WY3BPvb6?yba5V89WsoW{M+Ooiv@I zW$&lcv8h7R=;aXhMf4BDc$$knRD`a?OuK_vTSxbw`F|3Yk$Gb}sJJxez@GG$9ac(< zd9>Q>t!l49$yuVGk2kCP~S-#$Kg%#K3=i(?}FE7T{F7bK;%my!mZl=#|MSfeIipUJg zvobKpQ%h1{U5Qnv&xbFBbm5&e{|V^}%N@MtmJ)pn7h!w;?=!i2b#Cm+_39q*@O1bU z4i)6a1}fB)I*f^>9kxXGpi@S$Y34&iWwW446hJW?=K>u)-kF09FshD8c}8(SqJ>hC zV}3edOEffqd0-0ue&=TO=hh+Wq*0)Bt;3;J#Wfm`WGT3F>GqokpC+wFY$8kp101K0w4p@&Rzf#k3&Z9u zKy0#}Q=dGlq1vQ7b8-^~4J!M^X#}y16)&g3_AF!pwax^|Qz3%SWVF1IR7B`Z@iq#u z8LuqgU&C>F>7cvzt!H9^$QuRtGk9-{T)F2j?X`*??e(u1L(Q4BFIDGa)m^K##Rbdi z9?9Py6ZQb#7cnPFTWcfU)0USZa^bGgc+}o!1(&dv%3))4!L0r8V#5W|qPt*karp+s-8xnhszNc6YHq zGr^p3^qP9^d{E2tu7RpL-Aky^DlZSKL=r2-6i-C2(_u9>n8YW0H<;LL5r0t_-^eaz zb@A?453GFtUPx(GDjQqwPBzb&eJ+uIdws(c(~Z&ZVmw?mA8YG0eOY};e=6fA=|*+* z_)RPQaUk+X!O8D!82t#u^&8D`G3#^DLA|+6r5#$=Tf@uMLRUQHrI|%pu4OwCelBAN z4b9;zNx{|aVOM+f9nx9dn~`UX3T>-Sg?-MIV6SJF)q#v&jVQn`L|P+mm?Z15aPI4u zjfrjx4@hu(&1@0^yQLPbRlFo0(sIqUtlgcqsY7@|*Ge9f?b3z)9Ft*#BxAg>A`tkh zC;Brcq0xFGRh}u_)7R+n`I3XEF>l82U7qHV^ zr_4T@bAu5iAM6?zu&-10Z%a>o`j!ACB--kt%6up8EkT7jGDi=!PuF+rrZjbTwv6PJ zPH>af0S79(`Q^j}sOLJi&3b=87o4cQ^v!mD++`0Hh67_N%NdYo>w-OgkL}pVji#5$-hO-}znm56E!Rm#I={ZzM&;SpzFZPlsuD1_8fC%FnYtv0b6A6f=&( zJ4(9acb{}y)+O-{u-q{vv1LDSO$Fjee8n^$zLM zyDHmMqnx4HgsiD0Y}Vn7O4HWbKF2XqKm7GZ+S(7!>erSIkGop`;DcEU^|kgk zp0K@_i#J6lpmV^x-r9si;brP=Mq^PihLB@8DGD%@L;*fTfi!}!%%()j9q(AahNu(! zxXk*u?`WtK>rJyCkstPKoIIA(a&eHZ$ZjY!-8CTlnLdr%L{GyuK{^wbq-sS7nEy)s zzke4Gq{tWeY86)XgCvR`ijVE#cf3>(gcC$LSzf>P8N+q z?6R0H7GHS4`no%Gf`*tp;#r1i@p_p~$4i%dJXiS>i{>o8neQ~$Rdi|*E_M!C`h+I4 z)}+YGoF%{YH#m5`e2uR<$~o%7`{q*E_V!uLFgtwt6i^$iWyr>nbC?Sb7Y*c>G8kL3 z5mq|$WHZ+|W)EIaeZm_ya*IY%?mcL*g;3V7r0(b}Pwe^ytJC}&iMo#|#0?U2wq*Q> z8Rn$N`@5dmz^0F@tmZ1Mz$v4IZcT0Dc%3?{wR`!OXbN2o!-4dfyE+F7=do1%L#hfh zsz>vmM`jfqDAN|)<`?MSEAC3L^xmT3FaFvmn;rQ{i>vb+o>rwYOtV0VvyfD;=Ho}Q zfzemw5l_Ki736-%GcfM$rX$JWi*PtmCErxy`~EX2t$y=2*3lui8x0<*&8d8IPH~!E zwSu!{m*-CNeA0NBghi7IH>FH0MYn?=iW#LXW4UmXkR{ZHN*gZ||xofE!sG+Xx&oq@RRyg()vgN%#d6Y04C8 zeRgt{@Jr4n#!}gv2x3X&aX&iqWsjldm*rxkwqL`}xo73kZCIw1t;SF+NwYI+vS|9v zq#NoW>pA}=uOMBVpq$uE)dX06ilj3iAcz#XDu$++kwUDV)Sx~Hq|QrbbG!s9PO-abk;DlL`Vgq7kUb@nSOwssJ}TGZ9H$kJHq)4+UGVG; ztv0D$csI>3DNWB8!y4CMs`os%U+UoS9Y*XQ2IyMCUYm2&g|GNb1k{u~?*Dk*gB{sa zZm!)v_ad9Kaq1a_V#)sVrQLkR+H(%$n2@}T;2ZPuv>iLRdDq)u%G2 z2=yov&TyB13unJk$)t?S6YqJ|DAsqD!x0%42vvoIWca|GJHK(w|GLQ!JGlS?~ zg#$KOP3$Mb1*3n4nty{Fcat~|A7;ZG(|Xll;-&sJ{pUDZ<&nroI>alG9`NN--Q1XP zgkCv@o}j|EJw}x#W7jQFVcul5yyTH}LD^7ZcWoGBeiOo9WYCqN;SRZni5dx2>OJ=L zYN85akAk#a$z?CEX~9P(-JUMOT3Dq_Qf!aEeDNbu)s-h3{qLLW-_7@5&J8&{S7bhM z?wTuomT;dLV582t|CZd&t+%9olM}LJ(M7k1b|XxNd(M& zU*Ef9aKRu^2bEJ%R`9lS3U=bC*l01NIwp|SXMtKC5aTo@A{4jAf`ak(45dvH()Bkt zM=r6fn;^>scV1#l>zZX;u?O88!&&t1)LT%IUfixY3%_{vhD>0Ye$T|jF!l;kF|a=4 zSU+Z#Kv>g|A)MngPXHm6r+@Yb{F8r6LQ?jc*-Ixz@Y_zv-lEfVV54ptWvo}(`q=KL zf-E~Xo%vxNPl1t|hlsro$0Cz^n^5!FYr$kFLaJi^5*C>htTdB3RnF)6x!z>3%X_q6 zETRH$l~HkIKTY-Aw5IZs_eOcus*8T7%Q~WdLM(R$`i8szMdF3y+gjCcY3CFeGo!ku zW*xpRjDzLyVXQC@O>!IqrrZQV&HuQD{%tM&v)@m4mzYNDX1e@f84f<+MUlI-8QXq| zpKIUe5@{nzGOD%|;8c}G)(4+|1eRj#SG#l(=TZ1(%Zvw|Nb@(49MXS!S$rfjk<(>p z{3o7R=D-uks{2ZoM<=u2-3Dsi#K4gZ$&2R(Cn$gcVeufT+X*&H`@AE<_^uu?0<_Ym zv3)xWnOd}4QlmS*FF;;2A7!9`E`ovNsV2j?ZCS$l*go-j3?3bJs&#)a-yCP>x>-A4 z;^G#R{DHYp1W79QWE!cEEjn3Xaqy^{a1803*9PAO_PAj>3Tu1!$<8}3QW7GJIO?oV zZx2Bt|)*xh(H8mvRsV&-SCJSn<&QQs9Et9m#z}}$`T`-@fD+S?dD739@dj} zMDhw|hwU^=o%NMO)Y;Z?5+48+HLxv?I^nqrOf(sXd5-rts}8R_-6K&^==*mHNhk&u zX_7tKe=yC#>U42CZ>9T`q1fWtG=@#+B7_2-f{t@a_g(#>d@3J|^bAvO4-;rk>y0aE zJFAv)U#|y8v0az$ZRg@Odlg%k85eG5`C>2DT+cpElFsRRSh{&i4BDQ~NDOvNb?}RY zwv5{fs`Xyy-Z4b;$v_W7;PtUR&H<{9GAVrgZRIX#QlAdCma9hnbP)U8jK!zfIxSZ9 z7jEVQz!Il!>PGcesNdu_gy9`}6p)QBKN0bx0L1tDHU;hrmPKex~okLA_FKhqHLwmO4<45Lea9o=Z|7b6UYrW8O{ z%n+_fUo<67^5d`Cosv2_=|CJEYg_zjZeFvXrIO)1^*(-d66mtWnZeHxYKBhj_Zx>H z+x3FCPyR8M59Z&)21-sg*JzH=pCCCcnl|`;xjR*^@eskK+uTLfe9?Q)LXmugn4qIt`gNaaV?svXg(RlFkSCf-eS% z6`Gh&diB#q(3?on9=QZ}3@$%1IJ{UZ?fP~>@}~|Bkz`p0{L)XCDfX zy<*FqHE9^KLZNnGJS9Us7(Epqg!Cz4~{q*tbA_;%4+ zxZ4bf4aW0&r1gdmm-5W^8tt-k^w1m?#^-FV39)_aM|I6({4SPNpPYO)?4nA;)rO|#HX6vf8?ogj7n5UF zSI1-xnL|z%c@cWwm*)4ZevX*WyBvpi%SnXX5{T=s!8U1ZXFuq*=)Ee{;{uzJ}b2Upsng-M6U&j;Qw7^|5x`DI>-n{ G&Hf7xS2ZU9 literal 0 HcmV?d00001 diff --git a/docs/images/jwt-get-refresh.jpg b/docs/images/jwt-get-refresh.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f0620a0a571962ce69bb57a5b8e1436e2b7cfa63 GIT binary patch literal 15029 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 literal 0 HcmV?d00001 diff --git a/docs/images/jwt-revoke.jpg b/docs/images/jwt-revoke.jpg new file mode 100644 index 0000000000000000000000000000000000000000..336f5914986c3f800bab378b1152396b2d1b305b GIT binary patch literal 13340 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 f^$qV2K-2_0@2Eq@V;`b;|IZEke Date: Tue, 15 Apr 2025 16:22:04 -0400 Subject: [PATCH 15/30] improved makefile --- makefile | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/makefile b/makefile index d22122e..6b2ce3f 100644 --- a/makefile +++ b/makefile @@ -9,20 +9,30 @@ # Indicate targets which are not files but commands .PHONY: clean help test pylint -help: - @echo " " - @echo " The following targets are available:" - @echo " " - @echo "test: Build and test ojwt.js file, JWT, and test with nginx." - @echo "clean: Clean out intermediate files which we can auto-generate." - @echo "help: Shows this help message." - @echo " " - -pylint: +# The stuff below implements an auto help feature +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([.a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +help: ## Show help for avaiable targets. + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + + +README.md: README.org ## Create markdown README file. + pandoc --from=org --to=markdown -o $@ $< + +pylint: ## Run pylint on python files. pylint src/ox_jwt -test: +test: ## Test nginx. $(MAKE) -C nginx test_ojwt_nginx -clean: +clean: ## Clean up generated files. $(MAKE) -C nginx clean From fbd0f6f2bc3ca7ba9857806034f2e2cc0a7d3f23 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 16:22:42 -0400 Subject: [PATCH 16/30] Created pyproject.toml file --- pyproject.toml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fc2328b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "ox_jwt" +version = "0.1.0" +description = "Demonstration of how to use JWTs." +authors = [ + { name = "Emin Martinian", email = "emin@aocks.com" } +] +license-files = ["LICEN[CS]E*"] +readme = "README.md" +dependencies = [ + "requests", + "flask", + "pyjwt[crypto]", +] + +[project.urls] +Homepage = "https://github.com/aocks/ox_jwt" +Documentation = "https://github.com/aocks/ox_jwt" +Repository = "https://github.com/aocks/ox_jwt.git" +Issues = "https://github.com/aocks/ox_jwt/issues" + + +[project.optional-dependencies] +dev = [ + "pytest", + "pylint", +] + + +[tool.setuptools] +# If there are data files included in your packages that need to be +# installed, specify them here. +package-data = { "sample" = ["*.dat"] } + From 1c1041e42bf98b6e12fe363f656e05c4e81a3ac9 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 16:58:07 -0400 Subject: [PATCH 17/30] Provide simple test to validate demo --- src/ox_jwt/test_app.py | 122 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/ox_jwt/test_app.py diff --git a/src/ox_jwt/test_app.py b/src/ox_jwt/test_app.py new file mode 100644 index 0000000..abb731a --- /dev/null +++ b/src/ox_jwt/test_app.py @@ -0,0 +1,122 @@ +"""Simple test file to verify demo in app.py +""" + +import base64 +import datetime +import os +import socket +import subprocess +import sys +import time + +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.backends import default_backend +import jwt +import pytest +import requests + + +class FlaskServerManager: + + process = None + port = None + secret_key = None + public_key = None + + @staticmethod + def get_free_port(): + "Finds and returns an available port on the system." + sock = socket.socket() + sock.bind(('', 0)) + port = sock.getsockname()[1] + sock.close() + return port + + @classmethod + def teardown(cls): + "Tear down subprocess for server." + + if cls.process is not None: + cls.process.kill() + cls.process = None + + @classmethod + def make_keys(cls, secret_key=None): + "Make secret and public key." + + if secret_key is None: + secret_key = ed25519.Ed25519PrivateKey.generate() + + cls.secret_key = base64.b64encode( # serialize key + secret_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption()) + ).decode('utf8') + + pk = serialization.load_der_private_key( # de-serialization secret + base64.b64decode(cls.secret_key), backend=default_backend(), + password=None).public_key() + cls.public_key = pk.public_bytes( # serialize + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf8') + + @classmethod + def setup(cls): + cls.teardown() # in case process is active + cls.make_keys() + cls.port = cls.get_free_port() + os.chdir(os.path.dirname(__file__)) + env = os.environ.copy() + env['FLASK_JWT_KEY'] = cls.public_key.split('\n')[1] # get DER piece + env['FLASK_JWT_ALGS'] = 'EdDSA,ES256' + cls.process = subprocess.Popen([ + 'flask', 'run', '--port', str(cls.port)], env=env) + + @classmethod + def make_jwt(cls, headers=None, payload=None): + sk = serialization.load_der_private_key( # de-serialize encoded key + base64.b64decode(cls.secret_key), backend=default_backend(), + password=None) + my_jwt = jwt.encode( + headers=headers or {'typ':'JWT', 'alg':'EdDSA'}, + payload=payload or {'sub': 'a', 'name': 'b', 'iat': 1}, + key=sk) + return my_jwt + + +@pytest.fixture(scope="session", autouse=True) +def manage_flask_server_for_tests(): + print('\nSetup FlaskServerManager\n') + FlaskServerManager.setup() + time.sleep(1) # let server get started + yield + print('\nTeardown FlaskServerManager\n') + FlaskServerManager.teardown() + + +def test_simple_enc_dec(): + my_jwt = FlaskServerManager.make_jwt() + req = requests.get(f'http://127.0.0.1:{FlaskServerManager.port}/hello', + headers={'Authorization': f'Bearer {my_jwt}'}) + assert req.status_code == 200 + assert req.text == 'Hello World!' + + bad_req = requests.get(f'http://127.0.0.1:{FlaskServerManager.port}/hello', + headers={'Authorization': f'Bearer {my_jwt}mybad'}) + assert bad_req.status_code == 401 + assert bad_req.text == ( + "problem=InvalidSignatureError('Signature verification failed')") + + +def test_premium_enc_dec(): + now = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + my_jwt = FlaskServerManager.make_jwt(payload={ + 'premium_user': 'user@example.com', 'iat': int(now)}) + req = requests.get( + f'http://127.0.0.1:{FlaskServerManager.port}/support/urgent', + headers={'Authorization': f'Bearer {my_jwt}'}) + assert req.status_code == 200 + assert req.text == 'processing support request for user user@example.com' From e40d3ae7746d4c94c6111871f61c57a65efafb05 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 17:03:10 -0400 Subject: [PATCH 18/30] Added basic testing --- pyproject.toml | 5 ++++- src/ox_jwt/test_app.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fc2328b..42fab47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,8 @@ readme = "README.md" dependencies = [ "requests", "flask", - "pyjwt[crypto]", + "pyjwt[crypto]", + "pytest" ] [project.urls] @@ -36,3 +37,5 @@ dev = [ # installed, specify them here. package-data = { "sample" = ["*.dat"] } +[project.scripts] +test_ox_jwt = "ox_jwt.test_app:main" \ No newline at end of file diff --git a/src/ox_jwt/test_app.py b/src/ox_jwt/test_app.py index abb731a..7647366 100644 --- a/src/ox_jwt/test_app.py +++ b/src/ox_jwt/test_app.py @@ -120,3 +120,13 @@ def test_premium_enc_dec(): headers={'Authorization': f'Bearer {my_jwt}'}) assert req.status_code == 200 assert req.text == 'processing support request for user user@example.com' + + +def main(): + print(f'\n\n Running tests in {__file__} \n\n') + sys.exit(pytest.main([__file__, '-s', '-vvv'])) + + +if __name__ == '__main__': + main() + From 5a2d3113f8f977a0726210e142924e25d2368ba3 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 19:24:25 -0400 Subject: [PATCH 19/30] cleanup --- src/ox_jwt/test_app.py | 121 ++++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 44 deletions(-) diff --git a/src/ox_jwt/test_app.py b/src/ox_jwt/test_app.py index 7647366..2a1e184 100644 --- a/src/ox_jwt/test_app.py +++ b/src/ox_jwt/test_app.py @@ -8,6 +8,7 @@ import subprocess import sys import time +import typing from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.primitives import serialization @@ -18,6 +19,8 @@ class FlaskServerManager: + """Class to manage flask server. + """ process = None port = None @@ -25,7 +28,7 @@ class FlaskServerManager: public_key = None @staticmethod - def get_free_port(): + def get_free_port() -> int: "Finds and returns an available port on the system." sock = socket.socket() sock.bind(('', 0)) @@ -41,54 +44,77 @@ def teardown(cls): cls.process.kill() cls.process = None - @classmethod - def make_keys(cls, secret_key=None): - "Make secret and public key." - - if secret_key is None: - secret_key = ed25519.Ed25519PrivateKey.generate() - - cls.secret_key = base64.b64encode( # serialize key - secret_key.private_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption()) - ).decode('utf8') - - pk = serialization.load_der_private_key( # de-serialization secret - base64.b64decode(cls.secret_key), backend=default_backend(), - password=None).public_key() - cls.public_key = pk.public_bytes( # serialize - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo - ).decode('utf8') - @classmethod def setup(cls): + """Setup server (and keys) and run in subprocess + """ cls.teardown() # in case process is active - cls.make_keys() + cls.secret_key, cls.public_key = make_keys() cls.port = cls.get_free_port() os.chdir(os.path.dirname(__file__)) env = os.environ.copy() env['FLASK_JWT_KEY'] = cls.public_key.split('\n')[1] # get DER piece env['FLASK_JWT_ALGS'] = 'EdDSA,ES256' - cls.process = subprocess.Popen([ + cls.process = subprocess.Popen([ # pylint: disable=consider-using-with 'flask', 'run', '--port', str(cls.port)], env=env) - - @classmethod - def make_jwt(cls, headers=None, payload=None): - sk = serialization.load_der_private_key( # de-serialize encoded key - base64.b64decode(cls.secret_key), backend=default_backend(), - password=None) - my_jwt = jwt.encode( - headers=headers or {'typ':'JWT', 'alg':'EdDSA'}, - payload=payload or {'sub': 'a', 'name': 'b', 'iat': 1}, - key=sk) - return my_jwt - + + +def make_keys(secret_key=None): + "Make secret and public key." + + if secret_key is None: + secret_key = ed25519.Ed25519PrivateKey.generate() + + serialized_secret_key = base64.b64encode( # serialize key + secret_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption()) + ).decode('utf8') + + public_key = secret_key.public_key() + + serialized_public_key = public_key.public_bytes( # serialize + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf8') + + return serialized_secret_key, serialized_public_key + + +def make_jwt(secret_key: str, headers: typing.Optional[dict] = None, + payload: typing.Optional[dict] = None) -> str: + """Create JWT. + + :param secret_key: String represening serialized secret key. + + :param headers=None: Optional dictionary of headers for JWT. + + :param payload=None: Optional dictionary for payload of JWT. + + ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + :return: Encoded JWT. + + ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + PURPOSE: Create JWT from secret key, headers, and payload. + + """ + sk = serialization.load_der_private_key( # de-serialize encoded key + base64.b64decode(secret_key), backend=default_backend(), + password=None) + my_jwt = jwt.encode( + headers=headers or {'typ': 'JWT', 'alg': 'EdDSA'}, + payload=payload or {'sub': 'a', 'name': 'b', 'iat': 1}, + key=sk) + return my_jwt + @pytest.fixture(scope="session", autouse=True) def manage_flask_server_for_tests(): + "Pytest fixture to setup/teardown flask server for tests." + print('\nSetup FlaskServerManager\n') FlaskServerManager.setup() time.sleep(1) # let server get started @@ -98,35 +124,42 @@ def manage_flask_server_for_tests(): def test_simple_enc_dec(): - my_jwt = FlaskServerManager.make_jwt() + """Simple test of encoding/decoding. + """ + my_jwt = make_jwt(FlaskServerManager.secret_key) req = requests.get(f'http://127.0.0.1:{FlaskServerManager.port}/hello', - headers={'Authorization': f'Bearer {my_jwt}'}) + headers={'Authorization': f'Bearer {my_jwt}'}, + timeout=30) assert req.status_code == 200 assert req.text == 'Hello World!' bad_req = requests.get(f'http://127.0.0.1:{FlaskServerManager.port}/hello', - headers={'Authorization': f'Bearer {my_jwt}mybad'}) + headers={'Authorization': f'Bearer {my_jwt}mybad'}, + timeout=30) assert bad_req.status_code == 401 assert bad_req.text == ( "problem=InvalidSignatureError('Signature verification failed')") def test_premium_enc_dec(): + """Test encoding/decoding with claims for premium user. + """ now = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() - my_jwt = FlaskServerManager.make_jwt(payload={ + my_jwt = make_jwt(FlaskServerManager.secret_key, payload={ 'premium_user': 'user@example.com', 'iat': int(now)}) req = requests.get( f'http://127.0.0.1:{FlaskServerManager.port}/support/urgent', - headers={'Authorization': f'Bearer {my_jwt}'}) + headers={'Authorization': f'Bearer {my_jwt}'}, timeout=30) assert req.status_code == 200 assert req.text == 'processing support request for user user@example.com' def main(): + "Run tests if module is run as main command." + print(f'\n\n Running tests in {__file__} \n\n') sys.exit(pytest.main([__file__, '-s', '-vvv'])) - + if __name__ == '__main__': main() - From d3f5a854c8cb46c773bc8d1b91cb4c2a2c460ebe Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 19:24:35 -0400 Subject: [PATCH 20/30] add more test targets --- makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/makefile b/makefile index 6b2ce3f..b35e092 100644 --- a/makefile +++ b/makefile @@ -31,8 +31,15 @@ README.md: README.org ## Create markdown README file. pylint: ## Run pylint on python files. pylint src/ox_jwt -test: ## Test nginx. +test_python: ## Test python code + py.test src/ox_jwt + +test_nginx: ## Test nginx. $(MAKE) -C nginx test_ojwt_nginx +test: ## Test everything. + $(MAKE) test_python + $(MAKE) test_nginx + clean: ## Clean up generated files. $(MAKE) -C nginx clean From 0569a4bb7953c654b75f1e11c5d42857a884ca8c Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 19:41:14 -0400 Subject: [PATCH 21/30] more docs in README --- README.org | 58 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/README.org b/README.org index ba89c38..fbb36ba 100644 --- a/README.org +++ b/README.org @@ -2,7 +2,9 @@ * Introduction The =ox_jwt= repository provides some simple tools for working with JSON Web -Tokens (JWTs). +Tokens (JWTs) as well as illustrating some use cases with =nginx= and =python=. + +** nginx For example, the =nginx= sub-directory contains a fully working minimal example of how you can setup NGINX to only allow access to @@ -21,9 +23,26 @@ This is useful because: 3. All of the encryption/decryption used is open source, verifiable, and modifiable by you. +** python + +The slides in [[file:docs/slides.html][docs/slides.html]] provide a detailed presentation of what +JWTs are and how to use them in python. + +You can use JWTs in python with or without nginx. For example, you +could have nginx do validation of the JWT and then provide the decoded +JWT as a header to further python code. Alternatively, you can do JWT +encoding and decoding purely in python. + +If you =pip install ox_jwt=, you can then run the command +=test_ox_jwt= to do a simple local test of encoding JWTs and decoding +them with a flask server. See the python files in [[file:src/ox_jwt/][src/ox_jwt/]] to see +how the python example works. + * Quickstart and Demo -For a simple test of the system, you can do something like +** nginx + +For a simple test of the nginx system, you can do something like #+BEGIN_SRC sh make test #+END_SRC @@ -43,9 +62,34 @@ at the command line to do the following: 6. Uses =curl= to verify that you can only access the protected location in your NGINX server if you have the appropriate JWT. +** python + +For a test of the python implementation, you can do +#+BEGIN_SRC sh +pip install ox_jwt +#+END_SRC +and then run the =test_ox_jwt= command. + +Alternatively, you can clone the GitHub repository via something like +#+BEGIN_SRC sh +git clone https://github.com/aocks/ox_jwt.git +#+END_SRC +and then build a virtual env and install dependencies via +#+BEGIN_SRC sh +cd ox_jwt +python3 -m venv venv_ox_jwt +source venv_ox_jwt/bin/activate +pip install -e . +#+END_SRC +and then run the tests/demo via: +#+BEGIN_SRC sh +py.test src/ox_jwt +#+END_SRC + + * FAQ -** What are some alternatives to =ox_jwt=? +** What are some alternatives to =ox_jwt= for nginx? - SSL client certificates - but [[https://security.stackexchange.com/questions/198837/why-is-client-certificate-authentication-not-more-common][SSL client certificates are difficult to setup and maintain]] @@ -59,7 +103,7 @@ at the command line to do the following: - [[https://www.npmjs.com/package/jwt-simple][jwt-simple]] for javascript (used by this project as well) - [[https://pyjwt.readthedocs.io/en/stable/][pyjwt]] for general python JWT tools -** Aren't there some other easy alternatives? +** Aren't there some other easy alternatives for nginx? Not that I am aware of. Most other approaches to JWT validation in your *APPLICATION* server instead of your *NGINX* server or proxy or @@ -67,9 +111,9 @@ require a commercial (i.e., non-open-source) server. ** Why do JWT validation in NGINX? -Plenty of applications do JWT validation and decoding themselves. That -is a fine and useful thing to do and can also be combined with -validating the JWT in NGINX as well. +Plenty of applications do JWT validation and decoding themselves as +shown in the python examples. That is a fine and useful thing to do +and can also be combined with validating the JWT in NGINX as well. A few reasons why you might want to do JWT validation in the web server instead of or in addition to the application include: From a945fbdd261facb9ec0e6b8df7ae7d0d0dd67028 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 19:46:06 -0400 Subject: [PATCH 22/30] fixed options --- README.org | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.org b/README.org index fbb36ba..78db994 100644 --- a/README.org +++ b/README.org @@ -1,4 +1,6 @@ +#+OPTIONS: ^:{} + * Introduction The =ox_jwt= repository provides some simple tools for working with JSON Web From 515c1a16904e7be18f86de23be2e8a35c0353c46 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 19:57:54 -0400 Subject: [PATCH 23/30] mnior fix --- docs/slides.org | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/slides.org b/docs/slides.org index 7c7024f..c1b494a 100644 --- a/docs/slides.org +++ b/docs/slides.org @@ -319,7 +319,7 @@ Base64 encoded header.payload.signature: #+ATTR_REVEAL: :frag appear :frag_idx 1 #+BEGIN_src shell -HEADER: { "alg": "HS256", "typ": "JWT" } +HEADER: { "alg": "EdDSA", "typ": "JWT" } #+END_src #+ATTR_REVEAL: :frag appear :frag_idx 2 From d52d263a938815ed3edbe2686741fc25d6374dbf Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Tue, 15 Apr 2025 20:12:56 -0400 Subject: [PATCH 24/30] more minor tweaks --- docs/slides.org | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/slides.org b/docs/slides.org index c1b494a..83f887e 100644 --- a/docs/slides.org +++ b/docs/slides.org @@ -48,10 +48,11 @@ print("This appears after clicking") 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 -- scalable, performant, distributed trust +- slides/examples: https://github.com/aocks/ox_jwt #+BEGIN_NOTES @@ -614,6 +615,8 @@ and separate validation from parsing. - Validation can be slow for some keys - Can use middleware to verify signature - e.g., NGINX can verify before passing to app server + - See implementation in =nginx= directory: + - https://github.com/aocks/ox_jwt/ #+COMMENT: FIXME: consider diagram of NGINX idea @@ -796,7 +799,7 @@ 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) +#+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 @@ -804,7 +807,7 @@ so it's still useful to have a high level understanding of the basic diea. - [[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://aws.amazon.com/cognito/][cognito]], [[https://www.keycloak.org/][keycloak]] - +- Slides/examples: https://github.com/aocks/ox_jwt/ From 82d98c8165dd69105b848fe471ad12b6f98545ad Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Thu, 24 Apr 2025 14:10:43 -0400 Subject: [PATCH 25/30] Added issue route example --- src/ox_jwt/app.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ox_jwt/app.py b/src/ox_jwt/app.py index 7f91713..3a6ffdc 100644 --- a/src/ox_jwt/app.py +++ b/src/ox_jwt/app.py @@ -73,6 +73,19 @@ def decorated(*args, **kwargs): return make_decorator +@APP.route("/issue") +@requires_jwt +def issue(): + "Example route to create an issue." + effective_user = g.decoded_jwt.get('proxy', g.decoded_jwt.get('sub')) + # ... Create the actual issue here + msg = f'Created issue assigned to {effective_user}.' + real_user = g.decoded_jwt['sub'] + if real_user != effective_user: + msg += f'\n{real_user} acted on behalf of {effective_user}' + return msg + + @APP.route("/hello") @requires_jwt def hello(): From 3cdafc185ed907ed3f3b6d4744b8e19e561a325b Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Thu, 24 Apr 2025 14:10:53 -0400 Subject: [PATCH 26/30] added test_proxy --- src/ox_jwt/test_app.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ox_jwt/test_app.py b/src/ox_jwt/test_app.py index 2a1e184..6578efb 100644 --- a/src/ox_jwt/test_app.py +++ b/src/ox_jwt/test_app.py @@ -154,6 +154,20 @@ def test_premium_enc_dec(): assert req.text == 'processing support request for user user@example.com' +def test_proxy(): + """Test how a proxy user would owrk. + """ + now = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + my_jwt = make_jwt(FlaskServerManager.secret_key, payload={ + 'sub': 'Alice', 'proxy': 'Bob', 'iat': int(now)}) + req = requests.get( + f'http://127.0.0.1:{FlaskServerManager.port}/issue', + headers={'Authorization': f'Bearer {my_jwt}'}, timeout=30) + assert req.status_code == 200 + assert req.text == ( + 'Created issue assigned to Bob.\nAlice acted on behalf of Bob') + + def main(): "Run tests if module is run as main command." From d3791f05703b68017b5d956e4a87370a3efd9fc1 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Thu, 24 Apr 2025 14:10:59 -0400 Subject: [PATCH 27/30] More work revising slides --- docs/images/jwt-auth-vs-app-auth-response.jpg | Bin 8412 -> 8957 bytes docs/images/jwt-auth-vs-app-auth.jpg | Bin 11578 -> 11818 bytes docs/images/jwt-auth-vs-app-start.jpg | Bin 7257 -> 7637 bytes docs/images/jwt-get-access.jpg | Bin 13438 -> 13782 bytes docs/images/jwt-get-refresh.jpg | Bin 15029 -> 16391 bytes docs/images/jwt-revoke.jpg | Bin 13340 -> 13811 bytes docs/images/jwt-use-access.jpg | Bin 11020 -> 11301 bytes docs/images/nginx-example.jpg | Bin 0 -> 20572 bytes docs/slides.org | 158 ++++++++++++++---- 9 files changed, 128 insertions(+), 30 deletions(-) create mode 100644 docs/images/nginx-example.jpg diff --git a/docs/images/jwt-auth-vs-app-auth-response.jpg b/docs/images/jwt-auth-vs-app-auth-response.jpg index cc82d2ee089b131fec6523ece0e1b35df0ba712b..6dae253654f6f57bd49862de80cc4424e31c619e 100644 GIT binary patch delta 4054 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 delta 3578 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�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 delta 8251 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 Ae*gdg diff --git a/docs/images/jwt-auth-vs-app-start.jpg b/docs/images/jwt-auth-vs-app-start.jpg index 64377176cbb401b0460d18cb012f6854f60f6a66..0f2b437da0d67c380bf1b0659cec32358ceb91f7 100644 GIT binary patch literal 7637 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 delta 10146 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 Q|CzA*3l)2e1jJ1LFHM#3!2kdN diff --git a/docs/images/jwt-get-refresh.jpg b/docs/images/jwt-get-refresh.jpg index f0620a0a571962ce69bb57a5b8e1436e2b7cfa63..9c665228e57799c0995a48f56c9e3cdd53402bb5 100644 GIT binary patch literal 16391 zcmeIZ1ymf})+StNqanCU@C0|aBzSPQkl^l4pmBmb1a}A?f;$9<;O-jS-Fy1I^UamK zbHD$)cm7#x)~xAXXLZ-9s#CT1*?XTY&wA3VLTpa~o3%X&Ffh8AWkP3Kw41my8rf#wH(~t(++Aoj+Qc zJGrAk7JxSZ5+Wi95djGV0wE(Kp`hWQqoJar5n^Fu;E)oLlaUgUkWf%F(@{_|Qjw6* z^SoqaW#i=JB&XvO;$;_P=HO)iV-Of*WMni{Gy-&V0`?aqFWCR(52PKyL<06Fq` z0a#2JI7}Ew7eEdGFbGg;{~-83J}|Iw@Cb+?BxDp+=mj;;0azF~I9PZ%1O#|^=+$1( z^8h?10@e#QF+^;ok045W9CqKBY$U4Jl^wXsV<*%c1`dA6D0uh;ghVv7bo4J@adL6< z@bdA$c`Gg92xXS3nYo3dle3Gfo4bd9Kwwbt=aA6YxcG#`q%X-S zIk|cH1%+RWioaJ?*VNY4H#Gj}?CS36?du;HpO~DQo|&DSUt8bU{I#{cv%7bCc7Abr zb$xSt_Xk}t0Nj5k>#vmkMi(ZOE?9VYIC#(>biu&7LK_?=Ji-e$L@Y5S&_{c0N_Jl) zoYyhgl^w`b9LguS1`cB=c+{M0G^c-%_7}?jYlQjzTa^8ku)onY51_%pKqn6l6A%He z@91-UP=0Hyf@15^PCAGDJu^}blSju7Bh2(Pk%I6*CC!}Vn|)OX0DXPBM90OI-1PA~)DAF}HTch4_H9-dW0fQ{!-LD<$2{^lj(@X<5_iI{z2#vN235|=Xm zU&2HXpkHWQWD)#GFbOj?CXNFr{kg#mShk%hHq$))I(`b1+4~2sCrphZ=)F;{8n>E1 zdRk*ToJv58w1Y80X-lS1D_(C_@6~Ot9}}r$xs)X+OJ_#UZEvEDvCF#sPWLLHj_`iZ zt-V}BamE>4Vb!~kse9=%_%kQt!eTW(6HW=I5wm9Qr;d0(_gB&zS)4HYm;Nka7k!sr z`0`?o9F`~}=7M%{$clkyx~op_{pn(Qo>?ZIax&3H9+%GF-5@11fs}|i`sVBCY>Kj5k)!mrpx6~7+IY?}9 zs<5OGBH5*jQ);8iI@Z13=$&}yn_Y)fg(*$2sNO0kh$@yuD%F0w;@i##T)2q>3yO@A z5_vEeb)@9`4f)hNC|0itK`B-5Wg!5v)%=8+wsBzWuBF>dn?-e!vZvr;FJ(}KX#`=^ z&!bsg(?|(cbq0N``F&x$i5h*ZcTdijOQn@s?+IZ7UY}9KTv2iQJZJa9^#P>-mnjTG zv184+8#Ix}>eH9#B1#i_OybMAjD?46XMHfdi+f_qh+^t+gvG9G7P}R6{%dWClK$@^ zH-&nd?5!EUH`TjpARVl^_~1k%x}(p+O_^1^nmDMrU%}a(m#AD1YarTq{bjK>*}0aD z(IE#E;pa!(KuzZQEEspszPxXWhr=4MF^u&Z0vx6@&(vyc@8Pt;JNuzfS^D>_gG0*k zLp%#IxWN+wML+7XIPjy z6%J${m3tUWW&O=gnEUf|>4mLA2C|uYyzeqt>EUW}nu8!f7k|xl2a|-!Bn#&f_^pU> z3u|L(kW`=km-Eh-=m}IA!KAh~EnGsx`av`AW}M$fsZ7`!WjZ~53|v!lPT+yXrapWj zN+1Z>iGm8J|FU^&cGI9dJ3vUXMTtm3Qg;$DQM zwI}>FI<;e7G@i+<8(NcNJ)pox+k`=|bF!aeT$VhUDc^!`&J;L6Z;*sUnDk4KwcQ3o z8)zwX3zMC)iO4VO49g}=DBJRrGFF90?KUH3*i;aA-<<0(dPWVY=(I{UDk z&5BfG{X`7|`#Y6l1V%!PS7J!LbtHz(cG@?wI`&06`=>g)GagR)%cK^v{j|}v7qKs> zUw!pfCSXQHZihz!E-e`PX-8}1-mC?suS;=3falUh1*w+K)R-2^J#ocf&GAtre-6fE zGys=${K7QT;o3qgVNPN9F;=_g=BO>3VKyaH70KKy$zmvLuGZAJHW)I%MR8G)g?gi0 z*z$Yn?=%#4`N?0>UVSC0!V(1>q$}u7aoa+jGSO~=PPvXN=Nc*Ti^`^pmILS}apV6PKoD~8%HLmUcYDn{ri3RjTAT}DF!7`8o59hE zpDZV@T{l|uF)J>$CG(`aaxP@vuQZ!<2=(pzX82WY+<@1<9$gYweEWH}C)Mx1wLC&O zjMV$Hbd$toBXYJpB^20RI}r(f(Olc~G^Ax?ZOHb9tE+;eIAhL*`m|RF1Y5$=*@i}) z@+rjFE}}4P9;dBdpJ6xnVq*u`?RMr=MC1-zv!?2t;YlL0;^<}CzfD4?S)6c>cw8s_ zJkqAEvw~`lf9ZZi zbKX4L$tIKwZa~XprK6)0+)fGjByuu9%Gp`iZ~OQKsS5Af@hv5tz-N?IJU%`;>TB9Y z3wqeHtyI&mTvd7tc|Fq~?O|+JqhaAN?2t>_FmG5oTba(DHRN}@jbi_*G?CD^Dgu!$ zF?fUm?ZF)$=&e=OB|7u6=u6cMw#cD-s&79VB2l4N*#a@)*!^2|J2^glvI@7a=Ib7P z%S0}myPcf}w88!AQ*8(6<`5vaoS`HOHkAIq)*9qopJea@+7?@&0%xwL_mB3}hrh9Z z>=RFco7YjQylgy=?UvlO-Vpcp)AcdoHw6d&Rnx383wpSrPqrsdbDxCDq}N)=u8y^* zd?HmI^X_%hj_et;rGO81Qhn`kymOhICv!K3N=*}-sJSJ2vo($LgD&4?2|oG~xJKtt z(DADGfPgfV1GB7W9PJr6brYOIliq%k}nGLzu9Q+6+z zXgf81DJGc=>mzxvDBDiz;StF;=s=a*Wx6>&?2LkJdFC_Nx^Jw`7Ujre&-D~&I=?`G zhDHek{KuGXW32~{6YsRTLF1hK%GNg|hiKzeICL~>{>Cu9FQnk@#~TOeVS}P_waoa9 zv|TbuV&>m+_madlH-X;4Zbl1L`N~C!jpvNERbu_XI2d8@ z)=@hzKAeCU{-UbU*?n9fL@mF^j?mxc6-RnQkEdFTVW;?Eb;Csg!&kGjWUHZ{!}lG^ z6~s+gv&Ph>u}GjagGPROIP_+YygeS&ugm7gq?0{-g#p9&MB=DXY`C>j8WuiMBCerf z@5Y%;PMKyt*U(Qh1o>#q>y+4>eMGNMr1CqOr6U1~+1<;7LIH;O8%qd4eJuq6>>>f9Cp)~4I*-koS zVnOzzZQ2>R+7`v#L`r9{d5T7FRGVxs_eop!X+-Qnvc|$pm~i3(AyEoG34h|;(aT|B zI}HLz;5F0Fw8K0)pTWnq zst{oAE36P*uSab|zRsS9t=}bTm_XnPNr{&k3@JaM7>JL6in9E-Xr^X;1!#7COfIeq zbVjZ;_gFZXi$dmczGHn9eY*5|`TuJa2@YPLW)ZY3vgD5p7EXds@oFixQ zTνu~+-x??vYS>;Go68e$oFh@eP`d5YkCiWX`@3a?2hH}TZ#%w=}}(TnohP~exy zKSt8O9q#R9JyUtY(#Hai1{XnqRcGjIlIH&2QC|Lk%RnXX5hf})GZA(Z`2!G_kEm_g zDfbqnU3dp#F+i1nXf=J#e{T$tQ!EvND#asfk9g~{mc%A=#TH5=>=`Go!Q0v7?rlEg zD&1|kdvzJsb-~r<+!lAAZC4CMjC}0QG56SEBM&6A%Oc37sz$57axNe>H@RuR(a}UH z|MaZngRW&h)Dun%Oyu%@oeFibo|^bp_F1bKdtM4E>R%X!z>J(Y;HD7tgyQDi*>jy@lJDx}OjA>hUmF?g{-VP~U4(_A zgklH$S)&QYH+1ts90Fji5V@IZ+*kfQ+jabQk?^KB^M*te++I@p+XD9r>6po+F=d+J z%lq}`XqAjU%^$XjgO$=qOh-rIFOJr zDqXnrpHYgaD{p#GTxOw?VN5UwXz)Lk5z6VSQA$(U)Q(17Dn?z4^bg#i z)A{Qd1bbxUV@KVA<#8k?cc%tgH0IRsrS?GxqA2W|{=1PEFtcO2y23B~#ZPe(`IEvg z2Aed?^3AF06OoQ`6J*lZ3)s1B1u1&?h9&6gPd8aPM@fm5BTwe~-A&Pd^pY=NvNe5N zc8Q6WvMvQ#0Q+X8whoQ_^&UiT}LYHKf=a zjjp!-o~9PPA-oz)AGOurXLG9*?tqr!-BfA`e%35PQ&8jjtjWuzcKqi@pYMI8%O2ke zt^(C0DFBRt2EjBR4>{azVP0X{lkLEYnlFMH9<-5ehC!It7K&6-F|l)DG@oheDPZlx zQT8v=1ym=FZRED<9E;}rLN>#`WNUJi$3d;cH)(lgIqBB%Kz{~Cfr z$vYz?<}w9)&bLxfufdm|Ji^jkJDk5Y#<)l)tU8Q|O& z|7K9`+f4Sf;Y?}5h0d5IcLsCZ&qVxNQ(G2+5M@Q*s9Pg1%aSSQC7HpV45G?9%a@$# zI>XW&#Dr?Gw9)MK)=lz2&vI{uTgFOf66OtQvyjkI={Z-`LQSVOcQ1OhR_-~wG$=DO zR(d1<dOB;X68f&s-H{kXo49cPmyWP+%TFh=JkW1GkuyC(~JAT-u;b?AW!M-iu(4uGz zvU2k=_TK;W(~A{@*E?wg1ILnRE3*)6G;gkNJ)UunU&J7b_IW3qNF*t|vc0d5V$TukL({%Eae!;?C(B6n_^oxR*k*}{j(@nrw;xkvRi!r07J1PLM4J1RVtZqDhmBfp!1hJtn8ZuAu2;X()X$L)Un4EEc4&DU zo;|EMo%b$hAvr7H?h`q$TrskX3Fjl^EDqV@MYsyewWLY-@g@)01vCEi^4)w^*qM?) zT(!1ODsirmsnC!iIB3a_>z0}g|NI#{Z{Kd{HkUq}#Y|&hL1p1hu{DQw*b9E_URBy7oQPS;JLSY z@EN%C8TlPBXRgN?t#29F9vBLO=vRYy4HPSTvehI-fp#3=?iK1_eJSH+W3mZb5Bm>- z_Zq$5DHy>py1xp$z^LzgEjhIV`fXjF^jf;ER65ZjVp*x3P~^o(JrzM}x`K_*D%Vy> zEXXLwtW}i0PsOr<4Yx`i@?ydnV-kPFg#VHzN!w}){W*16&l&Z-XLQKX+4V)}{Pg0> zSalT54SN*UVrgpMJ4Fy?v73haDL8~n==HX|HSPR{XsS%qA$!kRGHEBM%c|3ikld4t zJV=nI1Sb&{0>Iuliq}U@OM$D);KjF1UN?w_#Y0WK5O1%b2VT-wu!Z^9(qrt=Xsejt z5exHMikQCbiLca{eI@ZQBR7-_ir-Fb5(@X!CC0Y~rWs7$ylOrCj8m%$RheGCUR|fs zK{)}!rO%^=2%Ym;EhbT>%Ht?J^s;)GilqoYAeRUr4qGD$@Vb(KA-IKCz2oUL%ZZJ2+)EtMHS6KQv@_QF(Tzq>r zW;r>_O=3g(w6^A}K+yzk{NmhO5kD=z+s!*ig5LYXJxdk!)5x$KU0pFRrddm~fQf2u z?4L0&`WJ^kt4Y8JW=uZPbvH`XxRaK}CwW_aBmUBLh22gaj*F0xCyplUyyH2F$kUCA zuA_MpkfI)yHw3n~XOx$b#jzxaRCTOxs!+^BgF6o0FQbiJALq-jNt~9{t;R}V3?7YV zY>VN-EBe}*XYR@$a5_S*zN0sd6doyhq10cnbC*Um9bsIo!x)CJi7aR!3Wx9ksSo~q zqzur@38e@AXyvl%5Fmu73!O^szIHxOE`O2ihgdQN9R#4PctFz7(QIWL%bWSctLYm z%@=%0^zd$A8WcJ#9kORR*1vWr3L7M@`%vlqr4L>aG+I5gh#3m#lhYgeC^VFMM&a(b zxbUYh&SW!P6urfd_{DdYcG%udDB|v6T4vGhh&Em1O!9S<9~n>7TRG2&*~-T8#r~!_+lvt}PfVH$f0>1$S~YdCBr~m(iY_s{N6?SK$WfAmIU8;FDkHf} z{4vguwxnoR#HwCGpv;u)U7#4FA)|vR5V!09UMI^fxI>u!G+@5G!gFmqk(7Rc>HC6| zV)3sGxrw(vqz%Lp-pcM=$!jeNolNE@HRa4s8?LBwXusxodITF_OSYqVzORyPQ~C%^ z_bPs$JXGITXs&CjGZ0N&R-(k>KYR9S$d3G(vmX1k)@eL9e&RW}YpqZ%q+{UA25}F+ z-@xKs=<&{9Wcz=}_`moI7k`>`%*BGNa}}p8A##F53*O3s)(iyA?@QWH^<08=K9k>@ zlf*<#bvru&8Y9|PNhwjCX9o7C)4?kl+wi|$pq|+9HmxyFqkF;=!qmQ!=SeJ z+TvhSrqZ5|B>re0lh;3a@;~o9;R-QcwSBgGhcN7q;Gh0#6E}9+GTB%ZneNxKUQ;?g zUB?k^VEdDtXVIQFPg18#Y@Re(I%(G{XLl>orR2%RYWoJSCXHXbBxyk-gh|>$Lt8B; zk6ef1M>Y8Oy7(GI-r4*TTfF*cfrKt~+{Yp939^F8O%JgnN4tnSw; z0us-J6@LvPtbS_ftu%<=W~yAF{aG{h(bU-#!dsIiVy|C>bQYOnzM^E{Qi}=}p9j4H{WqnW7K-MfXPOoq zse$)hga+|(Ez%So=(&J~p^1&tN=Rbj%dhVhtUYu4?HN^u^*&9;Ld$(8X>0_Fpc?a~ z8`RFEX$`z2b;m*Uo-umQxk2_K8Wop^0Qk6Xqz#A4IyVfjYp0o<&DNTDrely^B+9*B zyXpr8lXBWTa?tH8qOVT6&s~lP=?J*uPOg4&rSrV2ZH-f^ZW`w-(PFa7YL2CD{M?iG?lmhGdV4w2 z1_9qN$h#GLEa_eIL>KlKTTz`P9*5+7I3FVAg}z>brO}wtlzfcN74(I4!47M8Z1aJB zv;HF8lDDM}s+hDcX->QIKD-s^L+iGf^JWKKQWdO|^B7AkXis z?BmUIvhHCcE(zM~seYq~2iAz=&Y3ZUEHF<-e)zwOlEc$57=HOQ9vvaGZ811w_qeTl ze%XI4N@|TWDMn)96N>}zu+g2;#mWCABk=5vBm@Wr_uM6ZUSZ1kN~}^jB1S7(AMQ<~ zPkvaJ?|eDz26c^F#tqvhR9wpP2Ur-}_o?V#34ak*TxlV?|X1<97B=2g4bmA8rK<fpx*{~U=b8nA088)VaKnQvgJ%_tK4uZm_as1G|cdYv3v1+Sz??Xty>AUBVK`>s|e0>n?g$Yl*qe^Zh#h^wRdYsNLo>ZG=7_p3Le0P%+HyNWUexrgY}l!;n%yA1EIw6 zAKKY72<2HN?PGLpeQWM{AGAu`T$o&D)TKu)T+Tb}zDEpo?ViPjiwxtBJr)zU5#D6X zbi98#Idb)8j|eZHrtQH~iZX>pYGmo0z0`I47_*0M*++ob6SY1=V98Uts)0M8AG_P~ z$|*eIl~$NQ+Mx-l=;rAJ(WnCYRI@AISuZF1SGUawe}ml zdjs1tVefn+@@kD+m8;kT1x^T9`wa>OKQJQ*{2*E`DKHm?sZ3_fl%HZ$MnEdt>gu;` z(X6!-Ly_PPH!aG-kogx^s^3>Rxb)Mdfa@o;N%b*Ty@leX1>ea*Y-Ah^2StpYTF!+V zE>)em2`g({f$636GrDTy6h)g zE+!2{pD*j-!6^<6j1C6?U`X?s#CK8UfvjrBuscP6E@S-)UEXundMrWPtl)Noq7p8y z!9kwA;M*sGwKi;XBK-hoxLoP0CA^mhHWyd*VNPSx{#$Q5u8f}Z8gTee_=)nn3)7gz z=)9p$yo(*U-cU$fImpke`w|Vr!TVnO641GxY~$LwZM+K=GOt)`nplv0oA`@a$rZ=W zh5f(;?HLkWxf_KBY1xf!7nZJ`CEh&F#F7R7RKor|%}b!?IGl-s4ZMX6(TkKg)G4?r{E<6BIYdfW-_m?`;?>Mab2(QDXgm@4i!^ zz-(fr*SIMEuv3y#x(1ijj;C=m{q|y`3{VFXo6l=J&=BP;xCLD@GE?acx^7A5rw$P4hm0{Ydj|ZA- z*0ZQikFwsY+OoB!*N*nC1q90iK>+886taYsef{$RseK>M8ChMEKsfzR?@no}$hp6D zAf`p~OHz7Mf9Q@76EEkX!ISF(7;f4F8$D-ldG%;efz0WB;2LUwOG%m_} zz6;7`(|zVNt;S0Aj5kgh|0OvDKj0849Q^eNne>mmV&_S1?~?s)oZi-uRH_2#tlDrz(4ek`*7r+yf3Kp2%M_; zTs!hrQ{3F1raQJzE1DPM?7MGg2CxSb5-YJ@6|z_sL4jrI>`ub z7e57)xNgrk`mgIDJM}r!O;>MLrX_^_Dvy@fBzazzhJ{2GB{RlsT+3ohZPaMOBf_2* zi2;ha8HJBm7xphpSJj(edR=>$5YQ^Oh_c{Aji|1&L?}hEF-ay?_P65D|0ojuE58gW zr!GmZj4mn5F$7-vGA4-x=TX&fC8z^3uY5Mf66Bn!6%m-o)vISXQ?2EDoOPnzPPusU3;gE zK^iL2?H3f+I56;yUU@TD*W;oEdyDMd^lR~~9fYJ}7lKA`lPJ)<9iS$pAX_fr6esZx~ ziF6U?HIqg2T_ohM`Ed5=VNnGbtc8aeWO=br-GZ=qdjMRae@qa9Xx3$k%g9yr@phkc zj&~D;DOZO(y4vt9<~E1)>nkN3G30-1t(iQaX#Ji{S5q%Wt^8sw{({S(J}fV;oj<%W zuuSU%jo!`VUKn}Fk)er`CZFnysXe-h7k|;tD}gr z!gtXMOEiAllj{u?=wW3v%$zLGWxkm$(;B*OTZnp$MyCRK;gAfEgs>@y@D?} z*%@P`vSUU!Sq1$t>=m*wGBfqYF%CL$gRjyH=dbegO|e;$>ve;uY#h^@ANeX|E!)s; z-pDkb8fiBt;c4LQlUNJq(u^j@H>gEyx@^Wu!1NLxSk~tF2nKhv?h{D{I345mSC}P< z%q1~=?fOt_Hq;TF$%bdhfrwV#{?Wa?hoPnVv^VJZ=jnDFSsu zI1ovi#v8RGtBoc%wC>Qi>tbZy>K`2P?pDL2ygktXM}4f^mbmtrpdP+oW}87;xpivh zWVi|1qS?Oihaf*BC`B^oOp(;mGPa#U+bl0IX8zctQlK&sqAy?sOxHmG3+q<~M1S=+ePx3|Q&zFv*V&O9f-P;>Ej!D}9@v!TV1b|KBnH|L!q|GZ@3{?%}nNhFonE>NSyOhtakAUg099rvDF*+N5(gaMyKo z)~mfOSSjNE_JFE7z4Ba; zhqDA>=yt*;>Y-`Ro~vYg0t>)cm2;ZH9V%3+^Rn9SsUC()s>{xZ1pJ=J%SQ-;%o35reeqIpS& zHn{;z2%sWLYK%L8Izp-i?(K=m*^_<*@2mzwT$%z)VPm698y6^(LwnLX} zqPhqmP)HTyfF|7j%# z(WEBJJcFV}lZ!#^%Znu(pM+T2r!(H$_QDI|$EJsEJsa)cgXr_z=Ttv7SH8j+jRpb(c-p&2w z!F;6FCGjMQviPa_V7T@W;Py$@pankA`WVNvQw1Q*|BjYN`jT^ncqb)rv@*qvxxc-#9px+pRwjr-pAmVsOU z3Kk0L96K3>ff_EB5RsiXlMvv9AG-e`a_mn%GaBN6L*_~BP25v}kOEXF%0zAs$$vX` zc2Wer{L`)f3xjdx-4q<%cZtl-2L~T>HU-sK=5Z*X`Hty9(McsH9fOfI<1;UL9uiVn z?qPJnJ2#g90+7tPs;Z13tcf7yi=~CSt1i;v-^PiqkaU0n@1#9oq{{>Z) z?t*c1erT~7s-_Dl@7#6?%8{AM3-wxj<*7a6%}foJqbtUSX~ z3DGq*D?Id<*803VlpM0-ipLfHp=*UGU#N7g_)UM#M5P0*npSH!46ifGaoAivJUZc@ME?gn+tFcujLLVbWqxr^BPY z=DbGm!6%_p`Q;UP9%QRK>Vt3n0(~*wPh`*81SG$$x4NFJ{5U4HI2U&;+~rXhB^`9M zTQObOk+AFCgQ>7Rkq=%^M_orxs0(r4oHBjtPAi(%%()$XX#Lm;-R1_Jg;JP(TuHTQ zr$ekc8!qDd!?9UP#C~&v3pJ%d@(;)*Q`>cNO&e387NBd?9KMM+j}vUs?t$QixtVz% zevzAZ7s@ChHmwttc{&nR$#gNQMhPMDY-Fnj>4i51GiqZO5y}GtPnOp5qA~FS*w0_I zy0dz#-Flo)s^<^2lvXXs@k^Knerf+{C-zFL_W>jb4s_=U{?%M_BH2k;Rzv)iGUZA@ zt8Bxra-j5k-B6DLJ?}8zt|(czxI49LEA1)H%w1yo*Ey|4^SjbYw)$z~2+B}CnOg@P zq|lGiUh8xH!;(wS-?tXou8EMYkU!r$TQa)~&oqo*Oqi?X{Q_GOXuKJbXMNRA>my`u zljYgZgr~-X2@O5HrZm5)+$7iz6jX?uJf+&K;5Ey-x9-T)CMx?d|C-gBR&=F3EahE& z{`v~G1=fd8x^1&`4m^vp0o{Yu3B~%xf%~<-Lx69%y-!g=jg5cBll?29!oPJb1z9x| zUiJI)f6+p*@KweT-~&U{BRM*B??!s58UOn>n`nzfbGypcn|_-2+)roUUc}$~F-U|0 zeZiglx_DI!#d^FM9N7I#2jeo&FV$kw8l$pt<1^8lnMl(kQnZ%JS^^3zrDl{HVDUw| z+L|}+j5dl!w8Q~NS7DfwvX##>UD!Ld$HfdF?DWNNJKc`enGPI^Je^?Fma@l^dA(|) z06(2-{)B#PkrMCo%zfV-1}LPI?cjkacUHgC%hqarj~;Ky$TNf_UiWaRA#$CEgu^I2 z0}3ODB1%)AWQLL+eab~HK5&WMPQr zG(&(VEIFMgZwPQk1P+8Q9~LX;&`pTZ6A$!oqx2ObP%{w+Efi+9Jp^U0LH8G?)IB^x zfK5&afC5~gFpNNVGEz^qZ68B`7lk)a*94ii3<8LNZ_!15_z1y<{=cXHZ{qyM`{sY$f&v}~ zx9`6CegALY{qOEMyXWl8xzCyFo_p@+xvu+quKW#R60r!}dZeJN0HC0t0MC(c0C59I z$!W>UK2_HeqceZWZDns}PUp(UbB~VA#mUmvoKE?XBHbemc||%`k$dc1bT7?bnYg@m zrgLyHd2Q+Jj)Ry1Pad43fptk^26c7j%4TO%4 zhK5}2jr<-!BSI&p=aa>_t!aYE;6TFfADe;2_@KP)j@Ix#lfVnd0BoGQq-5k2%q*;I z>>PqZ!uLf)#pE8!D<~={KYH?1TSr&#nZ7AfSmqX%R!+_?u5J)_kH9xU!6Bhx;c@TY z$0sBveMrvC%FfBn%P%ObsH}ok*VNY4w|8`Qb@%js=^Ggx8;4CyPE9YtmseKT*1vCT z9vmJWpPZhZ|G48LiA^Ac@Lj$2<{^Sb<)eSj7L}=*rd>F*CnwTaI zw;A~Tu}B`oW|X&KGYV+!-+AFUjB}SsaEbZgCu@Ij_V*YI_)l^6Cu9GeuW0}egn~Rg z5D_2^oL#VG`r-UGM4H6AFS^T2?%bp;mOS!Q_UC)V%dKlaAQ1ePXtcISdOz;`-OU?G zbp!w^kv`v{#rIuFv1mEzL;&Ay3=ILtwBHx}M!Ps`V6r0sY~C>G?{Z@ZU|(z$0Yne~ z)69gf{Qni3joZj|*Tdp-XE#ZkF~{K0>ad(-^=kd|ovw}f`!FF-tg>SSfNu3< z`Z0F6rn^-`u)`-ozvz@M*Z4j!Ap{WTl8M~;HUh|i0<^!0`FHyNHL?F|g?Mpp*NG3u zweESRp4#pyo(oOJPN5DgEOL1uOJ*`(UG#om_saXc?53jvsgC(YMd_%GU;_m3_9h1b z7;7QtUNz2ljW1FfWe*5iH1 z)MLlk8(x{)gHDyv`=y7&dcceCRdQOy6}7LMlQ;KRGl@zV^QCiZWBC5{_IIr6IhVHQ(v zHp}1WD?=!s*%;H|1o_zMn#>wX2jk|>s`suLysl*O?+CzQZjf8;VJN4RVT}y{ixvn< z6KpoKpL`(ySIsk7>csS?GQ(LKgVR_4P97oT@ zk?*5L#)E;~iiQK6SgqaA`+gBUV7(5sl|Ft}xInnW>WgP7)&(obH=JFtOz76G{nThu zZSGj`y+?$#8AW1cJT27tP|Zsj0X(0HxxQ-w@)*34C_w;Q$q0ZWIinH*#BlmvthMyz z{*$);uDyTKb?U(_hiX;E7jWNW;nLDy$aI3z!kJASH@8IBgfndBu8$lB3VBKWY-WRF zw=9#ZGn3Wg9X!5|xT_p0w0J18;snkm1<|ZE&i`FC22S)j2X>sdnsmEw_(ypf<%&wG z`1JcZ)gs--cDr--BS?u5V-PLg{v33eUM6Nt+bSDX_G&a{`OV z2QSPiL!7VG*7_(5)Ql{$yg2!GJpw%8<-5)Z;FiVx`qIiJ@d#D$p6z46%#h` zmxtNg%Et8_G$|IKA?{N8MriQbT<6=b5lpsX{IPF+IT9MHOsUUk)zhxrWl(>h34l>C` z-w-FeZvWjuhNei~Jq4P5!2*Tz;(aUJJR{z`*Toah-JK;+&*-xgkTQjFUp$N|o>7t` zesTh8V{9~Z9Hr8mrg)f9m9TD*Om0e}C6e%vsP>b?ik;9otOwyNTj&m&y(M_P>m zBU4hxmAynhA0)4fKGATFyObq%P>@u#dstDV+LRn@7Aht!e^2)u!Zg&Iu89!D$G<93 z)PV*&jo@GA*kxNMNc$DHoIiwegYn_&M252&moR}%m_{fl-)Ka< z!m=X^n{iRK{cU9=*3nQ!3r&~Of-pYPeBvtY`t;;Fhn^z7S39xv;BugDee2zP{U#e~ z$=XNr{EZzGPum*dViLh8UQ4ZXQBzyJ)E;MrcYW>*Z@Xaa!E54oU!XlWBjx|L64-Xw zQH~wk(}#JdQXX)+T36~b zxRe%nyxI)rzQY}LNVabADwZrml%trEjX)GfhA}!GrYS=r=uOivJIXLxwUQvNq`(-h zJZ~774%9KBY<$A$I}bttOzWL#r}(u+eprJ5O0BO7t%Cn*{r)LT z{XJ~`lU|kj?NEQ4@DqZz5Ll)%fX&9@mb$7^UmK~;ezx(sx6okrg={f3rJ$K^zxy5h zBU(9(d$ks-L%7P_85^MZc-y3b=0}MK8|drtAO^H2>SS<-8>AVdl#E3Ot@l=EhS9YJ z?j-g0UHYR`wvvN#o{f2lI(Q#&3@GehPk+~$HYtzZoIapCoUQoGK`j54P2-6lA!~qW znOvqHr{#rrZoCP8R8+mJ4OY8Cp#Q?rfU@M5;-WN+@49vZO`pO}L(FUSc+7&#Ux~8F zE1)2N#b?XW@lD7BuV3(&;Dw8ODha7l%v%(XG1nUOtmfa`(uhg)?|I}Ug*#+(^3g_r zenii9Cj$(5tgDsDBu6GoX7dq(7Wq6?U8qGK(Eg}eSv$sUw^pOr044bR$fKJIn7mlj z5w2O3#yx-cBt&!k*xIwGRtA^xE;%UuIToJeN1@?i8M104lHubW*L8hpFl296=C<_o znwh-_kYQ6xzGw=n=ClfD1N|P8l|E@RJ~)H7ToUvnfXvL&Qa_vn8>bt&i^OYhWUP^q ze46|#4V)(bk)ssPIT?N4KM9%M%eMRQ#=e6hG0vUvp*&S3t@?%x>1u)qgS%O*X%xyU zHxV36Ep-!vcaXFv2b6+Rx1b8u127ox(+G#HRbG6)vnB6u!i0pIG#v4~jry75baFG_ zEvBNpDO_o{D6i9MB`+pk6y}GAW@sw_AOHtgJ6=Ga1RJsB6!Z&M7M3v_o(CTeUJIz# zAJPDvFPlB?ZdbjaFF{DwzH2d4)#~<0%4}z8UN>FRQswtQadQ2jKH;J8T z$Ml=N=hT)6pvSr>(@#|WT=}QRe?XIm0Q_NTSFSO?Pt?~VW%&DVEq~+Y(=&ti)o^8A8;2%;ItTh5wLAdt$jIMF`chF!igsgb4yrV`N>YU~9djEJ z85x7J`OtUbBn<&jXWbAR6Z;N@6d?7^1$m>?Wzy%Q2aK2cUnJOz)IV;$L^2RNDDV(- z76G^`s%st#?Kq?K`L?UT)h}+HNM8|DA%Nwo-)QLgunbk+6;m@4^3VyNAb{2_^&9bL zMl54qnxzo1_K3;8R0dT|LYz$W8SLZJinrLEO`>HonSKn_3ehkUli;Tsip?DQ{~Wsi z8T|Yf(xWyWwNIq7?Vkm|cK*nAdqyGucO;zuI`jG8n^)gp zSB;gxVWlpTVyaT$9W9ll#ki-j0c6V>$_k8ysV8GdxBfSL|KntfouxR-Y(03CN<(?8 z28J^0h@Zg8j_X9nG}vek(@{?mj|lIJL`i#hV8iVjNpv#{8rphK1aPk8^-#<3l{^F4 z)7|iP0R+Icd}6mJ{bV#pXMcqllBP|=(-M_1$*xK<)pJp+{;c=rDy4}OR9D*lA3FlL z6bppFE|fw(_#^ejGez`?G^QwWr>Npu5M$a$SOd~>cQ=Cei#qB_BThK2$5Qv6jI0E& z4Y%BME$`bWTc3H`ge|l}k&hnC@jMGp2^8zf9}Zy}zjP%vdqeu<=37#cYes z`}=y!j6{kE040*`V$Dx1NLwB!ox|S`XhlsxTK0G6@o>&ETnMh_*tUJmBVx9c;D@ib zNgXcCr8PKG^Oo!gA)XyitVk*32MV0*h~hlIr>Jf{H_IljECP)!fxnCE>K1JX>+imE z%GtaZR>Gr^9SZ5aIHg+*oD5&{T_T!zCz~i6)5Xq`ybrClE+XV#eZgqEa8IdFP2|f1 z3lL6Ej~jQk<+M+fjT>c0PMbY*^C(M$rMAD`VRcuvEf$z-_N7_m#W0QQWq&bw46kK(r?zv=DkEyNk>X0MByq;6o>8Il9FqOS`H34 z-gDpW8$u*5k4wz;{Xcb7MqO8{(_AI#e8u}mpRA$saUFI zE*#s%_4G2MiK-oStpK*;xo6H-e77;7x4H74M%6Oi*?KFxGoL#m2v(((sTwXBgXUD> zqK1ibrJJT1oLvlIen8sZm-=41!xGwg8?dygi6ySQ2EA^dQ898xhaSAA>D*&seBcY# zKxb+P7i!z1M^z3*#*}`|kKjsA;gYkCRQc~O63D+HYgIumxiKN%mhRMkcs|#}edtd_nwd`q_>iEidXNTq@fDnp_9VM>2jzJui*lO= z=}eLK;@e^$?n}v~LVAQYH}9pJi%_dNrqb+Jmt>u%>A`qTid3NE`u9_@wkf!a!^&OD z<3ICF(5+9`_x<^N4NM$|x?H1PKKitylCLkhTzG%~^BE?ZH%D6FNf@*Ye@5L^c=J|5 zb(8w8I{EbS0VCsN_*|*1G(7b@^t%t3@K#Tg_uWJ0Hkz4b;4*2j1aLY2Y1(4oHdZro zBX|>})u#&XcjwvHV>O9pqB?76+4@bJXLY(vRI{v{&YNd+Y_ujfP7fp;CO`7)-39Tk zxum*`*hk(04rFM)Y|G1PGuQ|7Fn|d z%xqW4z*y9!_-Dc!+ygUKp5uAKoUTM0#RNShj2JAqN}#+uxT*;c~ z`^c3U22$a@JXt25KPS~3hvwV47+Q^Z+HFq>&O}%opT>NPer!kII0~&mp`tQ9NXNll z_Pu?bskA(TKB8>KbkKKUv-(MAgl?`vG2VW$)V`0D>AC->@u>S<)7QQG?3 zMD*ZqtB8MKXiV`QvA6xO$GSP#(z2Y#I6#m7;AF}hwOdAiDGr{>>B@|MLsY{H4Se)q z1@AL(6zH#7vrhxHBMo{%!&t?>w9AM9!c4w)n8e^;?n=o;iy!UCP_0Ho;keSltlBLU zYUQ|_#c|BzmFx!d6?ePW2;Aa1?tPTG;nFqnEZaIes~?l1sQLVUgP>eOS2n`%k%p-J z^FBu49%c0Qr+wg?M&r3#e*cp4mAEyWZLuouc#csr_*DKVa>f^imvX*7WJ)>WzxmC- z`p|##nX?Nz_=9lM_~eB5CfIt~`mdw~sQLWO;~R2Sc4hn`BiXz-ai(aA!K+}J`kepf zrxtNmy$p}6QwbC%$o>Ha`Q@s!n8@s{uFk=NPbaL#@un~aJGyRUH- z5B&UOT4(JQ93m$~37|`!Ey1azDiJ%PyF1X67ZqYZ-gZyC-d2(*eBZ|EAE|*^{ROUy zL&>&h2CG`Biv4OSZb+x_-2SPsy^>_1XNs|EC4_iOv&w&0dW@@*;{#-{|J@)>{OUY+ z!>BVYh5JR&>RMv@rkG<+6D-9HZ>TYHcccg-T+P_=Q$A(GI*0@+ZXqF^1E!|cG^Mb_ zww<;MP477kYX0$v;T}+2%Y};aiKLe-onUvgn-DYn7jvaGHN0Azg73C^`I+wStdpS3tHLQS?Lt*D4dl&f5-wgR7CAIEe$C2EnQG6c zj43mX9jVGXkPcZJ%^rI(A=pzMcB<@H&$T*=^Acl z)EfIQBF0rBkd&o`x0)&1y^;x7#j^LnUrAH;sNFxriFukSZW?P%?$gpzFS{3GMUIc#7W{Yna%P zqWicB=4FN-wA#3EAYwWG!rn1gr28N`y`n}c(@*@54rS+oQTWBKzfe>x*Z6FJ@Pm#+dfJe&C zb?OJJ3EeY${h2a+u0yOLZ)K4Jx>U-1Vo!I|WO5RIWl9Vkn)GO_GLeI`Fz1h8J8Vdf z9i#9U$B~(jp*e@lxY>T%w3Prr5O>-RQ4 z`8pZS`vFw3?n>sz56%HRf}D(4CZODuy8Fj>VW(y0`3ohV4wb&G(Q|wi&R@=?QNH5T z!gqUIH^FnPXB!WteE5td&=OtSk8UNBCM!dv_nV_Jv5U93>R2OHP0eR;Y)KCvv50{J zW2_3*F;E|Qyyhp4eNw_Pp%I~8JGPzClt^RP_^z<`$3_q@*{j+-<-_t+C9!wwNewu zu5Xe1HX@jj^08@#=8^3nR7}!WA;A;EIKE8-4rs@Mt>Qj<`gTUay$y}Zxvh-7x^7=8 z4W0(-F>}ur`nYl5eo#6gmTOZR;K#$WHQ~jdg}$6b27y)QrkU=#)wjNoh9#&L_7xY_ znAMo(qcRY9W2F;%XC_C-DO4!5#rkpdIbun6iT%fk>~9VEKP#I*=YOq?{ZplE&&$WB zKgY7Z{4&|7z{ZLGII<>Teg7HR%^jGws=9j3_;FE_*oGT;?!kVq^we|N8<(iXlt=JA zIbn)GbM29AVO_tISLzL{!=u(QXHEH_?9A|$M(=!8^x*iMnZ5(5$^yCPhx8pxstIok zwjUK3oX#Z!-$$Q(j)N>lUrgn`b4#S?%Sp4O|19t>081*VNE_M1^hwFSQrrKO082Q{ zulGI_kS8;ay~~}x{P{~yuBsesG&EE~+!X$UyI4J>^p(u%W+@MS(87yeV^^3c$D2i6pdLOzs=1~7roYoZy8ZncD(Wqh@n zr?BVcDp)4N+lBHP_^+ZO@6yc}xqVn^Q)Dk^wg6btKUGj5dQG}g|ET*Wa%_6? ze@64>ly27_AILJ749>qJR3g+#nvVgZ@~SAi@DrO@`OVkekXY zdC5#S74Lus{CVkda13nF7-gXZ9_e2xK2Hv8U;pBw=;^*4jjsvq3h37eZ1>&+?J zW(gu*P>#B3)RngwYZ7tp635-H(85g*QophjaP3ao)2h$zDK=;B3ffwbL2;+80Z!5m zDZ=3sU#yM#;PTh?(Rses)L$o#K7DJJ$8(i_3KBuF2T=XQnJXiWp<8#|!`&@w4=DDP zj0=D?143CH&Veg(_(sVUcG29H+k;obyro>|W4j~+xBP;V4&wB9)W^=U_k~-`X(NQ} z2)atAH@}GCtIb1r?rPT6rbuZyg#2i~kcI*t+)hs{E^>kk@~w30uN#FE)hY(5Px3yl z+Gx}&60?lA4q(v{t2-HdTZp_;w_YqG*)+eTKf%Y3h+U)&` z*~}>vx4d-HSnN}H;R=}=}>l+HBI)Y8q)+w#tM_Q9YXM{u-W z694BDG?#P&9dDZD&Qzvh2~D%24(g~+THRc$pI;0x(LE~eYSZ}vv>Cq-eCzS_ZW>|g z!s8g(ce(-)TTXuoS8lt@p*vSyq5Ta>9Vp$ii>4x_^Wnfze@saI_&^j~Rp?5^veK|l zpl?}4G=h#3{rW63e?6D5jB36mPo>~ISI>ozVq@BH?!Em?wnuanUVy9i+*&IOs|9oU zArfRViF1mq$Fg>0X(Rg>9&;|%q3dIS{b;CHIUU{uQS~Ke*SN;#aU!Ks>LC=f&E#Xw zx(D*5Gdn7m@@o{&(DlBo#e)XA?*}msp^6UDM9BlsH<4+ettt)taM=cy#&Z@&GF{0@ z|BR9D-5Xneeh&gqm|K_xxo4sE&=wLfVk;=AY;t#0nF^O*^%k?=OIv@I`at9JQZ`xxLwKykk=^m~Uux zByMYQ=k5qL6F284f=|`_?k@nqoF-8oP()+5v!f#7nOxQgmjuY}F#lTa#k}K7m5MQL z4&%;p+uvCUR(&*wq53JLq#fl7^SpKAEW~3wBSAb=%i`rThFCeqjNa$&Xf*IoedLcm zqbs#xHaQT>x$Iw>Z?C%oLZ-jy(soOb@CoHyV^ zs9}t!#7n-~`$0RWN*E7U!h^5=t-kT^38?llHAFWF&E*02D<919ZJS2EHAq6Q>=Kmu zS=ETdIF`n)lpEOt1w|k4q9+}~{6^=A&ki1!OK2K%>A;j3;_Vpj`8X0XItkn&6amgk zEB?|W_&4>9UwR4uSoiO19@7@cWt{Ko_ETW-Zaf)FoVDA`u@QrLUvr9gDeI<1qhik9 z9bnY=v4IjQ51NYIluh82&jh(;2gX#YG00=C1(Lic#J@84uB_1C%ZVsi-c^LdusRhS~_X=wM5r$+$I;ATRh%JlO@wF5k@>-g;OO;GjP zz>-AW&dl)8p;TkwWb#C@q*FUgY_@!EH=+Nj-wXT)1fUTy1+ZB_3 zOE4d4cu7!K=%Td_Uy7f{w_9Igu9xC3bagG=Y>*y!Jf~Cl?jimcE;VvJj{p%t6d@8# z8T9|v0jaJ-0QA=?H_b>a;?Orw)!!xnPu0I(;??Fn%`kKfyKuubG3hebB9Q-@Q1e_T zo&=?j5UnX}E&~#wH!5@aS)zTDXs6! zaaDRr*i#k(+>XJwuJ@5Tj(E+JMRS?K<8XpnYB?{m-(0|&XjwX0wQ$s}{Lqo`9Awh+ zXrWL)I40L>_=iVyiX|j%uGar0?fJeSiv6wDTe6rLB+$N^jxziVZc}TATxpGIo7w8g zE24m#_rpu7R)aUP&4T0FzIDDN_f>wx>mZxm`Jm#=I(5z-SR?mDh|3-7m0)WfX)Oxb z3uFh&?!0i)H|Ja^7gS_<5vg@Ur@WdBZ046fxw&1NDZPbU)p|bB%!xusE9|Ri`LHKq^bY<~hFX<=I)-?ML$;yJ0Av5Xy(pD%Cf5UeZb{jeUIQmfI3H z;5%bnk3=`qS%8f7U#a41IKL>a9=RpkhG|(U-XVoPbV7(@aaJSuBn}Sm#y0cau`aKh zLSaQUAW+`W+T2@~q^+A$~Ak`!7hJXjzTHT@DFZH^Q03?u&cPc{!a4^03J)Pjm&7Hwu1hAfpY~Ke) zwA_d*UU8=3BY?r5NN!A0-z(RD;0%JCmRf$S|LpX`_x>v|8)v=4_h=B=pl|D1K=%Gi zOaD+PgLAtZcO;ZSqygF6hd$BX#|+u2@|;>SR5c5qS5&x}Xb_=S#{6JiT}ZKh6@dhI zcp%{(=@dZG5;lz;x&?pV_MudY?bmHby`%}I)U|4B!nU+ShI8MWw(5loc5BDKpCA7Z za^ioWNB$Y<{%2xsov9SWq-`cChZm@p>BqNw#k|yTbzRnRVX$%*Hy|wi;ZCv-B}9p; zBO4bf1+_%tHY9LmQdj#Aynz3753<--zZbd_@;te?6myB*MDdCtd5rd8eL2cZG3TrB zC?Sl_SO1SgxTNfMa3YN;UhT0?p`*HCXBb{Rzu`%wp{TuRlMqEdCi8!s!lhjd{pAHs zpriCgbRHM7!c6mK___%`h5()|entQ%9ok_n$8>)HD9Wyw#a2uO&MBXPZax_3BcUYF zAl@sMcf1!1*pLM2OP44FaEy)bBB+7MiL6xb#5oB^z(o{`ERK*H^*o&jJ8xNL4Ruxz zqj>i{(NQY#vZPw~^lKquLPN6-&c=X;sib>ZPEr3!OMt#&n3uUP9N$!{A=Wm9+~zsz zy$nrV^l0VFrg9lF3b!4UOly6ixr9nH;g4?I4Vk@ZO+iTopx7fG^>Uh%O(|^Wcdb4& zFQ4~cf3)Q%@K8vHX`O@Gn<<8PXOM+>`gog{odeA;16QTHrx^Ez$`%KI5P3=e#KPKL zBQYr%MOO9}(~`Rd&gxS;bnYuN6IKCglINx79a~6%phb24Dy(|i$w;B*mfj4w^FWaj zFWuFJXC3W!Kog|PIT;p@QJ1k`sml~?-4#e)YZPJ)tHP%ll^YWz=S}Vt zstLatfzy-ZXW@J;?c21+u&4YqgpMNIWVYObms?bz%4jfcO!-ky#6MErRV;mFMf!o> z@m})Cke6mIH39r$?j)j2{7Le>xM$bb9bx!mr94V)`$_z$3^j-FK%JV2>~sI6CRt}u zTW+qBU9B|;Ybb}{41hyDp2RbthT-&9T$k;J_<$v2=`t~~!ydQa?v{FChe?4l-sgFF zcCT*NN}%=rfjt%(>t{?8-p&%5Sq_6KtG9EwE1#3cW^5tMUN-YjDNEggLd$zu5~K`5(-oi!gAn?6fvrDkyU|UIz9(;4=%||RBLMYL)|+u? zL&BhH@|;pM>Fnc}_d2G-G6SMt*0Lv#X^E_pL!yYeF(CBg&b>A}uae<-g0XfY^aimF zJ}5o5NIYv|-fX|SDap~)pgCh;5-XdbdyhDIVrt{YuVZ~zQ&uVn)oX@gjqHGH@Ep0mqU?5rf;!-ATgf#9)q4Xk|O{H-Vg+^ zO=*L)6A7hTX@|VX4%Qa$jh|KGAGR9-{KI%-tp3&KYfMP57$a{HW_%5dUt}Qh9W4dE z6U8kTDCsyxzq1}6@|$%2eu(m0H^owSxKq<2|DO3}AnIUDI#7H;Hx_mQ|URn{9H1aOFS zP(jG-2Wf#=-XQ>K-ycZV(~g9?38wwHM7nUh8>AdC|Nlk**TjqFv^CBBt?{j@Q$4#z zR$0O_T_jO!slGJcIR;rATbicQ@Pk6094}P=A(Sr?tj0sfsZbTKY z7@N_Np~OYn4L)zn`RX4h_J3lD(mdrpWrkv+mYGQaL7tZ^?b#s`Pn1&ubEcN+p6`SC z^32aOhcivCTd}p0GqU=LAzj~JwJy_-@UOb_SDiyfdB5oEuPTR3Kz`L&#b1<;EgkfW k%KoD2|BqkPNV<5lTD^1=uhL9^?Eyje&A};BR*0$p0zBKb>Hq)$ diff --git a/docs/images/jwt-revoke.jpg b/docs/images/jwt-revoke.jpg index 336f5914986c3f800bab378b1152396b2d1b305b..18193fe959c6d151fca2fe88ea8495fb6a81d2f6 100644 GIT binary patch delta 8804 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 delta 8695 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 N5i8OKB)R|jzW{Jco{|6n diff --git a/docs/images/jwt-use-access.jpg b/docs/images/jwt-use-access.jpg index 92fa40718d1d57282db578b141dde726c1e6be72..3a864257dc819e2a66e0139e2eb7ebc0d901edae 100644 GIT binary patch delta 7771 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 a`!7-d00sa1DfKU){sBt#HzG@8C;tr|dOgqp diff --git a/docs/images/nginx-example.jpg b/docs/images/nginx-example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bb14b3acd48743b4037110d49c67d2033ce9d269 GIT binary patch literal 20572 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 literal 0 HcmV?d00001 diff --git a/docs/slides.org b/docs/slides.org index 83f887e..85febec 100644 --- a/docs/slides.org +++ b/docs/slides.org @@ -2,7 +2,7 @@ #+COMMENT: using timestamp:nil suppresses "created at" in title #+COMMENT: using num:nil prevents slide titles being numbered -#+OPTIONS: timestamp:nil num:nil +#+OPTIONS: timestamp:nil num:nil toc:nil ^:{} #+REVEAL_REVEAL_JS_VERSION: 4 #+REVEAL_ROOT: https://cdn.jsdelivr.net/npm/reveal.js@4.0.0/ @@ -91,7 +91,7 @@ digraph auth_system { rank=same; AuthServer [label="Auth Server", shape=box]; hidden [style=invis]; - AppServer [label="App Server", shape=box]; + AppServer [label="App Server(s)", shape=box]; } subgraph bottom { @@ -143,7 +143,7 @@ digraph auth_system { rank=same; AuthServer [label="Auth Server", shape=box]; hidden [style=invis]; - AppServer [label="App Server", shape=box]; + AppServer [label="App Server(s)", shape=box]; } subgraph bottom { @@ -186,7 +186,7 @@ digraph auth_system { rank=same; AuthServer [label="Auth Server", shape=box]; hidden [style=invis]; - AppServer [label="App Server", shape=box]; + AppServer [label="App Server(s)", shape=box]; } subgraph bottom { @@ -215,8 +215,8 @@ digraph auth_system { #+BEGIN_NOTES - Distributed Trust -- App Server can be load balanced or serverless -- App Server can be maintained with lower security requirements +- App Server(s) can be load balanced or serverless +- App Server(s) can be maintained with lower security requirements #+END_NOTES @@ -239,7 +239,7 @@ digraph auth_system { rank=same; AuthServer [label="Auth Server", shape=box]; hidden [style=invis]; - AppServer [label="App Server", shape=box]; + AppServer [label="App Server(s)", shape=box]; } subgraph bottom { @@ -272,9 +272,9 @@ digraph auth_system { Auth Server has **secrets**; needs **security** + maintenance #+ATTR_REVEAL: :frag (appear appear) -- App Server needs public keys; low security +- App Server(s) needs public keys; low security - Easy to deploy App Server(s); e.g., serverless -- Lower security for App Server, logs, debug, etc. +- Lower security for App Server(s), logs, debug, etc. #+name: jwt-auth-vs-app-separate #+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/jwt-auth-vs-app-separate.jpg :eval never-export @@ -285,7 +285,7 @@ digraph auth_system { rank=same; AuthServer [label="Auth Server", shape=box]; hidden [style=invis]; - AppServer [label="App Server", shape=box]; + AppServer [label="App Server(s)", shape=box]; } subgraph bottom { @@ -385,6 +385,8 @@ secret_key = ( # We hard code secret key so you can verify results ) #+END_SRC +#+RESULTS: create-keys + ** Public Key @@ -479,6 +481,7 @@ proc = subprocess.Popen([sys.executable, 'app.py'], env=my_env) #+RESULTS: start-flask + * Example of =@requires_jwt= #+BEGIN_SRC python @@ -489,8 +492,9 @@ def requires_jwt(func): if not token: return 'missing token', 401 # if no token return error try: - g.decoded_jwt = jwt.decode(token, algorithms=['ES256'], - key=current_app.config['J_KEY']) + 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 @@ -531,7 +535,7 @@ print(f'Good token response:\n code: {req.status_code}\n' #+COMMENT: or maybe have as backup slide #+BEGIN_SRC python -def jwt_claims(claims_list): +def jwt_claims(claims_list: typing.Sequence[str]): def make_decorator(func): @wraps(func) def decorated(*args, **kwargs): @@ -602,29 +606,83 @@ print(f'Recent premium token response:\n code: {req.status_code}\n' : code: 200 : text: processing support request for user b +* Example Use Case: Proxy -* Separate validation from parsing +#+ATTR_REVEAL: :frag (none appear appear) +- Auth Server grants JWT letting Alice to act for Bob +- claims: ={"sub": "Alice", "proxy": "Bob"}= +- Alice sends request combining to act for Bob -#+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 +* Example Use Case: Proxy + + +- Auth Server grants JWT letting Alice to act for Bob +- claims: ={"sub": "Alice", "proxy": "Bob"}= +- Alice sends request combining to act for Bob + +#+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 + +* Example Use Case: Proxy + +- Auth Server grants JWT letting Alice to act for Bob +- claims: ={"sub": "Alice", "proxy": "Bob"}= +- Alice sends request combining to act for Bob + +#+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 + +** Python demo +#+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 + -- Validation can be slow for some keys -- Can use middleware to verify signature -- e.g., NGINX can verify before passing to app server - - See implementation in =nginx= directory: - - https://github.com/aocks/ox_jwt/ -#+COMMENT: FIXME: consider diagram of NGINX idea * Anti-Patterns #+ATTR_REVEAL: :frag (appear 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 - Token revocation issue: access/refresh tokens @@ -650,6 +708,7 @@ 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 @@ -662,7 +721,7 @@ digraph auth_system { rank=same; AuthServer [label="Auth Server", shape=box]; hidden [style=invis]; - AppServer [label="App Server", shape=box]; + AppServer [label="App Server(s)", shape=box]; } subgraph bottom { @@ -672,7 +731,7 @@ digraph auth_system { // 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)", 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 @@ -685,6 +744,7 @@ digraph auth_system { #+RESULTS: jwt-get-refresh [[file:images/jwt-get-refresh.jpg]] + * Get Access Token #+name: jwt-get-access @@ -696,7 +756,7 @@ digraph auth_system { rank=same; AuthServer [label="Auth Server", shape=box]; hidden [style=invis]; - AppServer [label="App Server", shape=box]; + AppServer [label="App Server(s)", shape=box]; } subgraph bottom { @@ -719,6 +779,7 @@ digraph auth_system { #+RESULTS: jwt-get-access [[file:images/jwt-get-access.jpg]] + * Use Access Token #+name: jwt-use-access @@ -730,7 +791,7 @@ digraph auth_system { rank=same; AuthServer [label="Auth Server", shape=box]; hidden [style=invis]; - AppServer [label="App Server", shape=box]; + AppServer [label="App Server(s)", shape=box]; } subgraph bottom { @@ -753,6 +814,7 @@ digraph auth_system { #+RESULTS: jwt-use-access [[file:images/jwt-use-access.jpg]] + * Revocation #+name: jwt-revoke @@ -764,7 +826,7 @@ digraph auth_system { rank=same; AuthServer [label="Auth Server", shape=box]; hidden [style=invis]; - AppServer [label="App Server", shape=box]; + AppServer [label="App Server(s)", shape=box]; } subgraph bottom { @@ -788,6 +850,42 @@ digraph auth_system { #+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 implementation in =nginx= directory: +- https://github.com/aocks/ox_jwt ([[https://github.com/aocks/ox_jwt/blob/main/nginx/conf.d/example.conf#L44][example.conf]]) + +#+name: nginx-example +#+begin_src dot :cmdline -Kdot -Tjpg :exports results :file images/nginx-example.jpg :eval never-export + +,#+BEGIN_SRC dot +digraph RequestFlow { + 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 From a18cd1126e9a8bd3f6e86dadcdae4c8e4f7a22f3 Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Sun, 27 Apr 2025 20:16:17 -0400 Subject: [PATCH 28/30] more improvements to slides --- docs/slides.org | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/slides.org b/docs/slides.org index 85febec..a4d5c00 100644 --- a/docs/slides.org +++ b/docs/slides.org @@ -447,6 +447,12 @@ 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 @@ -904,7 +910,7 @@ so it's still useful to have a high level understanding of the basic diea. - 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://aws.amazon.com/cognito/][cognito]], [[https://www.keycloak.org/][keycloak]] + - [[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/ From f27afada89276283dc9e16301b85ee9d89c4cf5f Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Sun, 27 Apr 2025 20:17:12 -0400 Subject: [PATCH 29/30] created slides.html from slides.org --- docs/slides.html | 876 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 876 insertions(+) create mode 100644 docs/slides.html diff --git a/docs/slides.html b/docs/slides.html new file mode 100644 index 0000000..fc44200 --- /dev/null +++ b/docs/slides.html @@ -0,0 +1,876 @@ + + + + +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
    • + +
  • + +
+ + +
+
+
+
+

JWT Architecture

+ + +

+Separate authentication from validation/application: +

+
    +
  • Authentication requires secret keys (high security)
  • +
  • Validation can use public key (less security)
  • +
  • Easier to manage secrets, keys, load, sync, etc.
  • + +
+ + +
+

jwt-auth-vs-app-start.jpg +

+
+ + +
+
+
+
+

JWT: Authentication Request

+ + + +

+Client authenticates to server: +

+ +
    +
  • Auth server must be secure
  • +
  • Payment or Login with username/password/MFA
  • +
  • May require database check, locks, other slow ops
  • + +
+ + + + +
+

jwt-auth-vs-app-auth.jpg +

+
+ + +
+
+
+
+

JWT: Authentication Response

+

+Server responds with JWT: +

+ +
    +
  • header describing JWT
  • +
  • claims describing info/rights (iat, nbf, exp, etc.)
  • +
  • signature from Auth Server
  • + +
+ + +
+

jwt-auth-vs-app-auth-response.jpg +

+
+ + +
+
+
+
+

JWT: Application Request

+ + + +

+Client sends JWT to App Server: +

+ +
    +
  • App Server validates JWT with public key
  • +
  • No DB/state/sync/update; can be serverless
  • +
  • Checks JWT for rights + provides service
  • + +
+ + + + + +
+

jwt-auth-vs-app-request-app.jpg +

+
+ + + + + +
+
+
+
+

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.
  • + +
+ + +
+

jwt-auth-vs-app-separate.jpg +

+
+ + + + + + +
+
+
+
+

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
+
+
+ + + + +
+
+

Secret Key

+ + +
+ +
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'
+)
+
+
+ + +
+
+

Public Key

+
+ +
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)
+
+
+ + +
+-----BEGIN PUBLIC KEY-----
+MCowBQYDK2VwAyEAUVLjZWAVK5ZE1ewI5QBdr0Nig1Qkx3kl5zHIADvw0M8=
+-----END PUBLIC KEY-----
+
+ + + +
+
+

Encoding Example JWT

+
+ +
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='  '))
+
+
+ +

+Encoded JWT: +

+
+eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9
+.eyJzdWIiOiJhIiwibmFtZSI6ImIiLCJpYXQiOjF9
+.SU6aXJ0YbH7Vg1jROpQfvnhn98Rt9zBeS7-c5O9jH-
+LL5mQqMMFq61eZjf0tLLqExm-dckRUNa3-qT7R2SKmCw
+
+ +
+
+

Decoding Example JWT

+
+ +
decoded_jwt = jwt.decode(example_jwt, algorithms=['EdDSA'], key=pk)
+print(decoded_jwt)
+
+
+ +

+Decoded Payload from 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

+

+Easy to verify/decode using libraries (e.g., pyjwt) and compose +checks using decorators: +

+ +
+ +
@app.route('/support/urgent')
+@requires_jwt                  # validates 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
+
+
+ +
+
+

Starting Flask

+
+ +
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
+
+
+
+ + +
+
+
+
+

Example of @requires_jwt

+
+ +
def requires_jwt(func):
+    @wraps(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
+
+
+ +
+
+

Ensure Valid Token

+
+ +
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')
+
+
+ +
+Bad token response:
+  code: 401
+  text: problem=InvalidSignatureError('Signature verification failed')
+
+Good token response:
+  code: 200
+  text: Hello World!
+
+ + + +
+
+
+
+

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
+
+
+ +
+
+

Ensure Claims (Bad Token)

+
+ +
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')
+
+
+ +
+Bad token response:
+  code: 401
+  text: Missing claims: ['premium_user']
+
+ +
+
+

Ensure Claims (Bad Claims)

+
+ +

+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')
+
+
+ +
+Premium token response:
+  code: 401
+  text: Token age 20193 days, 17:37:05.670865 not within 0:00:30
+
+ +
+
+

Ensure Claims (Success)

+
+ +
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')
+
+
+ +
+Recent premium token response:
+  code: 200
+  text: processing support request for user b
+
+ +
+
+
+
+

Example Use Case: Proxy

+
    +
  • Auth Server grants JWT letting Alice to act for Bob
  • +
  • claims: {"sub": "Alice", "proxy": "Bob"}
  • +
  • Alice sends request combining 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 combining 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 combining 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
+
+
+ +
+
+

Python demo

+
+ +

+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)
+
+
+
+ +
+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
+
+ + + + +
+
+
+
+

Anti-Patterns

+
    +
  • 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
  • +
  • Token revocation issue: access/refresh tokens
  • + +
+ + +
+
+
+
+

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

+ +
+

jwt-get-refresh.jpg +

+
+ + +
+
+
+
+

Get Access Token

+ +
+

jwt-get-access.jpg +

+
+ + +
+
+
+
+

Use Access Token

+ +
+

jwt-use-access.jpg +

+
+ + +
+
+
+
+

Revocation

+ +
+

jwt-revoke.jpg +

+
+ + +
+
+
+
+

Separate validation from parsing

+ + + + +
+
+
+
+

Summary and next steps

+ + + +
+
+
+
+ + + + + + + From d5f8d70ec36cb5a54d947c2eef2e42bafc3a71ad Mon Sep 17 00:00:00 2001 From: Emin Martinian Date: Sun, 27 Apr 2025 21:07:41 -0400 Subject: [PATCH 30/30] pip install project before make test --- .github/workflows/docker-build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-test.yml b/.github/workflows/docker-build-test.yml index b2fb9d4..495d862 100644 --- a/.github/workflows/docker-build-test.yml +++ b/.github/workflows/docker-build-test.yml @@ -12,4 +12,4 @@ jobs: steps: - uses: actions/checkout@v2 - name: Run test via makefile - run: make test \ No newline at end of file + run: pip install -e . && make test \ No newline at end of file