When CAS is configured to act as an OAuth identity provider, it begins to issue access tokens that are by default opaque identifiers. There is also the option to generate JWTs as access tokens on a per-application basis. Using JWTs, CAS can create JSON documents to encode all relevant parts of an access token into the token itself. The main benefit of this is that API servers can verify access tokens without doing a token lookup on every API request, making the API much more easily scalable. Also, this means that applications don’t need to be aware of how CAS implements access tokens which makes it possible to change the implementation later without affecting clients.
Our starting position is based on:
6.2.x
11
jq
First, let’s create a few mock attributes that ought to be released to our sample yet-to-be-registered OAuth application:
cas.authn.attribute-repository.stub.attributes.cn=Misagh
cas.authn.attribute-repository.stub.attributes.sn=Moayyed
cas.authn.attribute-repository.stub.attributes.mail=mm1844@gmail.com
Once the OAuth module is included in the WAR Overlay, we can begin to register a simple OAuth application with CAS using the following JSON service definition:
{
"@class" : "org.apereo.cas.support.oauth.services.OAuthRegisteredService",
"clientId": "client",
"clientSecret": "secret",
"serviceId" : "https://example.net/dashboard",
"name" : "OAUTH",
"id" : 1,
"attributeReleasePolicy" : {
"@class" : "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy",
"allowedAttributes" : [ "java.util.ArrayList", [ "cn", "mail", "sn" ] ]
},
"supportedGrantTypes": [ "java.util.HashSet", [ "password" ] ]
}
A few things to note:
clientId
, clientSecret
and redirectUri
(i.e. serviceId
) defined.cn
, mail
, and sn
attributes are selectively defined to be released to the application.password
grant, which we will use to request access tokens
either in plain or JWT format.Let’s start simple, by using the password
grant to request
an access token without any extra configurations:
$ curl https://sso.example.org/cas/oauth2.0/token?grant_type=password'&'\
client_id=client'&'client_secret=secret'&'username=casuser'&'password=Mellon | jq
The above request first authenticates the request using the provided username
and password
. Once the application policy is located
and verified by CAS, an access token can be provided in the response:
{
"access_token": "AT-1-wiNsTgaHzXLUIyaaoFoip-znohWPihea",
"token_type": "bearer",
"expires_in": 28800,
"scope": ""
}
We can, of course, use the access token in exchange for user profile information:
curl -k --user client:secret https://sso.example.org/cas/oauth2.0/profile?\
access_token=AT-1-wiNsTgaHzXLUIyaaoFoip-znohWPihea
…where the result would give us access to allowed claims:
{
"cn": "Misagh",
"mail": "mm1844@gmail.com",
"sn": "Moayyed",
"service": "client",
"id": "casuser",
"client_id": "client"
}
As a next step, let’s modify our service definition to ask for access tokens as JWTs:
{
"@class" : "org.apereo.cas.support.oauth.services.OAuthRegisteredService",
"clientId": "client",
"clientSecret": "secret",
"serviceId" : "https://example.net/dashboard",
"name" : "OAUTH",
"jwtAccessToken": true,
"id" : 1,
"attributeReleasePolicy" : {
"@class" : "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy",
"allowedAttributes" : [ "java.util.ArrayList", [ "cn", "mail", "sn" ] ]
},
"supportedGrantTypes": [ "java.util.HashSet", [ "password" ] ]
}
With the addition of the jwtAccessToken
field, CAS will render access tokens as JWTs that are by default signed and encrypted using (pre-generated, if undefined) keys. So, let’s start simple and force CAS to disable signing and encryption of such tokens so we can
unpack them easier later for verification:
# Force keys to be blank
cas.authn.oauth.access-token.crypto.encryption.key=
cas.authn.oauth.access-token.crypto.signing.key=
cas.authn.oauth.access-token.crypto.enabled=false
cas.authn.oauth.access-token.crypto.signing-enabled=false
cas.authn.oauth.access-token.crypto.encryption-enabled=false
Using the same command to request an access token, the response now delivers a JWT instead:
{
"access_token": "eyJhbGciOi...",
"token_type": "bearer",
"expires_in": 28800,
"scope": ""
}
Since the JWT is plain this time around, we can easily unpack it using a service like jwt.io to verify the embedded JSON:
{
"sub": "casuser",
"mail": "mm1844@gmail.com",
"roles": [],
"iss": "https://sso.example.org/cas",
"cn": "Misagh",
"nonce": "",
"client_id": "client",
"aud": "client",
"grant_type": "PASSWORD",
"permissions": [],
"scope": [],
"claims": [],
"scopes": [],
"state": "",
"sn": "Moayyed",
"exp": 1572837100,
"iat": 1572808300,
"jti": "AT-1-ibYxeSXhcU1N-0sF1JQXdgX4YAmBgCXY"
}
Of course, we can exchange the very same JWT for user profile information just as we did with a plain access token:
{
"cn": "Misagh",
"mail": "mm1844@gmail.com",
"sn": "Moayyed",
"service": "client",
"id": "casuser",
"client_id": "client"
}
If we wanted, we could turn on signing and encryption of our JWT access tokens:
cas.authn.oauth.accessToken.crypto.encryption.key=4fdqpa_mlx1XMtQR...
cas.authn.oauth.accessToken.crypto.signing.key=FXdUERkUNGqmai8oociQOyrHCQVYSW...
cas.authn.oauth.accessToken.crypto.enabled=true
cas.authn.oauth.accessToken.crypto.signing-enabled=true
cas.authn.oauth.accessToken.crypto.encryption-enabled=true
The same exercise can be repeated to make sure an encrypted/signed JWT can be decoded back to produce user profile information.
Of course, keys can always belong to a specific service definition, overriding the global default. If we wanted to, we could modify our sample service definition as such:
{
"@class" : "org.apereo.cas.support.oauth.services.OAuthRegisteredService",
"clientId": "client",
"clientSecret": "secret",
"serviceId" : "https://example.net/dashboard",
"name" : "OAUTH",
"jwtAccessToken": true,
"id" : 1,
"attributeReleasePolicy" : {
"@class" : "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy",
"allowedAttributes" : [ "java.util.ArrayList", [ "cn", "mail", "sn" ] ]
},
"supportedGrantTypes": [ "java.util.HashSet", [ "password" ] ],
"properties" : {
"@class" : "java.util.HashMap",
"accessTokenAsJwtSigningKey" : {
"@class" : "org.apereo.cas.services.DefaultRegisteredServiceProperty",
"values" : [ "java.util.HashSet", [ "..." ] ]
},
"accessTokenAsJwtEncryptionKey" : {
"@class" : "org.apereo.cas.services.DefaultRegisteredServiceProperty",
"values" : [ "java.util.HashSet", [ "..." ] ]
},
"accessTokenAsJwtSigningEnabled" : {
"@class" : "org.apereo.cas.services.DefaultRegisteredServiceProperty",
"values" : [ "java.util.HashSet", [ "true" ] ]
},
"accessTokenAsJwtEncryptionEnabled" : {
"@class" : "org.apereo.cas.services.DefaultRegisteredServiceProperty",
"values" : [ "java.util.HashSet", [ "true" ] ]
}
}
}
All properties should be optional; You may only specify that which you intend to override.
While it’s nice to allow JWT access tokens on a per-service basis, you may want to extend that behavior to all applications and make JWT access tokens the global default. To do, you would need to turn on the following setting:
cas.authn.oauth.accessToken.createAsJwt=true
When ciphers are turned on, JWT access tokens are by default (whether it’s global or for a specific service) are always encrypted first and then signed. You can certainly change the strategy type to reverse this behavior either globally or for a specific relying party:
# cas.authn.oauth.accessToken.crypto.strategy-type=ENCRYPT_AND_SIGN
cas.authn.oauth.accessToken.crypto.strategy-type=SIGN_AND_ENCRYPT
You may have noticed that our JSON service definition contains a client secret in plain text. However, client secrets can also be kept as encrypted secrets; To be clear, authorized relying parties always have access to and submit the client secret in plain text and CAS will auto-reverse the encryption of the secret found in the service definition file for verification and matching.
Skipping other details for brevity, our service file could take on the following form:
{
"@class" : "org.apereo.cas.support.oauth.services.OAuthRegisteredService",
"clientId": "client",
"clientSecret": "{cas-cipher}eyJhbGciOiJIUzUxMiIs...",
"serviceId" : "https://example.net/dashboard",
"name" : "OAUTH",
"jwtAccessToken": true,
"id" : 1
...
}
All you’d have to do is to take a plain secret and use the CAS Command-line Shell to transform it into encrypted form. The encryption and signing keys for client secrets may be defined via the following settings:
cas.authn.oauth.crypto.encryption.key=...
cas.authn.oauth.crypto.signing.key=...
cas.authn.oauth.crypto.enabled=true
cas.authn.oauth.crypto.signing-enabled=true
cas.authn.oauth.crypto.encryption-enabled=true
crypto
namespace. This is not by chance,
as configuration namespaces in CAS are internally reused everywhere to streamline the specification
and validation process as much as possible for maximum code re-use. In most cases, such namespaces
in CAS configuration settings are transferable to other areas that declare support for the same feature
or namespace.
I hope this review was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. Please know that all other use cases, scenarios, features, and theories certainly are possible as well. Feel free to engage and contribute as best as you can.
Finally, if you benefit from Apereo CAS as free and open-source software, we invite you to join the Apereo Foundation and financially support the project at a capacity that best suits your deployment. If you consider your CAS deployment to be a critical part of the identity and access management ecosystem and care about its long-term success and sustainability, this is a viable option to consider.
Happy Coding,
Monday-Friday
9am-6pm, Central European Time
7am-1pm, U.S. Eastern Time
Monday-Friday
9am-6pm, Central European Time