Introduction
This post describes the necessary configuration for propagating an end user identity from OAG (Oracle API Gateway) to REST APIs protected by OWSM (Oracle Web Services Manager).
The requirements are:
1) Have a Java Subject established in the REST API implementation.
2) Prevent direct access to the REST API, i.e., only OAG should be able to successfully invoke it.
A recurrent question is how OWSM protects REST APIs and which types of tokens it supports when doing so.
If we look at the current OWSM (11.1.1.7) predefined policies, we notice a policy named
oracle/multi_token_rest_service_policy, described (verbatim) as:
"This policy enforces one of the following authentication policies, based on the token sent by the client:
HTTP Basic—Extracts username and password credentials from the HTTP header.
SAML 2.0 Bearer token in the HTTP header—Extracts SAML 2.0 Bearer assertion in the HTTP header.
HTTP OAM security—Verifies that the OAM agent has authenticated user and establishes identity.
SPNEGO over HTTP security—Extracts Simple and Protected GSSAPI Negotiation Mechanism (SPNEGO) Kerberos token from the HTTP header."
In this specific use case, we are assuming the end user has already been authenticated by some other means before reaching OAG. In other words, we are assuming OAG gets some sort of token
and validates the user locally, thus populating its authentication.subject.id attribute. This token OAG receives can be an OAM token, a Kerberos token, SAML token, you name it. It is matter of
a design decision based on OAG's client capabilities.
In a use case like this, it's very unlikely that OAG will have the end user password, which eliminates the HTTP Basic header option. The remaining three are all good candidates. In this post we deal with a SAML 2.0 Bearer token in the HTTP Header. Our flow ends up being something like this: OAG Client -> "some token" -> OAG -> SAML 2.0 Bearer -> OWSM -> REST API.
We're going to examine all necessary configuration in OAG, OWSM and in the REST API application. Buckle up, folks! And let's do it backwards.
REST API Web Application
Here's my REST API Implementation in all its beauty:
import java.security.Principal;
import javax.security.auth.Subject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import weblogic.security.Security;
@Path("/carmodels")
public class CarModels {
public CarModels() {
super();
}
@GET
@Produces("application/json")
public String getModels() {
Subject s = Security.getCurrentSubject();
System.out.println("[CarModels] Principals established for the propagated user id:");
for (Principal p : s.getPrincipals()) {
System.out.println(p.getName());
}
String json = "{\"models\":[\"Nice Car\",\"Fast Car\",\"Lightweight Car\",\"Sports Car\",\"Lovely Car\",\"Family Car\"]}";
return json;
}
}
It prints out the user principals and gives back a list of cars. Simple as that.
There's a need for a servlet filter (plus a filter-mapping) to intercept requests to this API. Such a filter is provided by OWSM and works hand in hand with the policy we've briefly talked about previously.
<filter>
<filter-name>OWSM Security Filter</filter-name>
<filter-class>oracle.wsm.agent.handler.servlet.SecurityFilter</filter-class>
<init-param>
<param-name>servlet-name</param-name>
<param-value>ateam.rest.impl.Services</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>OWSM Security Filter</filter-name>
<servlet-name>ateam.rest.impl.Services</servlet-name>
</filter-mapping>
See that the filter mentions an specific servlet in <init-param>. This servlet simply exposes the REST API Implementation to be protected.
package ateam.rest.impl;
import javax.ws.rs.core.Application;
import javax.ws.rs.ApplicationPath;
import java.util.Set;
import java.util.HashSet;
@ApplicationPath("resources")
public class Services extends Application {
public Set<java.lang.Class<?>> getClasses() {
Set<java.lang.Class<?>> s = new HashSet<Class<?>>();
s.add(CarModels.class);
return s;
}
}
The servlet definition completes the necessary configuration in web.xml. Notice the servlet-class is actually Jersey's ServletContainer.
<servlet>
<servlet-name>ateam.rest.impl.Services</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>javax.ws.rs.Application</param-name>
<param-value>ateam.rest.impl.Services</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
There's a need for a servlet filter (plus a filter-mapping) to intercept requests to this API. Such a filter is provided by OWSM and works hand in hand with the policy we've briefly talked about previously.
<filter>
<filter-name>OWSM Security Filter</filter-name>
<filter-class>oracle.wsm.agent.handler.servlet.SecurityFilter</filter-class>
<init-param>
<param-name>servlet-name</param-name>
<param-value>ateam.rest.impl.Services</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>OWSM Security Filter</filter-name>
<servlet-name>ateam.rest.impl.Services</servlet-name>
</filter-mapping>
See that the filter mentions an specific servlet in <init-param>. This servlet simply exposes the REST API Implementation to be protected.
package ateam.rest.impl;
import javax.ws.rs.core.Application;
import javax.ws.rs.ApplicationPath;
import java.util.Set;
import java.util.HashSet;
@ApplicationPath("resources")
public class Services extends Application {
public Set<java.lang.Class<?>> getClasses() {
Set<java.lang.Class<?>> s = new HashSet<Class<?>>();
s.add(CarModels.class);
return s;
}
}
The servlet definition completes the necessary configuration in web.xml. Notice the servlet-class is actually Jersey's ServletContainer.
<servlet>
<servlet-name>ateam.rest.impl.Services</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>javax.ws.rs.Application</param-name>
<param-value>ateam.rest.impl.Services</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
OWSM
We're going to attach oracle/multi_token_rest_service_policy policy to all REST endpoints in the domain. But only the implementations with the setup shown previously are going to have requests intercepted.
The way to attach the policy is via wlst, as shown:
> connect('weblogic','*****','t3://<admin-server-name>:<admin-port>')
> beginRepositorySession()
> createPolicySet('owsm-policy-set-multi-token','rest-resource','Domain("<domain-name>")')
> attachPolicySetPolicy('oracle/multi_token_rest_service_policy')
> commitRepositorySession()
This is it. Notice that createPolicySet mentions 'rest-resource' as the resource type. This is key here.
Before asserting the user identity in the incoming token and thus establishing the Java subject, 'oracle/multi_token_rest_service_policy' requires the following characteristics from the received token:
1) It has to be Base64 encoded.
2) It has to be gzipped.
3) It has to be digitally signed.
#1 and #2 requires no configuration in OWSM. But for #3 we need to import OAG's certificate into OWSM's keystore so that the token can be properly validated. Export OAG's certificate into a a file using OAG Policy Studio and then import it into OWSM's default-keystore.jks using JDK's keytool.
> keytool -import -file ~/oag_cert.cer -keystore ./config/fmwconfig/default-keystore.jks -storepass <keystore-password> -alias oag_cert -keypass welcome1
OAG
The filter circuit in OAG has to create a SAML 2.0 Bearer assertion, sign it, gzip it, Base64 encode it and then add it to the Authorization HTTP header. Here's the filter circuit.
I now highlight the most relevant aspects of each filter:
1) Create SOAP Envelope: this is just to please "Create SAML Authentication Assertion" filter. It expects an XML message. Here I use a SOAP envelope, but any simple XML document would work.
2) Set Authentication Subject id as DN: the point here is that OWSM policy honors the Subject NameIdentifier format in the SAML Assertion. Therefore, if format is X509SubjectName, we need to make sure to set the subject value as the user Distinguished Name (DN). If the format is unspecified, sticking with the username is enough.
Tip: You can set the format by setting the attribute authentication.subject.format. For example:
3) Create SAML Authentication Assertion: the following screenshots describe the filter.
4) Update Message: this step is necessary just to copy the saml.assertion attribute value created in the previous step to content.body, as expected by the next filter in the chain.
5) Sign SAML Assertion:
Notice the Signing Key certificate. That's the one to be exported and then imported into OWSM's key store.
Notice "Create enveloped signature" is checked. It is required by the OWSM policy.
6) Retrieve SAML Assertion from Message:
7) Gzip SAML Assertion (script): OAG has no filter to gzip messages. Therefore we rely on a script to do so. Notice it also Base64 encodes the message after gzipping it. The script outputs an attribute named data.base64, containing the assertion gzipped and encoded, ready to be sent.
importPackage(Packages.java.util.zip);
importPackage(Packages.java.io);
importPackage(Packages.javax.xml.transform);
importPackage(Packages.javax.xml.transform.dom);
importPackage(Packages.javax.xml.transform.stream);
importPackage(Packages.java.lang);
importPackage(Packages.oracle.security.xmlsec.util);
importPackage(Packages.com.vordel.trace);
function invoke(msg) {
var data = msg.get("saml.assertion");
var source = new DOMSource(data.get(0).getOwnerDocument());
var baos = new ByteArrayOutputStream();
var result = new StreamResult(baos);
var factory = TransformerFactory.newInstance();
var transformer = factory.newTransformer();
transformer.transform(source, result);
var tokba = baos.toByteArray();
baos = new ByteArrayOutputStream();
var gzos = new GZIPOutputStream(baos);
gzos.write(tokba);
gzos.flush();
gzos.finish();
var gzdata = baos.toByteArray();
var b64 = new Base64();
b64.setUseLineBreaks(false);
var b64tok = b64.encode(gzdata);
msg.put("data.base64", b64tok);
return true;
}
8) Add SAML Assertion to HTTP Header: the Authorization header mechanism must be set to "oit", as shown:
9) Connect to Car Models Service:
At the end, this is what a good assertion would look like:
<?xml version="1.0"?>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="Id-cffa4f53f9490000090000004f131aad-1" IssueInstant="2014-04-17T16:01:19Z" Version="2.0">
<saml:Issuer Format="urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName">www.oracle.com</saml:Issuer>
<dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Id="Id-0001397750479781-ffffffffd55f69c1-1">
<dsig:SignedInfo>
<dsig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<dsig:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<dsig:Reference URI="#Id-cffa4f53f9490000090000004f131aad-1">
<dsig:Transforms>
<dsig:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<dsig:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</dsig:Transforms>
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<dsig:DigestValue>87KiwbLN11S3qwJw23Zm0Odh9QQ=</dsig:DigestValue>
</dsig:Reference>
</dsig:SignedInfo>
<dsig:SignatureValue>UO6S7++uxuqqLPl4cege7vmZpQ1q6MXL51s/e/fDd74aZdrEOx+G1tqA4YQtVQIh
fTuOcd1CtOyEUqOLNy9F4e87Ld/cqNcr8iWGlokPEPP153r19MIaWSYDslYq10xe
cArsGeayx0PpWjXo0VSH+u26grsTWIY+YATuU7BcKnqrrWFjmRxHAK/towXtuiPL
NtNYVgI6dPXVzJ+2lGSiZKBDBFoV9zUFE98kU0f050e3mq2x2BwvQ7MQUkPYyadt
b+Ifn0Hcr77Fp7FYfM0gPAMt3X0Dm5qsrEo5WS47RkWDq6EEdQx9HFEQJMLdwABL
xC8gNTETalZs73xUUQu2CA==</dsig:SignatureValue>
<dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Id="Id-0001397750479781-ffffffffd55f69c1-2">
<dsig:X509Data>
<dsig:X509Certificate>
MIICtjCCAZ4CBgE9RZO/rjANBgkqhkiG9w0BAQUFADAaMRgwFgYDVQQDEw9TYW1wbGVzIFRlc3Qg
Q0EwHhcNMTMwMzA3MTU1ODAwWhcNMzcwMTEwMTA1NjAwWjAjMSEwHwYDVQQDExhTYW1wbGVzIFRl
c3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQClSoXx8QPLrHMS
Ff/5m3uLrDhxHycPYkamDCouu89mSKhD7aEZy3QS0mvZHvY2N1TmuQcdTuOgSE5qyT20mBEUVBnU
1y4WLQqM5fKu0TmIAajtYWTOdTnSuwR3f9W4poSwRMDNkUb8gPiXZNHZiyzriRMus29ER61eYAdr
XFlv5emXqi2ZK2bpBdtO6Q641TM9kUWB4ZyMqkGtRys9m2hNaXVR8e7r2WUrA9LEx3bRpku/OodI
GS6Qy0C2vueHDrdLYhYGKfNIllagEXY+dBQI8t2qH7rXBmr16lYyKK8VYJqeud9/NCAxD78vzOLY
0q6WaisVCa6FE/KpgpNF8sbZAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAH3W3yCTSORmIq5uhNTC
Zvd6wPz+zNXT19GqKgeDgjYMJj/Bl8czRO9YZo5MslwHILLgDVdz+ux4nSS3mCZ+PQGO42p/6o6n
IQ31yGzfYjTZ/1+1//CWyVtEhuSv5oFE+Le5mvkf1kNUrW4//qOXtfwXy/Hq09E9eaXlnBxUTHls
cQkpfQW5bi5Go7FDUNpW5EXUgXrQ96qKWMMK7i1hm7r5o6TldxCq5ANlPo/sObFNooQDkBWSKJ5t
GTtPiXO8kqYWdNBvnSRDk1Qqsn6fdFz485WB0e0pqWg2SuZa1026gIqtQPekJDQzTm0qvAnh/Aoh
oKs1dNQxruBf+MFLisw=
</dsig:X509Certificate>
</dsig:X509Data>
</dsig:KeyInfo>
</dsig:Signature>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName">cn=jane,cn=Users,dc=us,dc=oracle,dc=com</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"/>
</saml:Subject>
<saml:Conditions NotBefore="2014-04-17T16:01:18Z" NotOnOrAfter="2014-04-17T16:06:18Z"/>
<saml:AuthnStatement AuthnInstant="2014-04-17T16:01:19Z">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
</saml:Assertion>
Wrapping up...
With this configuration in place, at runtime the REST API implementation writes the following in the server's log for a user authenticated as jane:
[CarModels] Principals established for the propagated user id:
jane
And any SAML assertion not signed by OAG is going to be promptly rejected by OWSM.
See you next time!
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.