Lately we implemented a Single Sign On solution for Apex, based on Weblogic 12cR2, ORDS 3.0.9, and ADFS as a federated Identity Provider. This combination turns out to be a marriage of 3 different worlds. So we ran in to a several issues that were not described in one simple how-to document. So in this document I try to assemble the information needed to do the end 2 end configuration (apart from the OHS configuration).
For most of the SAML2 configuration on Weblogic, we could have my earlier article on SAML2.0 on Weblogic 11g, as a guide:
Service Provider initiated SSO on WLS11g using SAML2.0 .
This helped a great deal with regards to ADFS and 12c. The rest of the issues I'd like to cover here, for future reference.
ORDS
ORDS can be installed in the regular way. I downloaded ORDS and unzipped it in the weblogic domain home. Then I did the setup using:
[oracle@darlin-vce-db ords309]$ java -jar ords.war setup
Then create the
i.war containing the Apex and ORDS images. First copy the Apex images from the Apex home to the ORDS images folder. And then create the
i.war:
[oracle@darlin-vce-db ords309]$ java -jar ords.war static /u01/data/oracle/config/domains/SP_domain/ords309/images
It is important to provide a complete/absolute path to the this command. This command creates an
i.war that contains a reference to the images folder. You can see this as a virtual directory configuration as you would do in Apache/Oracle HTTP Server. It in fact only contains a
web.xml,
sun-web.xml and a
weblogic.xml that contain a reference to that folder. For instance, the
weblogic.xml contains:
<weblogic-web-app xmlns="http://www.bea.com/ns/weblogic/weblogic-web-app">
<!-- This element specifies the context path the static resources are served from -->
<context-root>/i</context-root>
<virtual-directory-mapping>
<!-- This element specifies the location on disk where the static resources are located -->
<local-path>/u01/data/oracle/config/domains/SP_domain/ords309/images</local-path>
<url-pattern>/*</url-pattern>
</virtual-directory-mapping>
</weblogic-web-app>
Before deploying the ords.war and i.war files to Weblogic (with Custom Roles), you'll need to make a manual adjustment to the ords.war.
The thing is that ORDS must be instructed to hand over the authentication to Weblogic. To do so, you'll need to add the following to the
WEB-INF/web.xml file in the ords.war:
<!-- Security Constraint -->
<security-constraint>
<web-resource-collection>
<web-resource-name>SecurePages</web-resource-name>
<description>These pages are only accessible by authorized users.</description>
<url-pattern>/f/*</url-pattern>
<!-- <http-method>GET</http-method> -->
</web-resource-collection>
<auth-constraint>
<description>These are the roles who have access.</description>
<role-name>Anonymous</role-name>
</auth-constraint>
<user-data-constraint>
<description>This is how the user data must be transmitted.</description>
<transport-guarantee>NONE</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- Login Config -->
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>myrealm</realm-name>
</login-config>
<security-role>
<role-name>Anonymous</role-name>
</security-role>
In WEB-INF/weblogic.xml add the security-role-assignment for the role
Anonymous:
<weblogic-web-app xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.oracle.com/weblogic/weblogic-web-app
http://xmlns.oracle.com/weblogic/weblogic-web-app/1.6/weblogic-web-app.xsd"> <!-- Weblogic 12c -->
<container-descriptor>
<prefer-web-inf-classes>true</prefer-web-inf-classes>
</container-descriptor>
<session-descriptor>
<persistent-store-type>replicated_if_clustered</persistent-store-type>
</session-descriptor>
<security-role-assignment>
<!--<role-name>valid-users</role-name> -->
<role-name>Anonymous</role-name>
<principal-name>users</principal-name>
</security-role-assignment>
<context-root>/ords</context-root>
</weblogic-web-app>
This is necessary, because after the Authentication using SAML2, the Rolemapper kicks in. And that one has to find a valid role.
APEX-Schema's
APEX uses REST-calls to fetch the images. It is important that all the database users relating to ORDS and APEX are unlocked, that passwords are known. Check for each of the following schema's if you can logon with the known password.
- ORDS_PUBLIC_USER
- APEX_PUBLIC_USER
- APEX_LISTENER
- APEX_REST_PUBLIC_USER
- ORDS_METADATA
Then perform the following steps from doc
2075837.1:
- Go into the directory where the extracted the full installation of APEX resides
- Login to SQLPlus as the SYS user (as sysdba)
- Rerun the @apex_rest_config.sql script to recreate the "connect through" privileges for the installation:
@apex_rest_config
- Hit enter for the APEX_LISTENER and APEX_REST_PUBLIC_USER password prompt. (passwords apparently not used for this script)
- Verify with the following statement that the connect through grant is present:
select * from dba_proxies
where proxy='APEX_REST_PUBLIC_USER'
and client='APEX_PUBLIC_USER';
- If the grant is still missing the customer should verify that the file apex_rest_config_core.sql contains the following statement and run the script again.
-- Allow REST user to proxy into APEX_PUBLIC_USER for built-in RESTful Services
alter user APEX_PUBLIC_USER grant connect through ^RESTUN.;
ORDS Validation
From ORDS 3.0.5 onwards ORDS expects a column in the table
ords_metadata.apex_pool_config called
pool_name. So you could end up with an ORDS installation/configuration that seems to work, but you might not get the Apex application images shown. This is described in
this community question. If the applications images don't show up:
- Validate that this column exists, through a describe of the table, and check if the table contains any rows.
- If necessary, perform the following to chreate the column and fill the table:
cd /u01/data/oracle/config/domains/SP_domain/ords309
java -jar ordsname.war validate
APEX Authentication Schema
To have the Apex application use the authenticated user from Weblogic/ADFS, the authentication scheme needs to be changed.
This has to be done in a way that in the authentication scheme (shared components --> security --> authentication) APEX fetches the user from the REMOTE_USER header variable:
Weblogic 12 and ADFS
ADFS will use SHA-256 for signing by default. But WLS currently does not support that for SAML2. Although for many other services WLS does 'know' how to do SHA-256. I found articles how to update the policies for OWSM to use SHA-256. But I could not find how to do the same for the SAML2 configuration of Weblogic. So have ADFS do the signing with SHA-1. This might seem not secure, but when using TLS this is a minor thing. Although Weblogic 12c should solve this, in my opinion. See also this
blog on Weblogic with ADFS, point 5 under the Takeway or Gotchas.
Another thing we found is that ADFS expects a valid 'https://' url to the ServiceProvider for the entity-id. A regular unique string does not suffice. ADFS apparently checks this URL to be valid. So I used the default TLS-url to the Oracle HTTP server that I used to reverse proxy to the SP Weblogic.
Lastly, in ADFS we needed to add an extra explicit claim had to fill the
urn:mace:dir:attribute-def:uid saml2-attribute. This is needed for the identy mapper.
Identity Mapper Class
I updated the IdentityMapper class that I used in my earlier blog. I found a little bug in determining the actual identity/username, that apparently did not occur in 11g. But I also refactored the class a bit, to my latest Java knowledge, for as far as applicable.
package nl.darwinit.wls.saml;
import com.bea.security.saml2.providers.SAML2AttributeInfo;
import com.bea.security.saml2.providers.SAML2AttributeStatementInfo;
import com.bea.security.saml2.providers.SAML2IdentityAsserterAttributeMapper;
import com.bea.security.saml2.providers.SAML2IdentityAsserterNameMapper;
import com.bea.security.saml2.providers.SAML2NameMapperInfo;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;
import weblogic.logging.LoggingHelper;
import weblogic.security.service.ContextHandler;
public class WLSSaml2IdentityMapper implements SAML2IdentityAsserterNameMapper, SAML2IdentityAsserterAttributeMapper {
public static final String ATTR_PRINCIPALS = "com.bea.contextelement.saml.AttributePrincipals";
public static final String ATTR_USERNAME = "urn:mace:dir:attribute-def:uid";
private Logger lgr = LoggingHelper.getServerLogger();
private final String className = "nl.darwinit.wls.saml.WLSSaml2IdentityMapper";
//
/**
* Map Name Info to String
* @param saml2NameMapperInfo
* @param contextHandler
* @return
*/
@Override
public String mapNameInfo(SAML2NameMapperInfo saml2NameMapperInfo, ContextHandler contextHandler) {
final String methodName = className + ".mapNameInfo";
debugStart(methodName);
String user = null;
debug(methodName, "saml2NameMapperInfo: " + saml2NameMapperInfo.toString());
debug(methodName, "contextHandler: " + contextHandler.toString());
debug(methodName, "contextHandler number of elements: " + contextHandler.size());
// getNames gets a list of ContextElement names that can be requested.
String[] names = contextHandler.getNames();
// For each possible element
for (String element : names) {
debug(methodName, "ContextHandler element: " + element);
// If one of those possible elements has the AttributePrinciples
if (element.equals(ATTR_PRINCIPALS)) {
// Put the AttributesPrincipals into an ArrayList of CustomPrincipals
ArrayList<CustomPrincipal> customPrincipals =
(ArrayList<CustomPrincipal>) contextHandler.getValue(ATTR_PRINCIPALS);
int i = 0;
String attr;
if (customPrincipals != null) {
// For each AttributePrincipal in the ArrayList
for (CustomPrincipal customPrincipal : customPrincipals) {
// Get the Attribute Name and the Attribute Value
attr = customPrincipal.toString();
debug(methodName, "Attribute " + i + " Name: " + attr);
debug(methodName, "Attribute " + i + " Value: " + customPrincipal.getCollectionAsString());
// If the Attribute is "loginAccount"
if (attr.equals(ATTR_USERNAME)) {
user = customPrincipal.getCollectionAsString();
// Remove the "@DNS.DOMAIN.COM" (case insensitive) and set the username to that string
if (!user.equals("null")) {
user = user.replaceAll("(?i)\\@CLIENT\\.COMPANY\\.COM", "");
debug(methodName, "Username (from loginAccount): " + user);
break;
}
}
i++;
}
}
// For some reason the ArrayList of CustomPrincipals was blank - just set the username to the Subject
if (user == null || "".equals(user)) {
user = saml2NameMapperInfo.getName(); // Subject = BRID
debug(methodName, "Username (from Subject): " + user);
}
return user;
}
}
// Just in case AttributePrincipals does not exist
user = saml2NameMapperInfo.getName(); // Subject = BRID
debug(methodName, "Username (from Subject): " + user);
debugEnd(methodName);
// Set the username to the Subject
return user;
// debug(methodName,"com.bea.contextelement.saml.AttributePrincipals: " + arg1.getValue(ATTR_PRINCIPALS));
// debug(methodName,"com.bea.contextelement.saml.AttributePrincipals CLASS: " + arg1.getValue(ATTR_PRINCIPALS).getClass().getName());
// debug(methodName,"ArrayList toString: " + arr2.toString());
// debug(methodName,"Initial size of arr2: " + arr2.size());
}
//
/**
* Map Attribute Info to Collection<Principal>
* @param attrStmtInfos
* @param contextHandler
* @return
*/
public Collection<Principal> mapAttributeInfo(Collection<SAML2AttributeStatementInfo> attrStmtInfos,
ContextHandler contextHandler) {
final String methodName = className + ".mapAttributeInfo";
Collection<Principal> principals = null;
if (attrStmtInfos == null || attrStmtInfos.size() == 0) {
debug(methodName, "AttrStmtInfos has no elements");
} else {
principals = new ArrayList<Principal>();
for (SAML2AttributeStatementInfo stmtInfo : attrStmtInfos) {
Collection<SAML2AttributeInfo> attrs = stmtInfo.getAttributeInfo();
if (attrs == null || attrs.size() == 0) {
debug(methodName, "No attribute in statement: " + stmtInfo.toString());
} else {
for (SAML2AttributeInfo attr : attrs) {
CustomPrincipal principal = null;
String principalName = "";
Collection<String> attrValues = attr.getAttributeValues();
if (!attrValues.isEmpty()) {
int attrValIdx = 0;
for (String attrValue : attrValues) {
debug(methodName,
"Value " + ++attrValIdx + " of " + attr.getAttributeName() + "= " + attrValue);
if (attrValIdx == 1) {
principalName = attrValue;
}
}
} else {
principalName = attr.getAttributeName();
}
principal = new CustomPrincipal(principalName, attr.getAttributeValues());
debug(methodName, "Add principal: " + principal.toString());
principals.add(principal);
}
}
}
}
return principals;
}
//
private void debug(String methodName, String msg) {
lgr.fine(methodName + ": " + msg);
}
//
private void debugStart(String methodName) {
debug(methodName, "Start");
}
//
private void debugEnd(String methodName) {
debug(methodName, "End");
}
}
To be complete, here is the principal class:
package nl.darwinit.wls.saml;
import java.util.Collection;
import weblogic.security.principal.WLSAbstractPrincipal;
import weblogic.security.spi.WLSUser;
public class CustomPrincipal extends WLSAbstractPrincipal implements WLSUser {
@SuppressWarnings("compatibility:1303497257301553869")
private static final long serialVersionUID = 1L;
private String commonName;
private Collection<String> collection;
public CustomPrincipal(String name, Collection<String> collection) {
super();
// Feed the WLSAbstractPrincipal.name. Mandatory
this.setName(name);
this.setCommonName(name);
this.setCollection(collection);
}
public CustomPrincipal() {
super();
}
public CustomPrincipal(String commonName) {
super();
this.setName(commonName);
this.setCommonName(commonName);
}
public void setCommonName(String commonName) {
// Feed the WLSAbstractPrincipal.name. Mandatory
this.setName(commonName);
this.commonName = commonName;
System.out.println("Attribute: " + this.getName());
// System.out.println("Custom Principle commonName is " + this.commonName);
}
public Collection<String> getCollection() {
return collection;
}
public String getCollectionAsString() {
String collasstr = "null";
if (collection != null && collection.size() > 0) {
for (String value : collection) {
collasstr = value;
break;
}
/*for (Iterator iterator = collection.iterator(); iterator.hasNext();) {
collasstr = (String) iterator.next();
return collasstr;
}*/
}
return collasstr;
}
public void setCollection(Collection<String> collection) {
this.collection = collection;
// System.out.println("set collection in CustomPrinciple!");
if (collection != null && collection.size() > 0) {
/*for (Iterator iterator = collection.iterator(); iterator.hasNext();) {
final String value = (String) iterator.next();
System.out.println("Attribute Value: " + value);
}*/
for (String value : collection) {
System.out.println("Attribute Value: " + value);
}
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((collection == null) ? 0 : collection.hashCode());
result = prime * result + ((commonName == null) ? 0 : commonName.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (getClass() != obj.getClass())
return false;
CustomPrincipal other = (CustomPrincipal) obj;
if (collection == null) {
if (other.collection != null)
return false;
} else if (!collection.equals(other.collection))
return false;
if (commonName == null) {
if (other.commonName != null)
return false;
} else if (!commonName.equals(other.commonName))
return false;
return true;
}
}
In my earlier article I described how to add a reference to the jar file containing these classes to the java classpath field on the Server Start tab in the console.
In 12c this apparently does not work, the class is not picked up. Add it to the class path by creating/editing the
setUserOverrides.sh/.cmd file.
Conclusion
This must be about it. We had quite a bit of Trial & Error. But most of the gotchas are listed here I think. But feel free to hire us if you need help. (Because I think you'll need the
A-team for different issues...)