Microsoft Graph

Toda buena película adolescente de los 80, comienza con un pelele al que todo el mundo margina. Este personaje esconde algo en su interior de lo que, al principio, solo se notan pequeños y casuales atisbos. Después de una gran aventura de autoconocimiento y superación personal, el protagonista consigue dominar este poder oculto y convertirse en un verdadero héroe. Esto es lo que le pasa a Naruto Microsoft Graph, que es como Daniel Larusso.

Todos llegamos a esta API por lo que parece que promete, por esos atisbos de poder que nos dejan vislumbrar las demos que Microsoft nos presenta. Luego, cuando llega la hora de utilizarla a fondo, nos damos cuenta de sus problemas, de lo crazy de algunas decisiones de su modelado. Entonces, la marginamos. Nos metemos con ella. Ya no la invitamos a nuestros meetups. Una vez superado este duro camino plagado de frustración, la aceptamos y nos damos cuenta de todas las posibilidades que nos aporta, llegando finalmente, a un idilio llano y sincero.

Microsoft Graph es una API con un modelo de datos unificado para poder explotar todas las aplicaciones que forman parte de Office 365, Azure Active Directory, Enterprise Mobility, Windows 10 y Education. Está basada en OData v4, aunque no todos los recursos tienen hoy en día las mismas capacidades. Y podemos encontrar toda la documentación de la versión 1.0 (y beta) aquí.

Existe un SDK para, prácticamente, todos los lenguajes y plataformas de programación conocidas, que nos ayudará a autenticarnos y solicitar datos de esta API. No obstante, al ser OData una especificación por encima de REST, resulta simple utilizarla sin necesidad de usar el SDK.

App registration

Lo primero que tenemos que saber para explotar Microsoft Graph es cómo conseguir acceso. Para ello tendremos que solicitar un token a la API v2 del protocolo de autenticación de Azure Active Directory. Afortunadamente, esta API usa la especificación OIDC que se basa en OAuth 2.0. Así que este paso lo tenemos prácticamente superado. Lo único que tendremos que saber es que hay dos tipos de permisos dentro de Graph que podemos solicitar:

Para configurar estos permisos tendremos que dirigirnos al portal de Azure con una cuenta de administrador de nuestro tenant. Ahí nos dirigiremos a la opción de menú “Azure Active Directory” dentro de esta opción a “App registrations (Preview)” y al botón de “New registration”:

New app registration

Después le daremos un nombre al “App registration” comprobaremos que está marcada la opción de permitir solo accesos con cuentas de nuestro directorio y presionaremos el botón “Register”:

New app registration

Al crearla, podremos observar en el panel de “Overview”:

client_id y tenant_id

Una vez hemos creado el nuevo registro, podremos generar un nuevo secreto navegando a “Certificates & secrets” y presionando el botón de “New client secret”:

New secret

No olvides guardar en un lugar seguro el secreto que se acaba de generar, ya que coincidirá con el valor de client_secret.

Ahora vamos a asignar los permisos del registro. Para ello iremos a “API permissions” y dentro de esta opción, pulsaremos “Add a permission”:

Add permissions

En el nuevo modal que se abrirá lo primero que tendremos que elegir es “Microsoft Graph”, para poder elegir los permisos para esta API:

Select Microsoft Graph

Después nos dará a elegir entre “Delegated permissions” o “Application permissions”. Deberemos elegir una en dependencia de cómo tenemos pensado obtener el token. Después tendremos un listado de recursos y al desplegar alguno de ellos encontraremos los permisos específicos que podemos asignar:

Search your permissions

Si queremos saber qué permisos deberíamos añadir, todo depende de la operación dentro de Microsoft Graph que queremos realizar. Afortunadamente, en la documentación, en la referencia a la API, podremos encontrar antes de cada acción, qué permisos son necesarios para ejecutarla:

Microsoft Graph Reference: Action Permissions

Una vez hayamos añadido los que necesitemos, en el portal de Azure, podemos presionar en “Add Permissions”. Y si queremos que ya cuenten con la validación del administrador, en la pantalla de “API permissions”, presionaremos el botón de “Grant admin consent for [your directory]”.

Ahora ya podremos conectar con Microsoft Graph y jugar con todas las posibilidades que nos ofrece. Para ello proponemos 4 formas:

A mano

Access token

En un artículo anterior vimos las diferentes formas que podríamos usar para pedir este token a un servicio que usara OAuth 2.0. En este caso vamos a usar el más simple, usando tan solo el Grant Type de “client_credentials”.

Para ello nos crearemos una clase donde almacenar la respuesta de una petición de token:

class AuthResponse
{
    [JsonProperty("access_token")]
    public string AccessToken { get; set; }

    [JsonProperty("token_type")]
    public string Type { get; set; }

    [JsonProperty("expires_in")]
    public int ExpiresIn { get; set; }

    [JsonProperty("ext_expires_in")]
    public int ExtExpiresIn { get; set; }

    public AuthenticationHeaderValue AsHeader()
    {
        return new AuthenticationHeaderValue(Type, AccessToken);
    }
}

Y realizaremos la petición al servidor de la forma que indicábamos:

async Task<AuthResponse> GetAuthAsync(string tenantId, string clientId, string clientSecret)
{
    var url = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token";
    var data = new Dictionary<string, string>();
    data.Add("grant_type", "client_credentials");
    data.Add("client_id", clientId);
    data.Add("client_secret", clientSecret);
    data.Add("scope", "https://graph.microsoft.com/.default");

    using (var client = new HttpClient())
    {
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        var response =  await client.PostAsync(url, new FormUrlEncodedContent(data));
        var json = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<AuthResponse>(json);
    }
}

Donde los valores que le pasamos a la función coincidirán que aquellos que recogimos en el proceso de creación del “App registration” en el portal de Azure. Y el valor de scope serán los permisos que queremos solicitar para llamar a la API (que tenemos que haber seleccionado y consentido en el “App registration”); o el valor “https://graph.microsoft.com/.default”, que indicará que estamos solicitando todos los permisos que hemos seleccionado en el “App registration” y así evitar estar constantemente indicándolos.

Llamando a Microsoft Graph

Vamos a usar como ejemplo una llamada al listado de usuario basándonos en la propia referencia de Microsoft Graph. Para ello tendremos que tener al menos el permiso de aplicación de “User.Read.All” consentido en nuestro “App registration”.

Una vez tenemos esto, tendremos que realizar una llamada al endpoint “https://graph.microsoft.com/v1.0/users” usando un token válido y tendremos ese listado.

La respuesta de listados desde Microsoft Graph está envuelta en un objeto que nos indica cual es la siguiente página, así pues, crearemos una estructura compleja para mapear los resultados:

class GraphListResponse<T>
{
    [JsonProperty("@odata.context")]
    public string Context { get; set; }
    [JsonProperty("@odata.nextLink")]
    public string NextLink { get; set; }
    [JsonProperty("value")]
    public IEnumerable<T> Value { get; set; }
}

De un usuario nos interesaría en un principio el id, el displayNamey el mail, por lo que no vamos a crear más propiedades:

class User
{
    [JsonProperty("id")]
    public string Id { get; set; }
    [JsonProperty("displayName")]
    public string DisplayName { get; set; }
    [JsonProperty("mail")]
    public string Email { get; set; }
}

Por lo que la llamada para listar todos los usuarios, podremos filtrar las propiedades a devolver añadiendo la propiedad $select con el valor “id,displayName,mail”, quedando el endpoint como: “https://graph.microsoft.com/v1.0/users?$select=id,displayName,mail”:

async Task<IEnumerable<User>> GetUsersAsync(AuthResponse auth, string nextLink = null)
{
    var result = new List<User>();
    var url = "https://graph.microsoft.com/v1.0/users?$select=id,displayName,mail";
    using (var client = new HttpClient())
    {
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Authorization = auth.AsHeader();

        GraphListResponse<User> res = null;
        do {
            var response = await client.GetAsync(url);
            var json = await response.Content.ReadAsStringAsync();
            res = JsonConvert.DeserializeObject<GraphListResponse<User>>(json);
            if (res != null)
            {
                result.AddRange(res.Value);
                url = res.NextLink;
            }
        } while (res != null && !string.IsNullOrEmpty(res.NextLink));
    }

    return result;
}

Así pues, podríamos recoger e imprimir todos los usuarios por pantalla realizando una llamada para recoger el token de acceso y otra para recoger los usuarios:

var auth = await Functions.GetAuthAsync(tenant_id, client_id, client_secret);
var users = await Functions.GetUsersAsync(auth);

Console.WriteLine("User List:");
foreach(var user in users)
{
    Console.WriteLine($" - {user.DisplayName} ({user.Email})");
}

Usando el SDK

Si queremos usar el SDK para .Net que nos provee Microsoft, primero tendremos que instalar los paquetes de Microsoft.Identity.Client y Microsoft.Graph.

En un proyecto de dotnet core se puede realizar mediante consola:

$ dotnet add MyProject.csproj package Microsoft.Identity.Client
...
$ dotnet add MyProject.csproj package Microsoft.Graph
...

Y si estamos en Visual Studio, siempre podemos abrir el panel de “Package Manager Console”:

> Install-Package Microsoft.Identity.Client
...
> Install-Package Microsoft.Graph
...

El código es bastante parecido. En un principio usamos los artefactos de Microsoft.Identity.Client para poder recoger un token para acceder a Microsoft Graph. Después declaramos un cliente de Graph que use el proveedor de tokens que hemos generado. Finalmente realizamos la llamada a la API y escribimos los resultados por pantalla:

var clientCredential = new ClientCredential(clientSecret);
var authClient = new ConfidentialClientApplication(
        clientId,
        $"https://login.microsoftonline.com/{tenantId}/v2.0",
        "urn:ietf:wg:oauth:2.0:oob",
        clientCredential,
        new TokenCache(),
        new TokenCache());
var authProvider = new DelegateAuthenticationProvider(async request =>
    {
        var authResult = await authClient.AcquireTokenForClientAsync(new [] { "https://graph.microsoft.com/.default" });
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
    });
var graphServiceClient = new GraphServiceClient("https://graph.microsoft.com/v1.0", authProvider);
IGraphServiceUsersCollectionPage page = null;

Console.WriteLine("User List:");
do {
    var task = page != null ? page.NextPageRequest : graphServiceClient.Users.Request().Select(u => new { u.Id, u.DisplayName, u.Mail });
    page = await task.GetAsync();
    foreach(var user in page)
    {
        Console.WriteLine($" - {user.DisplayName} ({user.Mail})");
    }
} while(page != null && page.NextPageRequest != null);

El futuro del SDK

Como el código actual para obtener tokens puede resultar un poco confuso, se está desarrollando un paquete que está actualmente en fase de pruebas (y falla como una escopeta de feria), donde la idea es simplificar esa parte. El paquete en cuestión se llama Microsoft.Graph.Auth:

$ dotnet add MyProject.csproj package Microsoft.Graph
...
$ dotnet add MyProject.csproj package Microsoft.Graph.Auth
...

o en Visual Studio:

> Install-Package Microsoft.Graph
...
> Install-Package Microsoft.Graph.Auth
...

El código, que hoy en día no funciona, sería el siguiente:

var clientCredential = new ClientCredential(clientSecret);
var clientApplication = ClientCredentialProvider.CreateClientApplication(clientId, clientCredential, tenant: tenantId);
var authenticationProvider = new ClientCredentialProvider(clientApplication);

var graphServiceClient = new GraphServiceClient(authenticationProvider);
IGraphServiceUsersCollectionPage page = null;

Console.WriteLine("User List:");
do {
    var task = page != null ? page.NextPageRequest : graphServiceClient.Users.Request().Select(u => new { u.Id, u.DisplayName, u.Mail });
    page = await task.GetAsync();
    foreach(var user in page)
    {
        Console.WriteLine($" - {user.DisplayName} ({user.Mail})");
    }
} while(page != null && page.NextPageRequest != null);

Como podemos observar, quedaría mucho mejor expuesta la obtención del token, simplificando mucho el código anterior.

Microsoft Graph Explorer

Si queréis conocer bien Microsoft Graph (y así entender un poco mejor lo que significa la palabra frustración), siempre podéis usar una herramienta que Microsoft nos ofrece para que, junto con la documentación, podamos explorar y entender la potencia interior oculta en esta API.

Es muy sencillo de utilizar:

Es un funcionamiento de petición/respuesta de HTTP, pero si abrimos unas cuantas ventanas, podremos realizar operaciones más complejas y navegar en los oscuros y desconocidos fondos que ahí se ocultan.

Conclusiones

Hoy hemos mostrado como utilizar Microsoft Graph, a partir de aquí, si seguís profundizando en el tema, ya no es mi culpa.

Homer se esconde entre los matorrales