In WSO2 Products multiple attribute login (login with either email or username for example) can be done with LDAP Userstore manager with simply by changing some configurations. But with JDBC Userstore manager we need some customization to achieve that. We can achieve that by using Implementing a custom userstore manager. In this blog entry I am going to make work with email and username. You can find the full sample here[1].
For login purposes
@Override
public boolean doAuthenticate(String attribute, Object credential) throws UserStoreException {
if (!checkUserNameValid(attribute)) {
return false;
}
if (!checkUserPasswordValid(credential)) {
return false;
}
if (UserCoreUtil.isRegistryAnnonymousUser(attribute)) {
log.error("Anonnymous user trying to login");
return false;
}
Connection dbConnection = null;
ResultSet rs = null;
PreparedStatement prepStmt = null;
String sqlstmt = null;
String password = (String) credential;
boolean isAuthed = false;
try {
dbConnection = getDBConnection();
dbConnection.setAutoCommit(false);
sqlstmt = realmConfig.getUserStoreProperty(JDBCRealmConstants.SELECT_USER);
if (log.isDebugEnabled()) {
log.debug(sqlstmt);
}
prepStmt = dbConnection.prepareStatement(sqlstmt);
// Insert attribute as parameter for each occurrence of
int paramCount = StringUtils.countMatches(sqlstmt, "?");
// If we specify the tenant into query, we assume that it is the last parameter
if (sqlstmt.contains(UserCoreConstants.UM_TENANT_COLUMN)) {
// Assign attribute value to parameters, except the last one
for (int i = 1; i < paramCount; i++) {
prepStmt.setString(i, attribute);
}
prepStmt.setInt(paramCount, tenantId);
} else {
// There is no tenant indication, set all parameters with attribute value
for (int i = 1; i <= paramCount; i++) {
prepStmt.setString(i, attribute);
}
}
rs = prepStmt.executeQuery();
if (rs.next() == true) {
String storedPassword = rs.getString(3);
String saltValue = null;
if ("true".equalsIgnoreCase(realmConfig
.getUserStoreProperty(JDBCRealmConstants.STORE_SALTED_PASSWORDS))) {
saltValue = rs.getString(4);
}
boolean requireChange = rs.getBoolean(5);
Timestamp changedTime = rs.getTimestamp(6);
GregorianCalendar gc = new GregorianCalendar();
gc.add(GregorianCalendar.HOUR, -24);
Date date = gc.getTime();
if (requireChange == true && changedTime.before(date)) {
isAuthed = false;
} else {
password = this.preparePassword(password, saltValue);
if ((storedPassword != null) && (storedPassword.equals(password))) {
isAuthed = true;
}
}
}
} catch (SQLException e) {
String msg = "Error occurred while retrieving user authentication info.";
log.error(msg, e);
throw new UserStoreException("Authentication Failure");
} finally {
DatabaseUtil.closeAllConnections(dbConnection, rs, prepStmt);
}
if (log.isDebugEnabled()) {
log.debug("User " + attribute + " login attempt. Login success :: " + isAuthed);
}
return isAuthed;
}
@Override
public Date getPasswordExpirationTime(String attribute) throws UserStoreException {
Connection dbConnection = null;
ResultSet rs = null;
PreparedStatement prepStmt = null;
String sqlstmt;
Date date = null;
try {
dbConnection = getDBConnection();
dbConnection.setAutoCommit(false);
sqlstmt = realmConfig.getUserStoreProperty(JDBCRealmConstants.SELECT_USER);
if (log.isDebugEnabled()) {
log.debug(sqlstmt);
}
prepStmt = dbConnection.prepareStatement(sqlstmt);
// Insert attribute as parameter for each occurrence of
int paramCount = StringUtils.countMatches(sqlstmt, "?");
// If we specify the tenant into query, we assume that it is the last parameter
if (sqlstmt.contains(UserCoreConstants.UM_TENANT_COLUMN)) {
// Assign attribute value to parameters, except the last one
for (int i = 1; i < paramCount; i++) {
prepStmt.setString(i, attribute);
}
prepStmt.setInt(paramCount, tenantId);
} else {
// There is no tenant indication, set all parameters with attribute value
for (int i = 1; i <= paramCount; i++) {
prepStmt.setString(i, attribute);
}
}
rs = prepStmt.executeQuery();
if (rs.next() == true) {
boolean requireChange = rs.getBoolean(5);
Timestamp changedTime = rs.getTimestamp(6);
if (requireChange) {
GregorianCalendar gc = new GregorianCalendar();
gc.setTime(changedTime);
gc.add(GregorianCalendar.HOUR, 24);
date = gc.getTime();
}
}
} catch (SQLException e) {
String msg = "Error occurred while retrieving password expiration time.";
log.error(msg, e);
throw new UserStoreException(msg, e);
} finally {
DatabaseUtil.closeAllConnections(dbConnection, rs, prepStmt);
}
return date;
}
public String[] doGetExternalRoleListOfUser(String userName, String filter) throws UserStoreException {
if(log.isDebugEnabled()) {
log.debug("Getting roles of user: " + userName + " with filter: " + filter);
}
String sqlStmt;
if(this.isCaseSensitiveUsername()) {
sqlStmt = this.realmConfig.getUserStoreProperty("UserRoleSQL");
} else {
sqlStmt = this.realmConfig.getUserStoreProperty("UserRoleSQLCaseInsensitive");
}
ArrayList roles = new ArrayList();
if(sqlStmt == null) {
throw new UserStoreException("The sql statement for retrieving user roles is null");
} else {
String[] names;
if(sqlStmt.contains("UM_TENANT_ID")) {
names = this.getStringValuesFromDatabase(sqlStmt, new Object[]{userName, userName, Integer.valueOf(this.tenantId), Integer.valueOf(this.tenantId), Integer.valueOf(this.tenantId), Integer.valueOf(this.tenantId)});
} else {
names = this.getStringValuesFromDatabase(sqlStmt, new Object[]{userName});
}
if(log.isDebugEnabled()) {
if(names != null) {
String[] arr$ = names;
int len$ = names.length;
for(int i$ = 0; i$ < len$; ++i$) {
String name = arr$[i$];
log.debug("Found role: " + name);
}
} else {
log.debug("No external role found for the user: " + userName);
}
}
Collections.addAll(roles, names);
return (String[])roles.toArray(new String[roles.size()]);
}
}
And with this you will need to modify your $CARBON_HOME/repository/conf/user-mgt.xml user store manager configuration section as below.
<UserStoreManager class="org.wso2.carbon.userstore.jdbc.CustomJDBCUserStoreManager">
<Property name="TenantManager">org.wso2.carbon.user.core.tenant.JDBCTenantManager</Property>
<Property name="ReadOnly">false</Property>
<Property name="ReadGroups">true</Property>
<Property name="WriteGroups">true</Property>
<Property name="UsernameJavaRegEx">^[\S]{3,30}$</Property>
<Property name="UsernameJavaScriptRegEx">[a-zA-Z0-9@._-|//]{3,30}$</Property>
<Property name="UsernameWithEmailJavaScriptRegEx">[a-zA-Z0-9@._-|//]{3,30}$</Property>
<Property name="UsernameJavaRegExViolationErrorMsg">Username pattern policy violated</Property>
<Property name="PasswordJavaRegEx">^[\S]{5,30}$</Property>
<Property name="PasswordJavaScriptRegEx">^[\S]{5,30}$</Property>
<Property name="PasswordJavaRegExViolationErrorMsg">Password length should be within 5 to 30 characters</Property>
<Property name="RolenameJavaRegEx">^[\S]{3,255}$</Property>
<Property name="RolenameJavaScriptRegEx">^[\S]{3,255}$</Property>
<Property name="CaseInsensitiveUsername">true</Property>
<Property name="SCIMEnabled">false</Property>
<Property name="IsBulkImportSupported">false</Property>
<Property name="PasswordDigest">SHA-256</Property>
<Property name="StoreSaltedPassword">true</Property>
<Property name="MultiAttributeSeparator">,</Property>
<Property name="MaxUserNameListLength">100</Property>
<Property name="MaxRoleNameListLength">100</Property>
<Property name="UserRolesCacheEnabled">true</Property>
<Property name="UserNameUniqueAcrossTenants">false</Property>
<Property name="PasswordHashMethod">SHA</Property>
<Property name="SelectUserSQL">SELECT distinct u.* FROM UM_USER u left join UM_USER_ATTRIBUTE ua on u.UM_ID = ua.UM_USER_ID WHERE u.UM_USER_NAME = ? OR (ua.UM_ATTR_NAME = "mail" AND ua.UM_ATTR_VALUE = ?) AND u.UM_TENANT_ID = ?</Property>
<Property name="UserRoleSQLCaseInsensitive">SELECT UM_ROLE_NAME FROM UM_USER_ROLE, UM_ROLE, UM_USER WHERE LOWER(UM_USER.UM_USER_NAME) IN (SELECT LCASE(u.UM_USER_NAME) FROM UM_USER u left join UM_USER_ATTRIBUTE ua on u.UM_ID = ua.UM_USER_ID WHERE u.UM_USER_NAME = ? OR (ua.UM_ATTR_NAME = "mail" AND ua.UM_ATTR_VALUE = ?) AND u.UM_TENANT_ID = ? GROUP BY u.UM_USER_NAME) AND UM_USER.UM_ID=UM_USER_ROLE.UM_USER_ID AND UM_ROLE.UM_ID=UM_USER_ROLE.UM_ROLE_ID AND UM_USER_ROLE.UM_TENANT_ID = ? AND UM_ROLE.UM_TENANT_ID = ? AND UM_USER.UM_TENANT_ID = ?</Property>
</UserStoreManager>
To use User Info endpoint with Oauth
Further extending this If you need to use User Info endpoint with oauth2, then you will need to furthe extend following method as well.
public boolean doCheckExistingUser(String userName) throws UserStoreException {
String sqlStmt;
if(this.isCaseSensitiveUsername()) {
sqlStmt = this.realmConfig.getUserStoreProperty("IsUserExistingSQL");
} else {
sqlStmt = this.realmConfig.getUserStoreProperty("IsUserExistingSQLCaseInsensitive");
}
if(sqlStmt == null) {
throw new UserStoreException("The sql statement for is user existing null");
} else {
boolean isExisting = false;
String isUnique = this.realmConfig.getUserStoreProperty("UserNameUniqueAcrossTenants");
if(Boolean.parseBoolean(isUnique) && !"wso2.anonymous.user".equals(userName)) {
String uniquenesSql;
if(this.isCaseSensitiveUsername()) {
uniquenesSql = this.realmConfig.getUserStoreProperty("UserNameUniqueAcrossTenantsSQL");
} else {
uniquenesSql = this.realmConfig.getUserStoreProperty("UserNameUniqueAcrossTenantsSQLCaseInsensitive");
}
isExisting = this.isValueExisting(uniquenesSql, (Connection)null, new Object[]{userName});
if(log.isDebugEnabled()) {
log.debug("The username should be unique across tenants.");
}
} else if(sqlStmt.contains("UM_TENANT_ID")) {
isExisting = this.isValueExisting(sqlStmt, (Connection)null, new Object[]{userName, userName, Integer.valueOf(this.tenantId)});
} else {
isExisting = this.isValueExisting(sqlStmt, (Connection)null, new Object[]{userName});
}
return isExisting;
}
}
And you will need to add more configurations and following is the updated user store manager configuration.
<UserStoreManager class="org.wso2.sample.userstore.jdbc.CustomJDBCUserStoreManager">
<Property name="TenantManager">org.wso2.carbon.user.core.tenant.JDBCTenantManager</Property>
<Property name="ReadOnly">false</Property>
<Property name="ReadGroups">true</Property>
<Property name="WriteGroups">true</Property>
<Property name="UsernameJavaRegEx">^[\S]{3,30}$</Property>
<Property name="UsernameJavaScriptRegEx">[a-zA-Z0-9@._-|//]{3,30}$</Property>
<Property name="UsernameWithEmailJavaScriptRegEx">[a-zA-Z0-9@._-|//]{3,30}$</Property>
<Property name="UsernameJavaRegExViolationErrorMsg">Username pattern policy violated</Property>
<Property name="PasswordJavaRegEx">^[\S]{5,30}$</Property>
<Property name="PasswordJavaScriptRegEx">^[\S]{5,30}$</Property>
<Property name="PasswordJavaRegExViolationErrorMsg">Password length should be within 5 to 30 characters</Property>
<Property name="RolenameJavaRegEx">^[\S]{3,255}$</Property>
<Property name="RolenameJavaScriptRegEx">^[\S]{3,255}$</Property>
<Property name="CaseInsensitiveUsername">true</Property>
<Property name="SCIMEnabled">false</Property>
<Property name="IsBulkImportSupported">false</Property>
<Property name="PasswordDigest">SHA-256</Property>
<Property name="StoreSaltedPassword">true</Property>
<Property name="MultiAttributeSeparator">,</Property>
<Property name="MaxUserNameListLength">100</Property>
<Property name="MaxRoleNameListLength">100</Property>
<Property name="UserRolesCacheEnabled">true</Property>
<Property name="UserNameUniqueAcrossTenants">false</Property>
<Property name="PasswordHashMethod">SHA</Property>
<Property name="SelectUserSQL">SELECT distinct u.* FROM UM_USER u left join UM_USER_ATTRIBUTE ua on u.UM_ID = ua.UM_USER_ID WHERE u.UM_USER_NAME = ? OR (ua.UM_ATTR_NAME = "mail" AND ua.UM_ATTR_VALUE = ?) AND u.UM_TENANT_ID = ?</Property>
<Property name="UserRoleSQLCaseInsensitive">SELECT UM_ROLE_NAME FROM UM_USER_ROLE, UM_ROLE, UM_USER WHERE LOWER(UM_USER.UM_USER_NAME) IN (SELECT LCASE(u.UM_USER_NAME) FROM UM_USER u left join UM_USER_ATTRIBUTE ua on u.UM_ID = ua.UM_USER_ID WHERE u.UM_USER_NAME = ? OR (ua.UM_ATTR_NAME = "mail" AND ua.UM_ATTR_VALUE = ?) AND u.UM_TENANT_ID = ? GROUP BY u.UM_USER_NAME) AND UM_USER.UM_ID=UM_USER_ROLE.UM_USER_ID AND UM_ROLE.UM_ID=UM_USER_ROLE.UM_ROLE_ID AND UM_USER_ROLE.UM_TENANT_ID = ? AND UM_ROLE.UM_TENANT_ID = ? AND UM_USER.UM_TENANT_ID = ?</Property>
<Property name="GetUserPropertiesForProfileSQLCaseInsensitive">SELECT UM_ATTR_NAME, UM_ATTR_VALUE FROM UM_USER_ATTRIBUTE, UM_USER WHERE (UM_USER.UM_ID = UM_USER_ATTRIBUTE.UM_USER_ID OR (UM_USER_ATTRIBUTE.UM_ATTR_NAME = 'mail' AND LOWER(UM_USER_ATTRIBUTE.UM_ATTR_VALUE) = LOWER(?))) AND UM_PROFILE_ID=? AND UM_USER_ATTRIBUTE.UM_TENANT_ID=? AND UM_USER.UM_TENANT_ID=?</Property>
<Property name="IsUserExistingSQLCaseInsensitive">SELECT distinct u.UM_ID FROM UM_USER u left join UM_USER_ATTRIBUTE ua on u.UM_ID = ua.UM_USER_ID WHERE u.UM_USER_NAME = ? OR (ua.UM_ATTR_NAME = "mail" AND ua.UM_ATTR_VALUE = ?) AND u.UM_TENANT_ID = ?</Property>
</UserStoreManager>
And further in $IS_HOME/repository/conf/identity/application-authentication.xml you will need to add following property under Authenticator Config for Basic Authenticator
<Parameter name="UserNameAttributeClaimUri">http://wso2.org/claims/username</Parameter>
So my Basic Authenticator Config tag is as below.
<AuthenticatorConfig name="BasicAuthenticator" enabled="true">
<Parameter name="UserNameAttributeClaimUri">http://wso2.org/claims/username</Parameter>
<!--Parameter name="showAuthFailureReason">true</Parameter-->
</AuthenticatorConfig>
With this you will be able to get configured claims when you logged in using different attributes.
References
Comments
Post a Comment