En artículo anterior de la serie armamos nuestro Conector y configuramos Azure AD B2C para utilizarlo y así detectar y prevenir usuarios duplicados. Además creamos una Azure Function con código base para probar el conector.

En este post final completaremos la implementación escribiendo el código necesario para consultar Microsoft Graph API por usuarios con el mismo email ya sea como identidad o email adicional.

Preparación

Antes de comenzar con la implementación necesitamos recolectar las siguientes piezas de información y hacer algunas configuraciones:

  1. ID de Tenant o Directorio
  2. ID de Aplicación
  3. Generar un Secreto de Cliente
  4. Dar permisos para Microsoft Graph API

ID de Tenant y ID de Aplicación

Para obtener las primeras dos piezas de información que necesitamos debemos ir al registro de aplicaciones seleccionar la aplicación con la cuál vamos a trabajar. Cuándo realizamos la configuración inicial creamos un registro de aplicación llamado Sitio Web Increíble, ese es el que seleccionaremos en este caso.

Dela sección Información general del registro de aplicación podremos ver la información necesaria.

Registro de aplicación
Registro de aplicación

Generar secreto de cliente

Desde el mismo registro de aplicación podremos generar un secreto de cliente. En la sección Certificados y Secretos creamos un Nuevo secreto de cliente. Sólo necesitamos completar la descripción (la cuál es usada como referencia) y seleccionar una expiración.

Generación de secreto de cliente
Generación de secreto de cliente

Al finalizar veremos el secreto de cliente en la lista de secretos, necesitamos copiar el valor. Es importante tener en cuenta que una vez que abandonemos esta página ya no podremos a volver a ver el valor.

Valor del secreto de cliente
Valor del secreto de cliente

Permisos para Microsoft Graph API

Desde la sección Permisos de API en el registro de aplicación, agregamos un nuevo permiso de Microsoft Graph. Dentro de la categoría Permisos de Aplicación agregamos el permiso User.Read.All como se muestra a continuación:

Permisos de Microsoft Graph para leer usuarios

Es muy importante conceder consentimiento permisos de administrador para nuestro tenant, de lo contrario los permisos no serán efectivos.

Implementación

Si bien Microsoft Graph expone una API REST muy bien documentada, el equipo de Microsoft creó una librería para facilitarnos la tarea y que se encuentra disponible vía paquetes de NuGet. Comenzaremos agregando al proyecto de Azure Function los siguientes paquetes de NuGet:

Microsoft.Graph
Microsoft.Graph.Auth
Microsoft.Identity.Client

Luego, a partir del ID de Aplicación, el ID de Tenant y el Secreto de Cliente que obtuvimos anteriormente, vamos a generar una instancia de GraphServiceClient:

// Usings
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;

// Código
var confidentialClientApplication = ConfidentialClientApplicationBuilder
    .Create("<< ID de Aplicación >>")
    .WithTenantId("<< ID de Tenant >>")
    .WithClientSecret("<< Secreto de Cliente >>")
    .Build();

var authProvider = new ClientCredentialProvider(confidentialClientApplication);

var graphClient = new GraphServiceClient(authProvider);

Buscando usuarios por email

Estamos buscando encontrar usuarios con el mismo email que el usuario actual está intentando usar, con lo cuál buscamos usuario cuya identidad sea el mail o usuarios que tengan el email como parte de sus emails adicionales. Esto requiere dos peticiones diferentes.

  • Búsqueda por identidad
var usersByIdentity = (await _graphClient.Users
    .Request()
    .Filter($"identities/any(c:c/issuerAssignedId eq '{email}' and c/issuer eq '{"<< ID de Tenant >>"}')")
    .GetAsync())
        .ToArray();
  • Usuarios por otros emails:
var usersByOtherEmails = (await _graphClient.Users
    .Request()
    .Filter($"otherMails/any(c:c eq '{email}') and UserType eq 'Member'")
    .GetAsync())
        .ToArray();

Si alguna de las dos peticiones anteriores contiene algún resultado, eso quiere decir entonces que hemos encontrado otro usuario con el mismo email que el usuario actual y demos detener el flujo de usuario. De lo contrario podemos continuar con la ejecución del flujo de usuario.

Quiero hacer mención al los usuarios de Stackoverflow boehlefeld y LuisEduardox de quién tomé las dos consultas necesarias para interactuar con Microsoft Graph a partir de las respuestas aquí y aquí mencionadas.

Como vimos en el post anterior, detenemos la ejecución del flujo de usuario retornando como respuesta "action" : "ShowBLockPage".

if (usersByIdentity.Any() || usersByOtherEmails.Any())
{
    return new OkObjectResult(
        new
        {
            version = "1.0.0",
            action = "ShowBlockPage",
            userMessage = $"Email {req.email} is duplicated."
        });
}

return new OkObjectResult(
    new
    {
        version = "1.0.0",
        action = "Continue"
    });

Solución completa

Juntando todas las piezas, nuestra Azure Function se ve de la siguiente forma:

var confidentialClientApplication = ConfidentialClientApplicationBuilder
    .Create("<< ID de Aplicación >>")
    .WithTenantId("<< ID de Tenant >>")
    .WithClientSecret("<< Secreto de Cliente >>")
    .Build();

var authProvider = new ClientCredentialProvider(confidentialClientApplication);

var graphClient = new GraphServiceClient(authProvider);

var usersByIdentity = (await _graphClient.Users
    .Request()
    .Filter($"identities/any(c:c/issuerAssignedId eq '{email}' and c/issuer eq '{"<< ID de Tenant >>"}')")
    .GetAsync())
        .ToArray();

var usersByOtherEmails = (await _graphClient.Users
    .Request()
    .Filter($"otherMails/any(c:c eq '{email}') and UserType eq 'Member'")
    .GetAsync())
        .ToArray();

if (usersByIdentity.Any() || usersByOtherEmails.Any())
{
    return new OkObjectResult(
        new
        {
            version = "1.0.0",
            action = "ShowBlockPage",
            userMessage = $"Email {req.email} is duplicated."
        });
}

return new OkObjectResult(
    new
    {
        version = "1.0.0",
        action = "Continue"
    });

Sólo nos resta desplegar la nueva versión de nuestra Azure Function y probar.

Conclusión

En este post nos enfocamos en la parte final de la solución la cual implicó la interacción con Microsoft Graph para detectar usuarios con el mismo email que el usuario que se está intentando crear.

Una solución un poco más elegante pero mucho más compleja es la unificación de identidades, para lo que necesitamos utilizar Políticas Personalizadas de Azure AD B2C. Dejaremos esa para más adelante.

Para ver la implementación final en detalle te recomiendo revisar el repositorio en GitHub.