Autenticar usuarios en React.js con Azure Active Directory

Estos días he vuelto a la carga con Azure Active Directory para ver de qué forma es posible integrarlo con diferentes tecnologías que no sean .NET :-). Para el mundo JavaScript, Microsoft ofrece una librería para este fin conocida como ADAL.js (Active Directory Authentication Library (ADAL) for JavaScript). Sin embargo, cuando nos adentramos en el mundo de node.js y react.js, entre otros, acostumbramos a trabajar mucho mejor con módulos, ya sean nuestros o de otros. Hoy te quiero contar cómo puedes autenticarte en un frontend desarrollado con React.js gracias al módulo react-adal, que se basa en dicha librería.

Para verlo con un ejemplo, crea una aplicación react con los siguiente comandos:

npx create-react-app aad-demo
cd aad-demo
npm start

Si todo ha salido bien, deberías de ver el siguiente mensaje en tu consola y una nueva ventana de tu navegador se abrirá con tu nueva aplicación.

react app – npm start

A partir de este momento ya tenemos nuestra aplicación con react.js up and running :-). Ahora vamos a instalar una librería que nos permita conectar con Azure AD de manera de sencilla. En GitHub puedes encontrar una llamada react-adal.

npm install react-adal

Tal y como comenta el autor del módulo, me he creado un archivo llamado adalConfig.js que contiene la configuración y algunos métodos que me ayudarán a autenticarme contra el directorio:

import { AuthenticationContext, adalFetch, withAdalLogin } from 'react-adal';
import dotenv from 'dotenv';
dotenv.config();

export const endpoint =process.env.REACT_APP_CLIENT_ID;

export const adalConfig = {
    tenant: process.env.REACT_APP_TENANT_ID,
    clientId: process.env.REACT_APP_CLIENT_ID,
    endpoints: {
        api: endpoint,
    },
    cacheLocation: 'localStorage',
};

export const authContext = new AuthenticationContext(adalConfig);

export const adalApiFetch = (fetch, url, options) =>
    adalFetch(authContext, adalConfig.endpoints.api, fetch, url, options);

export const withAdalLoginApi = withAdalLogin(authContext, adalConfig.endpoints.api);

Como ves, son necesarios dos valores para la configuración:

Nota: recuerda que en React.js debes utilizar REACT_APP_ como prefijo de tus variables en tu archivo .env si quieres que funcione 🙂

Una vez configurado el módulo, me he creado una clase, AADService,  que me permita gestionar diferentes cosas (login, logout, recuperar el token, recuperar el usuario, recuperar todas las claims, roles, etcétera):

import { authContext, endpoint } from './adalConfig';
import decode from 'jwt-decode';

export default class AADService {    

    login() {
        authContext.login();
    }

    loggedIn() {
        return this.getUser() ? true : false;
    }

    logout() {
        authContext.logOut();
    }

    getToken() {
        return authContext.getCachedToken(endpoint);
    }

    getUser() {
        return authContext.getCachedUser();
    }

    getClaims() {
        return decode(this.getToken());
    }

    getRoles() {

        const claims = this.getClaims();

        if (claims.hasOwnProperty('roles')) {
            return claims.roles;
        }

        return null;
    }

    isInRole(role) {

        if (this.getRoles()) {
            for (var i = 0; i < this.getRoles().length; i++) {
                if (this.getRoles()[i] === role)
                    return true;
            }
        }
        return false;
    }
}

Como ves, también he utilizado el módulo jwt-decode que me permite decodificar el token JWT a un objeto JSON de manera que pueda leer las claims obtenidas.

Para comprobar que mi clase funciona correctamente, primero vamos a asegurarnos que cualquiera que entra en nuestra aplicación debe logarse primero, renombramos el archivo index.js a indexApp.js y creamos un nuevo index.js con el siguiente contenido:

import { runWithAdal } from 'react-adal';
import { authContext } from './adalConfig';
 
const DO_NOT_LOGIN = false;
 
runWithAdal(authContext, () => {
 
  // eslint-disable-next-line
  require('./indexApp.js');
 
},DO_NOT_LOGIN);

Ahora crea un componente que nos permita ver las claims que ha devuelvo nuestro Azure AD. En este ejemplo voy a utilizar el módulo react-bootstrap que nos proporciona diferentes componentes. Instalalo  a través de:

npm install react-bootstrap --save

En un archivo llamado ClaimsTable.js, dentro de la carpeta src del sitio, incluye el siguiente código:

//  /src/ClaimsTable.js
import React, { Component } from 'react';
import { Table } from 'react-bootstrap';
import AADService from './AADService';

export default class ClaimsTable extends Component {

    constructor(props) {
        super(props);

        this.state = {
            authenticated: null,
            userName: null,
            name: null,
            given_name: null,
            roles: null,
            isWriter: null,
            isContributor: null
        };

        this.AzureADService = new AADService();
        this.isAuthenticated = this.isAuthenticated.bind(this);
    }


    componentWillMount() {
        this.isAuthenticated();
    }

    isAuthenticated() {
        const user = this.AzureADService.getUser();
        const roles = this.AzureADService.getRoles();

        this.setState({
            userName: user.userName,
            name: user.profile.name,
            given_name: user.profile.given_name,
            family_name: user.profile.family_name,
            roles: roles ? roles : "none",
            isWriter: this.AzureADService.isInRole('Writer') ? "yes" : "no",
            isContributor: this.AzureADService.isInRole('Contributor') ? "yes" : "no"
        });
    }

    render() {
        return (
            <div>
                <Table striped bordered condensed hover>
                    <thead>
                        <tr>
                            <th>Claim name</th>
                            <th>Value</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>User name</td>
                            <td>{this.state.userName}</td>
                        </tr>
                        <tr>
                            <td>Name</td>
                            <td>{this.state.name}</td>
                        </tr>
                        <tr>
                            <td>Given Name</td>
                            <td>{this.state.given_name}</td>
                        </tr>
                        <tr>
                            <td>Family Name</td>
                            <td>{this.state.family_name}</td>
                        </tr>
                        <tr>
                            <td>Roles</td>
                            <td>{this.state.roles}</td>
                        </tr>
                        <tr>
                            <td>is a Writer?</td>
                            <td>{this.state.isWriter}</td>
                        </tr>
                        <tr>
                            <td>is a Contributor?</td>
                            <td>{this.state.isContributor}</td>
                        </tr>
                    </tbody>
                </Table>
            </div >
        )
    }
};

También podemos crear un menú superior para las operaciones de login y logout:

//  /src/Navbar.js
import React, { Component } from 'react';
import { Navbar, Nav, NavItem } from 'react-bootstrap';
import AADService from './AADService';

export default class NavBar extends Component {
    constructor(props) {
        super(props);       
        this.state = {
            authenticated: null
        };
        this.AzureADService = new AADService();
        this.isAuthenticated = this.isAuthenticated.bind(this);
        this.login = this.login.bind(this);
        this.logout = this.logout.bind(this);
    }

    componentWillMount() {
        this.isAuthenticated();
    }

    isAuthenticated() {
        this.setState({
            authenticated: this.AzureADService.loggedIn()
        });
    }

    login(){
        this.AzureADService.login();
    }

    logout(){
        this.AzureADService.logout();
    }

    render() {
        return (
            <div>
                <Navbar>
                    <Navbar.Header>
                        <Navbar.Brand>
                            <a href="#home">Azure AD Demo</a>
                        </Navbar.Brand>
                    </Navbar.Header>
                    <Nav pullRight>
                        {this.state.authenticated === false && <NavItem onClick={this.login}>Login</NavItem>}
                        {this.state.authenticated === true && <NavItem onClick={this.logout}>Logout</NavItem>}
                    </Nav>
                </Navbar>
            </div>
        )
    }
}

El resultado no es que sea muy glamuroso pero si que te da una idea de cómo puedes integrar Azure Active Directory con tu aplicación React.js:

React and Azure AD – Claims

El código de este ejemplo lo tienes mi GitHub.

¡Saludos!