Wednesday, 27 July 2016

Fun with OpenAM13 Authz Policies over REST - the ‘jwt’ parameter of the ‘Subject’


Summary

I've previously blogged about the 'claims' and 'ssoToken' parameters of the 'subject' item used in the REST call to evaluate a policy for a resource. These articles are:
Now we're going to look at the 'jwt' parameter.  

For reference, the REST call we'll be using is documented in the developer guide, here:

The 'JWT' Parameter

The documentation describes the 'jwt' paramter as:
The value is a JWT string
What does that mean?
Firstly, it's worth understanding the JWT specification: RFC7519
To summarise, a JWT is a URL-safe encoded, signed (and possibly encrypted) representation of a 'JWT Claims Set'. The JWT specification defines the 'JWT Claims Set' as:
A JSON object that contains the claims conveyed by the JWT.

Where 'claims' are name/value pairs about the 'subject' of the JWT.  Typically a 'subject' might be an identity representing a person, and the 'claims' might be attributes about that person such as their name, email address, and phone number etc

So a JWT is generic way of representing a subject's claims.

OpenID Connect (OIDC)

OIDC makes use of the JWT specification by stating that the id_token must be a JWT.  It also defines a set of claims that must be present within the JWT when generated by an OpenID Provider  See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken

The specification also says that additional claims may be present in the token.  Just hang on to that thought for the moment...we'll come back to it.

OpenAM OIDC configuration

For the purposes of investigating the 'jwt' parameter, let's configure OpenAM to generate OIDC id_tokens.  I'm not going to cover that here, but we'll assume you've followed the wizard to setup up an OIDC provider for the realm.  We'll also assume you've created/updated the OAuth2/OIDC Client Agent profile to allow the 'profile' and 'openid' scopes.  I'm also going to use an 'invoices' scope so the config must allow me to request that too.

Now I can issue:
curl --request POST --user "apiclient:password" --data "grant_type=password&username=bob&password=password&scope=invoices openid profile" http://as.uma.com:8080/openam/oauth2/access_token?realm=ScopeAz

Note the request for the openid and profile scopes in order to ensure I get the OpenID Connect response.

And I should get something similar to the following:
{
  "access_token":"0d0cbd2a-c99c-478a-84c9-78463ec16ad4",
  "scope":"invoices openid profile",
  "id_token":"eyAidHlwIjogIkpXVCIsICJraWQiOiAidWFtVkdtRktmejFaVGliVjU2dXlsT2dMOVEwPSIsICJhbGciOiAiUlMyNTYiIH0.eyAiYXRfaGFzaCI6ICJVbmFHMk0ydU5kS1JZMk5UOGlqcFRRIiwgInN1YiI6ICJib2IiLCAiaXNzIjogImh0dHA6Ly9hcy51bWEuY29tOjgwODAvb3BlbmFtL29hdXRoMi9TY29wZUF6IiwgInRva2VuTmFtZSI6ICJpZF90b2tlbiIsICJhdWQiOiBbICJhcGljbGllbnQiIF0sICJvcmcuZm9yZ2Vyb2NrLm9wZW5pZGNvbm5lY3Qub3BzIjogIjhjOWNhNTU3LTk0OTgtNGU2Yy04ZjZmLWY2ZjYwZjNlOWM4NyIsICJhenAiOiAiYXBpY2xpZW50IiwgImF1dGhfdGltZSI6IDE0NjkwMjc1MTMsICJyZWFsbSI6ICIvU2NvcGVBeiIsICJleHAiOiAxNDY5MDMxMTEzLCAidG9rZW5UeXBlIjogIkpXVFRva2VuIiwgImlhdCI6IDE0NjkwMjc1MTMgfQ.MS6jnMoeQ19y1DQky4UdD3Mqp28T0JYigNQ0d0tdm04HjicQb4ha818qdaErSxuKyXODaTmtqkGbBnELyrckkl7m2aJki9akbJ5vXVox44eaRMmQjdm4EcC9vmdNZSVORKi1gK6uNGscarBBmFOjvJWBBBPhdeOPKApV0lDIzX7xP8JoAtxCr8cnNAngmle6MyTnVQvhFGWIFjmEyumD6Bsh3TZz8Fjkw6xqOyYSwfCaOrG8BxsH4BQTCp9FgsEjI52dZd7J0otKLIk0EVmZIkI4-hgRIcrM1Rfiz9LMHvjAWY97JBMcGBciS8fLHjWWiLDqMHEE0Wn5haYkMSsHYg",
  "token_type":"Bearer",
  "expires_in":3599
}

Note the lengthy id_token field.  This is the OIDC JWT made up according to the specification.  Also note that, by default, OpenAM will sign this JWT with the 1024-bit 'test' certificate using the RS256 algorithm.  I've updated my instance to use a new 2048-bit certificate called 'test1' so my response will be longer than the default.  I've used a 2048-bit certificate because I want to use this tool to inspect the JWT and its signature: http://kjur.github.io/jsjws/tool_jwt.html.  And, this tool only seems to support 2048-bit certificates which is probably due to the JWS specification   (I could have used jwt.io to inspect the JWT, but this does not support verification of RSA based signatures).

So, in the JWT tool linked above you can paste the full value of the id_token field into 'Step 3', then click the 'Just Decode JWT' button.  You should see the decode JWT claims in the 'Payload' box:

You can also see that the header field shows how the signature was generated in order to allow clients to verify this signature.
In order to get this tool to verify the signature, you need to get the PEM formatted version of the public key of the signing certificate.  i.e. 'test1' in my case.
I've got this from the KeyStoreExplorer tool, and now I can paste it into the 'Step 4' box, using the 'X.509 certificate for RSA' option.  Now I can click 'Verify It':

The tool tells me the signature is valid, and also decodes the token as before.  If I was to change the content of the message, of the signature of the JWT then the tool would tell me that the signature is not valid. For example, changing one character of the message would return this:

Note that the message box says that the signature is *Invalid*, as well as the Payload now being incorrect.

The 'jwt' Parameter 

So now we've understood that the id_token field of the OIDC response is a JWT, we can use this as the 'jwt' parameter of the 'subject' field in the policy evaluation call.

For example, a call like this:
curl --request POST --header "iPlanetDirectoryPro: AQIC5wM2LY4Sfcx-sATGr4BojcF5viQOrP-1IeLDz2Un8VM.*AAJTSQACMDEAAlNLABQtMjUwMzE4OTQxMDA1NDk1MTAyNwACUzEAAA..*" --header "Content-Type: application/json" --data '{"resources":["invoices"],"application":"api","subject":{"jwt":"eyAidHlwIjogIkpXVCIsICJraWQiOiAidWFtVkdtRktmejFaVGliVjU2dXlsT2dMOVEwPSIsICJhbGciOiAiUlMyNTYiIH0.eyAiYXRfaGFzaCI6ICJVbmFHMk0ydU5kS1JZMk5UOGlqcFRRIiwgInN1YiI6ICJib2IiLCAiaXNzIjogImh0dHA6Ly9hcy51bWEuY29tOjgwODAvb3BlbmFtL29hdXRoMi9TY29wZUF6IiwgInRva2VuTmFtZSI6ICJpZF90b2tlbiIsICJhdWQiOiBbICJhcGljbGllbnQiIF0sICJvcmcuZm9yZ2Vyb2NrLm9wZW5pZGNvbm5lY3Qub3BzIjogIjhjOWNhNTU3LTk0OTgtNGU2Yy04ZjZmLWY2ZjYwZjNlOWM4NyIsICJhenAiOiAiYXBpY2xpZW50IiwgImF1dGhfdGltZSI6IDE0NjkwMjc1MTMsICJyZWFsbSI6ICIvU2NvcGVBeiIsICJleHAiOiAxNDY5MDMxMTEzLCAidG9rZW5UeXBlIjogIkpXVFRva2VuIiwgImlhdCI6IDE0NjkwMjc1MTMgfQ.MS6jnMoeQ19y1DQky4UdD3Mqp28T0JYigNQ0d0tdm04HjicQb4ha818qdaErSxuKyXODaTmtqkGbBnELyrckkl7m2aJki9akbJ5vXVox44eaRMmQjdm4EcC9vmdNZSVORKi1gK6uNGscarBBmFOjvJWBBBPhdeOPKApV0lDIzX7xP8JoAtxCr8cnNAngmle6MyTnVQvhFGWIFjmEyumD6Bsh3TZz8Fjkw6xqOyYSwfCaOrG8BxsH4BQTCp9FgsEjI52dZd7J0otKLIk0EVmZIkI4-hgRIcrM1Rfiz9LMHvjAWY97JBMcGBciS8fLHjWWiLDqMHEE0Wn5haYkMSsHYg"}}' http://as.uma.com:8080/openam/json/ScopeAz/policies?_action=evaluate

might return:
[
  {
    "ttl":9223372036854775807,
    "advices":{},
    "resource":"invoices",
    "actions":{"permit":true},
    "attributes":{"hello":["world"]}
  }
]

This assumes the following policy definition:




Note that in this case I am using the 'iss' claim within the token in order to ensure I trust the issuer of the token when evaluating the policy condition.

As mentioned in previous articles, it is imperative that the id_token claims includes a 'sub' field.  Fortunately, the OIDC specification makes this mandatory so using an OIDC token here will work just fine.

It's also worth noting that OpenAM does *not* verify the signature of the id_token submitted in 'jwt' field.  This means that you could shorten the 'curl' call above to remove the signature component of the 'jwt'. For example, this works just the same as above:
curl --request POST --header "iPlanetDirectoryPro: AQIC5wM2LY4Sfcx-sATGr4BojcF5viQOrP-1IeLDz2Un8VM.*AAJTSQACMDEAAlNLABQtMjUwMzE4OTQxMDA1NDk1MTAyNwACUzEAAA..*" --header "Content-Type: application/json" --data '{"resources":["invoices"],"application":"api","subject":{"jwt":"eyAidHlwIjogIkpXVCIsICJraWQiOiAidWFtVkdtRktmejFaVGliVjU2dXlsT2dMOVEwPSIsICJhbGciOiAiUlMyNTYiIH0.eyAiYXRfaGFzaCI6ICJVbmFHMk0ydU5kS1JZMk5UOGlqcFRRIiwgInN1YiI6ICJib2IiLCAiaXNzIjogImh0dHA6Ly9hcy51bWEuY29tOjgwODAvb3BlbmFtL29hdXRoMi9TY29wZUF6IiwgInRva2VuTmFtZSI6ICJpZF90b2tlbiIsICJhdWQiOiBbICJhcGljbGllbnQiIF0sICJvcmcuZm9yZ2Vyb2NrLm9wZW5pZGNvbm5lY3Qub3BzIjogIjhjOWNhNTU3LTk0OTgtNGU2Yy04ZjZmLWY2ZjYwZjNlOWM4NyIsICJhenAiOiAiYXBpY2xpZW50IiwgImF1dGhfdGltZSI6IDE0NjkwMjc1MTMsICJyZWFsbSI6ICIvU2NvcGVBeiIsICJleHAiOiAxNDY5MDMxMTEzLCAidG9rZW5UeXBlIjogIkpXVFRva2VuIiwgImlhdCI6IDE0NjkwMjc1MTMgfQ."}}' http://as.uma.com:8080/openam/json/ScopeAz/policies?_action=evaluate

Note that the 'jwt' string needs to have two dots '.' in it to conform to the JWT specification.  The content following the second dot is the signature, which has been removed entirely in this second curl example.  i.e. this is an unsigned-JWT which is completely valid.

But, just to prove that OpenAM does *not* validate signed JWTs, you could attempt a curl call that includes garbage for the signature.  For example:
curl --request POST --header "iPlanetDirectoryPro: AQIC5wM2LY4Sfcx-sATGr4BojcF5viQOrP-1IeLDz2Un8VM.*AAJTSQACMDEAAlNLABQtMjUwMzE4OTQxMDA1NDk1MTAyNwACUzEAAA..*" --header "Content-Type: application/json" --data '{"resources":["invoices"],"application":"api","subject":{"jwt":"eyAidHlwIjogIkpXVCIsICJraWQiOiAidWFtVkdtRktmejFaVGliVjU2dXlsT2dMOVEwPSIsICJhbGciOiAiUlMyNTYiIH0.eyAiYXRfaGFzaCI6ICJVbmFHMk0ydU5kS1JZMk5UOGlqcFRRIiwgInN1YiI6ICJib2IiLCAiaXNzIjogImh0dHA6Ly9hcy51bWEuY29tOjgwODAvb3BlbmFtL29hdXRoMi9TY29wZUF6IiwgInRva2VuTmFtZSI6ICJpZF90b2tlbiIsICJhdWQiOiBbICJhcGljbGllbnQiIF0sICJvcmcuZm9yZ2Vyb2NrLm9wZW5pZGNvbm5lY3Qub3BzIjogIjhjOWNhNTU3LTk0OTgtNGU2Yy04ZjZmLWY2ZjYwZjNlOWM4NyIsICJhenAiOiAiYXBpY2xpZW50IiwgImF1dGhfdGltZSI6IDE0NjkwMjc1MTMsICJyZWFsbSI6ICIvU2NvcGVBeiIsICJleHAiOiAxNDY5MDMxMTEzLCAidG9rZW5UeXBlIjogIkpXVFRva2VuIiwgImlhdCI6IDE0NjkwMjc1MTMgfQ.garbage!!"}}' http://as.uma.com:8080/openam/json/ScopeAz/policies?_action=evaluate
...would still successfully be authorised.

It's also worth noting that the id_token claims of an OIDC token includes an 'exp' field signifying the 'expiry time' of the id_token.  OpenAM does not evaluate this field in this call.

Signature Verification

You might be wondering if it is possible to verify the signature and other aspects, such as the 'exp' field.  Yes, it is!  With a little bit clever scripting - of course!

The first thing is that we need to ensure that jwt token can be parsed by a script.  Unfortunately, simply passing it in the jwt parameter does not permit this.  But, we can *also* pass the jwt token in the 'environment' field of the policy decision request.  I'll shorten the jwt tokens in the following CURL command to make it easier to read, but you should supply the full signed jwt in the 'environment' field:
curl --request POST --header "iPlanetDirectoryPro: "AQIC....*" --header "Content-Type: application/json" --data '{"resources":["invoices"],"application":"api","subject":{"jwt":"eyAidHlw...MyNTYiIH0.eyAiYXRfa...MTMgfQ.MS6jn...sHYg"},"environment":{"jwt":["eyAidHlw...MyNTYiIH0.eyAiYXRfa...MTMgfQ.MS6jn...sHYg"]}}' http://as.uma.com:8080/openam/json/ScopeAz/policies?_action=evaluate

Note in this that the 'environment' field now includes a 'jwt' field whose data can be utilised in a script.  And what would such a policy condition script look like?
Well head over to https://github.com/smof/openAM_scripts and take a look at the 'ExternalJWTVerifier.groovy' script.  The associated blogpost from my colleague, Simon Moffatt, will set this script into context: http://identityrelationshipmanagement.blogspot.co.uk/2016/05/federated-authorization-using-3rd-party.html.  This will validate either an HMAC signed JWT - if you enter the appropriate shared secret - as well as an RSA 256 signed OIDC JWT - if you specify the jwk_uri for the OpenID Connect Provider.
And, now that you have claims accessible to the scripting engine you can pretty much apply any form of logic to them to validate the token - including validating the 'exp' field.



No comments:

Post a Comment