JSON Web Token(JWT) is a technique of authenticating across systems. It depends on trust, and technology similar to TLS and SSL.
The high level picture
A client wants to use a service on a server.
On the client machine
- The client creates a payload containing authentication information, such as subject(=userid), issuer(=machine/company name), issued time, and expiry time.
- This payload is signed using a private key. Where signing is doing a checksum on the payload and encrypted the checksum with the private key.
- The payload and signature are sent to the server.
On the server machine
- The server has a list of definitions, containing a issuer and other information, the name of a keyring, and the name of a certificate. The trust comes from if you trust the client then you store the public key for the client in the keyring. If you do not trust the client, you do not make the certificate available.
- When the data from the client machine arrives, the list of definitions is processed until a match is found. It checks the issuer and other fields, (it can use filters) and if they match, checks the signature of the payload to find the public key in the keyring, and validates the signature.
- If everything matches, information from the payload and the definition on the server are used to look up a list of subject, and issuer to get the RACF userid to be used.
- The thread switches to the userid and does the application work.
How to create a JWT
- z/OSMF can do this for you
- You can create your own on z/OS – but you need to know how to do a checksum and encrypt with a private key. ICSF provides services which do this.
- use Python.
Use z/OSMF
In the logical server.xml file
<featureManager>
<feature>jwtSso-1.0</feature>
</featureManager>
<jwtSso cookieName="jwtToken"
jwtBuilderRef="zOSMFBuilder"
includeLtpaCookie="true"
useLtpaIfJwtAbsent="true"
/>
<jwtBuilder id="zOSMFBuilder"
issuer="zOSMF"
keyAlias="CONN2.IZUDFLT"
expiresInSeconds="10"
jwkEnabled="false"
signatureAlgorithm="RS384"
/>
<mpJwt id="myMpJwt"
issuer="zOSMF"
wksUri="https://${izu.jwks.hostname}:${izu.https.port}/jwt/ibm/api/zOSMFBuilder/jwk"
signatureAlgorithm="RS384"
/>
Interesting fields are
- issuer=”zOSMF” this is put into the payload. It is used to help identify the realm
- keyAlias=”CONN2.IZUDFLT”. This is the certificate with the private key in the keyring which is used to sign the JWT
- expiresInSeconds=”10″ how long the JWT is valid for. During testing I set this to 600 seconds.
- jwksUri=”https://${izu.jwks.hostname}:${izu.https.port}/jwt/ibm/api/zOSMFBuilder/jwk” if you want the server to issue a request to the client asking for validation, this is the URL the server would use.
Use Python
This is very easy
from datetime import datetime, timezone, timedelta
import jwt
# open the private key and read the contents
pemfile = open("/home/colinpaice/ssl/ssl2/colinpaice.key.pem", 'r')
keystring = pemfile.read()
pemfile.close()
# Create the header. Specify the type, and the encryption algorithm
header= {
"typ": "JWT",
"alg": "RS256"
}
# get time in number of seconds since "day 0"
now = datetime.now(timezone.utc)
payload = {
"token_type": "Bearer",
"sub": "ADCDC", # userid
"upn": "ADCDC",
"realm": "PYTHON",
"iss": "PYTHON", # issuer
"iat": now,
"exp": now + timedelta(seconds=6000) # valid for 6000 seconds
}
# encrypt it and create the jwt
token = jwt.encode(payload, keystring, algorithm='RS256')
print(token) # display it
Backend server set up
The page Configuration example: Securing a Liberty web application with a JWT and CICS transaction security explains how to do it for Liberty in a CICS region.
For my MQWEB server I added to mqwebuser.xml
<featureManager>
<feature>openidConnectClient-1.0</feature>
</featureManager>
<openidConnectClient id="RSCOOKIE"
clientId="COLINCOOK"
realmName="zOSMF"
inboundPropagation="required"
issuerIdentifier="zOSMF"
mapIdentityToRegistryUser="false"
signatureAlgorithm="RS384"
jwkEndpointUrl="https://10.1.1.2:10443/jwt/ibm/api/zOSMFBuilder/jwk"
trustAliasName="CONN2.IZUDFLT"
trustStoreRef="defaultKeyStore"
userIdentifier="sub"
>
</openidConnectClient>
mapIdentityToRegistryUser=”false” says use the realm in this definition and the userid(subject) from the payload to look up the in the RACF RACMAP to get the userid.
If you specify “true” it uses the userid(subject) from the payload, and the sysplex name. This means a userid “COLIN” from a z/OS system, and a userid “COLIN” from Linux – get the same userid on z/OS.
Note: If they public key for the JWT is not in the keyring, add it, and restart the server.
Map subject(userid) and realm to get a userid.
See RACMAP (Create, delete, list, or query a distributed identity filter)
For example
RACMAP ID(IBMUSER) MAP USERIDFILTERNAME("IBMUSER") REGISTRY("zPROD") WITHLABEL("zPROD")
RACMAP ID(NOONE) MAP USERIDFILTERNAME("IBMUSER") REGISTRY("LINUX") WITHLABEL("LINUX")
RACMAP ID(ZILTCH) MAP USERIDFILTERNAME("*") REGISTRY("*") WITHLABEL("CATCHALL")
The registry is the realm name from the server definitions.
if a valid userid is returned from the mapping, the thread is changed to run as that userid, and execute the application work – as that userid.
The backend server is a little more complex
You need a <openidConnectClient ../> for each “client” system the server supports.
You can specify one <authFilter../> to restrict what the <openidConnectClient ../> processes. For example if can be an IP address(10.1.0.2) or a range (10.1.*.*), or restrict it by URL.
For the <openidConnectClient ../> definition to match it needs
- The signature validated – so the public key needs to be in a keyring. (You could have a different keyring for each realm)
- The issuer from the payload to match the definition
- The input data must pass the authFilter.
You need to plan for
- the realms you need,
- the mapping of subjects and realms to userids in the server,
- public keys and keyrings
Some useful links
- The top level definition is the openidConnectClient.
- The authFilter element
- Liberty authentication filters
My Server’s definition
<featureManager>
<feature>transportSecurity-1.0</feature>
<feature>openidConnectClient-1.0</feature>
</featureManager>
<openidConnectClient id="RSCOOKIE"
headerName="colin"
clientId="COLINCOO2"
realmName="zOSMF"
inboundPropagation="required"
issuerIdentifier="zOSMF"
mapIdentityToRegistryUser="false"
signatureAlgorithm="RS384"
jwkEndpointUrl="https://10.1.1.2:10443/jwt/ibm/api/zOSMFBuilder/jwk"
trustAliasName="CONN1.IZUDFLT"
trustStoreRef="defaultKeyStore"
userIdentifier="sub"
>
<authFilter id="afint">
<remoteAddress id="myAddress" ip="10.1.0.2" matchType="equals" />
</authFilter >
</openidConnectClient>
<keyStore
id="defaultKeyStore"
filebased="false"
location="safkeyring://IZUSVR/CCPKeyring.IZUDFLT"
password="password" readOnly="true" type="JCERACFKS"
/>
Where
- headerName=”colin”. My curl request has -H “colin: $token” where token is the bash variable with the is the JWT token. Without this, the request needs -H “Authorization: Bearer $token”
- realmName=”zOSMF” used with the userid to lookup in the RACMAP table for the z/OS userid to use
- signatureAlgorithm=”RS384″ matches the value in the JWT. (I had problems using RS256)
- trustAliasName=”CONN1.IZUDFLT” the name of the certificate to use on the server
- trustStoreRef=”defaultKeyStore” points to the keyring definition
thank you Colin. I learned a lot on the subject with your post
LikeLike