Autenticar usuarios en React.js con Azure Active Directory B2C

Si estás trabajando en un entorno corporativo y quieres aprovecharte de las capacidades de tu directorio activo en la nube (y mucho más tus empleados, que podrán hacer SSO) Azure Active Directory es el servicio a usar. Si lo que necesitas es que tus clientes puedan utilizar credenciales que ya conocen, facilitando así su registro y login entonces Azure Active Directory B2C es el servicio perfecto. Ayer te conté cómo integrar Azure AD con un front-end en React.js así que hoy me gustaría contarte lo mismo pero con Azure AD B2C 🙂

Es importante saber que al igual que el módulo react-adal utiliza la librería ADAL.js, de Microsoft, el módulo que vamos a utilizar hoy, react-azure-adb2c utiliza la librería MSAL.js (Microsoft Authentication Library Preview). A diferencia de la anterior, esta te permite trabajar no sólo con Azure AD, sino también con Azure AD B2C y Microsoft Accounts. Dicho de otra forma, ADAL.js sólo trabaja con el endpoint v1 y MSAL.js con v2. Esto es importante tenerlo en cuenta si el día de mañana prefieres crear tu propio módulo para trabajar con cualquiera de estos dos servicios. Aquí tienes más información al respecto.

El ejemplo en este caso es bastante similar al que te mostré ayer. Crea un proyecto react con los siguientes comandos:

npx create-react-app react-azure-adb2c-demo
cd react-azure-adb2c-demo
npm start

Instala las librerías react-azure-adb2c, para autenticarte con Azure AD B2C, dotenv, que nos ayuda a recuperar valores de configuración de un archivo .env, jwt-decode, para decodificar el token que llega de Azure AD B2C en este caso, y semantic-ui-react, que te ayudará a generar algunos componentes.

npm install react-azure-adb2c dotenv jwt-decode semantic-ui-react --save

Por simplicidad en la demo, deja la configuración de react-azure-adb2c en el archivo index.js (¡pero solo para la demo! 🙂 ):

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import authentication from 'react-azure-adb2c';
require('dotenv').config();
console.log(process.env.REACT_APP_TENANT_NAME);
authentication.initialize({
    tenant: process.env.REACT_APP_TENANT_NAME,
    signInPolicy: process.env.REACT_APP_SIGN_IN_POLICY,
    applicationId: process.env.REACT_APP_APP_ID,
    cacheLocation: process.env.REACT_APP_CACHE_LOCATION,
    scopes: [process.env.REACT_APP_SCOPES],
    postLogoutRedirectUri: window.location.origin
});

//Protect the entire app with Azure AD B2C
authentication.run(() => {
    
    ReactDOM.render(<App />, document.getElementById('root'));
    // If you want your app to work offline and load faster, you can change
    // unregister() to register() below. Note this comes with some pitfalls.
    // Learn more about service workers: http://bit.ly/CRA-PWA
    serviceWorker.unregister();
});

Como ves, será necesario el nombre del tenant, una política de inicio de sesión (también vale una de tipo Sign-up or sign-in), el id de la aplicación, la localización de la caché (esta puede ser localStorage o sessionStorage), el scope y la dirección de vuelta una vez logado. Para que te hagas una idea, este sería un ejemplo de configuración:

REACT_APP_APP_ID="5f057f7c-91d9-43df-a449-b76cecbe1f0c"
REACT_APP_TENANT_NAME="returngisb2c.onmicrosoft.com"
REACT_APP_SIGN_IN_POLICY="B2C_1_SignIn_react-azure-adb2c-demo"
REACT_APP_CACHE_LOCATION="sessionStorage"
REACT_APP_SCOPES="https://returngisb2c.onmicrosoft.com/react-azure-adb2c-demo/user_impersonation"

Al igual que en el anterior, estoy obligando a que el usuario inicie sesión nada más entrar en el sitio web, pero con ambas librerías también sería posible hacerlo solamente para paths concretos. Del mismo modo he creado una clase AAD_B2CService que me permite manejar diferentes acciones de manera centralizada:

import authentication from 'react-azure-adb2c';
import decode from 'jwt-decode';
export default class AAD_B2CService {
    loggedIn() {
        if (authentication.getAccessToken())
            return true;
        return false;
    }
    logout() {
        authentication.signOut();
    }
    getToken() {
        return authentication.getAccessToken();
    }
    getClaims() {
        return decode(this.getToken());
    }
    getUser() {
        const claims = decode(authentication.getAccessToken());
        return {
            name: claims.name,
            country: claims.country,
            city: claims.city,
            provider: claims.idp
        };
    }
}

En el caso de Azure AD B2C las claims pueden ser las generadas por el propio servicio (built-in) o incluso mías propias que haya creado en el apartado User attributes, por lo que en tu ejemplo el método getUser() podría variar en función de las claims que tengas.

Por último crea un par de componentes para ver el resultado de todo lo anterior:

ClaimsComponent que simplemente mostrará el JSON con todas las claims que me devuelve Azure AD B2C, además de los valores devueltos en getUser():

import React, { Component } from 'react';
import { Container, Header } from 'semantic-ui-react';
import AAD_B2CService from './AAD_B2CService';
export default class ClaimsComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            raw_claims: null,
            user: null
        };
        this.AzureADB2CService = new AAD_B2CService();
    }
    componentWillMount() {
        this.setState({
            raw_claims: this.AzureADB2CService.getClaims(),
            user: this.AzureADB2CService.getUser()
        });
    }
    render() {
        return (
            <div>
                <Container textAlign="left" className="container">
                    <Header as='h2'>Claims from Azure AD B2C</Header>
                    <pre className="claims">
                        {JSON.stringify(this.state.raw_claims, null, 2)}
                    </pre>
                    <pre className="claims">
                        {JSON.stringify(this.state.user, null, 2)}
                    </pre>
                </Container>
            </div>
        )
    };
};

Y una NavBar para poder hacer logout y cambiar de proveedor de identidad, si es que estás haciendo diferentes pruebas:

import React, { Component } from 'react';
import { Menu } from 'semantic-ui-react'
import AAD_B2CService from './AAD_B2CService';
export default class NavBar extends Component {
    constructor(props) {
        super(props);
        this.state = {
            authenticated: null
        };
        this.AzureADB2CService = new AAD_B2CService();
        this.isAuthenticated = this.isAuthenticated.bind(this);
        this.logout = this.logout.bind(this);
    }
    componentWillMount() {
        this.isAuthenticated();
    }
    isAuthenticated() {
        this.setState({
            authenticated: this.AzureADB2CService.loggedIn()
        });
    }
    logout() {
        this.AzureADB2CService.logout()
    }
    render() {
        return (
            <div>
                <Menu>
                    <Menu.Item name="Azure Active Directory B2C Demo">
                    </Menu.Item>
                    <Menu.Menu position='right'>
                        {this.state.authenticated === true && <Menu.Item name="logout" onClick={this.logout}></Menu.Item>}
                    </Menu.Menu>
                </Menu>
            </div>
        )
    }
}

El resultado debería ser parecido a este:

React.js – Claims from Azure Active Directory B2C

El código también lo tienes en GitHub.

¡Saludos!