OAuth2 client credentials flow
The OAuth2 client credentials flow consists of an interaction pattern between 3 actors which all have their own roll in the flow.
- The client. This can be anything which supports the OAuth2 standard. For testing I've used Postman
- The OAuth2 authorization server. In this example I've created a custom JAX-RS service which generates and returns JWT tokens based on the authenticated user.
- A protected service. In this example I'll use an Oracle Service Bus REST service. The protection consists of validating the token (authentication using standard OWSM policies) and providing role based access (authorization).
In our case the client authenticates using basic authentication to a JAX-RS servlet. This uses the HTTP header Authorization which contains 'Basic' followed by Base64 encoded username:password. Of course Base64 encoded strings can be decoded easily (e.g. by using sites like these) so never use this over plain HTTP!
When this token is obtained, it can be used in the Authorization HTTP header using the Bearer keyword. A service which needs to be protected can be configured with the following standard OWSM policies for authentication: oracle/http_jwt_token_service_policy and oracle/http_jwt_token_over_ssl_service_policy and a custom policy for role based access / authorization.
JSON Web Tokens (JWT) can look something like:
eyJraWQiOiJvYXV0aDJrZXlwYWlyIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJ3ZWJsb2dpYyIsImlzcyI6Ind3dy5vcmFjbGUuY29tIiwiZXhwIjoxNTQwNDY2NDI4LCJpYXQiOjE1NDA0NjU4Mjh9.ZE8wMnFyjHcmFpdswgx3H8azVCPtHkrRjqhiKt-qZaV1Y5YlN9jAOshUnPIQ76L8K4SAduhJg7MyLQsAipzCFeT_Omxnxu0lgbD2UYtz-TUIt23bjcsJLub5pNrLXJWL3k7tSdkcVxlyHuRPYCvoLhLZzCksqnRdD6Zf9VjxGLFPktknXwpn7_aOAdzXEatj-Gd9lm321R2BdFL7ii9sXh9A1KL8cblLbhLlrXGwTF_ifTxuHSBz1B_p6xng6kmOfIwDIAJQ9t6KESQm8dQQeilcny1uRmhg4o85uc4gGzhH435q1DRuHQm22wN39FHbNT4WP3EuoZ49PpsTeQzSKA
This is not very helpful at first sight. When we look a little bit closer, we notice it consists of 3 parts separated by a '.' character. These are the header, body and signature of the token. The first 2 parts can be Base64 decoded.
Header
The header typically consists of 2 parts (see here for an overview of fields and their meaning). The type of token and the hashing algorithm. In this case the header is
{"kid":"oauth2keypair","alg":"RS256"}
kid refers to the key id. In this case it provides a hint to the resource server on which key alias to use in its key store to validate the signature.
Body
The JWT body contains so-called claims. In this case the body is
{"sub":"weblogic","iss":"www.oracle.com","exp":1540466428,"iat":1540465828}
The subject is the subject for which the token was issued. www.oracle.com is the issuer of the token. iat indicates an epoch at which the token was issued and exp indicates until when the token is valid. Tokens are valid only for a limited duration. www.oracle.com is an issuer which is accepted by default so no additional configuration was required.
Signature
The signature contains an encrypted hash of the header/body of the token. If those are altered, the signature validation will fail. To encrypt the signature, a key-pair is used. Tokens are signed using a public/private key pair.
Challenges
Implementing the OAuth2 client credentials flow using only a WebLogic server and OWSM can be challenging. Why?
- Authentication server. Bare WebLogic + Service Bus do not contain an authentication server which can provide JWT tokens.
- Resource Server. Authentication of tokens. The predefined OWSM policies which provide authentication based on JWT tokens (oracle/http_jwt_token_service_policy and oracle/http_jwt_token_over_ssl_service_policy) are picky to what tokens they accept.
- Resource Server. Authorization of tokens. OWSM provides a predefined policy to do role based access to resources: oracle/binding_permission_authorization_policy. This policy works for SOAP and REST composites and Service Bus SOAP services, but not for Service Bus REST services.
- Create a simple authentication server to provide tokens which conform to what the predefined OWSM policies expect. By increasing the OWSM logging and checking for errors when sending in tokens, it becomes clear which fields are expected.
- Create a custom OWSM policy to provide role based access to Service Bus REST resources
Authentication server
The authentication server has several tasks:
- authenticate the user (client credentials)
- using the WebLogic security realm
- validate the client credentials request
- using Apache HTTP components
- obtain a public and private key for signing
- from the OPSS KeyStoreService (KSS)
- generate a token and sign it
- using the Nimbus JOSE+JWT library
User authentication on WebLogic Server of servlets consists of 2 configuration files.
A web.xml. This file indicates
- which resources are protected
- how they are protected (authentication method, TLS or not)
- who can access the resources (security role)
The weblogic.xml indicates how the security roles map to WebLogic Server roles. In this case any user in the WebLogic security realm group tokenusers (which can be in an external authentication provider such as for example an AD or other LDAP) can access the token service to obtain tokens.
From Postman you can do a request to the token service to obtain a token. This can also be used if the response of the token service conforms to the OAuth2 standard.
By default certificates are checked. With self-signed certificates / development environments, those checks (such as host name verification) might fail. You can disable the certificate checks in the Postman settings screen.
Also Postman has a console available which allows you to inspect requests and responses in more detail. The request looked like
Thus this is what needed to be validated; an HTTP POST request with a body containing application/x-www-form-urlencoded grant_type=client_credentials. I've used the Apache HTTP components org.apache.http.client.utils.URLEncodedUtils class for this.
After deployment I of course needed to test the token service. Postman worked great for this but I could also have used Curl commands like:
curl -u tokenuser:Welcome01 -X POST -d "grant_type=client_credentials" http://localhost:7101/oauth2/resources/tokenservice
Accessing the OPSS keystore
Oracle WebLogic Server provides Oracle Platform Security Services.
OPSS provides secure storage of credentials and keys. A policy store can be configured to allow secure access to these resources. This policy store can be file based, LDAP based and database based. You can look at your jps-config.xml file to see which is in use in your case;
You can also look this up from the EM
In this case the file based policy store system-jazn-data.xml is used. Presence of the file on the filesystem does not mean it is actually used! If there are multiple policy stores defined, for example a file based and an LDAP based, the last one appears to be used.
The policy store can be edited from the EM
You can create a new permission:
Codebase: file:${domain.home}/servers/${weblogic.Name}/tmp/_WL_user/oauth2/-
Permission class: oracle.security.jps.service.keystore.KeyStoreAccessPermission
Resource name: stripeName=owsm,keystoreName=keystore,alias=*
Actions: read
The codebase indicates the location of the deployment of the authentication server (Java WAR) on WebLogic Server.
Or when file-based, you can edit the (usually system-jazn-data.xml) file directly
In this case add:
<grant>
<grantee>
<codesource>
<url>file:${domain.home}/servers/${weblogic.Name}/tmp/_WL_user/oauth2/-</url>
</codesource>
</grantee>
<permissions>
<permission>
<class>oracle.security.jps.service.keystore.KeyStoreAccessPermission</class>
<name>stripeName=owsm,keystoreName=keystore,alias=*</name>
<actions>*</actions>
</permission>
</permissions>
</grant>
At the location shown below
Now if you create a stripe owsm with a policy based keystore called keystore, the authentication server is allowed to access it!
The name of the stripe and name of the keystore are the default names which are used by the predefined OWSM policies. Thus when using these, you do not need to change any additional configuration (WSM domain config, policy config). OWSM only supports policy based KSS keystores. When using JKS keystores, you need to define credentials in the credential store framework and update policy configuration to point to the credential store entries for the keystore password, key alias and key password. The provided code created for accessing the keystore / keypair is currently KSS based. Inside the keystore you can import or generate a keypair. The current Java code of the authentication server expects a keypair oauth2keypair to be present in the keystore.
Accessing the keystore and key from Java
I defined a property file with some parameters. The file contained (among some other things relevant for token generation):
keystorestripe=owsm
keystorename=keystore
keyalias=oauth2keypair
Accessing the keystore can be done as is shown below.
AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
try {
JpsContext ctx = JpsContextFactory.getContextFactory().getContext();
KeyStoreService kss = ctx.getServiceInstance(KeyStoreService.class);
ks = kss.getKeyStore(prop.getProperty("keystorestripe"), prop.getProperty("keystorename"), null);
} catch (Exception e) {
return "error";
}
return "done";
}
});
When you have the keystore, accessing keys is easy
PasswordProtection pp = new PasswordProtection(prop.getProperty("keypassword").toCharArray());
KeyStore.PrivateKeyEntry pkEntry = (KeyStore.PrivateKeyEntry) ks.getEntry(prop.getProperty("keyalias"), pp);
(my key didn't have a password but this still worked)
Generating the JWT token
After obtaining the keypair at the keyalias, the JWT token libraries required instances of RSAPrivateKey and RSAPublicKey. That could be done as is shown below
RSAPrivateKey myPrivateKey = (RSAPrivateKey) pkEntry.getPrivateKey();
RSAPublicKey myPublicKey = (RSAPublicKey) pkEntry.getCertificate().getPublicKey();
In order to sign the token, an RSAKey instance was required. I could create this from the public and private key using a RSAKey.Builder method.
RSAKey rsaJWK = new RSAKey.Builder(myPublicKey).privateKey(myPrivateKey).keyID(prop.getProperty("keyalias")).build();
Using the RSAKey, I could create a Signer
JWSSigner signer = new RSASSASigner(rsaJWK);
Preparations were done! Now only the header and body of the token. These were quite easy with the provided builder.
Claims:
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject(user)
.issuer(prop.getProperty("tokenissuer"))
.expirationTime(expires)
.issueTime(new Date(new Date().getTime()))
.build();
Generate and sign the token:
SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(), claimsSet);
signedJWT.sign(signer);
String token = signedJWT.serialize();
Returning an OAuth2 JSON message could be done with
String output = String.format("{ \"access_token\" : \"%s\",\n" + " \"scope\" : \"read write\",\n" + " \"token_type\" : \"Bearer\",\n" + " \"expires_in\" : %s\n}", token,expirytime);
Role based authorization policy
The predefined OWSM policies oracle/http_jwt_token_service_policy and oracle/http_jwt_token_over_ssl_service_policy create a SecurityContext which is available from the $inbound/ctx:security/ctx:transportClient inside Service Bus. Thus you do not need a custom identity asserter for this!
However, the policy does not allow you to configure role based access and the predefined policy oracle/binding_permission_authorization_policy does not work for Service Bus REST services. Thus we need a custom policy in order to achieve this. Luckily this policy can use the previously set SecurityContext to obtain principles to validate.
Challenges
Provide the correct capabilities to the policy definition was a challenge. The policy should work for Service Bus REST services. Predefined policies provide examples, however they could not be exported from the WSM Policies screen. I did 'Create like' a predefined policy which provided the correct capabilities and then copied those capability definitions to my custom policy definition file. Good to know: some capabilities required the text 'rest' to be part of the policy name.
Also I encountered a bug in 12.2.1.2 which is fixed with the following patch: Patch 24669800: Unable to configure Custom OWSM policy for OSB REST Services. In 12.2.1.3 there were no issues.
An OWSM policy consists of two deployments
A JAR file
- This JAR contains the Java code of the policy. The Java code uses the parameters defined in the file below.
- A policy-config.xml file. This file indicates which class is implementing the policy. Important part of this file is the reference to restUserAssertion. This maps to an entry in the file below
- This contains a policy description file.
- Which parameters can be set for the policy?
- Of which type are the parameters?
- What are the default values of the parameters?
- Is it an authentication or authorization policy?
- Which bindings are supported by the policy?
Thus the name of the policy is CUSTOM/rest_user_assertion_policy
This name is also part of the contents of the rest_user_assertion_policy file. You can also see there is again a reference to the implementation class and the restUserAssertion element which is in the policy-config.xml file is also there. The capabilities of the policy are mentioned in the restUserAssertion attributes.
Finally
As mentioned before, the installation manual and code can be found here. Of course this solution does not provide all the capabilities of a product like API Platform Cloud Service, OAM, OES. Usually you don't need all those capabilities and complexity and just a simple token service /policy is enough. In such cases you can consider this alternative. Of course since it is hosted on WebLogic / Service Bus, it needs some extra protection when exposed to the internet such as a firewall, IP whitelisting, SSL offloading, etc.
nice
ReplyDeleteHi Maarten, excellent work, I trying implement oauth in my composite, I followed step by step your manual, but I get 401 error. I would like know if you could help me please?
ReplyDeleteHi Maarten, I need to implement Oauth JWT authorization mechanism in my project, I followed step by step your documentation but i get 401 error, the log show me the next errors
ReplyDelete<Failure in Oracle WSM Agent processRequest, category=security, function=agent.function.service, application=Service Bus Kernel, composite=null, modelObj=null, policy=oracle/http_jwt_token_service_policy, policyVersion=null, assertionName={http://schemas.oracle.com/ws/2006/01/securitypolicy}http-jwt-security.
oracle.wsm.common.sdk.WSMException: GenericFault : generic error