jwt authentication - hokiegeek2/slurm-cloud-integration GitHub Wiki
Background
As detailed presented here, JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with a HMAC algorithm such as HS256) or a public/private key pair using RSA (such as RS256) or ECDSA.
Structure of a JWT Token
As discussed here, there are three parts to a JWT token: header, payload, and signature which together are encoded as a dot-delimited alphanumeric string.
Header
The JWT token header at a minimum contains two elements: the signing algorithm and token type:
{ "alg": "HS256", "typ": "JWT" } However, depending upon the system, other, additional elements are required. For example, Slurm requires the key ID (kid) header parameter that is used to indicate which key was used to secure the JWT. Accordingly, an example Slurm JWT header is as follows;
{ 'alg': 'RS256', 'kid': 'eYRTfaCstZaFQ-rL-FvfjUDFOFQrah8Pn3E_GOj2mGE', 'typ': 'JWT' }
Claims
The second part of the token is the payload, which encapsulates the claims containing statements about the requesting entity as well additional items needed to authenticate. There are three types of claims: registered, public, and private claims.
Registered Claims
Registered claims are typically data items that describe the scope of the JWT token such as the audience token issuer, token issue time and expiration, etc... Again, depending upon the system, these elements are either optional or required. All registered claims are maintained in the IANA Web Token Registry.
Public Claims
Public claims can be negotiated between the interacting parties and defined as desired, and these claims may or may not be in the aforementioned Web Token Registry. For example, Slurm specifies a claims field entitled "sun" for RS256 tokens, which corresponds to the authenticated user. Accordingly, an example Slurm JWT RS256 claims section is as follows:
{
'algorithm': 'RS256',
'exp': 1644419813146,
'iat': 1644419289,
'jti': 'KBHjbCgQf1mqtQKbYqIojA',
'nbf': 1644419289,
'sun': 'slurm'
}
Note the exp (expiration), iat (issued at), and nbf (not before) fields pertain to the token validity timeframe.
Private Claims
Private claims can basically be any kind of key/value pair.
Signature
The signature is used to verify the message wasn't changed along the way, and, in the case of tokens signed with a private key, it can also verify that the identify of the sender.
JWT Signing Algorithms
The two types of JWT signing algorithms studied in the context of slurmrestd are HS256 and RS256 as detailed here. As defined by auth0, RS256 and HS256 can be summarized as follows:
RS256 is an asymmetric algorithm which means that there are two keys: one public and one private (secret). Auth0 has the secret key, which is used to generate the signature, and the consumer of the JWT has the public key, which is used to validate the signature.
HS256 is a symmetric algorithm which means that there is only one secret key, shared between the two parties. The same key is used both to generate the signature and to validate it. Special care should be taken in order for the key to remain confidential.
Python JWT Libraries
There are multiple Python libraries for generating and validating all JWT artifacts, two of which I experimented with: python-jwt and jwt. Both libraries delegate some logic to the jwcrypto library, and jwcrypto is a Python implementation of the JSON Web Key standard that is used for some steps in JWT artifact generation as discussed below. Of the two JWT libraries, python-jwt is perhaps a bit better as it makes handling custom claim and header sections in token generate easier. In addition, python-jwt can be used to genrate public and private pems as well as keys, and to generate jwks.json files.
RS256 JWT Artifacts
There are four sets of JWT artifacts needed to deliver RS256 JWT authentication: (1) public and private pems (2) private and public keys, (3) jwks.json, and (4) encoded tokens generated via private keys. All of the JWT artifacts can be generated with python-jwt as detailed below.
Public and Private Privacy-Enhanced Mail (PEM) Files
Privacy Enhanced Mail (PEM) files are a type of Public Key Infrastructure (PKI) file used to generate keys and certificates. The first step in creating the private and public PEM files needed for RS256 authentication is creating a SON Web Key. The jwk object is created via the jwk.JWK.generate class method. The priv and pem files are generated with the JWK export_to_pem instance method. An example sequence is shown below:
import jwcrypto.jwk as jwk
# Generate JSON Web Key
key = jwk.JWK.generate(kty='RSA', size=2048)
# Generate Private and Public PEM bytes objects
priv_pem = key.export_to_pem(private_key=True, password=None)
pub_pem = key.export_to_pem()
# Save Private PEM bytes object to file
with open('/etc/slurm/private.pem', 'wb') as f:
... f.write(priv_pem)
JSON Web Key (JWK)
The JSON Web Key (JWK) is used to sign each token and is generated from the private.pem file detailed above:
import jwcrypto.jwk as jwk
priv_key = jwk.JWK.from_pem(priv_pem)
JSON Web Key Set (JWKS) File
Background
As detailed here, the JWKS file, which can be entitled as the jwks.json file, contains 1..n public JSON Web Keys as a JSON array. Basically, the JWKS is a set of keys containing the public keys that are used to verify JWT tokens submitted as an HTTP/S request. An example jwks.json file is as follows:
{
"keys": [
{
"alg": "RS256",
"kty": "RSA",
"use": "sig",
"x5c": [
"MIIC+DCCAeCgAwIBAgIJBIGjYW6hFpn2MA0GCSqGSIb3DQEBBQUAMCMxITAfBgNVBAMTGGN1c3RvbWVyLWRlbW9zLmF1dGgwLmNvbTAeFw0xNjExMjIyMjIyMDVaFw0zMDA4MDEyMjIyMDVaMCMxITAfBgNVBAMTGGN1c3RvbWVyLWRlbW9zLmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMnjZc5bm/eGIHq09N9HKHahM7Y31P0ul+A2wwP4lSpIwFrWHzxw88/7Dwk9QMc+orGXX95R6av4GF+Es/nG3uK45ooMVMa/hYCh0Mtx3gnSuoTavQEkLzCvSwTqVwzZ+5noukWVqJuMKNwjL77GNcPLY7Xy2/skMCT5bR8UoWaufooQvYq6SyPcRAU4BtdquZRiBT4U5f+4pwNTxSvey7ki50yc1tG49Per/0zA4O6Tlpv8x7Red6m1bCNHt7+Z5nSl3RX/QYyAEUX1a28VcYmR41Osy+o2OUCXYdUAphDaHo4/8rbKTJhlu8jEcc1KoMXAKjgaVZtG/v5ltx6AXY0CAwEAAaMvMC0wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUQxFG602h1cG+pnyvJoy9pGJJoCswDQYJKoZIhvcNAQEFBQADggEBAGvtCbzGNBUJPLICth3mLsX0Z4z8T8iu4tyoiuAshP/Ry/ZBnFnXmhD8vwgMZ2lTgUWwlrvlgN+fAtYKnwFO2G3BOCFw96Nm8So9sjTda9CCZ3dhoH57F/hVMBB0K6xhklAc0b5ZxUpCIN92v/w+xZoz1XQBHe8ZbRHaP1HpRM4M7DJk2G5cgUCyu3UBvYS41sHvzrxQ3z7vIePRA4WF4bEkfX12gvny0RsPkrbVMXX1Rj9t6V7QXrbPYBAO+43JvDGYawxYVvLhz+BJ45x50GFQmHszfY3BR9TPK8xmMmQwtIvLu1PMttNCs7niCYkSiUv2sc2mlq1i3IashGkkgmo="
],
"n": "yeNlzlub94YgerT030codqEztjfU_S6X4DbDA_iVKkjAWtYfPHDzz_sPCT1Axz6isZdf3lHpq_gYX4Sz-cbe4rjmigxUxr-FgKHQy3HeCdK6hNq9ASQvMK9LBOpXDNn7mei6RZWom4wo3CMvvsY1w8tjtfLb-yQwJPltHxShZq5-ihC9irpLI9xEBTgG12q5lGIFPhTl_7inA1PFK97LuSLnTJzW0bj096v_TMDg7pOWm_zHtF53qbVsI0e3v5nmdKXdFf9BjIARRfVrbxVxiZHjU6zL6jY5QJdh1QCmENoejj_ytspMmGW7yMRxzUqgxcAqOBpVm0b-_mW3HoBdjQ",
"e": "AQAB",
"kid": "NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg",
"x5t": "NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg"
}
]}
Generating jwks.json File
To generate a jwks.json file, first open a text editor, add the following, and save off as jwks.json
{
"keys": [
]
}
Next, execute the following Python code to retrieve the JSON Web Key (JWK) JSON string:
import jwcrypto.jwk as jwk
# Load private.pem file containing JSON Web Key (JWK)
with open('/etc/slurm/private.pem', 'rb') as fh:
... priv_pem = fh.read()
...
# Load JWK from private.pem file
priv_key = jwk.JWK.from_pem(priv_pem)
# Export JWK JSON string
jwk.JWK.export_private(priv_key)
'{"d":"m6nOL9jf2XWXouI6gfiqt4aZ8MKhio3XoaLZZmGa3tZJbtZJm_4Nu6Lu2ZHHXDm82tX1EXagu4nBJFX4iYNKcjcPcGeWqH3QsOC_BJ03PhCLNSQHSzHUfv7yq3ExXc2PzHFUkIcXAiJAPx8dbXEk8nUf_L2dTr6uktjqESq-P-bYKQPoVdG9lboSsdZHmHitGZwPr2tOieyrG_QwsHPU8KZ9ifp31zLERbCddq15l01JclS1RvXolz9vagjx2EsM2s4bghe1X1IOiFfC8DQFNEnhSp6L1Ds7c_-t6SzGXdo2rA8Ev85Z7C6Sd2ihw7HVebyTb81Ib0HlGtKSn4z_QQ","dp":"mX6rlJmsXREeH596LWjN2npaQDIX_QVYgewg8qZc_5L4YcHpOe-NJ50jFBLFUI7TOTlCtyCi1HW-sP0cb2gQORGGZ1MeModKxwpn1gdBUTOtwHhdyb2MLo_5W74xgKKeIBue8SOKVLlGkfjlgJ8stjO-S9oUzP6TaL4-_fuSA1E","dq":"SHuqeqQAvzlhV1Uu0FBsTdb01z9eOPxsluNZFTPojsEDC4KECCHJR3qNyU5OjuYerlYJ0N5AdsEULtsowsEviUxSNj9Rl61euwMfqKak4wjATlN975Iym-pPC4FL2JCFJUqd5YS1QcNuk0ItUvayokH3yJFncxmrOzCh7xI2MV0","e":"AQAB","kid":"eYRTfaCstZaFQ-rL-FvfjUDFOFQrah8Pn3E_GOj2mGE","kty":"RSA","n":"v9ZND0oSC1HWV7CdiDJRig4wW5BPAw3K9SWbSfOdbp2A44Y1r7FAnatG9HbLA1nY-yWLGviSs1S0UhU24ZIAYfr4-8h7MtA1SyXGP79H5IB2BsPD8Arb1kA8SCt2zFOxbVMxwstFUlks0cm6-wU0qtPVlrBEYPjFYOWq2gHusTZ8Ku2bl2KM_3u9sZS8FmFcnluPAswHCj7YnFe1nECXmjYJJzj6VF4Ebg_e0VSAtnnqZjEAg5XMOjtRszry8wdzKEggO2dJd3Tzh-CKupS2Y5Xcuprgp3JxwdA7cPGbGWdGOZv8ucefOYJuSIbLGbXv1depzgtXH8-MUUaAw1dLtQ","p":"8ruuW7BrYD48bfacBzVoyoNkhjgtHMzLa_Mk__PXV2__b6qTcqWN-hJc3_hynoTk4Ft-5led1uBDP8P91C6sHXBUEGQnTAG82A9DU9ls_7DtB-a8rQF50gfR3gKwRsp98cQpg-Q5ZljPkpY81GZNTzmRafvdhpcbsCCQeQnqEHE","q":"ylJ7xz4LWlXySpWu1HReDiT92rDj7X4-0VJwo0GEA6MUUZL7s0KaKl5fGPsJmOInJeWjKChmi9HYa4snyRF7M8zhNEbIaiTQbsxhQWmdpe8uCOqv0T9y2KjnTUadLmtqr_Sx_Oj4OyW36h0_yUPvOCBL0-t1UJO-gIFp0FqZUYU","qi":"UiBMTyiLMg9O55b7wu9GiiFzVUGvmwkeYUo-P79yD_Pji23mI7rQvaEybOhxltkYqFw7I50MzpCOwpQzBEzfX6P6cJmMijZlwenVRfAOxjGJ5Koqev5IdQxEB7IJ3-lrklS7Aoo-vuxcWsSnkgqzTfjHvUbWbus8ocA-14IKE1Y"}'
Finally, copy the generated JSON Web Key JSON string and insert into the keys JSON array in the jwks.json file
JWT Tokens
Generating RS256 JWT Tokens
The python_jwt.generate_jwt method accepts the following parameters:
claims–Python dict containing registered, public, and/or private claims. The claims dict in particular is used to add parameters that are optional and/or unregistered. For example, the private claims sun field required by Slurm is entered here. algorithm–a string indicating the encoding algorithm that, in the case of Slurm, is either RS256 or HS256. priv_key–the private RSA key used to sign the JWT token lifetime–the time duration the token is valid other_headers–Python dict containing optional header fields such as kid, which is required by slurm. An example token generation is shown below:
token = python_jwt.generate_jwt(claims={'sun': 'slurm','algorithm':'RS256'},priv_key=priv_key, algorithm='RS256',lifetime=datetime.timedelta(minutes=60),other_headers={"kid":"eYRTfaCstZaFQ-rL-FvfjUDFOFQrah8Pn3E_GOj2mGE"})
Validating RS256 JWT Tokens
Any generated JWT token can be decoded and verified with the python_jwt.verify_jwt method which will (1) verify the token's validity and (2) break out the header and claims sections as Python dict objects so that the contents can verified as well. An example is as follows:
token
'eyJhbGciOiJSUzI1NiIsImtpZCI6ImVZUlRmYUNzdFphRlEtckwtRnZmalVERk9GUXJhaDhQbjNFX0dPajJtR0UiLCJ0eXAiOiJKV1QifQ.eyJhbGdvcml0aG0iOiJSUzI1NiIsImV4cCI6MTY0NDQxOTgxMzE0NiwiaWF0IjoxNjQ0NDE5Mjg5LCJqdGkiOiJLQkhqYkNnUWYxbXF0UUtiWXFJb2pBIiwibmJmIjoxNjQ0NDE5Mjg5LCJzdW4iOiJzbHVybSJ9.EmldY3Mebozfi3hGq9Gtft60ICUAxRmfgIFq195Fsc_nTt3Epqiu2jfvfU4qcDUSWq2f-oK_HQtexa1kk9RzS9qQEXTQnRU7RWc3XGTvzUweduWyS9GeVAQJgg7JUDEZI5U9CeY5-U9FfiX-NXxsXakHpXFj34FcVszNiwNs94fSkhhXTSDQ3zRaxc6JtKh-JIsWcLq1uPmUR6P0gULmdyFX17KlRvllx3T5xypI_S67UhdZrmNKocqNpo4m9oDRTX-s1FZb5MIaf-mXfUNRVO9KdCeKNQ3Ohyyv7k5HjJv4JmE-7E4mzfi-fd6AR9rlsGiQUoqL4Bh_ZJZlUGuz9Q'
header, claims = python_jwt.verify_jwt(token, pub_key, ['RS256'])
header
{'alg': 'RS256', 'kid': 'eYRTfaCstZaFQ-rL-FvfjUDFOFQrah8Pn3E_GOj2mGE', 'typ': 'JWT'}
claims
{'algorithm': 'RS256', 'exp': 1644419813146, 'iat': 1644419289, 'jti': 'KBHjbCgQf1mqtQKbYqIojA', 'nbf': 1644419289, 'sun': 'slurm'}
```
### Retrieving Key ID (kid)
There are some systems such as Slurm where the kid value must be passed in with the JWT header. The kid value is embedded within the jwks.json file as well as the JSON Web Key used to compose the key entries within the jwks.json file. The kid value can be retrieved as follows within Python:
```
import jwcrypto.jwk as jwk
with open('/etc/slurm/private.pem', 'rb') as fh:
... priv_pem = fh.read()
...
priv_key = jwk.JWK.from_pem(priv_pem)
kid = priv_key['kid']
```
# HS256 JWT Artifacts
HS256 JWT artifacts can be generated in Python with the jwt package.
## JWT Key
The HS256-compliant JWT key can be generated on any 'nix platform via the dd command. The example below demonstrates generation of an HS256 key for a Slurm installation:
```
dd if=/dev/random of=/etc/slurm/jwt_hs256.key bs=32 count=1
```
## JWT Signing Key
The signing key used to sign HS256 keys can be generated as follows:
```
from jwt.jwk import jwk_from_dict
from jwt.utils import b64decode,b64encode
# Read in the HS256 key
priv_key = open('/etc/slurm/jwt_hs256.key', 'rb').read()
# Create the signing key used to generate HS256 tokens
signing_key = jwk_from_dict({ 'kty': 'oct', 'k': b64encode(priv_key)})
JWT Tokens
HS256 JWT tokens are generated as follows:
# Import jwt libraries
from jwt import JWT
from jwt.jwk import jwk_from_dict
from jwt.utils import b64decode,b64encode
# specify user
user = 'slurm'
# time interval in seconds
time_interval = 3600
# Create HS256 token string
token_dict = {"exp": int(time.time() + time_interval),"iat": int(time.time()),"sun": user}
# Encode token
a = JWT()
token = a.encode(token_dict, signing_key, alg='HS256')
```