In a recent blog post, I added a throwaway reference to the use of signed assertions as a better mechanism for interacting with the Oracle Identity Cloud Service REST APIs than the use of Client id/secret, though qualified it with ‘if you want to handle the additional complexity in your consuming client’. Reflecting upon this, I thought that perhaps it was worth trying to explain this ‘additional complexity’, since the use of signed assertions have a number of benefits; primarily that it does not require an exchange of sensitive information, as the private keys used to sign the assertion never need to leave the machine on which they are generated. In this blog post, I will delve deeper into what is required to leverage this authentication mechanism, for both clients and users.
To begin with, it may be worth revisiting my earlier post about the structure of JWTs, and how they work to identify how they should be validated, since IDCS expects this format for their client assertions, and leverages either an ‘x5t’ or a ‘kid’ to identify the public certificate that should be used to validate the assertion. If you are confident in dealing with those concepts, we can jump straight into how they can be employed in IDCS.
IDCS supports the use of signed assertions, in the form of JWTs, to identify both clients and users. These take advantage of public/private key cryptography, allowing IDCS to use a pre-shared public certificate in order to validate JWTs which were generated externally to IDCS and signed with a private key. Typically you use these as part of a ‘token exchange’ in order to use your local JWT to obtain an IDCS JWT, which can then be used to consume IDCS APIs, or other Oracle Cloud service APIs.
In order to start creating your own local JWTs, you will need a key-pair, and it needs to be an RSA keypair for IDCS. Different organisations will have their own requirements around certificate signing, but for the purposes of this explanation, I am going to assume a keypair with a self-signed public certificate is fine (and given how JWT validation works, there is no need to tweak anything for the use of a self-signed certificate in IDCS, since it is just a keypair, the cert doesn’t make any claims about the domain ownership, unlike for SSL). You can generate this pairing with the following openssl command:
openssl req -newkey rsa:2048 -nodes -keyout private_key.pem -x509 -days 1024 -out publc_certificate.crt
Feel free to tweak the names of the output files and validity period to something more appropriate to your purposes, and enter any relevant information for your x509 certificate (though it isn’t validated or used anywhere – it does help to identify your client). You will also probably want to take some steps to protect your private key, such as encrypting it for storage – to do so, you can exclude the ‘-nodes’ parameter in the above command. My examples going forward are going to assume clear-text, for ease of understanding.
Alternatively, you can encrypt it after the fact, but running a command similar to the following, and you will be prompted to enter a passphrase:
openssl rsa -aes256 -in private_key.pem -out encrypted_key.pem
This private key is going to remain on the machine we generated it forever (as all good private keys should), but we are going to need to upload the public certificate to IDCS. In IDCS, create a new Confidential Application, then, in the configuration of the Client, you have the option of importing a Certificate, as shown below:
It is also worth noting the Allowed Grant types – Client Credentials needs to be enabled to use a Client Assertion to obtain a client token, and JWT Assertion needs to be enabled to use a User Assertion to obtain a user token. I have these set just to demonstrate capability for this blog post, and would advise analysing which of these make sense for your use case, and only enabling those that are required.
When uploading the certificate, you are able to specify a Certificate Alias, which is an arbitrary value, but is potentially relevant, as this value can be used as the ‘kid’ entry in your JWT Assertions.
Now that IDCS is aware of the certificate, you are able to start creating and signing your own JWTs which can be presented to IDCS.
Using a Client JWT in place of a Client Secret
These instructions assume you have complete control over the creation of your JWTs, if you are simply hoping to be able to accept a JWT from some other service, then provide it to IDCS, you will likely struggle, as IDCS has some pretty specific requirements.
In order for IDCS to accept a JWT as a client assertion as part of the Client Credentials flow, it needs to check the following boxes:
- Signed using RS256 (RSASSA-PKCS1-v1_5 and the SHA-256 hash function)
- Include an ‘x5t’ or ‘kid’ which corresponds with either the signature of your uploaded certificate, or the alias you specified when uploading it, respectively
- Have the sub (subject) attribute match the client id
- Have the iss (issuer) attribute match the client id
- Include both the iat and exp (‘issued at’ and ‘expiry’) attributes, and not have expired
- Include https://identity.oraclecloud.com/ in the audience attribute array
As such, the core of the token can be assembled using some code like this (javascript shown, and it displays the resultant JSON structure quite well, but you can adapt to your language of choice).
//Little Helper function for UrlEncoded Base64, since Javascript doesn't have one by default... function urlEncode(s) { s = s.split('=')[0]; s = s.replace(/\+/g, '-'); s = s.replace(/\//g, '_'); return s; } const TOKEN_EXPIRY = 3600; //1 hour var clientId = ‘<client_id_here>’; var certAlias = '<cert_alias_here>'; //JWT spec asks for number of seconds since Jan 1, 1970. //Since everything returns number of millis, we need to divide by 1000 var tokenIssued = Math.floor(Date.now()/1000); var tokenExpiry = tokenIssued + TOKEN_EXPIRY; var header = { "alg": "RS256", "typ": "JWT", "kid":certAlias }; var payload = { "sub":clientId, "iss":clientId, "aud": ["https://identity.oraclecloud.com/"], "iat":tokenIssued, "exp":tokenExpiry }; var tokenstr = urlEncode(Buffer.from(JSON.stringify(header)).toString('base64')) +"." +urlEncode(Buffer.from(JSON.stringify(payload)).toString('base64'));
This creates a string representing the unsigned token (in tokenstr), containing all of the relevant details, but it still needs to be signed. If you would prefer to use an ‘x5t’ over the ‘kid’, which ties the token more closely to the certificate rather than the alias you specified in IDCS, then my earlier post on this offers some sample code for generating it. This unsigned token string needs to have the signature appended to it, which means you need some implementation of the RS256 JSON Web Signature algorithm. In Javascript, I tend to use the library ‘jwa’, available from npm here, but other languages will have their own implementations (or as always, you can write it yourself if you are incredibly keen).
This library can be used to sign the resulting token string, and append it after a period (.) character, i.e. continuing our javascript example:
const rsassa = require('jwa')('RS256'); var signature = rsassa.sign(tokenstr, signingPrivateKey); var JWT = tokenstr + "." +signature;
‘signingPrivateKey’ is the contents of the private key file which was generated earlier.
If you are using jwa, the following line can be used to display the validity of the token signature (where ‘signingPublicKey’ is the contents of the public certificate file):
console.log("JWT signature is " +(rsassa.verify(tokenstr, signature, signingPublicKey) ? "valid!" : "NOT valid!") );
Once you have the ability to generate arbitrary signed JWTs, you can start using them in lieu of client credentials against the IDCS REST APIs.
The API call for this is a variant on the default Client Credentials invocation, and looks like this:
POST https://<idcs-instance>.identity.oraclecloud.com/oauth2/v1/token Headers: Content-Type: application/x-www-form-urlencoded Body (newlines and non-url encoded for clarity): grant_type=client_credentials &scope=urn:opc:idm:__myscopes__ &client_id=<client_id> &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer &client_assertion=< JWT generated earlier, with the client id in the iss and sub attributes>
Typically in early development, you will be copying and pasting the JWT between your generating code and a REST client or curl in order to validate it. If your JWT is set up correctly, you should receive an IDCS access token in response, but if it isn’t, you will receive an error message which hopefully offers some guidance – though I can’t guarantee it.
If all is working, then you have successfully obtained a client token without ever having sensitive information leave the location in which it was created. The client secret was never sent back and forth between IDCS and the requesting client, and the private key which was used to sign the client JWT assertion was never transmitted anywhere.
There are a number of good use cases for this mode of authentication, since you will need to authenticate a client almost any time you need to consume the IDCS REST APIs, such as to create or update users, handle user activation or password reset, etc. In addition, OAuth Clients also need to identify themselves as part of a 3-legged OAuth flow, when they exchange the authorisation token for the access token, and the client assertion can be used in the same manner for that class of request. i.e.
POST https://<idcs-instance>.identity.oraclecloud.com/oauth2/v1/token Headers: Content-Type: application/x-www-form-urlencoded Body (newlines and non-url encoded for clarity): grant_type= authorization_code &code=<authorisation code received as part of the callback> &client_id=<client_id> &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer &client_assertion=< JWT generated earlier, with the client id in the iss and sub attributes>
Using a User JWT to obtain a User Token
In most cases in which you are performing tasks on behalf of users, a 3-legged OAuth flow should be used, since it allows for reuse of user sessions; informed consent for any authorisation grants; ensures users are only entering credentials into trusted interfaces, etc. That said, while there are a number of use cases in which you need to identify a client, and can use the certificate upload to establish a 1:1 trusted relationship between the client and IDCS, there are also isolated cases in which you may need to identify a user via a trust assertion. The most obvious example of this is a ‘service account’ use case, in which the service you are consuming requires a user token for API calls, rather than a client token. I would advise against using this pattern in the general case, as it places a lot of trust in your consuming client, since it allows for trivial impersonation of more highly privileged accounts on services which do not validate the client identifier/audience as part of their authorisation.
In this scenario, it is possible to take advantage of the ‘JWT Assertion’ grant type available in IDCS, which allows you to provide a user assertion, with a very similar structure to the client assertion. User JWTs need to satisfy the following criteria:
- Signed using RS256 (RSASSA-PKCS1-v1_5 and the SHA-256 hash function)
- Include an ‘x5t’ or ‘kid’ which corresponds with either the signature of your uploaded certificate, or the alias you specified when uploading it, respectively
- Have the sub (subject) attribute match the user name
- Have the iss (issuer) attribute match the client id of the requesting client
- Include both the iat and exp (‘issued at’ and ‘expiry’) attributes, and not have expired
- Include ‘https://identity.oraclecloud.com/’ in the audience attribute array
This is basically the same as for the client JWT above, so you can re-use most of the same code, just replacing the client id with the user name in the ‘sub’ attribute.
This can then be used as part of a token request, in the following way (this repeats the use of a client assertion to identify the client, though using client id/secret in the Authorization header is also a valid way to make this invocation):
POST https://<idcs-instance>.identity.oraclecloud.com/oauth2/v1/token Headers: Content-Type: application/x-www-form-urlencoded Body (newlines and non-url encoded for clarity): grant_type= urn:ietf:params:oauth:grant-type:jwt-bearer &scope= urn:opc:idm:__myscopes__ &assertion=<User JWT generated earlier, with the username in the sub attribute> &client_id=<client_id> &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer &client_assertion=<Client JWT generated earlier, with the client id in the iss and sub attributes>
This returns a user token, obtained without requiring the user’s password, or the client secret to be transferred as part of an API call. This has some benefits for service account users, in which having to deal with password expiry or rotation would be disruptive to service delivery. The user doesn’t even have to have a password set in IDCS – which means there is no way for them to login or perform SSO, yet can still invoke APIs which call for a User Token. That said, take care – since it is easy to substitute a different username into the user JWT if you are generating and signing it on the fly. In the service account use-case, it probably makes much more sense to generate long-lived client and user assertions and supply them to the group implementing the automation requiring a service account rather than entrusting them with the private key. I have tested using assertions with a validity of 10 years with no issue, and these long-lived assertions can always be effectively revoked by changing the certificate associated with the application, or simply rotating the client secret if the consuming service is using client id/secret.
The ability to use locally signed assertions, based upon an established trust relationship with IDCS, allows for the consumption of IDCS APIs (as well as other APIs which have been written to use IDCS as an Identity Provider) without needing to exchange client secrets, instead leveraging public/private key encryption as an authentication mechanism. This is often a significantly more secure approach, as protecting private keys usually has established processes and procedures, and doesn’t require embedding client secrets in clear text throughout your application configuration files. It also has some limited viability for service accounts, and allows for the use of user assertions to obtain a user token, without needing to establish a password for the service account in IDCS.
Can you share your git repo
LikeLike
An implementation of this was used as part of this example – https://redthunder.blog/2020/10/02/simple-secure-log-retention-using-oci-services/.
LikeLike