local md5 = require "md5"
local uuid = require "uuid"
require "redis"
local json = require "cjson"
local WrapRedis = require "wrapredis"
local cassandra = require "lua_cassandra"
local wsapi_response = require "wsapi.response"
local wsapi_request = require "wsapi.request"
local LuaConfigurationControl = require "LuaConfigurationControl"
local m_model = require "authfilter.model"
local m_licenses = require "authfilter.licenses"

local Synchronize = require "Synchronize"
local lualdap = Synchronize("lualdap_mutex", function()
	return require ("lualdap")
end)
local m_helpers = require "helpers"

local jwt = require "authfilter.jwt"
local icuregex = require "icu.regex"
local ustring  = require "icu.ustring"

module((...), package.seeall)

local wsapiUtil = require "wsapi.util"
local openid = require "authfilter.openid"

local https = Synchronize("luasec_mutex", function()
	return require ("ssl.https")
end)

--a table with a metatable pointing to itself
local API = {}
API.__index = API

--map that contains filter instances, one for each app that require it
local filterInstances = {}
local redis = nil
local configControl = nil

local MICROSOFT_API_GRAPH_URL_BASE = "https://graph.microsoft.com/"
local MICROSOFT_API_GRAPH_VERSION = "v1.0"

--App status control table for the appReadyFilter
--It validates the configuration received from the config server
app_status = {
	actualStatus = "waiting",

	CheckDependencies = function (newConfig, checkList)
		for _, dep in ipairs (checkList) do
			if newConfig[dep] == nil or newConfig:DiffState(dep) == "deleted" then
				LogError("[AUTHFILTER.app_status.CheckDependencies] Dependency '%s' is not met", dep)
				app_status.actualStatus = "invalid"
				error("Unmet dependency "..dep)
			end
		end
	end,

	--Actually this is the only function called from the appReadyFilter
	CheckStatus = function ()
		local hasChanged, newConfig = configControl:ConfigurationHasChanged()
		if not hasChanged and app_status.actualStatus ~= "invalid" then
			return app_status.actualStatus
		end

		newConfig = newConfig or app_status.current
		app_status.CheckDependencies(newConfig, {"redis", "cassandra"})

		app_status.current = newConfig

		LogInfo("[AUTHFILTER.app_status.CheckStatus] Connecting to Redis on %s:%s", newConfig["redis"]["host"], newConfig["redis"]["port"])
		redis = WrapRedis(newConfig["redis"]["host"], tonumber(newConfig["redis"]["port"]), newConfig["redis"]["pwd"])

		for key, value, state in newConfig["cassandra"]:childs() do
			if state ~= "deleted" then
				LogInfo("[AUTHFILTER.app_status.CheckStatus] Connecting to cassandra on %s:%s", value.host, tostring(value.port))
				cassandra.seed(value.host, value.port)
			end
		end

		API:SetDatasource(cassandra, redis)

		app_status.actualStatus = "ready"
		return "ready"
	end
}

--[[----------------------------------------------------------
Funciones Locales
--------------------------------------------------------------]]--

--[[
]]--
local function IsUserFullSystemAccess (user)
	--LogDebug("[AUTHFILTER:IsUserFullSystemAccess] --> user: '%s.", json.encode(user or {}))
	if not user then
		return false
	end
	local isSuperUser = m_helpers.utils.IsUserFullSystemAccess({
		Id        = user.id,
		VCC       = user.virtualContactCenter,
		UserGroup = user.usergroup,
		VccGroup  = user.groupVCC
	})
	--LogDebug("[AUTHFILTER:IsUserFullSystemAccess] userId: '%s' isSuperUser: '%s'.", user.id, isSuperUser)
	return isSuperUser
end

-- END Funciones locales ---------------------------------------------------------


--[[------------------------------------
	MIGRATIONS (VCC ALLEGRO to NATIVE)

	Atencion la Key de redis debe de coincidir con la especificada en:
		repo   : agent
		app    : provisioning
		archivo : \inconcert\apps\provisioning\controllers\migrations\init.lua
		funcion: m_running.MakeKey
--------------------------------------]]
local m_migration = {}

--[[

]]--
m_migration.IsRunningCurrentVcc = function(vcc)
	assert(type(vcc) == "string")
	local runningInfo, encodedCurrentRunning = m_migration.Get()
	--LogDebug("[AUTHFILTER.migration:IsRunningCurrentVcc] 1------------> encodedCurrentRunning: '%s'", encodedCurrentRunning or "NADA")
	if not runningInfo then
		local result = "NO MIGRATION IS RUNNING FOR ANY VCC"
		--LogDebug("[AUTHFILTER.migration:IsRunningCurrentVcc] 2------------ %s", result)
		return false, result
	end
	if vcc:lower() == (runningInfo.vcc or ""):lower() then
		local result = "THE CURRENT VCC IS RUNNING"
		--LogDebug("[AUTHFILTER.migration:IsRunningCurrentVcc] 3------------ %s", result)
		return true, result
	end
	local result = "THE CURRENT VCC IS RUNNING"
	--LogDebug("[AUTHFILTER.migration:IsRunningCurrentVcc] 4------------ %s", result)
	return false, result
end

--[[
	Atencion la Key de redis debe de coincidir con la especificada en:
		repo   : agent
		app    : provisioning
		archvo : \inconcert\apps\provisioning\controllers\migrations\init.lua
		funcion: m_running.MakeKey
]]--
m_migration.MakeKey = function()
	local key = "Migration:Allegro:VCC:Status"
	--LogDebug("[AUTHFILTER.migration:MakeKey]------------> key: '%s'", key)
	return key
end

--[[

]]--
m_migration.Get = function()
	local key = m_migration.MakeKey()
	local encodedCurrentRunning = redis:get(key)
	local ok, runningInfo = pcall(json.decode, encodedCurrentRunning)
	--LogDebug("[AUTHFILTER.migration:Get]------------> key: '%s', encodedCurrentRunning: '%s'", key, encodedCurrentRunning)
	return runningInfo, encodedCurrentRunning
end

---END MIGRATION-----------------------

---------------------------------------
-- API
---------------------------------------

function API:IsUserFullSystemAccess (user)
	return IsUserFullSystemAccess(user)
end

---
-- Takes sessionId and removes it from redis
-- The operation is idempotent
--
function API:RevokeSession (sessionId)
	LogDebug("[AUTHFILTER.API:RevokeSession] Revoking sessionId %s from app %s", sessionId, self.appName)

	--remove session from redis
	local encodedUser = redis:hget(self.USER_BY_SID, sessionId)

	if encodedUser then
		--convendria hacer esto en un transaction pipelinesco
		redis:hdel(self.USER_BY_SID, sessionId)
		--LogDebug("encodedUser: %s", encodedUser)
		local user = json.decode(encodedUser)
		local sessionType = user.sessionData and user.sessionData.type or "WEB"
		LogDebug("[AUTHFILTER.API:RevokeSession] SessionId %s was bound to user %s", sessionId, user.id)
		redis:hdel(self.SID_BY_USER, user.id)
		redis:hdel(self.NOTIFICATIONS_BY_USER, user.id)
		redis:hdel(self.APPS_BY_USER(user.id), self.appName)
		redis:zrem(self.AUTH_TOKEN(sessionType, user.virtualContactCenter), sessionId)

		m_licenses.IncreaseVCCAvailableLicenses(self, user, user.virtualContactCenter, sessionId)
	else
		LogWarning("[AUTHFILTER.API:RevokeSession] SessionId %s not found", sessionId)
	end
end


---
-- Takes userAtVcc and removes all sessions
-- The operation is idempotent
-- If excludeSession is specified, it will not be revoked
--
function API:RevokeAllSessions (userAtVcc, excludeSession)
	LogDebug("[AUTHFILTER.API:RevokeAllSessions] Revoking all sessions from user %s", userAtVcc)

	--convendria hacer esto dentro de un transaction pipelinesco

	local allSessions = redis:hgetall(self.APPS_BY_USER(userAtVcc))

	for app, sessionId in pairs(allSessions or {}) do
		if sessionId == excludeSession then
			LogDebug("[AUTHFILTER.API:RevokeAllSessions] Excluding revocation of session %s from app %s", sessionId, app)
		else
			LogDebug("[AUTHFILTER.API:RevokeAllSessions] Revoking session %s from app %s", sessionId, app)
			redis:hdel("authFilter:userBySID:"..app, sessionId)
			redis:hdel("authFilter:SIDByUser:"..app, userAtVcc)
			redis:hdel(self.APPS_BY_USER(userAtVcc), app)
		end
	end
end

--
-- Method used to revoke all credentials from friend apps
-- that are found in the current web parameter
--
function API:RevokeFriendCredentials (web)
	LogDebug("[AUTHFILTER.API:RevokeFriendCredentials] Revoking friend credentials from AuthFilter")
	for _, app in ipairs(self.currentFilterOptions.acceptCredentialsFrom) do
		if app ~= self.appName then
			local sessionId = web.cookies["AUTH_SID_"..app]
			if sessionId then
				LogDebug("[AUTHFILTER.API:RevokeFriendCredentials] SessionId %s found for app %s", sessionId, app)
				local user = redis:hget("authFilter:userBySID:"..app, sessionId)
				if (user) then
					user = json.decode(user)
					LogDebug("[AUTHFILTER.API:RevokeFriendCredentials] Revoking session %s from app %s, bound to user %s", sessionId, app, user.id)
					redis:hdel("authFilter:userBySID:"..app, sessionId)
					redis:hdel("authFilter:SIDByUser:"..app, user.id)
				end
			end
		end
	end
end

--
-- Creates a unique token with expiration time
-- This token will be used to perform a hash login
--
function API:getNonceToken (web, expire, ksAgent)
	local username = web.input.username
	local vcc = web.input.callcenter

	local validUser, err, pwdExpirationDays, hasChangePasswordPermission, userInfo = IsUserValid(username, vcc, ksAgent)
	if not validUser then
		return false, err
	end
	if expire == nil or expire <= 0 then
		expire = 10
	end

	local token = uuid.new():gsub("%-", "")
	LogInfo("[AUTHFILTER.API:getNonceToken] Authfilter: created nonce token %s for app %s", token, self.appName)

	redis:set(self.NONCE_TOKEN(token), token)
	redis:expire(self.NONCE_TOKEN(token), expire)

	return token, err, pwdExpirationDays, hasChangePasswordPermission, userInfo
end

--
-- This function will generate a one time token to make requests on behalf ot the user
-- this token must be used by internal apps, it is not intended to use externally
--
function API:GetOneTimeRequestToken (username, vcc)
	username = username:lower()
	vcc = vcc:lower()

	local user = m_model.GetUserFromCassandra(username, vcc)

	if not user then
		LogWarning("[AUTHFILTER.API:GetOneTimeRequestToken] user %s@%s not found", username, vcc)
		return nil
	end

	local token = uuid.new():gsub("%-", "")
	LogInfo("[AUTHFILTER.API:GetOneTimeRequestToken] created one time request token %s for user %s@%s in app %s", token, username, vcc, self.appName)

	redis:set(self.ONE_TIME_TOKEN(token), json.encode(user))
	redis:expire(self.ONE_TIME_TOKEN(token), 10)

	return token
end

function API:GetUserFromToken (sessionId)
	local user_data = redis:hget(self.USER_BY_SID, sessionId)
	if user_data then
		user_data = json.decode(user_data)
		return {
			userName = user_data.username,
			virtualContactCenter = user_data.virtualContactCenter,
			authToken = sessionId,
			languageId = user_data.languageId,
			sessionData = user_data.sessionData
		}
	end
	return nil
end

function API:GetTokenFromUser (user, vcc)
	return redis:hget(self.SID_BY_USER, user.."@"..vcc)
end

--
-- return an array with all valid sessions for the specified vcc
--
function API:GetAllSessions (vcc, pFilterUserFn, excludeSuperUsers)
	if type(excludeSuperUsers) == "boolean" then
		excludeSuperUsers = excludeSuperUsers
	else
		excludeSuperUsers = true -- por defecto se excluyen
	end

	LogDebug("[AUTHFILTER.API:GetAllSessions] Loading sessions for VCC '%s', pFilterUserFn: %s, excludeSuperUsers: %s.", vcc, pFilterUserFn, excludeSuperUsers)

	pFilterUserFn = pFilterUserFn or function (user)
		return true
	end

	--[[
		Aca se filtran los superUsuarios (por defecto)
	]]--
	local function FilterUser (user)
		--LogDebug("[AUTHFILTER.API:GetAllSessions] --> FilterUser VCC '%s', user: %s.", vcc, json.encode(user))
		if excludeSuperUsers then
			local isSuperUser = IsUserFullSystemAccess(user)
			if isSuperUser then
				return false
			end
		end
		return pFilterUserFn(user)
	end

	local sessions = redis:hgetall(self.USER_BY_SID)
	local result = {}
	for sessionId, userData in pairs (sessions or {}) do
		local user = json.decode(userData)
		if user.virtualContactCenter == vcc and FilterUser(user) then
			result[sessionId] = user
		end
	end

	--LogDebug("[AUTHFILTER.API:GetAllSessions] Loaded sessions for VCC '%s'. Sessions: %s.", vcc, json.encode(result))

	return result
end

--
-- returns the domain in which the cookies are set
--
function API:GetCookiesDomain ()
	return self.m_cookies_domain
end

--
-- helper for logout current user
-- if user is not logged in, then this is a NOP
--
function API:Logout (web, redirect)
	LogDebug("[AUTHFILTER.API:Logout] Login Out, AUTH_SID '%s' cookie domain '%s' redirect '%s'.", self.AUTH_SID, self.m_cookies_domain, redirect)
	if web.cookies and web.cookies[self.AUTH_SID] then
		self:RevokeSession(web.cookies[self.AUTH_SID])
		web:delete_cookie(self.AUTH_SID, "/", self.m_cookies_domain)
		--disable frien app authentication for 1 hour
		web:set_cookie(self.NO_FRIEND_AUTH, {
			value = "1", 
			path = "/", 
			domain = self.m_cookies_domain, 
			expires = os.time() + 3600, 
			sameSite = "None", 
			secure = true
		})
	end
	if redirect then
		return web:redirect(redirect)
	end
end

--
-- Create fresh credentials for the user
--
local function CreateCredentials (self, web, user, preemptive)
	local username, vcc = user.username, user.virtualContactCenter
	if preemptive == nil then
		preemptive = true
	end

	LogDebug("[AUTHFILTER.API:CreateCredentials] --> Creating credentials for User '%s@%s', preemptive: '%s' for app '%s'.", username, vcc, sessionId, self.appName)

	local sessionId
	repeat
		--try to see if the user already has a sessionId
		sessionId = redis:hget(self.SID_BY_USER, user.id)

		if sessionId then
			LogDebug("[AUTHFILTER.API:CreateCredentials] User %s@%s already has sessionId %s for app %s", username, vcc, sessionId, self.appName)
			if preemptive then
				--user has a sessionId, invalidate it
				self:RevokeSession(sessionId)
			else
				--only returns a copy of the existing sessionId
				--this implies that eventually two browsers may be authenticated to the same app with same user
				--Caller is responsible for set the cookie
				return sessionId
			end
		end

		--create a new one and try to assign it to him
		sessionId = uuid.new():gsub("%-", "")
	until redis:hsetnx(self.SID_BY_USER, user.id, sessionId)
	
	local sessionType = user.sessionData and user.sessionData.type or "WEB"
	local now = os.time()
	if self.appName == "AGENT" then
		local keyPresence = self.AUTH_TOKEN(sessionType, user.virtualContactCenter)
		redis:zadd(keyPresence, now, sessionId)
	end

	--LogDebug("\n\n\n-------------->%s\n\n\n", json.encode(user))
	redis:hset(self.USER_BY_SID, sessionId, json.encode(user))

	redis:hset(self.APPS_BY_USER(username.."@"..vcc), self.appName, sessionId)

	if user.notificationsData then
		redis:hset(self.NOTIFICATIONS_BY_USER, user.id, json.encode(user.notificationsData))
	end

	if web and web.set_cookie then
		local vHTTP = (web.vars and web.vars.HTTP_X_HTTP_PROTOCOL) or self.HTTP_X_HTTP_PROTOCOL or "https"
		--we are in a response, we can set cookies direct to the client
		local vAUTH_SID = {value = sessionId, path = "/", domain = self.m_cookies_domain}
		if vHTTP == "https" then
			vAUTH_SID.sameSite = "None"
			vAUTH_SID.secure = true
		end
		web:set_cookie(self.AUTH_SID, vAUTH_SID)
	else
		--we are in an incoming request, we can not set cookies here
	end
	LogDebug("[AUTHFILTER.API:CreateCredentials] User %s@%s authenticated with sessionId %s", username, vcc, sessionId)

	return sessionId
end

-- Validates credentials
-- NOTA: Esto es un toque por allegro para tener mensaje de error descriptivo
-- Por eso copio la de abajo y no me pongo a hacer factor común
function API:ValidateAuthentication (options, ksAgent)
	local username = assert(options.username, "username is missing")
	local vcc = options.vcc --vcc may be inferred by mail address
	local web = options.web

	local password
	local clientHash
	local nonceToken

	if options.hashLogin then
		clientHash = options.hashLogin
		nonceToken = assert(options.nonceToken, "nonce Token is missing")
	else
		password = assert(options.password, "password is missing")
	end

	if options.mail then
		LogDebug("[AUTHFILTER.API:ValidateAuthentication] Validating user with mail %s to app %s", options.mail, self.appName)
		local result, err =	m_model.GetUserByEmail(options.mail, ksAgent)
		if not result then
			LogError("[AUTHFILTER.API:ValidateAuthentication] Error authenticating by mail: %s", err)
			return false, err
		end
		username, vcc = result:match("(.+)@([^@]+)")
	end

	username = username:lower()
	vcc = vcc:lower()

	LogDebug("[AUTHFILTER.API:ValidateAuthentication] Validating user %s@%s to app %s", username, vcc, self.appName)

	local user, hashedPassword = m_model.GetUserFromCassandra(username, vcc, ksAgent)

	if not user then
		LogDebug("[AUTHFILTER.API:ValidateAuthentication] User %s@%s not found", username, vcc)
		return false, "invalid username or password" --same error as if the user or password where wrong for security reasons
	end

	if not user.IsFullSystemAccess and user.Status == "0" then
		LogDebug("[AUTHFILTER.API:ValidateAuthentication] User %s@%s is not enabled", username, vcc)
		return false, "user not enabled"
	end

	if options.allegro then
		--validates user from allegro
		local result = options.allegro.generic.AuthenticateUser(vcc, username, clientHash)
		if not result or result.result ~= "OK" then
			local reason = result and result.reason or "unexpected error"
			LogDebug("[AUTHFILTER.API:ValidateAuthentication] Error authenticating user %s@%s in app %s through allegro: %s", username, vcc, self.appName, reason)
			return false, reason
		end
	elseif options.hashLogin then
		--validate nonce token
		local token = redis:get(self.NONCE_TOKEN(nonceToken))
		if token ~= nonceToken then
			LogDebug("[AUTHFILTER.API:ValidateAuthentication] Invalid nonce token for user %s@%s in app %s: %s", username, vcc, self.appName, token)
			return false, "invalid username or password"
		end

		--hash the nonce and the hashed password
		local serverHashedToken = md5.sumhexa(hashedPassword..token)

		if serverHashedToken ~= clientHash then
			LogDebug("[AUTHFILTER.API:ValidateAuthentication] Invalid login hash for user %s@%s in app %s: %s", username, vcc, self.appName, clientHash)
			return false, "invalid username or password"
		end

	else --plain login
		local hash = md5.sumhexa(username..password)

		if hashedPassword ~= hash then
			LogDebug("[AUTHFILTER.API:ValidateAuthentication] Invalid username or password for user %s@%s", username, vcc)
			return false, "invalid username or password"
		end
	end

	--correctly authenticated
	return true
end

--
-- Process user session before to set it
--

local function processSessionNotificationsData(sessionType, user)
	if not user.sessionData.notifications then
		return nil
	end
	return {
		vcc = user.sessionData.virtualContactCenter,
		sessionType = sessionType,
		platform = user.sessionData.platform or "WEB",
		languageId = user.languageId,
		data = user.sessionData.notifications
	}
end

local m_processSessionType = {
	MOBILE = function(user)
		user.sessionData.automaticPollerDisabled = true
		return user
	end
}

local function processSessionUser(user, sessionData, appsPermissions)
	user.AppsPermissions = appsPermissions
	user.sessionData = sessionData
	local sessionType = user.sessionData.type or "WEB"
	if m_processSessionType[sessionType] then
		user = m_processSessionType[sessionType](user)
	end
	user.notificationsData = processSessionNotificationsData(sessionType, user)
	return user
end


local function SplitString(inputstr, sep)
    if sep == nil then
            sep = "%s"
    end
    local t={} ; i=1
    for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
            t[i] = str
            i = i + 1
    end
    return t
end

--Obtiene el RDN (Relative Distinguish Name)m de un DN (Distinguish Name)
function getRDN(DN)
	if type(DN) == "string" then
		local dnParts = SplitString(DN, ",")
		return dnParts and #dnParts > 0 and dnParts[1]
	end
	return ""
end

-- Obtiene el valor de un RDN
function getCN(RDN)
	if type(RDN) == "string" then
		local cnParts = SplitString(RDN, "=")
		if cnParts and #cnParts > 1 then 
			return cnParts[2]
		end
	end
	return ""
end

local function GetLdapConnection(server, adminUserDN, adminPassword, useTLS)
	if not server or not adminUserDN or not adminPassword then
		LogError("[AUTHFILTER.API:GetLdapConnection]  - missing parameters")
		return nil, "missing parameters"
	end
	useTLS = useTLS or false
	local who = adminUserDN
	LogDebug("[AUTHFILTER.API:GetLdapConnection] - Connecting to LDAP server '%s' - useTLS:%s", server, useTLS)
	local ld, ld_error = lualdap.open_simple (server, who, adminPassword, useTLS)
	local isAD = false;
	if ld then
		for dn, attribs in ld:search { base = "", filter = "(objectclass=*)", scope = "base", attrs = "forestFunctionality", attrsonly = true } do
			if attribs.forestFunctionality then
				isAD = true
			end
		end
	else
		LogError("[AUTHFILTER.API:GetLdapConnection]  - Connection to LDAP Server %s failed:'%s'", server, ld_error)
	end
	return ld, ld_error, isAD
end

local function getLdapGroupAndMemberIds(ld, baseDN, groupDN)
	local result = {}
	local rdn = getRDN(groupDN)
	if rdn then
		local cn = getCN(rdn)
		if cn then
			local searchConfig = {
				base = baseDN, 
				filter = rdn,
				scope = "subtree"
			}
			for dn, attribs in ld:search(searchConfig) do
				if attribs and attribs.cn == cn then
					-- LDAP membership through group id
					result.gidNumber = attribs and attribs.gidNumber
					-- LDAP membership through member uid: contains just one uid or a table of uid
					result.memberUid = attribs and attribs.memberUid
					-- AD membership: contains just one DN or a table of DN
					result.members = attribs and attribs.member
				end
			end
		end
	end
	return result
end

local function AuthenticateUserOnLdap (ldap, userGroup, username, password)
	local ld, ld_error, isAD = GetLdapConnection(ldap.server, ldap.adminUserDN, ldap.adminPassword, ldap.useTLS)
	if not ld then
		return ld_error
	end
	--Comprobacion de que el usuario pertenece al grupo indicado en el server LDAP
	local groupDN = userGroup and ldap.groupFilters[userGroup] or ldap.groupFilters["allusers"]
	local groupAndMembers = getLdapGroupAndMemberIds(ld, ldap.baseDN, groupDN)
	--Busqueda del usuario en servidor LDAP para obtener su DN, a partir del username de OCC
	local userDN = nil
	local userIdAttributte = ldap.userIdAttributte or (isAD and "sAMAccountName" or "uid")
	local filter = isAD and "(&(objectClass=user)(" .. userIdAttributte .."=" .. username .. "))" or "(&(|(objectClass=posixAccount)(objectClass=inetOrgPerson))(" .. userIdAttributte .. "=" .. username .. "))"
	for dn, userAttribs in ld:search { base = ldap.baseDN, filter = filter, scope = "subtree" } do
		--1. Comprobar si el gidNumber del usuario coincide con el del grupo
		if userAttribs and userAttribs.gidNumber and userAttribs.gidNumber == groupAndMembers.gidNumber then
			userDN = dn
			break
		--2. Comprobar si el memberUid del usuario esta incluido en el grupo			
		elseif userAttribs and userAttribs[userIdAttributte] and groupAndMembers.memberUid then
			if type(groupAndMembers.memberUid) == "table" then
				local memberUidFound = false
				for _, memberUid in ipairs(groupAndMembers.memberUid) do
					if memberUid == userAttribs[userIdAttributte] then
						memberUidFound = true
						break
					end
				end
				if memberUidFound then
					userDN = dn
					break
				end
			elseif groupAndMembers.memberUid == userAttribs[userIdAttributte] then
				userDN = dn
				break
			end
		--3. Comprobar si el dn del usuario esta incluido en el members del grupo
		elseif groupAndMembers.members then
			if type(groupAndMembers.members) == "table" then
				local memberFound = false
				for _, member in ipairs(groupAndMembers.members) do
					if member == dn then
						memberFound = true
						break
					end
				end
				if memberFound then
					userDN = dn
					break
				end
			elseif groupAndMembers.members == dn then
				userDN = dn
				break
			end
		--4.Comprobar si el dn es del usuario y cuelga directamente en el grupo indicado
		else
			dnParts = SplitString(dn, ",")
			if dnParts and #dnParts > 0 then
				local dnWithoutRDN = string.gsub(dn, dnParts[1]..",", "")
				if dnWithoutRDN == groupDN then
					userDN = dn
					break
				end
			end
		end
	end
	--
	if not userDN then
		LogError("[AUTHFILTER.AuthenticateUserOnLdap] - user '%s' not found on groupDN '%s' on LDAP Server '%s'", username, groupDN, ldap.server)
		return "USER NOT FOUND ON LDAP GROUP"
	end
	--Autenticacion del usuario
	ld, ld_error = ld:bind_simple(userDN, password)
	if ld then
		ld:close()
	end
	-- siempre debe de retornar
	return ld_error
end

local function MustAuthenticateUserOnLdap (vcc, ldap, user, password)
	if IsUserFullSystemAccess(user) then
		LogInfo("[AUTHFILTER.MustAuthenticateUserOnLdap] - vcc '%s' has LDAP disabled for SuperUsers", vcc)
		return false -- los super usuarios NO validan LDAP
	end
	if ldap.enabled ~= "1" then
		return false --LDAP not enabled on vcc
	end
	LogInfo("[AUTHFILTER.MustAuthenticateUserOnLdap] - vcc '%s' has LDAP enabled", vcc)
	local ld_error = AuthenticateUserOnLdap(ldap, user.usergroup, user.username, password)
	return true, ld_error --LDAP enabled on vcc, ldap error (if any)
end

function API:GetLdapGroups(server, baseDN, adminUserDN, adminPassword, useTLS)
	getLeafName = function(dn, data)
		return data and (data.description or data.cn or data.dc or data.ou) or dn or "No Name"
	end

	local result = {}
	local ld, ld_error, isAD = GetLdapConnection(server, adminUserDN, adminPassword, useTLS)
	if ld then
		result.success = true
		result.result = {}

		local objectClass = isAD and "group" or "posixGroup"
		local groupFilter = "(|(objectClass=" .. objectClass .. ")(objectClass=organizationalUnit))"

		for dn, attribs in ld:search { base = baseDN, filter = groupFilter, scope = "subtree" } do
			result.result[dn] = getLeafName(dn, attribs)
		end
	else
		result.success = false
		result.error = ld_error
	end
 
	return result
end

function GetLdapUsers (ld, isAD, baseDN, groupDN)
	local result = {
		success = true,
		result = {}
	}

	--1. Users directly created under groupDN
	local searchConfig = {
		base = groupDN, 
		filter = isAD and "objectClass=user" or "(|(objectClass=posixAccount)(objectClass=Account)(objectClass=inetOrgPerson))", 
		scope = "onelevel"
	}
	for dn, attribs in ld:search(searchConfig) do
		result.result[dn] = attribs
	end

	--2. Users sharing gidNumber with groupDN
	local groupAndMembers = getLdapGroupAndMemberIds(ld, baseDN, groupDN)
	if groupAndMembers.gidNumber then
		local userFilter = isAD and "(&(objectClass=user)(gidNumber=" .. groupAndMembers.gidNumber .."))" or "(&(objectClass=posixAccount)(gidNumber=" .. groupAndMembers.gidNumber .."))"
		for dn, attribs in ld:search { base = baseDN, filter = userFilter, scope = "subtree" } do
			result.result[dn] = result.result[dn] or attribs
		end		
	end

	--3. Users defined by memberUid attribute on groupDN
	local userIdAttributte = isAD and "sAMAccountName" or "uid"
	if groupAndMembers.memberUid then
		if type(groupAndMembers.memberUid) == "table" then
			for _, memberUid in ipairs(groupAndMembers.memberUid) do
				local userFilter = isAD and "(&(objectClass=user)(" .. userIdAttributte .."=" .. memberUid .. "))" or "(&(objectClass=posixAccount)(" .. userIdAttributte .."=" .. memberUid .. "))"
				for dn, attribs in ld:search { base = baseDN, filter = userFilter, scope = "subtree" } do
					result.result[dn] = result.result[dn] or attribs
				end
			end
		else
			local userFilter = isAD and "(&(objectClass=user)(" .. userIdAttributte .."=" .. groupAndMembers.memberUid .. "))" or "(&(objectClass=posixAccount)(" .. userIdAttributte .."=" .. groupAndMembers.memberUid .. "))"
			for dn, attribs in ld:search { base = baseDN, filter = userFilter, scope = "subtree" } do
				result.result[dn] = result.result[dn] or attribs
			end
		end
	end

	--3. Only in AD: Users defined by member attribute on groupDN
	if isAD then
		if groupAndMembers.members then
			if type(groupAndMembers.members) == "table" then
				for _, member in ipairs(groupAndMembers.members) do
					for dn, attribs in ld:search { base = member, scope = "base" } do
						result.result[dn] = result.result[dn] or attribs
					end
				end
			else
				for dn, attribs in ld:search { base = groupAndMembers.members, scope = "base" } do
					result.result[dn] = result.result[dn] or attribs
				end
			end
		end
	end
 
	return result
end

--
-- Returns true if vcc has external authentication
--
function API:HasExternalAuth(vccInfo)
	local auth_type = vccInfo.Security and vccInfo.Security.authentication and vccInfo.Security.authentication.type or "BASIC"
	--TODO: El codigo que sigue deberia estar en cassandra, en funcion del auth_type, para que este metodo sea generico
	if auth_type ~= "MFA" and auth_type ~= "BASIC" then
		auth_type = auth_type:lower()
		local ldap = vccInfo.Security.ldap or {}
		return tonumber(vccInfo.Security[auth_type].enabled) == 1
	end

	return false
end

--
-- Returns auth_type if vcc has external authentication. Otherwise returns nil
--
function API:ExternalAuthType(vccInfo)
	local auth_type = vccInfo.Security and vccInfo.Security.authentication and vccInfo.Security.authentication.type or "BASIC"
	--TODO: El codigo que sigue deberia estar en cassandra, en funcion del auth_type, para que este metodo sea generico
	if auth_type ~= "MFA" and auth_type ~= "BASIC" then
		return tonumber(vccInfo.Security[auth_type:lower()].enabled) == 1 and auth_type or nil
	end
end

--
-- Returns true if vcc has external users synchronization
--
function API:HasExternalUsersSync(vccInfo)
	if not API:HasExternalAuth(vccInfo) then
		return false
	end
	return vccInfo.Security[vccInfo.Security.authentication.type:lower()].sync_enabled
end

--
-- Returns sync_method if vcc has external users synchronization. Otherwise returns nil
--
function API:ExternalSyncMethod(vccInfo)
	return API:HasExternalAuth(vccInfo) and vccInfo.Security[vccInfo.Security.authentication.type:lower()].sync_method or nil
end

--- Email regexes table
local emails_regexp = {
	parse    = icuregex.compile([[^\s*("<[^<>]*>"\s+|[^<>]*\s+)?(<[^<>\s]*>|[^<>\s]*)$]]),
	validate = icuregex.compile([[^((([a-z]|\d|[\\!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[\\!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$]], "i")
}

--- Validate email address against regex.
function ValidateEmail (address)
	if type(address) == "string" then address = ustring(address) end
	for icumatch in icuregex.gmatch(emails_regexp.validate, address) do
		return true
	end
	return false
end

function GetLdapUsersHandler (vccInfo)
	local ldap = vccInfo.Security.ldap or {}
	local server = ldap.server
	local adminUserDN = ldap.adminUserDN
	local adminPassword = ldap.adminPassword
	local useTLS = ldap.useTLS
	local groupFilters = ldap.groupFilters or {}
	local baseDN = ldap.baseDN
	
	--Para procesar los grupos en un orden que haga que si un usuario existe en varios grupos se quede en el gupo de mas permisos
	local systemGroups = {
		administrator = 1,
		businessmanager = 2,
		securitymanager = 3,
		dbamanagment = 4,
		qualityauditor = 5,
		designer = 6,
		supervisor = 7,
		agent = 8
	}
	local sortedGroups = {}
	local next_index = 9
	for groupName, groupDn in pairs(groupFilters) do
		local groupIndex = systemGroups[groupName] or next_index
		next_index = systemGroups[groupName] and next_index or next_index + 1
		table.insert(sortedGroups, {groupName = groupName, groupDn = groupDn, groupIndex = groupIndex})
	end
	table.sort(sortedGroups, function (k1, k2)
		return k1 and k2 and type(k1) == "table" and type(k2) == "table" and k1.groupIndex and k2.groupIndex and k1.groupIndex < k2.groupIndex or false
	end)

	-- Default attribute values 
	local defaultValues = {
		TimeZoneId = ldap.timezone,
		LanguageId = ldap.language,
		countryCode = ldap.country,
		areaCode = ldap.area,
		DefaultCampaign = ldap.default_campaign,
		initialState = ldap.initial_state,
		Password = ""
	}

	local occUsers = {}

	local ld, ld_error, isAD = GetLdapConnection(server, adminUserDN, adminPassword, useTLS)
	if ld then
		local userIdAttributte = ldap.userIdAttributte or (isAD and "sAMAccountName" or "uid")
		local totalServerUsers = 0
		-- Un usuario puede pertenecer en LDAP a varios grupos -> Hay que ir agregando los usuarios que se van procesando a una tabla para no volverlos a procesar en otro grupo
		local alreadyProcessedUsers = {}

		--for groupName, groupDn in pairs(groupFilters) do
		for _, group in ipairs(sortedGroups) do
			local groupName = group.groupName
			local groupDn = group.groupDn
			local groupResult = GetLdapUsers(ld, isAD, baseDN, groupDn)
			if groupResult.success then
				local ldapUsers = groupResult.result or {}
				local totalGroupUsers = 0
				for ldapUserDn, ldapUser in pairs(ldapUsers) do
					local userId  = ldapUser[userIdAttributte]
					if userId then
						userId = userId:lower()
						if not alreadyProcessedUsers[userId] then
							alreadyProcessedUsers[userId] = true
							totalGroupUsers = totalGroupUsers + 1
							local occUser = {}
							occUser.Id = userId
							occUser.Name = ldapUser.givenName or ldapUser.cn or ldapUser.description or occUser.Id
							occUser.FirstSurname = ldapUser.sn or occUser.Name
							occUser.UserGroup = groupName
							occUser.VCC = vccInfo.Id:lower()
							occUser.VccGroup = "system"
							occUser.Status = "1"
							occUser.Mail = ldapUser.mail and type(ldapUser.mail) == "string" and ValidateEmail(ldapUser.mail) and ldapUser.mail or userId.."@"..vccInfo.Id..".com"
							occUser.groups = {}
							local assignedCampaign = {
								id = ldap.default_campaign,
								role = groupName,
								invite = false
							}
							table.insert(occUser.groups, assignedCampaign)

							local occUserKey = occUser.Id .. "@" .. occUser.VCC
							occUsers[occUserKey] = occUser
						end
					else
						LogWarning("[AUTHFILTER.GetLdapUsersHandler] - vcc '%s' - userId not found for user '%s' on external server '%s'", vccInfo.Id, ldapUserDn, server)
					end
				end
				LogDebug("[AUTHFILTER.GetLdapUsersHandler] - vcc '%s' - %d users got from group '%s' on external server '%s'", vccInfo.Id, totalGroupUsers, groupName, server)
				totalServerUsers = totalServerUsers + totalGroupUsers
			else
				LogWarning("[AUTHFILTER.GetLdapUsersHandler] - vcc '%s' - get users from group '%s' on external server '%s' failed", vccInfo.Id, groupName, server)
			end
		end
		LogDebug("[AUTHFILTER.GetLdapUsersHandler] - vcc '%s' external users synchronization got %d users from external server '%s'", vccInfo.Id, totalServerUsers, server)
	else
		LogWarning("[AUTHFILTER.GetLdapUsersHandler] - external server '%s' connection error:'%s'", server, ld_error)
	end
	return occUsers, defaultValues
end

function GetAzureAdAccessToken (url, client_id, client_secret)
	local body = string.format([[client_id=%s&client_secret=%s&grant_type=%s&scope=%s]], client_id, client_secret, "client_credentials", "https://graph.microsoft.com/.default")
	local headers = {}
	headers["content-length"] = string.len(body)
	local response_body = {}
	local result, code, headers, statusline = https.request{
		url = url,
		method = "POST",
		headers = headers,
		source = ltn12.source.string(body),
		sink = ltn12.sink.table(response_body)
	}
	local response = json.decode(table.concat(response_body, ""))
	return response
end

function API:GetOpenIdConfiguration(openid_configuration_url)
	local result = { result = {} }
	local openid_configuration_values = {}
	local res, code, headers, statusline = https.request{
		url = openid_configuration_url,
		method = "GET",
		sink = ltn12.sink.table(openid_configuration_values)
	}
	if code ~= 200 then
		LogWarning("[AUTHFILTER.GetOpenIdConfiguration] - error: '%s'", statusline)
		result.success = false
		result.error = statusline
		return result
	end
	result.result = json.decode(table.concat(openid_configuration_values, ""))
	if result.result.error then
		LogWarning("[AUTHFILTER.GetOpenIdConfiguration] - error: '%s'", response.error)
		result.success = false
		result.error = result.result.error
		result.result = {}
		return result
	end
	result.success = true
	return result
end

function API:GetAzureADGroups(openid_configuration_url, client_id, client_secret)
	local result = { result = {} }
	local openid_configuration_values = {}
	local res, code, headers, statusline = https.request{
		url = openid_configuration_url,
		method = "GET",
		sink = ltn12.sink.table(openid_configuration_values)
	}
	if code ~= 200 then
		LogWarning("[AUTHFILTER.GetAzureADGroups] - access token request error: '%s'", statusline)
		result.success = false
		result.error = statusline
		return result
	end
	local response = json.decode(table.concat(openid_configuration_values, ""))
	if response.error then
		LogWarning("[AUTHFILTER.GetAzureADGroups] - access token request error: '%s'", response.error)
		result.success = false
		result.error = response.error
		return result
	end

	local token_endpoint = response.token_endpoint
	response = GetAzureAdAccessToken(token_endpoint, client_id, client_secret)	
	if response.error then
		LogWarning("[AUTHFILTER.GetAzureADGroups] - access token request error: '%s' - '%s'", response.error, response.error_description)
		result.success = false
		result.error = response.error
		return result
	end

	local url = MICROSOFT_API_GRAPH_URL_BASE .. MICROSOFT_API_GRAPH_VERSION .. "/servicePrincipals(appId='{" .. client_id .. "}')/appRoleAssignedTo"
	local headers = {}
	headers["Authorization"] = response.token_type .. " " .. response.access_token
	
	local groups = {}
	local more_data = true
	while more_data do
		local response_body = {}
		res, code, response_headers, statusline = https.request{
			url = url,
			method = "GET",
			headers = headers,
			sink = ltn12.sink.table(response_body)
		}
		if code ~= 200 then
			LogWarning("[AUTHFILTER.GetAzureADGroups] - Azure AD groups request returned error:'%s'", statusline)
			result.success = false
			result.error = statusline
			result.result = {}
			return result
		end
		response = json.decode(table.concat(response_body, ""))
		if response.error then
			LogWarning("[AUTHFILTER.GetAzureADGroups] - Azure AD groups request returned error: '%s'", response.error.message)
			result.success = false
			result.error = response.error.message
			result.result = {}
			return result
		end
		for _, group in pairs(response.value) do			
			if group.principalType == "Group" and not groups[group.principalId] then
				groups[group.principalId] = group.principalDisplayName
			end			
		end

		more_data = response["@odata.nextLink"] and true or false
		if more_data then
			url = response["@odata.nextLink"]
		end
	end

	for groupId, groupName in pairs(groups) do
		table.insert(result.result, { id = groupId, displayName = groupName })
	end

	result.success = true
	return result
end

function GetAzureAdGroupMembers (vccId, token_type, access_token, groupId)
	local url = MICROSOFT_API_GRAPH_URL_BASE .. MICROSOFT_API_GRAPH_VERSION .. "/groups/" .. groupId .. "/members/microsoft.graph.user"
	local headers = {}
	headers["Authorization"] = token_type .. " " .. access_token
	local result = { success = true, members = {}}

	local more_data = true
	while more_data do
		local response_body = {}
		local res, code, response_headers, statusline = https.request{
			url = url,
			method = "GET",
			headers = headers,
			sink = ltn12.sink.table(response_body)
		}
		if code ~= 200 then
			result.success = false
			result.error_code = code
			result.error_message = statusline
			result.members = {}
			return result
		end
		local response = json.decode(table.concat(response_body, ""))
		if response.error then
			result.success = false
			result.error_code = response.error.code
			result.error_message = response.error.message
			result.members = {}
			return result
		end
		for _, member in pairs(response.value) do
			table.insert(result.members, member)
		end

		more_data = response["@odata.nextLink"] and true or false
		if more_data then
			url = response["@odata.nextLink"]
		end
	end
	return result
end

function GetAzureAdUsersHandler(vccInfo)
	local occUsers = {}
	local defaultValues = {}

	local openid = vccInfo.Security.openid or {}
	local client_id = openid and openid.client_id or ""
	local client_secret = openid and openid.adminPassword or ""
	local token_endpoint = openid and openid.openid_configuration_values and openid.openid_configuration_values.token_endpoint or ""
	local groupFilters = openid.groupFilters or {}
	local response = GetAzureAdAccessToken(token_endpoint, client_id, client_secret)	
	if response.error then
		LogWarning("[AUTHFILTER.GetAzureAdUsersHandler] - VCC '%s' - access token request error: '%s' - '%s'", vccInfo.Id, response.error, response.error_description)
		return occUsers, defaultValues
	end
	local openid_configuration_url = openid and openid.openid_configuration_url or nil
	if not openid_configuration_url then
		LogWarning("[AUTHFILTER.GetAzureAdUsersHandler] - VCC '%s' - openid_configuration_url not found", vccInfo.Id)
		return occUsers, defaultValues
	end
	--Obtenemos los grupos actualmente existentes en Azure AD, para deshabilitar los usuarios que pertenezcan a grupos que ya no existan en Azure AD
	local azureAppGroupsResult = API:GetAzureADGroups(openid_configuration_url, client_id, client_secret)
	if not azureAppGroupsResult or not azureAppGroupsResult.success then
		LogWarning("[AUTHFILTER.GetAzureAdUsersHandler] - VCC '%s' - Azure AD Groups request error:'%s'", vccInfo.Id, azureAppGroupsResult.error or "")
		return occUsers, defaultValues
	end
	local azureAppGroups = {}
	for _, azureAppGroup in pairs(azureAppGroupsResult.result) do
		azureAppGroups[azureAppGroup.id] = azureAppGroup.displayName
	end
	
	--Para procesar los grupos en un orden que haga que si un usuario existe en varios grupos se quede en el gupo de mas permisos
	local systemGroups = {
		administrator = 1,
		businessmanager = 2,
		securitymanager = 3,
		dbamanagment = 4,
		qualityauditor = 5,
		designer = 6,
		supervisor = 7,
		agent = 8
	}
	local sortedGroups = {}
	local next_index = 9
	for groupName, groupInfo in pairs(groupFilters) do
		local groupIndex = systemGroups[groupName] or next_index
		next_index = systemGroups[groupName] and next_index or next_index + 1
		table.insert(sortedGroups, {groupName = groupName, groupId = groupInfo.id, groupIndex = groupIndex})
	end
	table.sort(sortedGroups, function (k1, k2)
		return k1 and k2 and type(k1) == "table" and type(k2) == "table" and k1.groupIndex and k2.groupIndex and k1.groupIndex < k2.groupIndex or false
	end)

	-- Default attribute values 
	defaultValues.TimeZoneId = openid.timezone
	defaultValues.LanguageId = openid.language
	defaultValues.countryCode = openid.country
	defaultValues.areaCode = openid.area
	defaultValues.DefaultCampaign = openid.default_campaign
	defaultValues.initialState = openid.initial_state
	defaultValues.Password = ""

	local totalServerUsers = 0
	-- Un usuario puede pertenecer en Azure AD a varios grupos -> Hay que ir agregando los usuarios que se van procesando a una tabla para no volverlos a procesar en otro grupo
	local alreadyProcessedUsers = {}

	for _, group in ipairs(sortedGroups) do
		--comprobamos si el grupo sigue existiendo en Azure AD y esta asociado a la app
		if azureAppGroups[group.groupId] then
			local groupResult = GetAzureAdGroupMembers(vccInfo.Id, response.token_type, response.access_token, group.groupId)
			if groupResult.success then
				local totalGroupUsers = 0
				for _, member in pairs(groupResult.members) do
					local userId  = m_helpers.utils:Separate(member.userPrincipalName or "", "@")
					if userId and userId ~= "" then
						userId = userId:lower()
						if not alreadyProcessedUsers[userId] then
							alreadyProcessedUsers[userId] = true
							totalGroupUsers = totalGroupUsers + 1
							local occUser = {}
							occUser.Id = userId
							occUser.Name = member.givenName or occUser.Id
							occUser.FirstSurname = member.surname or occUser.Name
							occUser.UserGroup = group.groupName
							occUser.VCC = vccInfo.Id:lower()
							occUser.VccGroup = "system"
							occUser.Status = "1"
							occUser.Mail = member.mail and type(member.mail) == "string" and ValidateEmail(member.mail) and member.mail or userId.."@"..vccInfo.Id..".com"
							occUser.groups = {}
							local assignedCampaign = {
								id = openid.default_campaign,
								role = group.groupName,
								invite = false
							}
							--Este atributo se debe configurar como notificacion en el token de la app de Azure, con el nombre local_user y el valor userPrincipalName
							occUser.openIdUserId = member.userPrincipalName
							table.insert(occUser.groups, assignedCampaign)

							local occUserKey = occUser.Id .. "@" .. occUser.VCC
							occUsers[occUserKey] = occUser
						end
					else
						LogWarning("[AUTHFILTER.GetAzureAdUsersHandler] - vcc '%s' - invalid userPrincipalName '%s'", vccInfo.Id, member.userPrincipalName)
					end			
				end
				LogDebug("[AUTHFILTER.GetAzureAdUsersHandler] - vcc '%s' - %d users got from group '%s'", vccInfo.Id, totalGroupUsers, group.groupName)
				totalServerUsers = totalServerUsers + totalGroupUsers
			else
				LogWarning("[AUTHFILTER.GetAzureAdUsersHandler] - vcc '%s' - get users from group '%s' failed: %s - %s", vccInfo.Id, group.groupName, groupResult.error_code, groupResult.error_message)
			end	
		else
			LogWarning("[AUTHFILTER.GetAzureAdUsersHandler] - vcc '%s' - get users from group '%s' failed: group Id '%s' does not exist or does not belong to the application '%s'", vccInfo.Id, group.groupName, group.groupId, client_id)
		end
	end
	LogDebug("[AUTHFILTER.GetAzureAdUsersHandler] - vcc '%s' external users synchronization got %d users", vccInfo.Id, totalServerUsers)

	return occUsers, defaultValues
end

local openIdUsersHandlers = {
	azuread = GetAzureAdUsersHandler
}

function GetOpenIdUsersHandler(vccInfo)
	local openid = vccInfo.Security.openid or {}
	local sync_method = openid and openid.sync_method and openid.sync_method:lower() or ""
	if openIdUsersHandlers[sync_method] then
		return openIdUsersHandlers[sync_method](vccInfo)
	end
	return {}, {}
end

local externalUsersHandlers = {
	ldap = GetLdapUsersHandler,
	openid = GetOpenIdUsersHandler
}

--
-- Syncronizes external users info in cassandra for VCC with external authentication type and active synchronization
--
function API:GetExternalUsers(vccInfo)
	local auth_type = vccInfo.Security.authentication.type:lower()
	local occUsers = {}
	local defaultValues = {}
	if API:HasExternalUsersSync(vccInfo) and externalUsersHandlers[auth_type] then
		--TODO: El codigo de los handlers deberia estar en cassandra, para cargarlo dinamicamenteen funcion del auth_type, de modo que este metodo quede generico
		occUsers, defaultValues = externalUsersHandlers[auth_type](vccInfo)
	end
	return occUsers, defaultValues
end


function API:IsPasswordValid(vccInfo, plainPassword, encriptedPassword, passwordHistory)
	local result = true
	local passwordCheckResults = {}
	if not API:HasExternalAuth(vccInfo) and vccInfo.Security and vccInfo.Security.authentication.passwordChecks then
		local passwordChecks = vccInfo.Security.authentication.passwordChecks

		local pwdLength = plainPassword:len()
		passwordCheckResults["LEN"] = { success = pwdLength >= passwordChecks.minLen and pwdLength <= passwordChecks.maxLen, range = {passwordChecks.minLen, passwordChecks.maxLen} }

		local upperChars = 0
		_, upperChars = string.gsub(plainPassword, "%u", "")
		passwordCheckResults["UPPERS"] = { success = upperChars >= passwordChecks.minUpperChars, range = {passwordChecks.minUpperChars} }
		
		local lowerChars = 0
		_, lowerChars = string.gsub(plainPassword, "%l", "")
		passwordCheckResults["LOWERS"] = { success = lowerChars >= passwordChecks.minLowerChars, range = {passwordChecks.minLowerChars} }

		local numericChars = 0
		_, numericChars = string.gsub(plainPassword, "%d", "") 
		passwordCheckResults["NUMBERS"] = { success = numericChars >= passwordChecks.minNumericChars, range = {passwordChecks.minNumericChars} }

		local symbolChars = 0
		_, symbolChars = string.gsub(plainPassword, "%W", "")
		passwordCheckResults["SYMBOLS"] = { success = symbolChars >= passwordChecks.minSpecialChars, range = {passwordChecks.minSpecialChars} }

		local passwordFoundInHistory = false
		if passwordChecks.history > 0 then
			for _, pwd in pairs(passwordHistory) do
				if pwd == encriptedPassword then
					passwordFoundInHistory = true
				end
			end
		end

		passwordCheckResults["HISTORY"] = { success = not passwordFoundInHistory, range = {passwordChecks.history} }

		result = passwordCheckResults["LEN"].success and passwordCheckResults["UPPERS"].success and passwordCheckResults["LOWERS"].success and passwordCheckResults["NUMBERS"].success and passwordCheckResults["SYMBOLS"].success and passwordCheckResults["HISTORY"].success
	end
	return result, passwordCheckResults
end

--
-- Returns true if user exists on vcc, is enabled, is not locked, his password hasn't expired and must not change his password
-- Otherwise returns false
function IsUserValid (username, vcc, ksAgent)
	username = username:lower()
	vcc = vcc:lower()

	LogInfo("[AUTHFILTER.API:IsUserValid] Getting User '%s@%s' authentication info.", username, vcc)

	-- 1. migracion vcc allegro a nativo: se valida que no exista ninguna migracion/rollback en curso.
	local isRunningMigration, result = m_migration.IsRunningCurrentVcc(vcc)
	if isRunningMigration then
		local err = result
		LogInfo("[AUTHFILTER.API:IsUserValid] Unable to login In vcc: '%s'. The vcc it is in the migration or rollback process (allegro / native). Err: &s ", err)
		return false, "migrating or rollbacking vcc"
	end
	-- 2. vcc/security: se valida que el vcc exista en la BD y que tenga el objeto de seguridad
	local vccInfo = m_model.GetVCC(vcc, ksAgent)
	if not vccInfo or not vccInfo.Security then
		LogInfo("[AUTHFILTER.API:IsUserValid] VCC %s not found", vcc)
		return false, "invalid username"
	end
	-- 3. migracion vcc allegro a nativo: valida si se hizo una migracion anterior y si la misma culmino correctamente;
		-- si la misma ha fallado no permite el login (ej: caida del webhandler o porque lo detuvieron).
		-- para volver a iniciar sesion se debe ejecutar un rollback para subsanar el error
	local ok, allegroIntegration = pcall(json.decode, vccInfo.AllegroIntegration)
	if ok and allegroIntegration then
		local lastMigration = allegroIntegration.LastMigration
		if lastMigration and (lastMigration.status ~= "ENDED" and lastMigration.status ~= "ROLLBACKED") then
			LogInfo("[AUTHFILTER.API:IsUserValid] Unable to login In vcc: '%s'. Allegro vcc was not migrated correctly. You have to rollback the migration to be able to log in.", vcc)
			-- el codigo de error que se retorna es el mismo que en la validacion si existe una migracion en curso 1); esto ya que del lado del cliente debe de visualizar el mismo mensaje.
			return false, "migrating or rollbacking vcc"
		end
	end

	-- for both internal and external vcc authentication user must exists in cassandra and has to be enabled
	local user = m_model.GetUserFromCassandra(username, vcc, ksAgent)
	if not user then
		LogInfo("[AUTHFILTER.API:IsUserValid] User %s@%s not found", username, vcc)
		return false, { message = "invalid username", field_id = "username"}
	end

	if not user.IsFullSystemAccess and user.Status == "0" then
		LogError("[AUTHFILTER.API:IsUserValid] User %s@%s is not enabled", username, vcc)
		return false, "user not enabled"
	end

	-- password restrictions only apply to internal authentication VCCs, excluded system and excluded users without a password change
	vccInfo.Security = json.decode(vccInfo.Security)
	local passwordChecks = vccInfo.Security.authentication.passwordChecks
	if vccInfo.Id ~= "system"
		and not API:HasExternalAuth(vccInfo)
		and passwordChecks
		and (user.PasswordChangeTimestamp or user.MustChangePassword == "1")
	then
		if user.LoginFailedAttemps
			and tonumber(user.LoginFailedAttemps) >= tonumber(passwordChecks.blockOnLoginFailures)
			and tonumber(passwordChecks.blockOnLoginFailures) > 0
		then
			LogInfo("[AUTHFILTER.API:IsUserValid] User %s@%s has no more login retries left", username, vcc)
			return false, "user locked"
		end

		local now = os.time()
		local passwordExpiration = tonumber(passwordChecks.expirationDays) * 24 * 60 * 60
		local passwordExpirationDate = user.PasswordChangeTimestamp and (tonumber(user.PasswordChangeTimestamp) + passwordExpiration) or now
		if passwordExpiration > 0 and passwordExpirationDate < now then
			LogInfo("[AUTHFILTER.API:IsUserValid] User %s@%s password has expired", username, vcc)
			return false, "password expired"
		end

		-- El usuario debe cambiar su password si explicitamente lo tiene configurado
		if user.MustChangePassword == "1" then
			LogInfo("[AUTHFILTER.API:IsUserValid] User %s@%s must change his password", username, vcc)
			-- OK (validUser, err, pwdExpirationDays, hasChangePasswordPermission, userInfo)
			return true, "change password", nil, nil, user
		end

		-- Si estamos en periodo de notificacion de proxima expiracion de password, devolvemos el numero de dias que quedan para que expire
		local timeToExpiration = (passwordExpirationDate - now)
		local daysToExpiration = math.floor(timeToExpiration / (24 * 60 * 60))
		local expirationNotificationDays =  tonumber(passwordChecks.expirationNotificationDays)
		if expirationNotificationDays > 0
			and daysToExpiration <= expirationNotificationDays
		then
			-- OK (validUser, err, pwdExpirationDays, hasChangePasswordPermission, userInfo)
			return true, "password expires", daysToExpiration, user.HasChangePasswordPermission, user
		end
	end

	-- OK (validUser, err, pwdExpirationDays, hasChangePasswordPermission, userInfo)
	return true, nil, nil, nil, user
end

--
-- Authenticate user against cassandra
-- options can be {username = "userId", password = "plainPassword", vcc = "vcc"}
-- or {mail = "email@domain.com", password = "plainPassword"}
-- or {username = "userId", vcc = "vcc", hashLogin = MD5(MD5(username + password) + nonceToken), nonceToken = "nonce token provided by server", jwtToken = "password encoded with jwt"}
-- or {username = "userId", vcc = "vcc", hashLogin = MD5(MD5(username + password) + nonceToken), nonceToken = "nonce token provided by server", jwtToken = "password encoded with jwt", jwtTokenNew = "new password encoded with jwt"}
-- Returns true and sessionId if user was authenticated
-- or false and error description if an error ocurred
-- If user is already authenticated and if mode is preemptive then old credentials are invalidated and a new one is created
-- Otherwise (if not preemptive) same credential is returned
-- Preemptive mode is default true
-- Options may contain a web object
-- In this case, this function also set client cookies
--
function API:AuthenticateUser (options, ksAgent)
	local username = assert(options.username, "username is missing")
	local vcc = options.vcc --vcc may be inferred by mail address
	local web = options.web
	local appsPermissions = options.appsPermissions
	local sessionData = options.sessionData or {}
	local sessionRejoin = options.sessionRejoin
	local password
	local clientHash
	local nonceToken
	local jwtToken = options.jwtToken
	local jwtTokenNew = options.jwtTokenNew
	local hashNew = options.hashNew

	LogInfo("[AUTHFILTER.API:AuthenticateUser] Authenticating User '%s@%s' to App: '%s'. Apps Permissions: '%s'.", username, vcc, self.appName, json.encode(appsPermissions or {}))

	local isRunningMigration, result = m_migration.IsRunningCurrentVcc(vcc)
	if isRunningMigration then
		local err = result
		LogDebug("[AUTHFILTER.API:AuthenticateUser] Unable to login In vcc: '%s'. The vcc it is in the migration or rollback process (allegro / native). Err: &s ", err)
		return false, "migrating or rollbacking vcc"
	end

	if options.hashLogin then
		clientHash = options.hashLogin
		nonceToken = assert(options.nonceToken, "nonce Token is missing")
	else
		password = assert(options.password, "password is missing")
	end

	if options.mail then
		LogDebug("[AUTHFILTER.API:AuthenticateUser] Authenticating user with mail %s to app %s", options.mail, self.appName)
		local result, err =	m_model.GetUserByEmail(options.mail, ksAgent)
		if not result then
			LogError("[AUTHFILTER.API:AuthenticateUser] Error authenticating by mail: %s", err)
			return false, err
		end
		username, vcc = result:match("(.+)@([^@]+)")
	end

	username = username:lower()
	vcc = vcc:lower()

	LogDebug("[AUTHFILTER.API:AuthenticateUser] Authenticating user %s@%s to app %s", username, vcc, self.appName)

	local user, hashedPassword = m_model.GetUserFromCassandra(username, vcc, ksAgent)
	if not user then
		LogDebug("[AUTHFILTER.API:AuthenticateUser] User %s@%s not found", username, vcc)
		return false,
			"invalid username or password", --same error as if the user or password where wrong for security reasons
			{ dontSaveUser = true }			-- dont save user login atemp information if user does not exists
	end

	if not user.IsFullSystemAccess and user.Status == "0" then
		LogDebug("[AUTHFILTER.API:AuthenticateUser] User %s@%s is not enabled", username, vcc)
		return false, "user not enabled"
	end

	user, err = processSessionUser(user, sessionData, appsPermissions)
	if not user then
		return false, err
	end

	local sessionId
	if not sessionRejoin then
		if options.allegro then
			--validates user from allegro
			local result = options.allegro.generic.AuthenticateUser(vcc, username, clientHash)
			if not result or result.result ~= "OK" then
				local reason = result and result.reason or "unexpected error"
				LogDebug("[AUTHFILTER.API:AuthenticateUser] Error authenticating user %s@%s in app %s through allegro: %s", username, vcc, self.appName, reason)
				return false, reason
			end
		elseif options.hashLogin then
			--validate nonce token
			local token = redis:get(self.NONCE_TOKEN(nonceToken))
			if token ~= nonceToken then
				LogDebug("[AUTHFILTER.API:AuthenticateUser] Invalid nonce token for user %s@%s in app %s: %s", username, vcc, self.appName, token)
				return false, "invalid user token"
			end

			local vccInfo = m_model.GetVCC(vcc, ksAgent)
			local vccSecurity = vccInfo and vccInfo.Security and json.decode(vccInfo.Security) or {}
			vccInfo.Security = vccSecurity
			local ldap = vccSecurity and vccSecurity.ldap or {}
			local jwtData, jwtErr
			local hasLdap = false
			local ld_error
			if jwtToken then
				jwtData, jwtErr = jwt.decode(jwtToken, md5.sumhexa(nonceToken), "HS256")	
				if jwtErr then
					LogError("[AUTHFILTER.API:AuthenticateUser] Error decoding jwtToken: '%s'", jwtErr)
					return true, "invalid username or password"
				end
				hasLdap, ld_error = MustAuthenticateUserOnLdap(vcc, ldap, user, jwtData.password)
			end

			if hasLdap then --ldap authentication
				if ld_error then
					LogDebug("[AUTHFILTER.API:AuthenticateUser] LDAP authentication failed for user %s@%s in app %s: %s", username, vcc, self.appName, ld_error)
					return false, "invalid username or password"
				end
				LogDebug("[AUTHFILTER.API:AuthenticateUser] LDAP authentication succeded for user %s@%s in app %s", username, vcc, self.appName)
			elseif web and web.input and web.input.authenticatedByOpenId then
				LogDebug("[AUTHFILTER.API:AuthenticateUser] OpenId authentication succeded for user %s@%s in app %s", username, vcc, self.appName)
			else --local authentication
				--hash the nonce and the hashed password
				local serverHashedToken = md5.sumhexa(hashedPassword..token)
				if serverHashedToken ~= clientHash then
					LogInfo("[AUTHFILTER.API:AuthenticateUser] Invalid login hash for user %s@%s in app %s: %s", username, vcc, self.appName, clientHash)
					return false, "invalid username or password", {IncrLoginFailedAttemps = true}
				end

				-- check password expiration
				if not user.IsFullSystemAccess then
					local passwordChecks = vccInfo.Security.authentication.passwordChecks
					if passwordChecks then
						local now = os.time()
						local passwordExpiration = tonumber(passwordChecks.expirationDays) * 24 * 60 * 60
						local passwordExpirationDate = user.PasswordChangeTimestamp and (tonumber(user.PasswordChangeTimestamp) + passwordExpiration) or now
						if passwordExpiration > 0 and passwordExpirationDate < now then
							LogInfo("[AUTHFILTER.API:AuthenticateUser] Password has expired for user %s@%s in app %s", username, vcc, self.appName)
							return false, "invalid username or password, contact your administrator"
						end
						--check user blocked on login failures
						if tonumber(passwordChecks.blockOnLoginFailures) > 0 and user.LoginFailedAttemps and tonumber(user.LoginFailedAttemps) >= tonumber(passwordChecks.blockOnLoginFailures) then
							LogInfo("[AUTHFILTER.API:AuthenticateUser] User %s@%s in app %s bloqued due to blockOnLoginFailures has been exceeded", username, vcc, self.appName)
							return false, "invalid username or password, contact your administrator"
						end
					else
						LogWarning("[AUTHFILTER.API:AuthenticateUser] Invalid password checks configuration in vcc %s.", vcc)
					end
				else
					LogInfo("[AUTHFILTER.API:AuthenticateUser] Disabled password checks for SuperUsers.")
				end
				-- user must change password, and this is the new one
				-- jwtTokenNew is the jwt encrypted new password
				-- hashNew is the md5 encrypted new password
				if jwtTokenNew and hashNew then
					jwtData, jwtErr = jwt.decode(jwtTokenNew, md5.sumhexa(nonceToken), "HS256")	
					if jwtErr then
						LogError("[AUTHFILTER.API:AuthenticateUser] Error decoding jwtToken: '%s'", jwtErr)
						return false, "invalid username or password"
					end

					local userPasswordHistory = user.PasswordHistory
					if not userPasswordHistory or #userPasswordHistory == 0  then
						userPasswordHistory = {}
						table.insert(userPasswordHistory, serverHashedToken)
					end
					local newPasswordIsValid, newPasswordErr = API:IsPasswordValid(vccInfo, jwtData.password, hashNew, userPasswordHistory)
					if not newPasswordIsValid then
						LogError("[AUTHFILTER.API:AuthenticateUser] New password error: '%s'", json.encode(newPasswordErr))
						return false, "new password error", newPasswordErr
					end
				end
			end
			--delete nonce
			redis:del(self.NONCE_TOKEN(nonceToken))
		else --plain login
			local hash = md5.sumhexa(username..password)

			if hashedPassword ~= hash then
				LogDebug("[AUTHFILTER.API:AuthenticateUser] Invalid username or password for user %s@%s", username, vcc)
				return false, "invalid username or password"
			end
		end

		--Delete NO_FRIEND_AUTH cookie to allow take credentials from friend apps
		if web and web.delete_cookie then
			web:delete_cookie(self.NO_FRIEND_AUTH, "/", self.m_cookies_domain)
		end

		--correctly authenticated

		local preemptive = true
		if options.preemptive == false then
			preemptive = false
		end

		--create credentials
		sessionId = CreateCredentials(self, web, user, preemptive)
		m_licenses.DecreaseVCCAvailableLicenses(self, user, vcc, sessionId)
	else
		sessionId = sessionRejoin

		local encodedUser = redis:hget(self.USER_BY_SID, sessionId)
		if encodedUser then
			local user = json.decode(encodedUser)
			user.sessionRejoin = sessionRejoin
			redis:hset(self.USER_BY_SID, sessionId, json.encode(user))
		end

		if web and web.set_cookie then
			local vHTTP = (web.vars and web.vars.HTTP_X_HTTP_PROTOCOL) or self.HTTP_X_HTTP_PROTOCOL or "https"
			local vAUTH_SID = {value = sessionId, path = "/", domain = self.m_cookies_domain}
			if vHTTP == "https" then
				vAUTH_SID.sameSite = "None" 
				vAUTH_SID.secure = true
			end
			--we are in a response, we can set cookies direct to the client
			web:set_cookie(self.AUTH_SID, vAUTH_SID)
		end
	end

	-- Make this token available so the presence filter can use to establish presence on the first request
	-- made to an application.
	inconcert.authUser = {
		authToken = sessionId,
		virtualContactCenter = vcc,
		sessionData = user.sessionData
	}
	return true, sessionId
end

-- Check friend apps credentials
local function CheckFriendAppsCredentials (web, self, acceptCredentialsFrom)
	for _, app in ipairs(acceptCredentialsFrom) do
		local sessionCookie = web.cookies["AUTH_SID_"..app]
		if sessionCookie then
			LogDebug("[AUTHFILTER.API:CheckFriendAppsCredentials] Session cookie %s found for friend app %s", sessionCookie, app)

			local user = redis:hget("authFilter:userBySID:"..app, sessionCookie)

			if user then
				LogDebug("[AUTHFILTER.API:CheckFriendAppsCredentials] Credentials are valid for friend app %s", app)
				return json.decode(user)
			end
		end
	end
	return nil
end

function API:GetLoggedUserInfo (userId, vcc, sessionId)
	assert(userId)
	assert(vcc)

	local fullUserId = userId .. "@" .. vcc

	--LogInfo("[AUTHFILTER.API:GetLoggedUserInfo] Retrieving Logged User '%s' SessionId '%s' Information.", fullUserId, sessionId)

	local ok, userSessionId
	if sessionId then
		ok = true
		userSessionId = sessionId
	else
		-- Si la session Id NO llega como parametro la tengo que ir a buscar a redis según el Id de Usuario.
		ok, userSessionId = pcall(redis.hget, self, self.SID_BY_USER, fullUserId)
	end

	local userInfo = {}
	local encodedUser

	if ok then
		local ok, info = pcall(redis.hget, self, self.USER_BY_SID, userSessionId)
		if ok then
			encodedUser = info
			local ok, user = pcall(json.decode, encodedUser)
			if ok then
				userInfo = user
			else
				local err = user
				LogWarning("[AUTHFILTER.API:GetLoggedUserInfo] Error Decoding UserInfo for User '%s' key: '%s', SessionId: '%s'. Err: '%s'.", fullUserId, self.SID_BY_USER, userSessionId, err)
			end
		else
			local err = info
			LogWarning("[AUTHFILTER.API:GetLoggedUserInfo] Error retrieving UserInfo for User '%s' key: '%s', SessionId: '%s'. Err: '%s'.", fullUserId, self.SID_BY_USER, userSessionId, err)
		end
	else
		local err = userSessionId
		LogWarning("[AUTHFILTER.API:GetLoggedUserInfo] Error retrieving SessionId for User '%s' key: '%s'. Err: '%s'.", fullUserId, self.SID_BY_USER, err)
	end

	-- LogInfo("[AUTHFILTER.API:GetLoggedUserInfo] Retrieved Logged User '%s' SessionId '%s' Information. Info: '%s'", fullUserId, userSessionId, encodedUser)

	return userInfo
end

function API:SetDatasource (cassandra, redis)
	assert(cassandra)
	assert(redis)
	m_model.SetDatasource(cassandra, redis, API)
end

function API:CleanUpVccAvailableLicenses (vcc)
	assert(vcc)
	m_licenses.CleanUpVccAvailableLicenses(self, vcc)
end

--[[
	Consume a license available for an agent
]]--
function API:DecreaseVCCAvailableLicenses (options)
	assert(type(options) == "table")
	assert(type(options.vcc) == "string")

	local user = options.user
	local sessionId = self:GetTokenFromUser(options.username, options.vcc)
	if not user then
		local encodedUser = redis:hget(self.USER_BY_SID, sessionId)
		if not encodedUser then
			return false
		end
		user = json.decode(encodedUser)
	end

	return m_licenses.DecreaseVCCAvailableLicenses(self, user, options.vcc, sessionId)
end

--
-- detecto si me pasaron la tabla web de Orbit o la tabla web de WSAPI
--
local function GetWeb (wsapi_env)
	if type(wsapi_env.cookies) == "table" then
		return wsapi_env
	else
		return wsapi_request.new(wsapi_env)
	end
end

---
-- Takes an http request environment and returns the bound session and the session_id or nil.
--
-- @param wsapi_env is either 'wsapi_env' as supplied by WSAPI or a Orbit request.
--
local function GetSession (web, self)
	local sessionCookie = web.cookies[self.AUTH_SID]
	if not sessionCookie then
		return nil, "No session cookie found"
	end
	local user = redis:hget(self.USER_BY_SID, sessionCookie)
	if not user then
		return nil, ("session %s not found"):format(sessionCookie)
	end
	return json.decode(user), sessionCookie
end


--
-- Calls next layer in filters architecture
-- returns a wrapped web response wich can be used to set or delete cookies
--
local function CallNextLayer (appToFilter, wsapi_env)
	local status, headers, iterator
	if type(appToFilter) == "table" and type(appToFilter["run"]) == "function" then
		status, headers, iterator = appToFilter.run(wsapi_env)
	else
		status, headers, iterator = appToFilter(wsapi_env)
	end
	--save old content length for posterior use
	local old_content_length = headers["Content-Length"]

	local resp = wsapi_response.new(status, headers)

	local oldFinish = resp.finish

	resp.finish = function (self)
		local new_status, new_headers = oldFinish(resp)
		--restore real content length
		new_headers["Content-Length"] = old_content_length
		return new_status, new_headers, iterator
	end

	return resp
end


--
-- Maps information about the current authenticated user
--
local function MapSessionEnvironment (filter, wsapi_env, user, session_token)
	-- inconcert.authUser tiene los datos relacionados al usuario actual
	-- for backward compatibility only
	inconcert.authUser = {
		userName = user.username,
		userGroup = user.usergroup,
		groupVCC = user.groupVCC,
		virtualContactCenter = user.virtualContactCenter,
		authToken = session_token,
		languageId = user.languageId,
		codepage = user.codepage,
		timezone = user.timezone,
		sessionData = user.sessionData,
		sessionRejoin = user.sessionRejoin,
		vccPermissions = user.vccPermissions,
		GetHttpCredentials = function(self)
			return filter.AUTH_SID.."=" .. session_token	-- devuelvo solo el cookie SID
		end,
		DisplayDate = function(self, date, format)
			assert(type(self) == "table", "self must be inconcert.authUser, so call this function like this: inconcert.authUser:DisplayDate(date)")
			return date:toDifferentTimezoneString(self.timezone, format)
		end,
		SetLanguage = function(self, languageId)
			if type(languageId) == "number" then
				user.languageId = languageId
			elseif type(languageId) == "string" then
				user.languageId = i18nlib.LCID[languageId]
			else
				error("language id must be numeric or string")
			end
		end,
		SetTimezone = function(self, timezone)
			assert(type(timezone) == "table")
			user.timezone = timezone
		end
	}

	--newer apps need to access this table
	--which can be found in web.vars.auth
	wsapi_env.auth = inconcert.authUser
end

--
-- Get username and vcc from orbit's web table
--
function API:GetUserAndVCC (web)
	if web.vars and web.vars.auth then
		return web.vars.auth.userName, web.vars.auth.virtualContactCenter
	else
		LogInfo("[AUTHFILTER.API:GetUserAndVCC] No authentication info available in request for method AuthFilter:GetUserAndVCC() in app %s", self.appName)
		return nil, nil
	end
end


---
-- Unmaps the session. It undoes the changes made by MapSessionEnvironment
-- @param wsapi_env
--
local function UnmapSessionEnvironment (wsapi_env)
	inconcert.authUser = nil
	wsapi_env.auth = nil
end

local function MatchAny (url, rules)
	--LogDebug("[AUTHFILTER.MatchAny] 1---> url: '%s', rules: '%s'.", url, json.encode(rules or {}))
	for _, rule in ipairs(rules or {}) do
		if url:match(rule) == url then
			--LogDebug("[AUTHFILTER.MatchAny] 2--->  YES excluded url: '%s', match with: '%s'.", url, rule)
			return true
		end
	end
	--LogDebug("[AUTHFILTER.MatchAny] 3--->  NO exclude url: '%s'.", url)
	return false
end

--[[
	Función encargada de validar si la App tiene permisos para acceder a la URL.
	Esta validación se ha realizado para validar permisos contra las URLs del WebDesigner (en tiempo de diseño), ya que esta App, no tiene acceso al código de permisos que se encuentra en los LUAS del webhandler.
	Para las demás Apps del Webhandler (agent, administration, etc) la validación se realiza en el mismo controller de la App.
]]--
local function ValidateAppPermission (appName, user_data, web)
	--LogDebug("[AUTHFILTER.ValidateAppPermission] ---> appName: '%s', user_data: '%s', web: '%s'.", appName, user_data, web)

	if not user_data or not user_data.AppsPermissions or not appName then
		-- Si tiene permisos
		--
		-- Por aca no deberia pasar, pero si llega a suceder retorno con permisos en TRUE, para que todo continue operando como siempre.
		LogDebug("[AUTHFILTER.ValidateAppPermission] 1- has permissions  %s", appName)
		return true
	end

	local appsDefinitions = {
		WEBDESIGNER = function()
			local desingerPermissions = {
				designer = "webdesigner_designer",
				runtime  = "webdesigner_runtime" -- la validación "runtime", no se esta utilizando pero queda acá como referencia y por si en algún momento es necesaria su implementacion
			}
			if (web.method or ""):lower() ~= "get" then
				--LogDebug("[AUTHFILTER.ValidateAppPermission] 1 ---> appName: '%s' hasPermission = %s.", appName, true)
				return true
			end
			local queryParams = web.GET
			local pathInfo = (web.path_info or ""):lower()
			--LogDebug("[AUTHFILTER.ValidateAppPermission] -------> appName: '%s', pathInfo: '%s', queryParams: '%s'.", appName, pathInfo, json.encode(queryParams))
			local isDesignerApp = pathInfo == "/" or pathInfo == "/designer"
			if isDesignerApp then
				local hasPermission = user_data.AppsPermissions[desingerPermissions.designer]
				LogDebug("[AUTHFILTER.ValidateAppPermission] 2 ---> appName: '%s' hasPermission = %s, queryParams: %s.", appName, hasPermission, json.encode(queryParams or {}))
				return hasPermission
			end
			--LogDebug("[AUTHFILTER.ValidateAppPermission] 3 ---> appName: '%s' hasPermission = %s.", appName, true)
			return true
		end
	}

	appName = appName:upper()
	local appDef = appsDefinitions[appName]
	if not appDef then
		-- Si tiene permisos
		--
		-- Por aca pasa si no existe una validacion de acceso a Apps, entonces retorno con permisos en TRUE, para que todo continue operando como siempre.
		return true
	end
	-- Aca si realizo la validacion contra la definicion de la App (por el momento solo el WebDesginer en modo Designer - no runtime)
	local hasPermission = appDef()
	if not hasPermission then
		LogWarning("[AUTHFILTER.ValidateAppPermission] You do not have permissions to use the application '%s'", appName)
		return false
	end

	-- Si tiene permisos
	return true
end

---
-- The filter itself
---
local function Filter (self, appToFilter, options)
	return function(wsapi_env)
		local bypassFilter = MatchAny(wsapi_env.PATH_INFO, options.excludedUrls)
		--these options may be different for each instance of the filter
		self.currentFilterOptions = options

		if bypassFilter then
			--LogDebug("[AUTHFILTER.Filter] Url %s from app %s is excluded from auth filter. It can continue.", wsapi_env.PATH_INFO, self.appName)
			local resp = CallNextLayer(appToFilter, wsapi_env)
			UnmapSessionEnvironment(wsapi_env)
			return resp:finish()
		end

		if options.appDefinedCredentialFn then
			local user_data, session_token = options.appDefinedCredentialFn(wsapi_env)
			if user_data and session_token then
				LogInfo("[AUTHFILTER.Filter] Authentication with custom credentials for app %s and url %s", self.appName, wsapi_env.PATH_INFO)

				assert(user_data.userName, "appDefinedCredentialFn must supply user_data.userName")
				assert(user_data.virtualContactCenter, "appDefinedCredentialFn must supply user_data.virtualContactCenter")

				user_data.username = user_data.userName
				user_data.usergroup = user_data.userGroup
				user_data.id = user_data.username.."@"..user_data.virtualContactCenter

				MapSessionEnvironment(self, wsapi_env, user_data, session_token)
				local resp = CallNextLayer(appToFilter, wsapi_env)
				UnmapSessionEnvironment (wsapi_env)
				return resp:finish()
			end
		end

		-- Check if the user is authenticated
		local web = GetWeb(wsapi_env)
		local user_data, session_token = GetSession(web, self)
		--LogDebug("[AUTHFILTER.Filter] 1 user_data '%s', session_token '%s'", user_data, session_token)

		-- Check if it's allowed to authenticate with friend apps credentials
		local authFromFriend = true
		if web.cookies[self.NO_FRIEND_AUTH] then
			LogInfo("[AUTHFILTER.Filter] Authentication from friend app is disabled for app %s", self.appName)
			authFromFriend = false
		end

		local setCookie = false
		-- If not session found for this app
		-- If it's allowed then verify friend apps credentials
		if not user_data and authFromFriend then
			user_data = CheckFriendAppsCredentials(web, self, options.acceptCredentialsFrom)
			if user_data then
				-- A friend app has valid credentials, use it to obtain credentials for this app
				session_token = CreateCredentials(self, web, user_data, false) --non preemptive
				setCookie = true
			end
		end

		--LogDebug("[AUTHFILTER.Filter] 2 user_data %s", user_data)

		-- maybe it's a one time token, let's give a chance
		if not user_data and web.cookies[self.AUTH_SID] then
			session_token = web.cookies[self.AUTH_SID]
			local data = redis:get(self.ONE_TIME_TOKEN(session_token))
			if data then
				user_data = json.decode(data)
				LogInfo("[AUTHFILTER.Filter] Authentication for user %s using OneTimeToken", user_data.id)
				redis:del(self.ONE_TIME_TOKEN(session_token))
			else
				session_token = nil
			end
		end

		--LogDebug("[AUTHFILTER.Filter] 3 user_data %s", json.encode(user_data or {}))

		local function ReturnAccessDenied ()
			LogInfo("[AUTHFILTER.Filter] Access forbidden with no authentication for url %s from app %s", wsapi_env.PATH_INFO, self.appName)
			local wsapiResponse
			if not options.renderLoginFn then
				wsapiResponse = wsapi_response.new(403, {["Content-Type"] = "text/plain"})
				wsapiResponse:write("forbidden")
				wsapiResponse:delete_cookie(self.AUTH_SID, "/", self.m_cookies_domain)
			else
				local status, headers, body = options.renderLoginFn(wsapi_env)
				wsapiResponse = wsapi_response.new(status, headers)
				if type(body) == "function" then
					local s = body()
					while s do
						wsapiResponse:write(s)
						s = body()
					end
				else
					wsapiResponse:write(body)
				end
			end
			wsapiResponse.status = 403
			wsapiResponse:delete_cookie(self.AUTH_SID, "/", self.m_cookies_domain)
			return wsapiResponse:finish()
		end

		local isOptional = MatchAny(wsapi_env.PATH_INFO, options.optionalUrls)
		if not user_data and not isOptional then
			return ReturnAccessDenied()
		elseif not user_data then
			LogInfo("[AUTHFILTER.Filter] User is not authenticated but authentication is optional for url %s from app %s", wsapi_env.PATH_INFO, self.appName)
			UnmapSessionEnvironment (wsapi_env)
			return CallNextLayer(appToFilter, wsapi_env):finish()
		else
			--[[
				Valido permisos sobre las URLs.
				Por el momento solo WebDesigner en modo diseño.
			]]--
			local hasPermission = ValidateAppPermission(self.appName, user_data, web)
			if not hasPermission then
				-- no tiene permisos
				return ReturnAccessDenied()
			end

			MapSessionEnvironment(self, wsapi_env, user_data, session_token)
			local resp = CallNextLayer(appToFilter, wsapi_env)
			UnmapSessionEnvironment (wsapi_env)

			if setCookie then
				local vHTTP = (web.vars and web.vars.HTTP_X_HTTP_PROTOCOL) or self.HTTP_X_HTTP_PROTOCOL or "https"
				-- set authentication cookie
				local vAUTH_SID = {value = session_token, path = "/", domain = self.m_cookies_domain}
				if vHTTP == "https" then
					vAUTH_SID.sameSite = "None"
					vAUTH_SID.secure = true
				end
				resp:set_cookie(self.AUTH_SID, vAUTH_SID)
			end

			return resp:finish()
		end
	end
end

--
-- Initialize config client for authfilter
--
function API:InitializeConfigClient (configServerData)
	configControl = LuaConfigurationControl.Client {
		type = "AUTHFILTER",
		address = configServerData.ip,
		port = configServerData.port,
		node_address = configServerData.myIp,
		node_port = configServerData.myPort
	}
	configControl:TriggerNodeReady()
end


-----------------------------------------------------------
-- When you are inside config.lua preparing your filters --
-- you should call this method to install the authFilter --
-----------------------------------------------------------
function API:makeFilter (options)
	assert(type(options) == "table")
	assert(type(options.base_domain) == "string" or type(options.base_domain) == "nil", "Invalid value for options.cookies_domain")
	self.m_cookies_domain = options.base_domain

	--LogDebug("[AUTHFILTER.API:makeFilter] self.m_cookies_domain '%s'", self.m_cookies_domain)

	if self.m_cookies_domain == "" then
		self.m_cookies_domain = nil
	end

	
	if self.m_cookies_domain then
		-- does it resemble an ipv4 address ?
		if self.m_cookies_domain:match("%d+%.%d+%.%d+%.%d+") then
			error("Invalid value for options.cookies_domain. It MUST NOT be an IP address.")
		end
		self.m_cookies_domain = "." .. self.m_cookies_domain
		--LogDebug("[AUTHFILTER.API:makeFilter] serving cookies with domain '%s'", self.m_cookies_domain)
	else
		LogWarning("[AUTHFILTER.API:makeFilter] serving cookies with no domain")
	end

	options.acceptCredentialsFrom = options.acceptCredentialsFrom or {}
	for i, app in ipairs(options.acceptCredentialsFrom) do
		options.acceptCredentialsFrom[i] = app:upper()
	end

	return function(appToFilter)
		return Filter(self, appToFilter, options)
	end
end

--
-- Set a redis key to save username and openId params
-- This key will be used to perform a later user authentication validation on OpenIdValidateAuthentication
-- Update vccSecurityInfo.openid to contains only an authorization_endpoint where the user must enter his credentials
--
function API:OpenIdSetAuthEndPoint (web, occToken, expireTokenSec, vccSecurityInfo)
	-- Check vccSecurityInfo contains a valid OpenId config. Otherwise returns without changes
	if not(web and web.input and web.input.username and web.input.callcenter and occToken and type(vccSecurityInfo) == "table" and vccSecurityInfo.authentication and vccSecurityInfo.authentication.type == "OPENID" and type(vccSecurityInfo.openid) == "table" and vccSecurityInfo.openid.enabled == "1" and vccSecurityInfo.openid.client_id and vccSecurityInfo.openid.openid_configuration_values and vccSecurityInfo.openid.openid_configuration_values.authorization_endpoint) then
		LogWarning("[AUTHFILTER.API:OpenIdSetAuthEndPoint] - invalid params")
		return
	end

	local username = web.input.username
	local vcc = web.input.callcenter
	local httpProtocol =  web.vars and web.vars.HTTP_X_HTTP_PROTOCOL or "https"
	local redirect_uri = httpProtocol .. "://" .. inconcert.environment.apps.agent.base_domain .. "/inconcert/apps/agent/login/openid/validate_authentication/"
	local encoded_redirect_uri = wsapiUtil.url_encode(redirect_uri)

	local state = uuid.new()
	local nonce = uuid.new()
	local redisKey = "OpenIdState:"..state
	redis:set(redisKey, json.encode({
		username = username,
		vcc = vcc,
		occToken = occToken,
		client_id = vccSecurityInfo.openid.client_id,
		nonce = nonce
	}))
	redis:expire(redisKey, expireTokenSec)

	local authorization_endpoint = vccSecurityInfo.openid.openid_configuration_values.authorization_endpoint .. "?"
	authorization_endpoint = authorization_endpoint .. "client_id=" .. vccSecurityInfo.openid.client_id
	authorization_endpoint = authorization_endpoint .. "&response_type=id_token"
	authorization_endpoint = authorization_endpoint .. "&redirect_uri=" .. encoded_redirect_uri
	authorization_endpoint = authorization_endpoint .. "&response_mode=form_post"
	authorization_endpoint = authorization_endpoint .. "&scope=openid"
	authorization_endpoint = authorization_endpoint .. "&state=" .. state
	authorization_endpoint = authorization_endpoint .. "&nonce=" .. nonce

	vccSecurityInfo.openid = {
		authorization_endpoint = authorization_endpoint,
		expireTokenSec = expireTokenSec
	}
end

--
-- Validate an openId authentication
--
function API:OpenIdValidateAuthentication (options, ksAgent)
	if not type(options) == "table" then
		return false, "OpenId authentication - bad request received"
	end
	if options.error then
		LogWarning("[AUTHFILTER.API:OpenIdValidateAuthentication] error received %s:%s", options.error, options.error_description or "")
		return false, options.error
	end
	local state = options.state
	local id_token = options.id_token

	-- 1.- validate options
	local stored_state_str = redis:get("OpenIdState:"..state)
	if not stored_state_str then
		LogWarning("[AUTHFILTER.API:OpenIdValidateAuthentication] Can not obtain state '%s' from Redis", state)
		return false, "OpenIdAuthentication_missing_state"
	end
	local ok, stored_state = pcall(json.decode,stored_state_str)
	if not ok then
		LogWarning("[AUTHFILTER.API:OpenIdValidateAuthentication] Invalid state '%s'", stored_state_str)
		return false, "OpenIdAuthentication_invalid_state"
	end
	if not stored_state.username then
		LogWarning("[AUTHFILTER.API:OpenIdValidateAuthentication] Missing username in state '%s'", stored_state_str)
		return false, "OpenIdAuthentication_missing_username"
	end
	if not stored_state.vcc then
		LogWarning("[AUTHFILTER.API:OpenIdValidateAuthentication] Missing vcc in state '%s'", stored_state_str)
		return false, "OpenIdAuthentication_missing_vcc"
	end
	if not stored_state.occToken then
		LogWarning("[AUTHFILTER.API:OpenIdValidateAuthentication] Missing occToken in state '%s'", stored_state_str)
		return false, "OpenIdAuthentication_missing_occToken"
	end
	if not id_token then
		LogWarning("[AUTHFILTER.API:OpenIdValidateAuthentication] Missing id_token in '%s'", json.encode(options))
		return false, "OpenIdAuthentication_missing_id_token"
	end

	-- 2.- validate vcc
	local vcc = m_model.GetVCC(stored_state.vcc, ksAgent)
	if not vcc then
		LogWarning("[AUTHFILTER.API:OpenIdValidateAuthentication] vcc '%s' not found", stored_state.vcc)
		return false, "OpenIdAuthentication_vcc_not_found"
	end

	local vccSecurity = json.decode(vcc.Security)
	local jwks_uri = vccSecurity.authentication and vccSecurity.authentication.type == "OPENID" and vccSecurity.openid and vccSecurity.openid.enabled == "1" and vccSecurity.openid.openid_configuration_values.jwks_uri
	local issuer = vccSecurity.authentication and vccSecurity.authentication.type == "OPENID" and vccSecurity.openid and vccSecurity.openid.enabled == "1" and vccSecurity.openid.openid_configuration_values.issuer
	if not (jwks_uri and issuer) then
		LogWarning("[AUTHFILTER.API:OpenIdValidateAuthentication] vcc '%s' openid OpenIdAuthentication_vcc_openid_missconfigured", stored_state.vcc)
		return false, "OpenIdAuthentication_vcc_openid_missconfigured"
	end	

	-- 3.- validate username
	local user = m_model.GetUserFromCassandra(stored_state.username, stored_state.vcc, ksAgent)
	if not user then
		LogWarning("[AUTHFILTER.API:OpenIdValidateAuthentication] user %s@%s not found", stored_state.username, stored_state.vcc)
		return false, "OpenIdAuthentication_user_not_found"
	end

	LogDebug("[AUTHFILTER.API:OpenIdValidateAuthentication] OpenId authentication - Validating user %s@%s", stored_state.username, stored_state.vcc)

	-- 4.- validate occToken
	local occToken = redis:get(self.NONCE_TOKEN(stored_state.occToken))
	if occToken ~= stored_state.occToken then
		LogDebug("[AUTHFILTER.API:OpenIdValidateAuthentication] Invalid nonce token for user %s@%s in app %s: %s", username, vcc, self.appName, stored_state.occToken)
		return false, "OpenIdAuthentication_invalid_occToken"
	end

	local jwtData, jwtErr

	-- 5.- validate id_token
	local claims = {
		iss = issuer,
		aud = stored_state.client_id,
		nonce = stored_state.nonce,
		--Este atributo se debe configurar como notificacion en el token de la app de Azure, con el nombre local_user y el valor userPrincipalName
		local_user = user.openIdUserId
	}

	local success, err = openid.Verify(vcc.Id, jwks_uri, id_token, claims)
	if not success then
		LogWarning("[AUTHFILTER.API:OpenIdValidateAuthentication] OpenId verification error: '%s'", err)
		return false, err
	end

	options.username = stored_state.username
	options.userGroup = user.UserGroup
	options.callcenter = stored_state.vcc
	options.loginToken = stored_state.occToken
	options.hash = true
	options.authenticatedByOpenId = true

	return true
end

----------------------------------------------------------------------------------
-- When you are inside a WebHandler application									--
-- you should call the module for your application								--
-- Then you must maintain a reference to the returned object in order to call	--
-- API functions 																--
----------------------------------------------------------------------------------

getmetatable(_M).__call = function(_, appName) -- we can ignore first parameter because is the module itself
	assert(type(appName) == "string", "missing parameter appName")
	appName = appName:upper()
	if filterInstances[appName] then
		return filterInstances[appName]
	end

	LogInfo("[AUTHFILTER] Creating AuthFilter instance for app %s", appName)

	-- creates a new table and assign the API metatable
	local new = setmetatable({}, API)

	new.appName = appName
	--this key must be consistent with the one defined in async.lua
	new.USER_BY_SID = "authFilter:userBySID:"..new.appName
	new.SID_BY_USER = "authFilter:SIDByUser:"..new.appName
	new.NOTIFICATIONS_BY_USER = "authFilter:userNotifications:"..new.appName
	new.APPS_BY_USER = function(userAtVcc) return "authFilter:AppsByUser:"..userAtVcc end
	--this cookie must be consistent with the one defined in async.lua
	new.AUTH_SID = "AUTH_SID_"..new.appName
	new.NO_FRIEND_AUTH = new.AUTH_SID.."_NO_FRIEND_AUTH"
	new.NONCE_TOKEN = function(token) return "authFilter:nonceToken:"..new.appName..":"..token end
	new.ONE_TIME_TOKEN = function(token) return "authFilter:oneTimeToken:"..new.appName..":"..token end
	--presencefilter key
	new.AUTH_TOKEN = function(session_type, vcc) return "presencefilter:auth_tokens:" .. session_type .. ":" .. vcc end

	new.VCC_AVAILABLE_LICENCES = function(vcc)
		local vccAvailableLicensesKey = string.format("authFilter:VCCAvailableLicenses:%s", vcc:lower())
		--LogDebug("[AUTHFILTER:new.VCC_AVAILABLE_LICENCES] Vcc '%s' Avaliable Licenses Key: %s", vcc, vccAvailableLicensesKey)
		return vccAvailableLicensesKey
	end

	filterInstances[appName] = new
	return new
end
