diff options
| author | mo khan <mo@mokhan.ca> | 2025-05-27 14:11:44 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-05-27 14:11:48 -0600 |
| commit | 6566ad4cab572685fa01ca3e22fa9ce3ea1663e8 (patch) | |
| tree | bcfe73492cc9d30af7cbec691e0f6b4386abc832 /share/man | |
| parent | 6185741dd87541aad997672e2cdecfb6bbdebe7c (diff) | |
docs: add example of verifying a JWT signature
Diffstat (limited to 'share/man')
| -rw-r--r-- | share/man/ENVOY.md | 121 |
1 files changed, 113 insertions, 8 deletions
diff --git a/share/man/ENVOY.md b/share/man/ENVOY.md index 5391aa3..0ea852c 100644 --- a/share/man/ENVOY.md +++ b/share/man/ENVOY.md @@ -333,6 +333,43 @@ corresponds to one of the keys that are published through the JSON Web Key Set (JWKS) URI. For example, the GitLab JWKS can be discovered through the OIDC Discovery Endpoint. +Here's an example of JWT: + +```plaintext +eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ3OTM3OTgzLCJpYXQiOjE3NDc5Mzc4NjMsImF1dGhfdGltZSI6MTc0Nzc3NDA2Nywic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJnaXRsYWItb3JnIiwidG9vbGJveCIsIm1hc3NfaW5zZXJ0X2dyb3VwX18wXzEwMCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwiZ251d2dldCIsIkNvbW1pdDQ1MSIsImphc2hrZW5hcyIsImZsaWdodGpzIiwidHdpdHRlciIsImdpdGxhYi1leGFtcGxlcyIsImdpdGxhYi1leGFtcGxlcy9zZWN1cml0eSIsIjQxMjcwOCIsImdpdGxhYi1leGFtcGxlcy9kZW1vLWdyb3VwIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAiLCI0MzQwNDQtZ3JvdXAtMSIsIjQzNDA0NC1ncm91cC0yIiwiZ2l0bGFiLW9yZzEiLCJnaXRsYWItb3JnL3NlY3VyZSIsImdpdGxhYi1vcmcvc2VjdXJlL21hbmFnZXJzIiwiZ2l0bGFiLW9yZy9zZWN1cml0eS1wcm9kdWN0cyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMvYW5hbHl6ZXJzIl19.TjTrGS5FjfPoY0HWkSLvgjogBxB27jX2beosOZAkwXi_gO3q9DTnL0csOgxjoF1UR8baPNfMFBqL1ipLxBdY9vvDxZve-sOhoSptjzLGkCi7uQKeu7r8wNyFWNWhcLwmbinZyENGSZqIDSkHy0lGdo9oj7qqnH6sYqU46jtWACDGSHTFjNNuo1s_P2SZgkaq4c4v4jdlVV_C_Qlvtl7-eaWV1LzTpB4Mz0VWGsRx1pk3-KnS24crhBjxSE383z4Nar4ZhrsrTK-bOj33l6U32gRKNb4g6GxrPXaRQ268n37spQmbQn0aDwmUOABv-aBRy203bCCZca8BJ0XBur8t6w +``` + +If we break the JWT apart using `.` delimeter it will look like the following: + +```plaintext +header: + + eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ + +body: + + eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ3OTM3OTgzLCJpYXQiOjE3NDc5Mzc4NjMsImF1dGhfdGltZSI6MTc0Nzc3NDA2Nywic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJnaXRsYWItb3JnIiwidG9vbGJveCIsIm1hc3NfaW5zZXJ0X2dyb3VwX18wXzEwMCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwiZ251d2dldCIsIkNvbW1pdDQ1MSIsImphc2hrZW5hcyIsImZsaWdodGpzIiwidHdpdHRlciIsImdpdGxhYi1leGFtcGxlcyIsImdpdGxhYi1leGFtcGxlcy9zZWN1cml0eSIsIjQxMjcwOCIsImdpdGxhYi1leGFtcGxlcy9kZW1vLWdyb3VwIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAiLCI0MzQwNDQtZ3JvdXAtMSIsIjQzNDA0NC1ncm91cC0yIiwiZ2l0bGFiLW9yZzEiLCJnaXRsYWItb3JnL3NlY3VyZSIsImdpdGxhYi1vcmcvc2VjdXJlL21hbmFnZXJzIiwiZ2l0bGFiLW9yZy9zZWN1cml0eS1wcm9kdWN0cyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMvYW5hbHl6ZXJzIl19 + +signature: + + TjTrGS5FjfPoY0HWkSLvgjogBxB27jX2beosOZAkwXi_gO3q9DTnL0csOgxjoF1UR8baPNfMFBqL1ipLxBdY9vvDxZve-sOhoSptjzLGkCi7uQKeu7r8wNyFWNWhcLwmbinZyENGSZqIDSkHy0lGdo9oj7qqnH6sYqU46jtWACDGSHTFjNNuo1s_P2SZgkaq4c4v4jdlVV_C_Qlvtl7-eaWV1LzTpB4Mz0VWGsRx1pk3-KnS24crhBjxSE383z4Nar4ZhrsrTK-bOj33l6U32gRKNb4g6GxrPXaRQ268n37spQmbQn0aDwmUOABv-aBRy203bCCZca8BJ0XBur8t6w +``` + +When we Base64 decode the header it takes the following form. This tells us that +the signature was produced using an "RS256" algorithm which is a short hand for +RSA public key cryptography with a SHA256 hash. The identifier for the public +key that can be used to decrypt the signature is marked by the `kid` name. This +`kid` will correspond to an identifer that can be discovered at the JWKS +metadata endpoint. + +```plaintext +{ + "typ": "JWT", + "kid": "td0SmdJQ4qPh5qNKzM260CXx2Uh-whG-MDjoOKWft8E", + "alg": "RS256" +} +``` + ```bash $ curl https://gitlab.com/.well-known/openid-configuration | jq '.' | grep jwks_uri "jwks_uri": "https://gitlab.com/oauth/discovery/keys", @@ -342,30 +379,25 @@ The following keys imply that GitLab uses RSA for public key cryptography and SHA256 as the hash algorithm for verifying digital signatures. ```bash +# note that I have ommitted some data to keep the example brief $ curl https://gitlab.com/oauth/discovery/keys | jq '.' { "keys": [ { "kty": "RSA", "kid": "kewiQq9jiC84CvSsJYOB-N6A8WFLSV20Mb-y7IlWDSQ", - "e": "AQAB", - "n": "5RyvCSgBoOGNE03CMcJ9Bzo1JDvsU8XgddvRuJtdJAIq5zJ8fiUEGCnMfAZI4of36YXBuBalIycqkgxrRkSOENRUCWN45bf8xsQCcQ8zZxozu0St4w5S-aC7N7UTTarPZTp4BZH8ttUm-VnK4aEdMx9L3Izo0hxaJ135undTuA6gQpK-0nVsm6tRVq4akDe3OhC-7b2h6z7GWJX1SD4sAD3iaq4LZa8y1mvBBz6AIM9co8R-vU1_CduxKQc3KxCnqKALbEKXm0mTGsXha9aNv3pLNRNs_J-cCjBpb1EXAe_7qOURTiIHdv8_sdjcFTJ0OTeLWywuSf7mD0Wpx2LKcD6ImENbyq5IBuR1e2ghnh5Y9H33cuQ0FRni8ikq5W3xP3HSMfwlayhIAJN_WnmbhENRU-m2_hDPiD9JYF2CrQneLkE3kcazSdtarPbg9ZDiydHbKWCV-X7HxxIKEr9N7P1V5HKatF4ZUrG60e3eBnRyccPwmT66i9NYyrcy1_ZNN8D1DY8xh9kflUDy4dSYu4R7AEWxNJWQQov525v0MjD5FNAS03rpk4SuW3Mt7IP73m-_BpmIhW3LZsnmfd8xHRjf0M9veyJD0--ETGmh8t3_CXh3I3R9IbcSEntUl_2lCvc_6B-m8W-t2nZr4wvOq9-iaTQXAn1Au6EaOYWvDRE", "use": "sig", "alg": "RS256" }, { "kty": "RSA", "kid": "4i3sFE7sxqNPOT7FdvcGA1ZVGGI_r-tsDXnEuYT4ZqE", - "e": "AQAB", - "n": "4cxDjTcJRJFID6UCgepPV45T1XDz_cLXSPgMur00WXB4jJrR9bfnZDx6dWqwps2dCw-lD3Fccj2oItwdRQ99In61l48MgiJaITf5JK2c63halNYiNo22_cyBG__nCkDZTZwEfGdfPRXSOWMg1E0pgGc1PoqwOdHZrQVqTcP3vWJt8bDQSOuoZBHSwVzDSjHPY6LmJMEO42H27t3ZkcYtS5crU8j2Yf-UH5U6rrSEyMdrCpc9IXe9WCmWjz5yOQa0r3U7M5OPEKD1-8wuP6_dPw0DyNO_Ei7UerVtsx5XSTd-Z5ujeB3PFVeAdtGxJ23oRNCq2MCOZBa58EGeRDLR7Q", "use": "sig", "alg": "RS256" }, { "kty": "RSA", "kid": "UEtnUohTq58JiJzxHhBLSU0yTpsmW-9EY1Wykha6VIg", - "e": "AQAB", - "n": "9UAG0U59NZ3MBMQkjCVuA8c0ZHEL8SEljnXYYxAuuvy4P79XxTYNodmBAioe1CBsdOmFjjdtXzPIxYv_zEHwkI5WoL1U0r83Q8RSbcl_YSCjfq32TW1hj1KQe0bjzx1TohtnOZSIq-0_8QLbdJrwN7LnBHkalAdMYFk9qUEFlTP-jwUIxztmjpok_-d6W1621iDQwUzYqKiTYc7ZQdC3Bf5jv-8yTm7pMQrR0W6XvPNnRwJmGZdkDH1ZC6okTzLsaMBgMV5awtXJeSZqrR3Qy3ATX6hiitmld9K3FyKFyyIOpaygjltKVzNy5giwnDfTHaGs24Y51Jy1SZ51vqfEFQ", "use": "sig", "alg": "RS256" } @@ -373,6 +405,72 @@ $ curl https://gitlab.com/oauth/discovery/keys | jq '.' } ``` +When the body of the JWT is decoded it takes the following form: + +```bash +{ + "iss": "http://gdk.test:3000", + "sub": "1", + "aud": "e31e1da0b8f6b6e35ca70c790b13c0406e44aca6b2bf67f55de7355a979a224f", + "exp": 1747937983, + "iat": 1747937863, + "auth_time": 1747774067, + "sub_legacy": "2474cf0b2211688a57297ace0e260a15944754d16b1bd42c9d6779c900367807", + "name": "Administrator", + "nickname": "root", + "preferred_username": "root", + "email": "admin@example.com", + "email_verified": true, + "profile": "http://gdk.test:3000/root", + "picture": "https://www.gravatar.com/avatar/258d8dc916db8cea2cafb6c3cd0cb0246efe061421dbd83ec3a350428cabda4f?s=80&d=identicon", + "groups_direct": [ + "gitlab-org" + ] +} +``` + +There are several non-standard claims in this token such as the `auth_time`, +`sub_legacy`, `name`, `nickname`, `preferred_username`, `email`, +`email_verified`, `profile`, `picture`, `groups_direct`. I think that `email` is +problematic because this is considered personally identifiable information and +this is something that I would like us to consider removing. + +Finally, we can validate the integrity of the JWT by decrypting the signature +and recomputing a hash of the header + "." + body. The following ruby code +demonstrates this: + +```ruby +#!/usr/bin/env ruby +require 'openssl' +require 'base64' + +# This key is fetched from the `jwks_uri` +metadata = { + kty: "RSA", + kid: "td0SmdJQ4qPh5qNKzM260CXx2Uh-whG-MDjoOKWft8E", + e: "AQAB", + n: "z4JrfdkUjeCPcMQEB1ai9OJbZ8xMrtdNI9K80XUYTcyfkQDlFnZNgRvwnkLkZJ0XjtLbc6Y0RMEyo32DivIfWb31US_1FRRJm0oS2mSFV4iHsfTXjVnlmExYW0ke2_BZ4Vu_rRIVxD1eJYNLjn8Uqb7ZllnUJFZDzTk5qQCVX9F5idQgWFh9DxtY3pGutz1-BxaQmTDts_p4cDu8HPnmJEiTCsx7opIfvqpaumfuiLlPZvozERnsnC8BDS1EQja3nJhOnaBFV6vrk57VH_IwmybVACk2w3uW8n0o63roDHfnpo5hQuSm2M-5mEcyXH0PA5YsDuYRi1uxF58Vob6NSw", + use: "sig", + alg: "RS256" +} + +jwt = "eyJ0eXAiOiJKV1QiLCJraWQiOiJ0ZDBTbWRKUTRxUGg1cU5Lek0yNjBDWHgyVWgtd2hHLU1Eam9PS1dmdDhFIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMCIsInN1YiI6IjEiLCJhdWQiOiJlMzFlMWRhMGI4ZjZiNmUzNWNhNzBjNzkwYjEzYzA0MDZlNDRhY2E2YjJiZjY3ZjU1ZGU3MzU1YTk3OWEyMjRmIiwiZXhwIjoxNzQ3OTM3OTgzLCJpYXQiOjE3NDc5Mzc4NjMsImF1dGhfdGltZSI6MTc0Nzc3NDA2Nywic3ViX2xlZ2FjeSI6IjI0NzRjZjBiMjIxMTY4OGE1NzI5N2FjZTBlMjYwYTE1OTQ0NzU0ZDE2YjFiZDQyYzlkNjc3OWM5MDAzNjc4MDciLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsIm5pY2tuYW1lIjoicm9vdCIsInByZWZlcnJlZF91c2VybmFtZSI6InJvb3QiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByb2ZpbGUiOiJodHRwOi8vZ2RrLnRlc3Q6MzAwMC9yb290IiwicGljdHVyZSI6Imh0dHBzOi8vd3d3LmdyYXZhdGFyLmNvbS9hdmF0YXIvMjU4ZDhkYzkxNmRiOGNlYTJjYWZiNmMzY2QwY2IwMjQ2ZWZlMDYxNDIxZGJkODNlYzNhMzUwNDI4Y2FiZGE0Zj9zPTgwJmQ9aWRlbnRpY29uIiwiZ3JvdXBzX2RpcmVjdCI6WyJnaXRsYWItb3JnIiwidG9vbGJveCIsIm1hc3NfaW5zZXJ0X2dyb3VwX18wXzEwMCIsImN1c3RvbS1yb2xlcy1yb290LWdyb3VwL2FhIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAvYWEvYWFhIiwiZ251d2dldCIsIkNvbW1pdDQ1MSIsImphc2hrZW5hcyIsImZsaWdodGpzIiwidHdpdHRlciIsImdpdGxhYi1leGFtcGxlcyIsImdpdGxhYi1leGFtcGxlcy9zZWN1cml0eSIsIjQxMjcwOCIsImdpdGxhYi1leGFtcGxlcy9kZW1vLWdyb3VwIiwiY3VzdG9tLXJvbGVzLXJvb3QtZ3JvdXAiLCI0MzQwNDQtZ3JvdXAtMSIsIjQzNDA0NC1ncm91cC0yIiwiZ2l0bGFiLW9yZzEiLCJnaXRsYWItb3JnL3NlY3VyZSIsImdpdGxhYi1vcmcvc2VjdXJlL21hbmFnZXJzIiwiZ2l0bGFiLW9yZy9zZWN1cml0eS1wcm9kdWN0cyIsImdpdGxhYi1vcmcvc2VjdXJpdHktcHJvZHVjdHMvYW5hbHl6ZXJzIl19.TjTrGS5FjfPoY0HWkSLvgjogBxB27jX2beosOZAkwXi_gO3q9DTnL0csOgxjoF1UR8baPNfMFBqL1ipLxBdY9vvDxZve-sOhoSptjzLGkCi7uQKeu7r8wNyFWNWhcLwmbinZyENGSZqIDSkHy0lGdo9oj7qqnH6sYqU46jtWACDGSHTFjNNuo1s_P2SZgkaq4c4v4jdlVV_C_Qlvtl7-eaWV1LzTpB4Mz0VWGsRx1pk3-KnS24crhBjxSE383z4Nar4ZhrsrTK-bOj33l6U32gRKNb4g6GxrPXaRQ268n37spQmbQn0aDwmUOABv-aBRy203bCCZca8BJ0XBur8t6w" +header, body, signature = jwt.split(".") + +n = OpenSSL::BN.new(Base64.urlsafe_decode64(metadata[:n]), 2) +e = OpenSSL::BN.new(Base64.urlsafe_decode64(metadata[:e]), 2) +public_key = OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence.new([ + OpenSSL::ASN1::Integer.new(n), + OpenSSL::ASN1::Integer.new(e) +]).to_der) + +puts public_key.verify( + OpenSSL::Digest::SHA256.new, + Base64.urlsafe_decode64(signature), + [header, body].join(".") +) +``` + The problem of sharing and distributing public keys is solved using Public Key Infrastructure (PKI). PKI provides a mechanism for distributing X.509 @@ -400,7 +498,9 @@ assumptions about user authentication is valid. The `id_token` in the OpenID Connect (OIDC) workflow represents the authentication context. This _DOES NOT_ represent an authorization context. -TODO:: Describe the sections of a JWT and the schema of the `id_token`. +OpenID Core specification describes the `id_token` as a JWT and the JWT +specification describes a set of standard claims that are found in the +JWT body. ### Authorization @@ -427,7 +527,10 @@ used to make authorization decisions. Authorization decisions should be made using the `access_token` because this represents the delegated authorization access granted to the service that the token was minted for. In general, most API's that receive a request should make authorization decisions based on the -privileges granted to the `access_token`. +privileges granted to the `access_token`. OAuth does not specify the format of +the `access_token` so this is up to the OAuth Authorization Server to decide the +schema. It is possible for us to adopt the JWT standard for `access_token` +representation. The authorization server that generates the `access_token` should only grant just enough privileges to this token that is required by the service (OAuth @@ -860,6 +963,8 @@ authentication/authorization strategy that GitLab as a whole supports. ## References * [Envoy Proxy](https://www.envoyproxy.io/) +* [OpenID Core Specificatioin](https://openid.net/specs/openid-connect-core-1_0-final.html#IDToken) +* [RFC-7519: JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) * [`envoy.filters.http.oauth2`](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/oauth2_filter.html) * [`envoy.filters.http.jwt_authn`](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/jwt_authn/v3/config.proto) * [`envoy.filters.http.ext_authz`](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto) |
