Introduction
Off late there are several projects that are being migrated
from oracle FORMS to ADF. When it comes to user authentication, general trend
in FORMS based application is that it uses user’s own database schema and password
to login. Typical migration from FORMS to ADF takes somewhere between few
months to couple of years depending upon the size of the application(s). ADF
applications are typically designed to use Oracle IAM suite of products for
authentication and authorization but to get there it takes a while. Until a
final security solution is arrived user profiles needs to be maintained
separately according to the underlying LDAP. If organizations want to follow similar
FORMS (database) based authentication process until a final security solution is
identified they could perhaps look into this approach that is explained in this
article.
Design & implementation
Below
diagram depicts the design for DB based authentication.
I followed the steps given in the URL http://knowaboutadf.blogspot.in/2012/09/custom-authentication-provider-in.html
to create a custom database authenticator. I don’t want to repeat the same
steps in this article. I have provided additional information below that you may
find it useful.
Code that implements
LoginModule class has the logic to read and authenticate the end user provided
credential is given below.
/**
* Author:
Aravindan Varadan
* Created on:
April 16 2014
* Updated on:
April 21 2014
* Description:
This is a custom authentication login module implementation that will
authenticate the users against database schema.
*
*/
package adf.common.security.provider;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.security.auth.Subject;
import
javax.security.auth.callback.Callback;
import
javax.security.auth.callback.CallbackHandler;
import
javax.security.auth.callback.NameCallback;
import
javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import weblogic.security.principal.WLSGroupImpl;
import weblogic.security.principal.WLSUserImpl;
public class CustomLoginModuleImpl implements
LoginModule {
private
Subject subject;
private
CallbackHandler callbackHandler;
private
HashMap userMap;
private
boolean loginSucceeded;
private
boolean principalsInSubject;
private
Vector<weblogic.security.principal.WLSAbstractPrincipal>
principalsBeforeCommit = new
Vector<weblogic.security.principal.WLSAbstractPrincipal>();
private final
static String DEVELOPER_ROLE = "DEVELOPER";
private final
static String DEV_MGR_ROLE = "DEV_MGR";
private final
static String REL_MGR_ROLE = "REL_MGR";
private final
static String TL_ROLE = "TL";
private
String username = null;
private
String password = null;
private String
host = null;
private
String port = null;
private
String db = null;
private
String tlId = null;
private
String mgrId = null;
private
String deptCd = null;
public
MFCustomLoginModuleImpl() {
super();
}
/**
* This
overridden method will be initialized during doLogin() method call from ADF
login screen.
* @param
subject
* @param
callbackHandler
* @param
sharedState
* @param
options
*/
public void
initialize(Subject subject, CallbackHandler callbackHandler,
Map<String, ?>
sharedState,
Map<String, ?>
options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
userMap =
(HashMap)options.get("usermap");
System.out.println("MULTIFONDS custom DB based login module
initialized");
if(subject.getPrincipals() != null){
System.out.println("MF Custom Authentication provider subject is
not null");
}
}
/**
* This is
the method that will be called during doLogin() action from ADF login screen.
* User
entered credentials are captured and authenticated against database.
* @return
* @throws
LoginException
*/
public
boolean login() throws LoginException {
Callback[] callbacks;
callbacks
= new Callback[2];
callbacks[0] = new NameCallback ("username:");
callbacks[1] = new PasswordCallback("password:",false);
try{
System.out.println("-------Invoking CustomLoginModuleImpl login()
method----");
callbackHandler.handle(callbacks);
}catch(IOException io){
throw
new LoginException(io.toString());
}catch(UnsupportedCallbackException uce){
throw
new LoginException(uce.toString());
}
String
username = ((NameCallback)callbacks[0]).getName();
System.out.println("MFCustomAuthenticator: DB param provided by the
user to authenticate -->"+username);
char[] pw
= ((PasswordCallback)callbacks[1]).getPassword();
password
= new String(pw);
try{
loginSucceeded = dbAuthenticate();
}catch(SQLException sqle){
loginSucceeded = false;
throw
new FailedLoginException(sqle.getMessage());
}
principalsBeforeCommit.add(new WLSUserImpl(username));
return
loginSucceeded;
}
/**
* User
belonged security groups will be added by this method.
*
Assumptions:
* If a user
record has TL ID not empty and dept_cd is not REL then it is assumed that that
users' role will be DEVELOPER
* If a user
record has MGR ID not empty and dept_cd is not REL then it is assumed that that
users' role will be TL
* If a user
record has both TL ID and MGR ID and dept_cd is not REL then it is assumed that
that users' role will be DEVELOPER
* If a user
record has MGR ID column empty and TL ID column empty and dept_cd is not REL
then it is assumed that that users' role will be DEV_MGR
* If a user
record has dept_cd as REL then it is assumed that that users' role will be
REL_MGR
*
*/
private void
addGroupsForSubject(){
String[]
userGroups = new String[1];
if(deptCd
!= null && deptCd.equalsIgnoreCase("REL")){
userGroups[0] = REL_MGR_ROLE;
}else{
if((tlId != null) && (mgrId != null))
userGroups[0] = DEVELOPER_ROLE;
if(tlId != null)
userGroups[0] = DEVELOPER_ROLE;
if((tlId == null) && (mgrId != null))
userGroups[0] = TL_ROLE;
if((tlId == null) && (mgrId == null))
userGroups[0] = DEV_MGR_ROLE;
}
System.out.println("\tgroupName\t= " + userGroups[0]);
principalsBeforeCommit.add(new WLSGroupImpl(userGroups[0]));
}
/**
* If
authentication is successful then this method returns true
* @return
*/
public
boolean commit() {
boolean
flag = false;
if
(loginSucceeded) {
subject.getPrincipals().addAll(principalsBeforeCommit);
System.out.println("CustomAuthenticator: Security groups -->
" +subject);
principalsInSubject = true;
flag
= true;
}
return
flag;
}
/**
* This
method will be called when authentication fails
* @return
*/
public
boolean abort() {
if
(principalsInSubject) {
subject.getPrincipals().removeAll(principalsBeforeCommit);
principalsInSubject = false;
}
System.out.println("CustomAuthenticator: Returning true in
abort");
return
true;
}
/**
* Not doing
anything in this method as of now.
* @return
*/
public
boolean logout() {
return
false;
}
/**
* @param
dbparam
* @param
password
* @return
* @throws
SQLException
*/
private
boolean dbAuthenticate() throws SQLException{
Connection conn = null;
boolean
flag = false;
Properties connProperties = new Properties();
connProperties.put("user", username);
connProperties.put("password", password);
PreparedStatement usergroupsPrepStmt = null;
ResultSet
rs = null;
String
userGrpSql = "SELECT TL_ID, MANAGER_ID,DEPT_CD FROM EMPLOYEE WHERE
SHORT_CD=UPPER(?)";
try{
conn
= DriverManager.getConnection("jdbc:oracle:thin:@"+host+":"+port
+":"+db, connProperties);
System.out.println("MFCustomAuthenticator: Successfully connected
to DB. Now attempting to fetch user groups from the database...");
usergroupsPrepStmt = conn.prepareStatement(userGrpSql);
usergroupsPrepStmt.setString(1, username);
rs =
usergroupsPrepStmt.executeQuery();
while(rs.next()){
tlId = rs.getString("TL_ID");
mgrId = rs.getString("MANAGER_ID");
deptCd = rs.getString("DEPT_CD");
}
System.out.println("MFCustomAuthenticator: TL_ID returned by SQL
"+tlId);
System.out.println("MFCustomAuthenticator: MANAGER_ID returned by
SQL "+mgrId);
System.out.println("MFCustomAuthenticator: DEPT_CD returned by SQL
"+deptCd);
addGroupsForSubject(); //Exception is not caught for this method.
//Reason is, even if
this fails authentication will still go through but usergroup will be null.
//That has
to be handled in the application code.
flag
= true;
}catch(SQLException sqle){
System.out.println("MFCustomAuthenticator: SQL error code returned
"+sqle.getErrorCode());
System.out.println("MFCustomAuthenticator: SQL error status
returned is "+sqle.getSQLState());
System.out.println("MFCustomAuthenticator:
Actual error --> "+sqle);
flag
= false;
if(sqle.getErrorCode() == 1017){
throw new SQLException("MFCustomAuthenticator: invalid
username/password; logon denied",sqle.getSQLState());
}else{
throw new SQLException("MFCustomAuthenticator:Authentication
failed");
}
}finally{
try{
if(usergroupsPrepStmt != null){
usergroupsPrepStmt.close();
System.out.println("MFCustomAuthenticator: Successfully closed
Prepared statement");
}
if(conn != null){
System.out.println("MFCustomAuthenticator: Closing the
connection...");
conn.close();
flag = true;
System.out.println("MFCustomAuthenticator: Successfully closed the
connection, setting the return flag to true");
}else{
System.out.println("MFCustomAuthenticator: No connection object was
obtained");
}
return flag;
}catch(Exception e){
System.out.println("MFCustomAuthenticator: Error in closing
connection "+e);
}
return flag;
}
}
}
Code
of CustomAuthenticationProvider that implements weblogic AuthenticationProvider
interface is provided below.
/**
* Author:
Aravindan Varadan
* Created on:
April 16 2014
* Updated on:
April 21 2014
* Description:
This is a custom authentication provider that will authenticate the users
aganist database schema
*/
package adf.common.security.provider;
import java.util.HashMap;
import javax.security.auth.login.AppConfigurationEntry;
import weblogic.management.security.ProviderMBean;
import weblogic.security.spi.IdentityAsserter;
import weblogic.security.spi.PrincipalValidator;
import weblogic.security.spi.SecurityServices;
import
javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
public final class CustomAuthenticationProvider
implements weblogic.security.spi.AuthenticationProvider {
private
LoginModuleControlFlag controlFlag;
private
String description;
private
HashMap userGroupMapping = null;
public CustomAuthenticationProvider() {
super();
}
public
AppConfigurationEntry getLoginModuleConfiguration() {
HashMap<String, Object> options = new HashMap<String,
Object>() ;
options.put("usermap",userGroupMapping);
return
new
AppConfigurationEntry("adf.common.security.provider.CustomLoginModuleImpl",controlFlag,
options);
}
public
AppConfigurationEntry getAssertionModuleConfiguration() {
return
null;
}
public
PrincipalValidator getPrincipalValidator() {
return
null;
}
public
IdentityAsserter getIdentityAsserter() {
return
null;
}
public void
initialize(ProviderMBean providerMBean,
SecurityServices
securityServices) {
CustomAuthenticationProviderMBean mBean =
(CustomAuthenticationProviderMBean)providerMBean;
description = mBean.getDescription();
System.out.println("Detected custom DB based authentication
provider in WebLogic Server");
String
flag = mBean.getControlFlag();
if(flag.equalsIgnoreCase("REQUIRED"))
controlFlag = LoginModuleControlFlag.REQUIRED;
else
if(flag.equalsIgnoreCase("OPTIONAL"))
controlFlag = LoginModuleControlFlag.OPTIONAL;
else
if(flag.equalsIgnoreCase("SUFFICIENT"))
controlFlag = LoginModuleControlFlag.SUFFICIENT;
else
if(flag.equalsIgnoreCase("REQUISITE"))
controlFlag = LoginModuleControlFlag.REQUISITE;
else
throw
new IllegalArgumentException("INVALID CONTROL FLAG "+flag);
}
public String
getDescription() {
return
null;
}
public void
shutdown() {
}
The
XML file that I used to generate mBean is given below.
<MBeanType Name="MFCustomAuthenticationProvider" DisplayName="MF
custom DB based authentication provider" Package="multifonds.adf.common.security.provider"
Extends="weblogic.management.security.authentication.Authenticator" PersistPolicy="OnUpdate">
<MBeanAttribute Name="ProviderClassName" Type="java.lang.String" Writeable="false"Default=""multifonds.adf.common.security.provider.MFCustomAuthenticationProvider""/>
<MBeanAttribute Name="Description" Type="java.lang.String" Writeable="false" Default=""MF
custom authentication provider""/>
<MBeanAttribute Name="Version" Type="java.lang.String" Writeable="false" Default=""1.0"" Description="The
version of the weblogic authorization provider."/>
After following all the steps given in the URL if you
restart the server you will see the log message “Detected custom DB based authentication provider in WebLogic Server”.
This means that the new mBean is configured correctly.
Post ADF login you should be taken to the home page of the
application ("/adfAuthentication?success_url=/faces/home.jspx";).
Conclusion
This could be considered as a temporary but simple and
efficient solution to leverage the same database that FORMS also uses to
authenticate the users. As you can see in the code above we can even pull the
user roles from the database that FORMS usually maintains in the database
tables and set it in the subject. This
can eventually be read through SecurityContext
object in ADF. Hope few of you find this article useful!
Do let me know if you have
any comments or better suggestions.