Microsoft Graph: Una API para dominarlas a todas

Según ha ido creciendo la familia Microsoft el número de APIs que la acompañan también. Como en cualquier otra compañía, es cierto que cada producto tiene su propio equipo detrás, lo cual hace también que las convenciones y formas de trabajar sean diferentes.

Hasta ahora cada vez que necesitabas integrarte con una API, de Office 365 o de cuentas personales, cada una estaba totalmente separada de la otra.

Cada API va por separado

Además, si hablamos de cuentas corporativas y cuentas personales también nos encontramos con dos sistemas de autorización diferentes.

Por todo ello, Microsoft Graph ha llegado a nuestras vidas para unificar todas estas APIs, que tienen como cometido gestionar toda tu información, pero ahora desde un único punto: https://graph.micrososft.com

Como ves, Microsoft Graph te ofrece una capa de abstracción de todas las APIs que tenemos por debajo, a través de un único punto de entrada y un único token. De hecho, si revisas la nueva nomenclatura común verás que es mucho más amigable:

Operaciones en Microsoft Graph

Microsoft Graph Explorer

La forma más sencilla de ver todo lo que nos ofrece Microsoft Graph es a través de la página Microsoft Graph Explorer. En ella puedes iniciar sesión con una cuenta personal o corporativa y realizar consultas con tu usuario sobre los diferentes aspectos del mismo.

Desarrollando para Microsoft Graph

En todos los artículos donde te hablé de OAuth 2.0 utilizo Microsoft Graph como recurso protegido. Puedes revisar cualquiera de los flujos para ver cómo interactuar con la misma:

Sin embargo, la forma más cómoda de trabajar con Microsoft Graph es sin duda los SDKs que tenemos a nuestra disposición. En GitHub puedes encontrar todos los que hay a día de hoy y los lenguajes soportados (.NET, JavaScript, PHP, Java, Ruby, Python, etcétera).

En cualquiera de los lenguajes, siempre deberemos seguir los siguientes pasos:

1. Registrar una aplicación en Azure Active Directory

Para que nuestra aplicación pueda trabajar con Microsoft Graph primero necesitamos registrarla como un cliente válido en Azure Active Directory. Dependiendo del flujo de autorización que vayamos a seguir, la configuración puede variar un poco. Si no lo tienes claro, te recomiendo que eches un vistazo a este artículo. En este artículo estoy utilizando Authorization Code Flow.

2. Configurar los permisos de la aplicación

Dependiendo de qué necesite tu aplicación del conjunto de APIs que ofrece Microsoft Graph deberás asignar unos permisos u otros a la misma. Es recomendable que sólo des acceso a aquellos scopes que realmente necesite tu aplicación. Para ello, dentro de Azure AD, accede a tu aplicación registrada y dirigete al apartado API Permissions. Haz clic sobre Add a permission y verás que en el lado derecho de la pantalla te aparecerá una sección donde podrás elegir la API sobre la que quieres otorgar permisos. Elige Microsoft Graph.

Al hacer esto verás que la segunda parte es elegir qué tipo de permisos necesitas: delegados o de aplicación. Delegados es para cuando tu aplicación va a actuar en nombre de tu usuario, es decir que va a llevar a cabo tareas en su nombre. Aplicación es cuando esta no va a interactuar en nombre de nadie, es decir que no representa a ningún usuario en concreto. Selecciona Delegated permissions para este ejemplo.

Esto hará que se despliegue un gran listado con todos los permisos disponibles en Microsoft Graph. Para este ejemplo selecciona los siguientes: user.read (está seleccionado por defecto), calendars.read y contacts.read, con los que estamos dando acceso al perfil básico del usuario, a sus calendarios y sus contactos. Pulsa en el botón Add permissions y deberías de tener un resultado como el siguiente:

3. Recuperar el token de acceso

Una vez que tienes tu aplicación registrada y configurada con los permisos que necesitas para Microsoft Graph, ya puedes centrarte en el código. Lo primero que necesitas es recuperar el token de acceso, llamando al servidor de autorización, que es Azure Active Directory, con el que después podrás llamar a Microsoft Graph. Para este tipo de llamadas necesitas normalmente los siguientes valores:

SCOPE="user.read calendars.read contacts.read"

#Azure Active Directory
TENANT_ID="<YOUR_TENANT_ID>"
CLIENT_ID="<YOUR_CLIENT_ID>"
CLIENT_SECRET="<YOUR_CLIENT_SECRET>"

Como ves, el scope son los permisos que elegimos antes. Por otro lado necesitas el tenant id, client id y en este caso el client secret, al ser una aplicación confidencial. Con estos valores ya podríamos realizar una llamada como esta:

//Set 1: Ask the authorization code
app.get('/get/the/code', (req, res) => {

    const Authorization_Endpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/authorize`; 
    // const Authorization_Endpoint = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize`; 
    const Response_Type = 'code';
    const Client_Id = process.env.CLIENT_ID;
    const Redirect_Uri = 'http://localhost:8000/give/me/the/code';
    const Scope = process.env.SCOPE;
    const State = 'ThisIsMyStateValue';

    let url = `${Authorization_Endpoint}?response_type=${Response_Type}&client_id=${Client_Id}&redirect_uri=${Redirect_Uri}&scope=${Scope}&state=${State}`;

    log.info(url);

    res.redirect(url);

});

En el flujo Authorization Code Flow necesito primero obtener el código, utilizando /authorize. Como ves, estoy trabajando con una cuenta corporativa, pero si quisiera utilizar este mismo código con una cuenta personal solo tendría que comentar el Authorization_Endpoint y habilitar la siguiente línea que utiliza “common” en lugar del tenant id. Cuando esta llamada se realiza se nos pedirá nuestro consentimiento sobre los permisos que mi aplicación está pidiendo.

Una vez que aceptemos, el flujo seguiría a la siguiente llamada, que será la que has incluido como Reply URL en el registro de tu aplicación. En mi caso, estas dos:

//Step 2: Get the code from the URL
app.get('/give/me/the/code', (req, res) => {
    //before continue, you should check that req.query.state is the same that the state you sent
    res.render('exchange-code', { code: req.query.code, state: req.query.state });
});

//Step 3: Exchange the code for a token
app.post('/exchange/the/code/for/a/token', (req, res) => {

    const Token_Endpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/v2.0/token`;
    // const Token_Endpoint = `https://login.microsoftonline.com/common/oauth2/v2.0/token`;
    const Grant_Type = 'authorization_code';
    const Code = req.body.code;
    const Redirect_Uri = 'http://localhost:8000/give/me/the/code';
    const Client_Id = process.env.CLIENT_ID;
    const Client_Secret = process.env.CLIENT_SECRET;
    const Scope = process.env.SCOPE;

    let body = `grant_type=${Grant_Type}&code=${Code}&redirect_uri=${encodeURIComponent(Redirect_Uri)}&client_id=${Client_Id}&client_secret=${Client_Secret}&scope=${encodeURIComponent(Scope)}`;

    log.info(`Body: ${body}`);

    fetch(Token_Endpoint, {
        method: 'POST',
        body: body,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    }).then(async response => {

        let json = await response.json();
        res.render('access-token', { token: JSON.stringify(json, undefined, 2) }); //you shouldn't share the access token with the client-side

    }).catch(error => {
        log.error(error.message);
    });
});

En el paso dos simplemente devuelvo el código de autorización al cliente, solo con fines educativos y, acto seguido, lo devuelvo de nuevo al servidor llamando al paso 3: /exchange/the/code/for/a/token. Aquí es donde finalmente consigo el access token llamando al endpoint /token.

Todo esto se puede hacer de una forma más transparente para ti utilizando módulos como passport.js, pero creo que es más sencillo utilizar cualquier módulo si sabes qué necesitas y por qué.

4. Llamadas a Microsoft Graph

El paso final es la llamada/s a Microsoft Graph con el token que acabamos de conseguir.

//Step 4: Call the protected API
app.post('/call/ms/graph', (req, res) => {

    let access_token = JSON.parse(req.body.token).access_token;

    const Microsoft_Graph_Endpoint = 'https://graph.microsoft.com/v1.0';
    const Acction_That_I_Have_Access_Because_Of_My_Scope = '/me';

    // const Acction_That_I_Have_Access_Because_Of_My_Scope = '/me/contacts?$skip=10';
    // const Acction_That_I_Have_Access_Because_Of_My_Scope = '/me/contacts';
    // const Acction_That_I_Have_Access_Because_Of_My_Scope = '/me/people';

    log.info(access_token);

    //Call Microsoft Graph with your access token
    fetch(`${Microsoft_Graph_Endpoint}${Acction_That_I_Have_Access_Because_Of_My_Scope}`, {
        headers: {
            'Authorization': `Bearer ${access_token}`
        }
    }).then(async response => {

        let json = await response.json();
        res.render('calling-ms-graph', { response: JSON.stringify(json, undefined, 2) });
    });
});

En esta función puedes ver que el punto de entrada es https://graph.microsoft.com/v1.0 y luego tengo diferentes acciones que puedo ir probando. En este caso utilizo directamente /me para que me devuelva la información del usuario.

Si pruebas cualquiera de las acciones comentadas deberías tener permisos gracias a los scopes solicitados (obviamente ese usuario debe tener asociada una cuenta de Office 365 o también puedes probar con una Microsoft Account). El código de este ejemplo lo tienes en mi GitHub.

Si utilizáramos por ejemplo la librería de JavaScript este código quedaría mucho más legible. Por un lado necesitaríamos generar un cliente de Microsoft Graph utilizando nuestro access token.

function getAuthenticatedClient(accessToken) {
  // Initialize Graph client
  const client = graph.Client.init({
    // Use the provided access token to authenticate
    // requests
    authProvider: (done) => {
      done(null, accessToken);
    }
  });

  return client;
}

y una vez obtenido podríamos tener llamadas como la siguiente:

  getEvents: async function(accessToken) {
    const client = getAuthenticatedClient(accessToken);

    const events = await client
      .api('/me/events')
      .select('subject,organizer,start,end')
      .orderby('createdDateTime DESC')
      .get();

    return events;
  },

¡Saludos!