Sunday, April 1, 2012

Mock Weblogic Login module - Identity Asserter and Authenticator

This module is strictly for testing as it does not validate user.

Based on this log http://weblogic-wonders.com/weblogic/2010/03/15/custom-identity-asserter-for-weblogic-server/

Oracle® Fusion Middleware Developing Security Providers for Oracle WebLogic Server
http://docs.oracle.com/cd/E14571_01/web.1111/e13718/atn.htm#i115061
Identity Assertion Providers
http://docs.oracle.com/cd/E14571_01/web.1111/e13718/ia.htm#i1156163


Steps to create login module
1. Create MBean definition file SimpleSampleIdentityAsserter.xml
<?xml version="1.0" ?>
<!DOCTYPE MBeanType SYSTEM "commo.dtd">

<!-- MBean Definition File (MDF) for the Simple Sample Identity Asserter.

Copyright (c) 2002 by BEA Systems, Inc. All Rights Reserved.
-->

<!-- Declare your mbean.

Since it is for an identity asserter, it must extend the
weblogic.management.security.authentication.IdentityAsserter mbean.

The Name and DisplayName must be the same.
They specify the name that will appear on the
console for this provider.

Set the PeristPolicy to "OnUpdate" so that if an attribute
value is changed, the new value is written to disk immediately.
See the "Developing Security Services" manual for more info.

Note that since this is an xml document, you can't use double
quotes directly. Instead you need to use &quot;

Note that setting "Writeable" to "false" on an attribute
makes the attribute read-only. The default is read-write.
-->

<MBeanType
Name = "SimpleSampleIdentityAsserter"
DisplayName = "SimpleSampleIdentityAsserter"
Package = "examples.security.providers.identityassertion.simple"
Extends = "weblogic.management.security.authentication.IdentityAsserter"
PersistPolicy = "OnUpdate"
>

<!-- You must set the value of the ProviderClassName attribute
(inherited from the weblogic.management.security.Provider mbean)
to the name of the java class you wrote that implements the
weblogic.security.spi.AuthenticationProvider interface.

You can think of the provider's mbean as the factory
for your provider's runtime implementation.
-->
<MBeanAttribute
Name = "ProviderClassName"
Type = "java.lang.String"
Writeable = "false"
Preprocessor = "weblogic.management.configuration.LegalHelper.checkClassName(value)"
Default = "&quot;examples.security.providers.identityassertion.simple.SimpleSampleIdentityAsserterProviderImpl&quot;"
/>

<!-- You must set the value of the Description attribute
(inherited from the weblogic.management.security.Provider mbean)
to a brief description of your provider.
It is displayed in the console.
-->
<MBeanAttribute
Name = "Description"
Type = "java.lang.String"
Writeable = "false"
Default = "&quot;WebLogic Simple Sample Identity Asserter Provider&quot;"
/>

<!-- You must set the value of the Version attribute
(inherited from the weblogic.management.security.Provider mbean)
to your provider's version. There is no required format.
-->
<MBeanAttribute
Name = "Version"
Type = "java.lang.String"
Writeable = "false"
Default = "&quot;1.0&quot;"
/>

<!-- You must set the value of the SupportedTypes attribute
(inherited from the
weblogic.management.security.authentication.IdentityAsserter mbean)
to the list of token types that your identity asserter supports.

Whoever is initiating the identity assertion (eg. a client sending
a perimeter authentication token via an HTTP request header), must
use the same token type.
-->
<MBeanAttribute
Name = "SupportedTypes"
Type = "java.lang.String[]"
Writeable = "false"
Default = "new String[] { &quot;SamplePerimeterAtnToken&quot; }"
/>
<!-- The ActiveTypes attribute (a settable attribute inherited from the
weblogic.management.security.authentication.IdentityAsserter mbean)
contains the subset of your mbean's SupportedTypes that are active
in the realm.

Which way you should default the active types attribute depends
on your token types. The basic rule is that for any token
type, there must only be one identity asserter in the realm with
that token type as an active type. In short, you can only have
one identity asserter turned on for a given type.

If your token types are commonly implemented by other identity
asserters (eg. X509 certificates), then you should not set them
as default active types. Otherwise, it would be very easy for
an administrator to configure an invalid realm where more than
one identity asserter has the same type turned on. Best
practice is that all the identity asserters turn off the type
by default then the administrator manually turns on the
type in one of the identity asserters that support it.
Look at the weblogic.security.spi.IdentityAsserter javadoc
for some standard token types.

On the other hand, if you have a custom token type that no
other identity asserter will ever implement, you may default the
active types attribute to include your token type. If you do,
then the adminstrator doesn't have to manually turn on your token
type.

Since the simple sample identity asserter's token type is very specific
to the sample (instead of a common type like X509), turn on the
token type by default.
-->
<MBeanAttribute
Name = "ActiveTypes"
Type = "java.lang.String[]"
Default = "new String[] { &quot;SamplePerimeterAtnToken&quot; }"
/>

<!-- Add any custom attributes for your provider here.

The simple sample identity asserter does not have any custom attributes.

Note: custom attributes do not appear in the
console in WLS 7.0. Use the admin command line tool
(java weblogic.Admin) to view and set their values.

Refer to the "Developing Security Services" manual
for more info on defining custom attributes.
-->

<MBeanAttribute
Name = "Base64DecodingRequired"
Type = "boolean"
Writeable = "false"
Default = "false"
Description = "See MyIdentityAsserter-doc.xml."
/>

</MBeanType>



2. copy commo.dtd from WLS installation
[dave@dave src]$ find /app/weblogic121/ -name commo.dtd
/app/weblogic121/wlserver_12.1/server/lib/commo.dtd
/app/weblogic121/wlserver_12.1/server/lib/mbeantypes/commo.dtd
[dave@dave src]$ cp /app/weblogic121/wlserver_12.1/server/lib/mbeantypes/commo.dtd .




3. modify classes for IdentityAsserter, LoginModule and CallbackHandler - only 3 classes have to be provided , rest is generated
[dave@dave src]$ ls -1
build.xml
commo.dtd
SimpleSampleCallbackHandlerImpl.java
SimpleSampleIdentityAsserterImpl.java
SimpleSampleIdentityAsserter.jar
SimpleSampleIdentityAsserterProviderImpl.java
SimpleSampleIdentityAsserter.xml
SimpleSampleLoginModuleImpl.java
test



This is main Provider class which drives the process SimpleSampleIdentityAsserterProviderImpl.
It provides IdentityAsserter and it calls SimpleSampleLoginModule
package examples.security.providers.identityassertion.simple;

import java.util.HashMap;

import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;

import weblogic.management.security.ProviderMBean;
import weblogic.security.provider.PrincipalValidatorImpl;
import weblogic.security.service.ContextHandler;
import weblogic.security.spi.AuthenticationProviderV2;
import weblogic.security.spi.IdentityAsserterV2;
import weblogic.security.spi.IdentityAssertionException;
import weblogic.security.spi.PrincipalValidator;
import weblogic.security.spi.SecurityServices;

/**
* The simple sample identity asserter's runtime implementation.
*
* It looks for tokens of type "SamplePerimeterAtnToken"
* whose matching token is an array of bytes containing a
* string in the form "username=someusername".
*
* It extracts the username from the token and stores it
* in a SimpleSampleCallbackHandlerImpl. This is returned to the
* security framework who hands it to the authenticators'
* login modules. The login modules can use a NameCallback
* to retrieve the user name from the simple sample identity
* asserter's callback handler.
*
* Since it is an identity asserter, it must implement
* the weblogic.security.spi.AuthenticationProvider and
* the weblogic.security.spi.IdentityAsserter interfaces.
*
* It can either implement two classes, and use the
* provider implementation as the factory as the
* factory for the identity asserter, or it can implement
* both interfaces in one class. The simple sample identity
* asserter implments both interfaces in one class.
*
* Note: The simple sample identity asserter's mbean's ProviderClassName
* attribute must be set the the name of this class.
*
* @author Copyright (c) 2002 by BEA Systems. All Rights Reserved.
*/
public final class SimpleSampleIdentityAsserterProviderImpl implements AuthenticationProviderV2, IdentityAsserterV2
{
final static private String TOKEN_TYPE = "SamplePerimeterAtnToken"; // the kind of token's we handle
final static private String TOKEN_PREFIX = "username="; // the token contains a string in the form "username=someusername"

private String description; // a description of this provider
private LoginModuleControlFlag controlFlag; // how this provider's login module should be used during the JAAS login


/**
* Initialize the simple sample identity asserter.
*
* @param mbean A ProviderMBean that holds the simple sample identity asserter's
* configuration data. This mbean must be an instance of the simple sample
* identity asserter's mbean.
*
* @param services The SecurityServices gives access to the auditor
* so that the provider can to post audit events.
* The simple sample role mapper doesn't use this parameter.
*
* @see SecurityProvider
*/
public void initialize(ProviderMBean mbean, SecurityServices services)
{
System.out.println("SimpleSampleIdentityAsserterProviderImpl.initialize");
SimpleSampleIdentityAsserterMBean myMBean = (SimpleSampleIdentityAsserterMBean)mbean;
description = myMBean.getDescription() + "\n" + myMBean.getVersion();

controlFlag = LoginModuleControlFlag.SUFFICIENT;
/* String flag = myMBean.getControlFlag();
if (flag.equalsIgnoreCase("REQUIRED")) {
controlFlag = LoginModuleControlFlag.REQUIRED;
} else if (flag.equalsIgnoreCase("OPTIONAL")) {
controlFlag = LoginModuleControlFlag.OPTIONAL;
} else if (flag.equalsIgnoreCase("REQUISITE")) {
controlFlag = LoginModuleControlFlag.REQUISITE;
} else if (flag.equalsIgnoreCase("SUFFICIENT")) {
controlFlag = LoginModuleControlFlag.SUFFICIENT;
} else {
throw new IllegalArgumentException("invalid flag value" + flag);
}*/
}

/**
* Get the simple sample identity asserter's description.
*
* @return A String containing a brief description of the simple sample identity asserter.
*
* @see SecurityProvider
*/
public String getDescription()
{
return description;
}

/**
* Shutdown the simple sample identity asserter.
*
* A no-op.
*
* @see SecurityProvider
*/
public void shutdown()
{
System.out.println("SimpleSampleIdentityAsserterProviderImpl.shutdown");
}

/**
* Gets the simple sample identity assertion provider's identity asserter object.
*
* @return The simple sample identity assertion provider's IdentityAsserter object.
*
* @see AuthenticationProvider
*/
public IdentityAsserterV2 getIdentityAsserter()
{
return this;
}

/**
* Assert identity given a token that identifies the user.
*
* @param type A String containing the token type. The simple sample identity
* asserter only supports tokens of type "SamplePerimeterAtnToken".
* Also, the simple sample identity asserter's mbean's "ActiveTypes" attribute
* must be set to "SamplePerimeterAtnToken" (which is done by default
* when the mbean is created).
*
* @param token An Object containing the token that identifies the user.
* The simple sample identity asserter's token must be an array of bytes
* containing a String of the form "username=someusername".
*
* @param handler A ContextHandler object that can optionally
* be used to obtain additional information that may be used in
* asserting the identity. If the caller is unable to provide additional
* information, a null value should be specified. This sample
* ignores the handler.
*
* While, for simplicity, this sample does not validate the
* contents of the token, identity asserters typically should do
* this (to prevent someone from forging a token). For
* example, when using Kerberos, the token may be generated
* and "signed" by a Kerberos server and the identity asserter
* hands the token back to the Kerberos server to get it
* validated. Another example: when asserting identity from
* X509 certificates, then identity asserter should validate the
* certificate - that it hasn't been tampered, that it's been
* signed by a trusted CA, that it hasn't expired or revoked, etc.
*
* @return a CallbackHandler that stores the username from the token.
* The username can only be retrieved from the callback handler by
* passing in a NameCallback. The sample returns an instance of
* its CallbackHandler implementation (SimpleSampleCallbackHandlerImpl).
*
* @throws IdentityAssertionException if another token type is passed
* in or the token doesn't have the correct form.
*/
public CallbackHandler assertIdentity(String type, Object token, ContextHandler context) throws IdentityAssertionException
{
System.out.println("SimpleSampleIdentityAsserterProviderImpl.assertIdentity");
System.out.println("\tType\t\t= " + type);
System.out.println("\tToken\t\t= " + token);

// check the token type
if (!(TOKEN_TYPE.equals(type))) {
String error =
"SimpleSampleIdentityAsserter received unknown token type \"" + type + "\"." +
" Expected " + TOKEN_TYPE;
System.out.println("\tError: " + error);
throw new IdentityAssertionException(error);
}

// make sure the token is an array of bytes
if (!(token instanceof byte[])) {
String error =
"SimpleSampleIdentityAsserter received unknown token class \"" + token.getClass() + "\"." +
" Expected a byte[].";
System.out.println("\tError: " + error);
throw new IdentityAssertionException(error);
}

// convert the array of bytes to a string
byte[] tokenBytes = (byte[])token;
if (tokenBytes == null || tokenBytes.length < 1) {
String error =
"SimpleSampleIdentityAsserter received empty token byte array";
System.out.println("\tError: " + error);
throw new IdentityAssertionException(error);
}

String tokenStr = new String(tokenBytes);

// make sure the string contains "username=someusername
if (!(tokenStr.startsWith(TOKEN_PREFIX))) {
String error =
"SimpleSampleIdentityAsserter received unknown token string \"" + type + "\"." +
" Expected " + TOKEN_PREFIX + "username";
System.out.println("\tError: " + error);
throw new IdentityAssertionException(error);
}

// extract the username from the token
String userName = tokenStr.substring(TOKEN_PREFIX.length());
System.out.println("\tuserName\t= " + userName);

// store it in a callback handler that authenticators can use
// to retrieve the username.
return new SimpleSampleCallbackHandlerImpl(userName);
}

/**
* Create a JAAS AppConfigurationEntry (which tells JAAS
* how to create the login module and how to use it) when
* the simple sample authenticator is used to authenticate (vs. to
* complete identity assertion).
*
* @return An AppConfigurationEntry that tells JAAS how to use the simple sample
* authenticator's login module for authentication.
*/
public AppConfigurationEntry getLoginModuleConfiguration()
{
// Don't pass in any special options.
// By default, the simple sample authenticator's login module
// will authenticate (by checking that the passwords match).
HashMap options = new HashMap();
return getConfiguration(options);
}

/**
* Create a JAAS AppConfigurationEntry (which tells JAAS
* how to create the login module and how to use it).
* This helper method is used both for authentication mode
* and identity assertion mode.
*
* @param options A HashMap containing the options to pass to the
* simple sample authenticator's login module. This method adds the
* "database helper" object to the options. This allows the
* login module to access the users and groups.
*
* @return An AppConfigurationEntry that tells JAAS how to use the simple sample
* authenticator's login module.
*/
private AppConfigurationEntry getConfiguration(HashMap options)
{
System.out.println("SimpleSampleIdentityAsserterProviderImpl: getConfiguration");
// make sure to specify the simple sample authenticator's login module
// and to use the control flag from the simple sample authenticator's mbean.
return new
AppConfigurationEntry(
"examples.security.providers.authentication.simple.SimpleSampleLoginModuleImpl",
controlFlag,
options
);
}

/**
* Create a JAAS AppConfigurationEntry (which tells JAAS
* how to create the login module and how to use it) when
* the simple sample authenticator is used to complete identity
* assertion (vs. to authenticate).
*
* @return An AppConfigurationEntry that tells JAAS how to use the simple sample
* authenticator's login module for identity assertion.
*/
public AppConfigurationEntry getAssertionModuleConfiguration()
{
System.out.println("SimpleSampleIdentityAsserterProviderImpl: getAssertionModuleConfiguration");
// Pass an option indicating that we're doing identity
// assertion (vs. authentication) therefore the login module
// should only check that the user exists (instead of checking
// the password)
HashMap options = new HashMap();
options.put("IdentityAssertion","true");
return getConfiguration(options);
}

/**
* Return the principal validator that can validate the
* principals that the authenticator's login module
* puts into the subject.
*
* Since the simple sample authenticator uses the built in
* WLSUserImpl and WLSGroupImpl principal classes, just
* returns the built in PrincipalValidatorImpl that knows
* how to handle these kinds of principals.
*/
public PrincipalValidator getPrincipalValidator()
{
return new PrincipalValidatorImpl();
}
}



SimpleSampleLoginModule - set as SUFFICIENT

package examples.security.providers.authentication.simple;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Map;
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.LoginException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.spi.LoginModule;
import weblogic.management.utils.NotFoundException;
import weblogic.security.principal.WLSGroupImpl;
import weblogic.security.principal.WLSUserImpl;

/**
* The simple sample authenticator's login module implementation.
*
* It is used in one of two modes:
* - authentication where it validates the user's password
* then populates the subject with the user and the user's groups.
* - identity assertion where it checks that the user exists,
* then populates the subject with the user and the user's groups.
*
* The SimpleSampleAuthenticationProviderImpl creates an options hash map
* that is passed to this login module. It contains one entry,
* named "database", that is an object that manages the
* user and group definitions. It optionally contains another entry,
* named "IdentityAssertion", that puts the login module in
* "identity assertion" mode (vs. the default which is "authenticadtion"
* mode).
*
* It uses the built in WLSUserImpl and WLSGroupImpl classes to
* populate the subject with users and groups.
*
* @author Copyright (c) 2002 by BEA Systems. All Rights Reserved.
*/
final public class SimpleSampleLoginModuleImpl implements LoginModule
{
private Subject subject; // the subject for this login
private CallbackHandler callbackHandler; // where to get user names, passwords, ... for this login

private boolean isIdentityAssertion; // are we in authentication or identity assertion mode?

// Authentication status
private boolean loginSucceeded; // have we successfully logged in?
private boolean principalsInSubject; // did we add principals to the subject?
private Vector principalsForSubject = new Vector(); // if so, what principals did we add to the subject
// (so we can remove the principals we added if the login is aborted)
/**
* Initialize a login attempt.
*
* @param subject the Subject this login attempt will populate.
*
* @param callbackhandler the CallbackHandler that can be used to
* get the user name, and in authentication mode, the user's password
*
* @param sharedState A Map containing data shared between login
* modules when there are multiple authenticators configured. This
* simple sample does not use this parameter.
*
* @param options A Map containing options that the authenticator's
* authentication provider impl wants to pass to its login module impl.
* For example, it can be used to pass in configuration data (where
* is the database holding user and group info) and to pass in whether
* the login module is used for authentication or to complete identity
* assertion.
* The SimpleSampleAuthenticationProviderImpl adds an option named "database".
* The value is a SimpleSampleAuthenticatorDatabase object. It gives the
* login module access to the user and group definitions.
* When the authenticator is being used in identity assertion mode,
* the SimpleSampleAuthenticationProviderImpl also adds an option named
* "IdentityAssertion". It indicates that the login module should only
* verify that the user exists (vs. checking the password too). If
* this option is not specified (or is set to false), then the
* login module checks the user's password too (that is, it assumes
* authentication mode).
*/
public void
initialize(
Subject subject,
CallbackHandler callbackHandler,
Map sharedState,
Map options
)
{
// only called (once!) after the constructor and before login

System.out.println("SimpleSampleLoginModuleImpl.initialize");
this.subject = subject;
this.callbackHandler = callbackHandler;

// Determine if we're in identity assertion or authentication mode
isIdentityAssertion =
"true".equalsIgnoreCase((String)options.get("IdentityAssertion"));

}

/**
* Attempt to login.
*
* If we're in authentication mode, extract the user name and password
* from the callback handler. If the user exists and the password matches,
* then populate the subject with the user and the user's group. Otherwise,
* the login fails.
*
* If we're in identity assertion mode, extract the user name (only)
* from the callback handler. If the user exists, then populate the
* subject with the user and the user's groups. Otherwise, the
* login fails.
*
* @return A boolean indicating whether or not the login for
* this login module succeeded.
*/
public boolean login() throws LoginException
{
// only called (once!) after initialize

System.out.println("SimpleSampleLoginModuleImpl.login");

// loginSucceeded should be false
// principalsInSubject should be false

// Call a method to get the callbacks.
// For authentication mode, it will have one for the
// username and one for the password.
// For identity assertion mode, it will have one for
// the user name.
Callback[] callbacks = getCallbacks();

// Get the user name.
String userName = getUserName(callbacks);

if (userName.length() > 0) {
// We have a user name
System.out.println("\tuserName=" + userName);
} else {
// anonymous login.
System.out.println("\tempty userName");
}

loginSucceeded = true;

// since the login succeeded, add the user and its groups to the
// list of principals we want to add to the subject.
principalsForSubject.add(new WLSUserImpl(userName));
addGroupsForSubject(userName);

return loginSucceeded;
}

/**
* Completes the login by adding the user and the user's groups
* to the subject.
*
* @return A boolean indicating whether or not the commit succeeded.
*/
public boolean commit() throws LoginException
{
// only called (once!) after login

// loginSucceeded should be true or false
// principalsInSubject should be false
// user should be null if !loginSucceeded, null or not-null otherwise
// group should be null if user == null, null or not-null otherwise

System.out.println("SimpleSampleLoginModule.commit");
if (loginSucceeded) {
// put the user and the user's groups (computed during the
// login method and stored in the principalsForSubject object)
// into the subject.
subject.getPrincipals().addAll(principalsForSubject);
principalsInSubject = true;
return true;
} else {
return false;
}
}

/**
* Aborts the login attempt. Remove any principals we put
* into the subject during the commit method from the subject.
*
* @return A boolean indicating whether or not the abort succeeded.
*/
public boolean abort() throws LoginException
{
// only called (once!) after login or commit
// or may be? called (n times) after abort

// loginSucceeded should be true or false
// principalsInSubject should be false if user is null, otherwise true or false

System.out.println("SimpleSampleLoginModule.abort");
if (principalsInSubject) {
subject.getPrincipals().removeAll(principalsForSubject);
principalsInSubject = false;
}
return true;
}

/**
* Logout. This should never be called.
*
* @return A boolean indicating whether or not the logout succeeded.
*/
public boolean logout() throws LoginException
{
// should never be called

System.out.println("SimpleSampleLoginModule.logout");
return true;
}

/**
* Throw an invalid login exception.
*
* @param msg A String containing the text of the LoginException.
*
* @throws LoginException
*/
private void throwLoginException(String msg) throws LoginException
{
System.out.println("Throwing LoginException(" + msg + ")");
throw new LoginException(msg);
}

/**
* Throws a failed login excception.
*
* @param msg A String containing the text of the FailedLoginException.
*
* @throws LoginException
*/
private void throwFailedLoginException(String msg) throws FailedLoginException
{
System.out.println("Throwing FailedLoginException(" + msg + ")");
throw new FailedLoginException(msg);
}

/**
* Get the list of callbacks needed by the login module.
*
* @return The array of Callback objects by the login module.
* Returns one for the user name and password if in authentication mode.
* Returns one for the user name if in identity assertion mode.
*/
private Callback[] getCallbacks() throws LoginException
{
if (callbackHandler == null) {
throwLoginException("No CallbackHandler Specified");
}

Callback[] callbacks;
if (isIdentityAssertion) {
callbacks = new Callback[1]; // need one for the user name
} else {
callbacks = new Callback[2]; // need one for the user name and one for the password

// add in the password callback
callbacks[1] = new PasswordCallback("password: ",false);
}

// add in the user name callback
callbacks[0] = new NameCallback("username: ");

// Call the callback handler, who in turn, calls back to the
// callback objects, handing them the user name and password.
// These callback objects hold onto the user name and password.
// The login module retrieves the user name and password from them later.
try {
callbackHandler.handle(callbacks);
} catch (IOException e) {
throw new LoginException(e.toString());
} catch (UnsupportedCallbackException e) {
throwLoginException(e.toString() + " " + e.getCallback().toString());
}

return callbacks;
}

/**
* Get the user name from the callbacks (that the callback handler
* has already handed the user name to).
*
* @param callbacks The array of Callback objects used by this login module.
* The first in the list must be the user name callback object.
*
* @return A String containing the user name (from the user name callback object)
*/
private String getUserName(Callback[] callbacks) throws LoginException
{
String userName = ((NameCallback)callbacks[0]).getName();
if (userName == null) {
throwLoginException("Username not supplied.");
}
System.out.println("\tuserName\t= " + userName);
return userName;
}

/**
* Add the user's groups to the list of principals to be added to the subject.
*
* @param A String containing the user name the user's name.
*/
private void addGroupsForSubject(String userName)
{
// Get the user's list of groups (recursively - so, if user1 is a member
// of group1 and group1 is a member of group2, then it returns group1 and
// group2). Iterate over the groups, adding each to the list of principals
// to add to the subject.
String groupName = "SamplePerimeterAtnUsers";
System.out.println("\tgroupName\t= " + groupName);
principalsForSubject.add(new WLSGroupImpl(groupName));
}

/**
* Get the password from the callbacks (that the callback handler
* has already handed the password to) - that is, the password from
* the login attempt. Must only be used for authentication mode, not
* for identity assertion mode.
*
* @param useName A String containing the name of the user
* (already retrieved from the callbacks). Only passed in
* so that we can print a better error message if the password
* is bogus.
*
* @param callbacks The array of Callback objects used by this login module.
* The second in the list must be the password callback object.
*
* @return A String containing the password from the login attempt
*
* @throws LoginException if no password was supplied in the login attempt.
*/
private String getPasswordHave(String userName, Callback[] callbacks) throws LoginException
{
PasswordCallback passwordCallback = (PasswordCallback)callbacks[1];
char[] password = passwordCallback.getPassword();
passwordCallback.clearPassword();
if (password == null || password.length < 1) {
throwLoginException("Authentication Failed: User " + userName + ". Password not supplied");
}
String passwd = new String(password);
System.out.println("\tpasswordHave\t= " + passwd);
return passwd;
}
}



CallbackHandler
package examples.security.providers.identityassertion.simple;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

/**
* The simple sample identity asserter's implementation of the
* CallbackHandler interface.
*
* It is used to make the name of the user from the identity
* assertion token available to the authenticators (who, in
* turn, will populate the subject with the user and the user's
* groups).
*
* This class is internal to the simple sample identity asserter.
* It is not a public class.
*
* @author Copyright (c) 2002 by BEA Systems. All Rights Reserved.
*/
/*package*/ class SimpleSampleCallbackHandlerImpl implements CallbackHandler
{
private String userName; // the name of the user from the identity assertion token

/**
* Create a callback handler that stores the user name.
*
* @param user A String containing the name of the user
* from the identity assertion token
*/
/*package*/ SimpleSampleCallbackHandlerImpl(String user)
{
userName = user;
}

/**
* Used by the authenticators' login modules to get the user name
* that the identity asserter extracted from the identity assertion token.
* This name can only be retrieved via a NameCallback.
*
* @param callbacks An array of Callback objects indicating what data
* the login module is trying to extract from this callback handler.
* It must only contain NameCallbacks.
*
* @exception UnsupportedCallbackException thrown if any of the callbacks
* aren't NameCallbacks.
*
* @see CallbackHandler
*/
public void handle(Callback[] callbacks) throws UnsupportedCallbackException
{
// loop over the callbacks
for (int i = 0; i < callbacks.length; i++) {

Callback callback = callbacks[i];

// we only handle NameCallbacks
if (!(callback instanceof NameCallback)) {
throw new UnsupportedCallbackException(callback, "Unrecognized Callback");
}

// send the user name to the name callback:
NameCallback nameCallback = (NameCallback)callback;
nameCallback.setName(userName);
}
}
}





4.Build jar with ant using build.xml
<project name="Expenselink Build" default="all" basedir=".">
<property name="fileDir" value="test" />

<target name="all" depends="build"/>

<target name="build" depends="clean,build.mdf,build.mjf"/>

<target name="clean">
<delete dir="${fileDir}" failonerror="false"/>
<delete file="SimpleSampleIdentityAsserter.jar" failonerror="false"/>
<echo message="Clean finish" />
</target>

<!-- helper to build an MDF (mbean definition file) -->
<target name="build.mdf">
<java dir="${basedir}" fork="false" classname="weblogic.management.commo.WebLogicMBeanMaker">
<arg line="-files ${fileDir}" />
<arg value="-createStubs" />
<arg line="-MDF SimpleSampleIdentityAsserter.xml" />
</java>
<echo message="Created Supporting Classes" />
</target>

<target name="build.mjf">

<copy todir="${fileDir}" flatten="true">
<fileset dir=".">
<include name="*.java" />
</fileset>
</copy>

<java dir="${basedir}" fork="false" classname="weblogic.management.commo.WebLogicMBeanMaker">
<arg line="-MJF SimpleSampleIdentityAsserter.jar" />
<arg line="-files ${fileDir}" />
</java>
<echo message="Created Mbean Jar" />
</target>

</project>



Set WLS environment

[dave@dave src]$ . /app/weblogic121/wlserver_12.1/server/bin/setWLSEnv.sh




[dave@dave src]$ ant
Buildfile: build.xml

clean:
[delete] Deleting directory /home/dave/workspace1211/testIdentityAsserter/src/test
[delete] Deleting: /home/dave/workspace1211/testIdentityAsserter/src/SimpleSampleIdentityAsserter.jar
[echo] Clean finish

build.mdf:
[java] Working directory ignored when same JVM is used.
[java] Parsing the MBean definition file: SimpleSampleIdentityAsserter.xml
[echo] Created Supporting Classes

build.mjf:
[copy] Copying 3 files to /home/dave/workspace1211/testIdentityAsserter/src/test
[java] Working directory ignored when same JVM is used.
[java] Creating an MJF from the contents of directory test...
[java] Compiling the files...
[java] Creating the list.
[java] Doing the compile.
[java] Note: /home/dave/workspace1211/testIdentityAsserter/src/test/SimpleSampleIdentityAsserterProviderImpl.java uses or overrides a deprecated API.
[java] Note: Recompile with -Xlint:deprecation for details.
[java] Note: Some input files use unchecked or unsafe operations.
[java] Note: Recompile with -Xlint:unchecked for details.
[java] WLMaker-SubProcess: : EXTRACT FROM /usr/app/weblogic121/wlserver_12.1/server/lib/mbeantypes/wlManagementMBean.jar
[java] WLMaker-SubProcess: : INTO wlMakerTempDir
[java] WLMaker-SubProcess: :
[java] WLMaker-SubProcess: :
[java] WLMaker-SubProcess: :
[java] WLMaker-SubProcess: : Generating the implementations for security MBeans
[java] WLMaker-SubProcess: : Generating for examples.security.providers.identityassertion.simple.SimpleSampleIdentityAsserterMBean to /home/dave/workspace1211/testIdentityAsserter/src/test/examples/security/providers/identityassertion/simple/SimpleSampleIdentityAsserterMBeanImpl.java
[java] WLMaker-SubProcess: : no annotation found for key [i]
[java] WLMaker-SubProcess: : no annotation found for key [velocityCount]
[java] WLMaker-SubProcess: : no annotation found for key [line]
[java] WLMaker-SubProcess: : no annotation found for key [f]
[java] WLMaker-SubProcess: : no annotation found for key [m]
[java] WLMaker-SubProcess: : no annotation found for key [p]
[java] WLMaker-SubProcess: : no annotation found for key [n]
[java] WLMaker-SubProcess: : done
[java] WLMaker-SubProcess: :
[java] WLMaker-SubProcess: :
[java] WLMaker-SubProcess: :
[java] WLMaker-SubProcess: : Generating the parsing binders for security MBeans
[java] WLMaker-SubProcess: : Generating for examples.security.providers.identityassertion.simple.SimpleSampleIdentityAsserterMBean to /home/dave/workspace1211/testIdentityAsserter/src/test/examples/security/providers/identityassertion/simple/SimpleSampleIdentityAsserterMBeanBinder.java
[java] WLMaker-SubProcess: : done
[java] WLMaker-SubProcess: :
[java] WLMaker-SubProcess: :
[java] WLMaker-SubProcess: :
[java] WLMaker-SubProcess: : Generating the bean infos for security MBeans ...
[java] WLMaker-SubProcess: : Generating for examples.security.providers.identityassertion.simple.SimpleSampleIdentityAsserterMBean to /home/dave/workspace1211/testIdentityAsserter/src/test/examples/security/providers/identityassertion/simple/SimpleSampleIdentityAsserterMBeanImplBeanInfo.java
[java] WLMaker-SubProcess: : no annotation found for key [import]
[java] WLMaker-SubProcess: : no annotation found for key [property]
[java] WLMaker-SubProcess: : no annotation found for key [beanConfigurable]
[java] WLMaker-SubProcess: : no annotation found for key [beanIntfExclude]
[java] WLMaker-SubProcess: : no annotation found for key [propertyMethod]
[java] WLMaker-SubProcess: : no annotation found for key [method]
[java] WLMaker-SubProcess: : Generating Bean Factory Class to test/weblogic/management/security/SIMPLESAMPLEIDENTITYASSERTERBeanInfoFactory.java
[java] WLMaker-SubProcess: : done
[java] WLMaker-SubProcess: : Stopped draining WLMaker-SubProcess:
[java] WLMaker-SubProcess: : Stopped draining WLMaker-SubProcess:
[java] WLMaker-SchemaGen-SubProcess: Generating schema for security provider mbeans ...
[java] WLMaker-SchemaGen-SubProcess: MBEAN TYPES DIR : null
[java] WLMaker-SchemaGen-SubProcess: SET BASE LIB /usr/app/weblogic121/wlserver_12.1/server/lib/schema/weblogic-domain-binding.jar
[java] WLMaker-SchemaGen-SubProcess: Stopped draining WLMaker-SchemaGen-SubProcess
[java] WLMaker-SchemaGen-SubProcess: Stopped draining WLMaker-SchemaGen-SubProcess
[java] Creating the list.
[java] Doing the compile.
[java] Note: Some input files use or override a deprecated API.
[java] Note: Recompile with -Xlint:deprecation for details.
[java] Note: Some input files use unchecked or unsafe operations.
[java] Note: Recompile with -Xlint:unchecked for details.
[java] Creating the MJF...
[java] MJF is created.
[echo] Created Mbean Jar

build:

all:

BUILD SUCCESSFUL



5. copy provider to Weblogic installation dir
see http://docs.oracle.com/cd/E21764_01/web.1111/e13718/ia.htm#autoId18
Note:
WL_HOME\server\lib\mbeantypes is the default directory for installing MBean types.
(Beginning with 9.0, security providers can be loaded from ...\domaindir\lib\mbeantypes as well.)
However, if you want WebLogic Server to look for MBean types in additional directories, use the -Dweblogic.alternateTypesDirectory=<dir> command-line flag when starting your server,
where <dir> is a comma-separated list of directory names. When you use this flag, WebLogic Server will always load MBean types from WL_HOME\server\lib\mbeantypes first,
then will look in the additional directories and load all valid archives present in those directories (regardless of their extension).
For example, if -Dweblogic.alternateTypesDirectory = dirX,dirY, WebLogic Server will first load MBean types from WL_HOME\server\lib\mbeantypes, then any valid archives present in dirX and dirY.


[dave@dave src]$ cp  SimpleSampleIdentityAsserter.jar /app/weblogic121/wlserver_12.1/server/lib/mbeantypes/
[dave@dave src]$


6. configure new provider using WLS console



7. test using Modify HTTP headers plugin or cookie editor ( see previous blog entry)
user token is retrieved by this custom provider from HTTP request header or from cookie

Weblogic logs
SimpleSampleIdentityAsserterProviderImpl.initialize
SimpleSampleIdentityAsserterProviderImpl: getAssertionModuleConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
<Apr 1, 2012 10:44:29 AM CEST> <Notice> <Security> <BEA-090082> <Security initializing using security realm myrealm.>
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
<Apr 1, 2012 10:44:32 AM CEST> <Notice> <LoggingService> <BEA-320400> <The log file /usr/app/domains/base_domain/servers/AdminServer/logs/access.log will be rotated. Reopen the log file if tailing has stopped. This can happen on some platforms, such as Windows.>
<Apr 1, 2012 10:44:32 AM CEST> <Notice> <LoggingService> <BEA-320401> <The log file has been rotated to /usr/app/domains/base_domain/servers/AdminServer/logs/access.log00029. Log messages will continue to be logged in /usr/app/domains/base_domain/servers/AdminServer/logs/access.log.>
<Apr 1, 2012 10:44:34 AM CEST> <Notice> <WebLogicServer> <BEA-000365> <Server state changed to STANDBY.>
<Apr 1, 2012 10:44:34 AM CEST> <Notice> <WebLogicServer> <BEA-000365> <Server state changed to STARTING.>
<Apr 1, 2012 10:44:37 AM CEST> <Notice> <LoggingService> <BEA-320400> <The log file /usr/app/domains/base_domain/servers/AdminServer/logs/base_domain.log will be rotated. Reopen the log file if tailing has stopped. This can happen on some platforms, such as Windows.>
<Apr 1, 2012 10:44:37 AM CEST> <Notice> <LoggingService> <BEA-320401> <The log file has been rotated to /usr/app/domains/base_domain/servers/AdminServer/logs/base_domain.log00032. Log messages will continue to be logged in /usr/app/domains/base_domain/servers/AdminServer/logs/base_domain.log.>
<Apr 1, 2012 10:44:37 AM CEST> <Notice> <Log Management> <BEA-170027> <The server has successfully established a connection with the Domain level Diagnostic Service.>
<Apr 1, 2012 10:44:37 AM CEST> <Notice> <WebLogicServer> <BEA-000365> <Server state changed to ADMIN.>
<Apr 1, 2012 10:44:37 AM CEST> <Notice> <WebLogicServer> <BEA-000365> <Server state changed to RESUMING.>
<Apr 1, 2012 10:44:37 AM CEST> <Notice> <Server> <BEA-002613> <Channel
<Apr 1, 2012 10:44:37 AM CEST> <Notice> <WebLogicServer> <BEA-000331> <Started the WebLogic Server Administration Server "AdminServer" for domain "base_domain" running in development mode.>
<Apr 1, 2012 10:44:37 AM CEST> <Notice> <WebLogicServer> <BEA-000365> <Server state changed to RUNNING.>
<Apr 1, 2012 10:44:37 AM CEST> <Notice> <WebLogicServer> <BEA-000360> <The server started in RUNNING mode.>
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleIdentityAsserterProviderImpl.assertIdentity
Type = SamplePerimeterAtnToken
Token = [B@8393a
userName = dave
SimpleSampleIdentityAsserterProviderImpl: getAssertionModuleConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleLoginModuleImpl.initialize
SimpleSampleLoginModuleImpl.login
userName = dave
userName=dave
groupName = SamplePerimeterAtnUsers
SimpleSampleLoginModule.commit
SimpleSampleIdentityAsserterProviderImpl.assertIdentity
Type = SamplePerimeterAtnToken
Token = [B@8393a
userName = dave
SimpleSampleIdentityAsserterProviderImpl.assertIdentity
Type = SamplePerimeterAtnToken
Token = [B@8393a
userName = dave
SimpleSampleIdentityAsserterProviderImpl.assertIdentity
Type = SamplePerimeterAtnToken
Token = [B@8393a
userName = dave
SimpleSampleIdentityAsserterProviderImpl.assertIdentity
Type = SamplePerimeterAtnToken
Token = [B@8393a
userName = dave
SimpleSampleIdentityAsserterProviderImpl.assertIdentity
Type = SamplePerimeterAtnToken
Token = [B@8393a
userName = dave
SimpleSampleIdentityAsserterProviderImpl.assertIdentity
Type = SamplePerimeterAtnToken
Token = [B@8393a
userName = dave
SimpleSampleIdentityAsserterProviderImpl.assertIdentity
Type = SamplePerimeterAtnToken
Token = [B@8393a
userName = dave
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleLoginModuleImpl.initialize
SimpleSampleLoginModuleImpl.login
userName = weblogic
userName=weblogic
groupName = SamplePerimeterAtnUsers
SimpleSampleLoginModule.commit
SimpleSampleIdentityAsserterProviderImpl.assertIdentity
Type = SamplePerimeterAtnToken
Token = [B@8393a
userName = dave
SimpleSampleIdentityAsserterProviderImpl.assertIdentity
Type = SamplePerimeterAtnToken
Token = [B@8393a
userName = dave
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleIdentityAsserterProviderImpl.assertIdentity
Type = SamplePerimeterAtnToken
Token = [B@8393a
userName = dave
SimpleSampleIdentityAsserterProviderImpl: getAssertionModuleConfiguration
SimpleSampleIdentityAsserterProviderImpl: getConfiguration
SimpleSampleLoginModuleImpl.initialize
SimpleSampleLoginModuleImpl.login
userName = dave
userName=dave
groupName = SamplePerimeterAtnUsers
SimpleSampleLoginModule.commit



Domain config with new provider - default provider must be set to SUFFICIENT because mock users do not exist in WLS LPAP
 <name>base_domain</name>
<domain-version>12.1.1.0</domain-version>
<security-configuration>
<name>base_domain</name>
<realm>
<sec:authentication-provider xsi:type="wls:default-authenticatorType">
<sec:control-flag>SUFFICIENT</sec:control-flag>
</sec:authentication-provider>
<sec:authentication-provider xsi:type="wls:default-identity-asserterType">
<sec:active-type>AuthenticatedUser</sec:active-type>
</sec:authentication-provider>
<sec:authentication-provider xmlns:ext="http://xmlns.oracle.com/weblogic/security/extension" xsi:type="ext:simple-sample-identity-asserterType">
<sec:name>SimpleSampleIdentityAserter</sec:name>
</sec:authentication-provider>
<sec:role-mapper xmlns:xac="http://xmlns.oracle.com/weblogic/security/xacml" xsi:type="xac:xacml-role-mapperType"></sec:role-mapper>
<sec:authorizer xmlns:xac="http://xmlns.oracle.com/weblogic/security/xacml" xsi:type="xac:xacml-authorizerType"></sec:authorizer>
<sec:adjudicator xsi:type="wls:default-adjudicatorType"></sec:adjudicator>
<sec:credential-mapper xsi:type="wls:default-credential-mapperType"></sec:credential-mapper>
<sec:cert-path-provider xsi:type="wls:web-logic-cert-path-providerType"></sec:cert-path-provider>
<sec:cert-path-builder>WebLogicCertPathProvider</sec:cert-path-builder>
<sec:name>myrealm</sec:name>
<sec:password-validator xmlns:pas="http://xmlns.oracle.com/weblogic/security/providers/passwordvalidator" xsi:type="pas:system-password-validatorType">
<sec:name>SystemPasswordValidator</sec:name>
<pas:min-password-length>8</pas:min-password-length>
<pas:min-numeric-or-special-characters>1</pas:min-numeric-or-special-characters>
</sec:password-validator>
</realm>
<default-realm>myrealm</default-realm>




Captured HTTP header using Firefox plugin Live HTTP headers
First run without added token in HTTP request header

http://localhost:7002/samplePerimeterAtnWebApp/SamplePerimeterAtn.jsp

GET /samplePerimeterAtnWebApp/SamplePerimeterAtn.jsp HTTP/1.1
Host: localhost:7001
User-Agent: Mozilla/5.0 (X11; Linux i686; rv:11.0) Gecko/20100101 Firefox/11.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: ADMINCONSOLESESSION=GGfvP4VP5YBZbBcQXb4vQJVF1TRPTq1Qsn4b8yLx1m2H7Q8PlT8v!385259249

HTTP/1.1 401 Unauthorized
Date: Sun, 01 Apr 2012 09:10:09 GMT
Content-Length: 1468
Content-Type: text/html; charset=UTF-8
X-Powered-By: Servlet/3.0 JSP/2.2
----------------------------------------------------------
http://localhost:7001/samplePerimeterAtnWebApp/SamplePerimeterAtn.jsp

GET /samplePerimeterAtnWebApp/SamplePerimeterAtn.jsp HTTP/1.1
Host: localhost:7001
User-Agent: Mozilla/5.0 (X11; Linux i686; rv:11.0) Gecko/20100101 Firefox/11.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: ADMINCONSOLESESSION=GGfvP4VP5YBZbBcQXb4vQJVF1TRPTq1Qsn4b8yLx1m2H7Q8PlT8v!385259249
SamplePerimeterAtnToken: username=dave

HTTP/1.1 200 OK
Date: Sun, 01 Apr 2012 09:10:18 GMT
Content-Length: 90
Content-Type: text/html; charset=ISO-8859-1
Set-Cookie: JSESSIONID=x1yHP4bhj2RjFQL7Ph46hkysTmZC4LXm0gglyL3DsSh2hmTGhVt2!385259249; path=/; HttpOnly
X-Powered-By: Servlet/3.0 JSP/2.2
----------------------------------------------------------

9 comments:

  1. You are a prince (This is a perfect listing compared to screwed Oracle documentation!).

    ReplyDelete
  2. I've try to configure the same IA, but it's never called.
    Any idea?
    The only reference is when the admin server started:"SimpleSampleIdentityAsserterProviderImpl.initialize"

    ReplyDelete
    Replies
    1. You need to create separate web application and deploy in weblogic server. Here is the servlet code for webapplication
      protected void doGet(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {
      try {
      String baseP8URL = http://localhost:9704/analytics/saw.dll?bieehome&startPage=1&NQUser=;
      String userName = "Weblogic";
      String password = "The_Unique_Password";

      //create html anchor tag by using response.getWriter().print and combine three string like,

      href=baseP8URL + userName +"&NQPassword="+ password;

      } catch (Exception e) {
      e.printStackTrace();
      }

      Delete
  3. I can able create custom identity Asserter successfully. But when i login OBIEE from custom login module by hitting this URL http://localhost:9704/analytics/saw.dll?bieehome&startPage=1&NQUser=weblogic&NQPassword=VGhlX1VuaXF1ZV9QYXNzd29yZA== Few roles and groups not coming from LDAP. But when i login through OBIEE all roles and groups are coming.

    ReplyDelete
  4. Hi Daniel,

    thank you for a very well commented source code! I am trying to develop a similar provider and I did everything as prescribed. I was able to deploy and configure my provider in the security realm by adding it to the list of authentication providers through WLS console. But if I call a service that is configured to require the custom token authentication, the login mudule is never called. In fact, even initialize() method of the provider is not called. Do you have any idea what might be wrong or how could I trace the problem? I do not see anything in the logs and I have already enabled debugging for weblogic.security.

    Thanks,

    Marian

    ReplyDelete
  5. Thank you. This helped us implementing our own Login module for WebLogic 10.3.
    However, when trying to mirgate to WebLogic 12c the MBeanMaker does not generate a class called "SettableBean.class" any more. The generated jar file cannot be deployed to the WebLogic (exceptions during Startup).

    Was anyone of you successfull building his own Authentication provider usings 12c libs and MBeanMaker?

    ReplyDelete
  6. Hello Daniel,

    with gread pleasure I read you posts 'Mock Weblogic Login module' and 'Weblogic login module test'. Thank you very much for that great posts.

    I successfully build the mockup following your excellent description. Now I'm looking for the SamplePerimeterAtn.jsp for testing. Unfortunately never I could find it. Please could you provide me with the webapp samplePerimeterAtnWebApp/SamplePerimeterAtn.jsp you mentioned.

    Best regards

    Stephan

    ReplyDelete
  7. You're blog has really helped me create our own custom identity asserter ! Thank you!

    ReplyDelete