Ir al contenido principal

Cómo implementar un almacén de usuarios personalizado en WSO2 Identity Server 5.7.0 | Tutorial WSO2 IS

En este artículo se explica paso a paso cómo implementar un almacén de usuarios personalizado para WSO2 Identity Server 5.7.0. 

Para entenderlo mejor, primero se definirá qué es un almacén de usuarios y además,   se revisará el siguiente caso de uso: Una empresa ya tiene una base de datos de usuarios y necesita autenticarse en Identity Server a través de esa base de datos.

¿Qué es un almacén de usuarios? (User Store)

Un almacén de usuarios es un repositorio de datos donde se guarda la información sobre los usuarios y las funciones de los usuarios, incluidos, entre otros muchos, el nombre de inicio de sesión, la contraseña, el nombre, el apellido y la dirección de correo electrónico.

Por default WSO2 Identity Server viene pre-configurado con los siguientes almacenes de usuarios:

  •    org.wso2.carbon.user.core.jdbc.JDBCUserStoreManager
  •    org.wso2.carbon.user.core.ldap.ReadOnlyLDAPUserStoreManager
  •    org.wso2.carbon.user.core.ldap.ReadWriteLDAPUserStoreManager
  •    org.wso2.carbon.user.core.ldap.ActiveDirectoryLDAPUserStoreManager
  •    org.wso2.carbon.identity.user.store.remote.CarbonRemoteUserStoreManger

Puede comprobarse desde la consola del producto (http://dominio:9443/carbon  → usuario admi/admin)

Yendo a Main → Identity → User Stores → Add.

WSO2 Identity Server es compatible con los almacenes de usuarios basados ​​en LDAP, AD y JDBC. La mayoría de los casos, el almacén de usuarios personalizado significa un almacén de usuarios basado en JDBC con un esquema diferente.

WSO2 Identity Server proporciona un almacén de usuarios basado en JDBC con un esquema predeterminado (su propio esquema). Si una empresa no tiene un almacén de usuarios o necesita un nuevo almacén, entonces el esquema predeterminado se puede usar sin modificaciones, pero en la mayoría de los casos, la empresa ya tiene un almacén de usuarios existente y desea conectarlo con el servidor de identidad de WSO2 para realizar una implementación de Single Sing-On.

Cómo autenticar en Identity Server una base de datos de usuarios de una empresa a través de esa base de datos 

Para el ejemplo (caso de uso) se requiere crear la siguiente tabla llamada USERS mediante el siguiente script:

CREATE TABLE USERS (
    ID INT NOT NULL PRIMARY KEY,
    USERNAME VARCHAR (100),
    PASSWORD VARCHAR (100),
    EMAIL VARCHAR (240)
);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (1, ‘admin’, ‘admin’, ‘admin@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (2, ‘user1’, ‘user1’, ‘user1@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (3, ‘user2’, ‘user2’, ‘user2@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (4, ‘user3’, ‘user3’, ‘user3@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (5, ‘user4’, ‘user4’, ‘user4@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (6, ‘user5’, ‘user5’, ‘user5@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (7, ‘user6’, ‘user6’, ‘user6@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (8, ‘user7’, ‘user7’, ‘user7@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (9, ‘user8’, ‘user8’, ‘user8@chakray.com’);

Para realizar la implementación de un almacén de usuarios personalizado, se requieren las siguientes herramientas:

  • Java 1.8
  • IDE (Eclipse, developer-studio6.4.0)
  • Apache Maven
  • JDBC Driver para la Base de datos donde se crea la tabla USERS

Posteriormente los pasos que se deben seguir son los siguientes:

1.Generar el proyecto en Maven desde el IDE

2.Copiar el JDBC Driver de su base de datos al directorio [IS_HOME]/lib

3.Utilizar el  código de ejemplo mostrado a continuación para el archivo pom.xml

Este archivo contiene información acerca del proyecto, fuentes, test, dependencias, plugins, versión, etc.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.wso2.custom</groupId>
    <artifactId>org.wso2.custom.user.store.manager</artifactId>
    <version>1.0</version>
    <packaging>bundle</packaging>
    <repositories>
        <repository>
            <id>wso2-nexus</id>
            <name>WSO2 internal Repository</name>
            <url>http://maven.wso2.org/nexus/content/groups/wso2-public/</url>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>daily</updatePolicy>
                <checksumPolicy>ignore</checksumPolicy>
            </releases>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.user.core</artifactId>
            <version>4.4.11</version>
        </dependency>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.utils</artifactId>
            <version>4.4.11</version>
        </dependency>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.user.api</artifactId>
            <version>4.4.11</version>
        </dependency>
		<dependency>
    <groupId>org.jasypt</groupId>
    <artifactId>jasypt</artifactId>
    <version>1.9.2</version>
</dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.0</version>
                <configuration>
                    <source>1.5</source>
                    <target>1.5</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-scr-plugin</artifactId>
                <version>1.0.10</version>
                <executions>
                    <execution>
                        <id>generate-scr-scrdescriptor</id>
                        <goals>
                            <goal>scr</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>1.4.0</version>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
                        <Bundle-Name>${pom.artifactId}</Bundle-Name>
                        <Private-Package>
                            org.wso2.sample.user.store.manager.internal
                        </Private-Package>
                        <Export-Package>
                            !org.wso2.sample.user.store.manager.internal,
                            org.wso2.sample.user.store.manager.*,
                        </Export-Package>
                        <Import-Package>
                            javax.servlet; version=2.4.0,
                            javax.servlet.http; version=2.4.0,
                            org.wso2.carbon.base.*,
                            org.wso2.carbon.user.core.*,
                            <!--                            org.apache.lucene.*,-->
                            *;resolution:=optional
                        </Import-Package>
                        <DynamicImport-Package>*</DynamicImport-Package>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4. Posteriormente tenemos que escribir el JDBCUserStoreManager personalizado para que sea compatible con el esquema anterior, por lo tanto, se debe crear una clase que extienda de org.wso2.carbon.user.core.jdbc.JDBCUserStoreManager, para este ejemplo he creado la clase CustomUserStoreManager.java

package org.wso2.custom.user.store.manager;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasypt.util.password.StrongPasswordEncryptor;
import org.wso2.carbon.CarbonConstants;
import org.wso2.carbon.user.api.Properties;
import org.wso2.carbon.utils.Secret;
import org.wso2.carbon.user.api.Property;
import org.wso2.carbon.user.core.UserRealm;
import org.wso2.carbon.user.core.UserStoreException;
import org.wso2.carbon.user.core.claim.ClaimManager;
import org.wso2.carbon.user.core.jdbc.JDBCRealmConstants;
import org.wso2.carbon.user.core.jdbc.JDBCUserStoreManager;
import org.wso2.carbon.user.core.profile.ProfileConfigurationManager;
import org.wso2.carbon.user.core.util.DatabaseUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
import java.util.Map;


public class CustomUserStoreManager extends JDBCUserStoreManager {


    private static Log log = LogFactory.getLog(CustomUserStoreManager.class);

    public CustomUserStoreManager() {
    }

    public CustomUserStoreManager(org.wso2.carbon.user.api.RealmConfiguration realmConfig,
                                  Map<String, Object> properties,
                                  ClaimManager claimManager,
                                  ProfileConfigurationManager profileManager,
                                  UserRealm realm, Integer tenantId)
            throws UserStoreException {
        super(realmConfig, properties, claimManager, profileManager, realm, tenantId, false);
    }

    @Override
    public boolean doAuthenticate(String userName, Object credential) throws UserStoreException {

        if (CarbonConstants.REGISTRY_ANONNYMOUS_USERNAME.equals(userName)) {
            log.error("Anonymous user trying to login");
            return false;
        }

        Connection dbConnection = null;
        ResultSet rs = null;
        PreparedStatement prepStmt = null;
        String sqlstmt = null;
        String password = String.copyValueOf(((Secret) credential).getChars());
		String storedPassword="";
        boolean isAuthed = false;

        try {
            dbConnection = getDBConnection();
            dbConnection.setAutoCommit(false);
            //paring the SELECT_USER_SQL from user_mgt.xml
            sqlstmt = realmConfig.getUserStoreProperty(JDBCRealmConstants.SELECT_USER);

            if (log.isDebugEnabled()) {
                log.debug(sqlstmt);
            }

            prepStmt = dbConnection.prepareStatement(sqlstmt);
            prepStmt.setString(1, userName);

            rs = prepStmt.executeQuery();

            if (rs.next()) {
                storedPassword = rs.getString("password");
                if ((storedPassword != null) && (storedPassword.trim().equals(password))) {
                    isAuthed = true;
                }

            }
        } catch (SQLException e) {
            throw new UserStoreException("Authentication Failure. Using sql :" + sqlstmt + " " + password + " " + storedPassword);
        } finally {
            DatabaseUtil.closeAllConnections(dbConnection, rs, prepStmt);
        }

        if (log.isDebugEnabled()) {
            log.debug("User " + userName + " login attempt. Login success :: " + isAuthed);
        }

        return isAuthed;

    }

    @Override
    public Date getPasswordExpirationTime(String userName) throws UserStoreException {
        return null;
    }

    protected boolean isValueExisting(String sqlStmt, Connection dbConnection, Object... params)
            throws UserStoreException {
        PreparedStatement prepStmt = null;
        ResultSet rs = null;
        boolean isExisting = false;
        boolean doClose = false;
        try {
            if (dbConnection == null) {
                dbConnection = getDBConnection();
                doClose = true; //because we created it
            }
            if (DatabaseUtil.getStringValuesFromDatabase(dbConnection, sqlStmt, params).length > 0) {
                isExisting = true;
            }
            return isExisting;
        } catch (SQLException e) {
            log.error(e.getMessage(), e);
            log.error("Using sql : " + sqlStmt);
            throw new UserStoreException(e.getMessage(), e);
        } finally {
            if (doClose) {
                DatabaseUtil.closeAllConnections(dbConnection, rs, prepStmt);
            }
        }
    }

    public String[] getUserListFromProperties(String property, String value, String profileName)
            throws UserStoreException {
        return new String[0];
    }


    /*@Override
    public Map<String, String> doGetUserClaimValues(String userName, String[] claims,
                                                    String domainName) throws UserStoreException {
        return new HashMap<String, String>();
    }*/

    /*@Override
    public String doGetUserClaimValue(String userName, String claim, String profileName)
            throws UserStoreException {
        return null;
    }*/

    @Override
    public boolean isReadOnly() throws UserStoreException {
        return true;
    }

    @Override
    public void doAddUser(String userName, Object credential, String[] roleList,
                          Map<String, String> claims, String profileName,
                          boolean requirePasswordChange) throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    public void doAddRole(String roleName, String[] userList, org.wso2.carbon.user.api.Permission[] permissions)
            throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    @Override
    public void doDeleteRole(String roleName) throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    @Override
    public void doDeleteUser(String userName) throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    @Override
    public boolean isBulkImportSupported() {
        return false;
    }

    @Override
    public void doUpdateRoleName(String roleName, String newRoleName) throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    @Override
    public void doUpdateUserListOfRole(String roleName, String[] deletedUsers, String[] newUsers)
            throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    @Override
    public void doUpdateRoleListOfUser(String userName, String[] deletedRoles, String[] newRoles)
            throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    @Override
    public void doSetUserClaimValue(String userName, String claimURI, String claimValue,
                                    String profileName) throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    @Override
    public void doSetUserClaimValues(String userName, Map<String, String> claims,
                                     String profileName) throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    @Override
    public void doDeleteUserClaimValue(String userName, String claimURI, String profileName)
            throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    @Override
    public void doDeleteUserClaimValues(String userName, String[] claims, String profileName)
            throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    @Override
    public void doUpdateCredential(String userName, Object newCredential, Object oldCredential)
            throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    @Override
    public void doUpdateCredentialByAdmin(String userName, Object newCredential)
            throws UserStoreException {
        throw new UserStoreException(
                "User store is operating in read only mode. Cannot write into the user store.");
    }

    public String[] getExternalRoleListOfUser(String userName) throws UserStoreException {
        return new String[0];
    }

    @Override
    public String[] doGetRoleNames(String filter, int maxItemLimit) throws UserStoreException {
        return new String[0];
    }

    @Override
    public boolean doCheckExistingRole(String roleName) throws UserStoreException {

        return false;
    }

    @Override
    public boolean doCheckExistingUser(String userName) throws UserStoreException {

        return true;
    }

    @Override
    public org.wso2.carbon.user.api.Properties getDefaultUserStoreProperties(){
        Properties properties = new Properties();
        properties.setMandatoryProperties(CustomUserStoreConstants.CUSTOM_UM_MANDATORY_PROPERTIES.toArray
                (new Property[CustomUserStoreConstants.CUSTOM_UM_MANDATORY_PROPERTIES.size()]));
        properties.setOptionalProperties(CustomUserStoreConstants.CUSTOM_UM_OPTIONAL_PROPERTIES.toArray
                (new Property[CustomUserStoreConstants.CUSTOM_UM_OPTIONAL_PROPERTIES.size()]));
        properties.setAdvancedProperties(CustomUserStoreConstants.CUSTOM_UM_ADVANCED_PROPERTIES.toArray
                (new Property[CustomUserStoreConstants.CUSTOM_UM_ADVANCED_PROPERTIES.size()]));
        return properties;
    }
}

5.En la clase CustomUserStoreConstants.java se puede establecer la configuración obligatoria, opcional y avanzada de la siguiente manera:

NOTA: Aquí deberá completar los valores correspondientes para acceder a la base de datos donde creó la tabla USERS (p.ej. “Driver Name, URL, user, password”).

package org.wso2.custom.user.store.manager;


import org.wso2.carbon.user.api.Property;
import org.wso2.carbon.user.core.UserStoreConfigConstants;
import org.wso2.carbon.user.core.jdbc.JDBCRealmConstants;

import java.util.ArrayList;

public class CustomUserStoreConstants {


    
    public static final ArrayList<Property> CUSTOM_UM_MANDATORY_PROPERTIES = new ArrayList<Property>();
    public static final ArrayList<Property> CUSTOM_UM_OPTIONAL_PROPERTIES = new ArrayList<Property>();
    public static final ArrayList<Property> CUSTOM_UM_ADVANCED_PROPERTIES = new ArrayList<Property>();


    static {

        setMandatoryProperty(JDBCRealmConstants.DRIVER_NAME, "Driver Name", "", "Full qualified driver name");
        setMandatoryProperty(JDBCRealmConstants.URL,"Connection URL", "", "URL of the user store database");
        setMandatoryProperty(JDBCRealmConstants.USER_NAME, "User Name","", "Username for the database");
        setMandatoryProperty(JDBCRealmConstants.PASSWORD, "Password","", "Password for the database");

        setProperty(UserStoreConfigConstants.disabled,"Disabled", "false", UserStoreConfigConstants.disabledDescription);

        setProperty("ReadOnly","Read Only", "true", "Indicates whether the user store of this realm operates in the user read only mode or not");
        setProperty(UserStoreConfigConstants.SCIMEnabled,"SCIM Enabled", "false", UserStoreConfigConstants.SCIMEnabledDescription);


        //Advanced Properties
        setAdvancedProperty("SelectUserSQL","Select User SQL", "SELECT * FROM USERS WHERE USERNAME=?", "");
        setAdvancedProperty("UserFilterSQL","User Filter SQL", "SELECT USERNAME FROM USERS WHERE USERNAME LIKE ? ORDER BY ID", "");

    }


    private static void setProperty(String name, String displayName, String value, String description) {
        Property property = new Property(name, value, displayName + "#" +description, null);
        CUSTOM_UM_OPTIONAL_PROPERTIES.add(property);

    }

    private static void setMandatoryProperty(String name, String displayName, String value, String description) {
        Property property = new Property(name, value, displayName + "#" +description, null);
        CUSTOM_UM_MANDATORY_PROPERTIES.add(property);

    }

    private static void setAdvancedProperty(String name, String displayName, String value, String description) {
        Property property = new Property(name, value, displayName + "#" +description, null);
        CUSTOM_UM_ADVANCED_PROPERTIES.add(property);

    }

}

6.Clase para registrar Custom User Store Manager en OSGI framework

package org.wso2.custom.user.store.manager.internal;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
import org.wso2.carbon.user.api.UserStoreManager;
import org.wso2.carbon.user.core.service.RealmService;
import org.wso2.custom.user.store.manager.CustomUserStoreManager;

/**
 * @scr.component name="custom.user.store.manager.dscomponent" immediate=true
 * @scr.reference name="user.realmservice.default"
 * interface="org.wso2.carbon.user.core.service.RealmService"
 * cardinality="1..1" policy="dynamic" bind="setRealmService"
 * unbind="unsetRealmService"
 */
public class CustomUserStoreMgtDSComponent {
    private static Log log = LogFactory.getLog(CustomUserStoreMgtDSComponent.class);
    private static RealmService realmService;

    protected void activate(ComponentContext ctxt) {

        CustomUserStoreManager customUserStoreManager = new CustomUserStoreManager();
        ctxt.getBundleContext().registerService(UserStoreManager.class.getName(), customUserStoreManager, null);
        log.info("CustomUserStoreManager bundle activated successfully..");
    }

    protected void deactivate(ComponentContext ctxt) {
        if (log.isDebugEnabled()) {
            log.debug("Custom User Store Manager is deactivated ");
        }
    }

    protected void setRealmService(RealmService rlmService) {
          realmService = rlmService;
    }

    protected void unsetRealmService(RealmService realmService) {
        realmService = null;
    }
}

7. Compilar el código del user store personalizado para generar el archivo: org.wso2.custom.user.store.manager-1.0.jar  

8. Posteriormente copiar el archivo org.wso2.custom.user.store.manager-1.0.jar en la carpeta <IS_HOME>/repository/components/dropins

9. Por último, reiniciar el servicio de WSO2 IS.

10. Una vez reiniciado, ingresar a la consola de administración.

11. Dar clic en la opción Add de la sección User Stores.

12. En la lista desplegable de la opción User Store Manager Class se podrá observar que se ha agregado la clase org.wso2.custom.user.store.manager.CustomUserStoreManager


13.   Seleccionar la clase org.wso2.custom.user.store.manager.CustomUserStoreManager

14.   Llenar los datos de conexión hacia la BD creada anteriormente.

15.   En la siguiente ventana se mostrará el user store creado.

16.   Para validar que se han agregado los usuarios, dar clic en la opción List de la sección Users

Conclusiones:

Se ha implementado una extensión en Identity Server para implementar un almacén de usuarios (User Store) personalizado, que utiliza una tabla de usuarios en una base de datos externa a la herramienta.

Esto nos va a permitir realizar tareas de autenticación sobre esos datos a través de los servicios del WSO2 Identity Manager. Contáctanos y te lo contamos todo.