2022-10-01

Integrare l'autenticazione in Shibboleth con un'applicazione React-NodeJs

Categoria: development

Single Sign-On di Shibboleth e Applicazioni React

Come autocritica, avrai notato, parlo di click-baiting e non per nulla.

Premessa

Mi sono trovato a dover risolvere questo problema:
integrare l'autenticazione Single Sign On di Shibboleth di un applicativo che volevo, per mio esercizio personale (per forza), sviluppare in React. Anche qui esagero volutamente.

In realtà, l'unica necessità era determinata dalla scelta (obbligata) di deploy su Amplify di AWS, per cui limitato nella scelta di sviluppo in JavaScript o ES o React / React-Native.

Come ho agito? Come chiunque al mio posto

immagine della ricerca in google per risolvere il problema

Che cos'è Shibboleth?

È un sistema di autentcazione Single Sign-On (SSO) che permette di autenticarsi su sistemi differenti, ovvero di effettuare il login su reti di organizzazioni o istituzioni diverse, utilizzando una sola identità.

L'infrastruttura è basata sullo standard aperto Security Assertion Markup Language (SAML) e consente identity management e autenticazione con identità federata.

Gli elementi fondamentali sono gli Identity Provider (IdP), che hanno il compito di fornire informazioni sugli utenti, i Service Provider (SP), che utilizzano le informazioni e forniscono accesso a contenuti protetti.

flusso di autenticazione

Prima risposta: il problema non è React

Dato che React agisce lato client, avrai già capito che con React possiamo fare ben poco.
Si limiterà a interrogare un'API e comunicare con un servizio che lato server si preoccuperà dell'autenticazione.

Node.js, Express, Passport e Passport-SAML

Lavorando con React e JavaScript in generale, quale migliore occasione per mettere in campo Express in Node.js! Rendiamo quindi il nostro progetto diviso in due cartelle principali:

- backend
- frontend

Onomastica funzionale, nel folder backend metteremo (rullo di tamburi) tutto il contenuto del nostro applicativo Node, nel folder frontend l'applicativo react. Concentriamoci come anticipato sul backend. Ognuno ha il proprio cocktail di packages preferito, fondamentale per quanto stiamo cercando di fare, aggiungere i due packages seguenti:

npm install --save passport passport-saml

Contrariamente alle best-practises, in questo post configureremo un unico documento, app.js senza complicare il tutto separando in util, router ecc...
Mantengo tutto in un unico file per rendere più semplice seguire passo-passo, sentiti naturalmente libero di gestire l'app Node come meglio credi da questo punto di vista.

Key-concepts di partenza

A questo punto, gli elementi necessari per configurare il Service Provider sono:

  • Cert keys, da generare
  • Identity Provider meta data
  • Identity Provider entry point

Dobbiamo comunicare al Identity Provider:

  • pagina metadata, lo vediamo in seguito
  • indirizzo callback, ci arriviamo

Creiamo Cert Keys

Apri il tuo terminale preferito, creiamo le cert keys necessarie per far funzionare il giro:

mkdir certs
openssl req -x509 -newkey rsa:4096 -keyout certs/privatekey.pem -out certs/certificate.pem -nodes -days 900

Nello stesso folder, salvo anche i dati relativi all'Identity Provider, si trovano analizzando il suo metadata.xml nel tag X509Certificate, in un documento che chiamo idp_key.pem per coerenza.

Configuriamo la Passport Strategy

Ora ci dobbiamo concntrare sulla generazione del Metadata.xml, un insieme di tag da consegnare all'Identity Provider che, in genere, aggiunge al suo elenco di servizi in whitelist permettendoci l'autenticazione.
Prima di fare ciò, nel file app.js dobbiamo anzitutto configurare la strategy di passport.

const https_cert = fs.readFileSync('./certs/certificate.pem', 'utf-8');
const https_pvk = fs.readFileSync('./certs/privatekey.pem', 'utf-8');
const idp_key = fs.readFileSync('./certs/idp_key.pem', 'utf-8');

//  from idp's metadata
const idp_cert_1 = process.env.idp_cert1;
const idp_cert_2 = process.env.idp_cert2;

const shib_strategy = new saml.Strategy(
    {
        'callbackUrl': process.env.callbackUrl,
        'entryPoint': process.env.entryPoint,
        'issuer': process.env.appUrl,
        'cert': [idp_cert_1, idp_cert_2],
        'decryptionPvk': https_pvk,
        'privateKey': https_pvk,
        'privateCert': https_pvk,
        'validateInResponseTo': false,
        'disableRequestedAuthnContext': true,
        'identifierFormat': 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', 
        'authnContext': ['urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified'],
        'authnRequestBinding': 'HTTP-REDIRECT',
        'protocol': 'https://',
        'signatureAlgorithm': 'sha256',
        'acceptedClockSkewMs': -1
    }, 
    function (profile, done) 
    {
        console.log('passport.use() profile: %s \n', JSON.stringify(profile));
        return done(
            null, profile
            // potrebbe essere utile isolare parametri nell'oggetto profilo, quindi andiamo così:
            // { 
            //     'nameIDFormat': profile.nameIDFormat, 
            //     'nameID': profile.nameID,
            //     'id': profile.uid,
            //     'displayName': profile.cn,
            //     'firstName': profile.givenName,
            //     'lastName': profile.sn
            // }

        );
    }
);

passport.serializeUser(function (user, done) {
    console.log('passport.serializeUser() user: %s \n', JSON.stringify(user));
    done(null, user);
});

passport.deserializeUser(function (user, done) {
    console.log('passport.deserializeUser() user: %s \n', JSON.stringify(user));
    done(null, user);
});

passport.use(shib_strategy);

Ovviamente ho messo tutto in un bel .env per mantenere pulita la lettura, presta come sempre attenzione a questa fase e ricorda che qualsiasi modifica a tale file necessità il riavvio dell'applicativo.

Creiamo il nostro metadata.xml

Se tutto procede come desiderato, adesso dobbiamo poter generare un Metadata.xml, il cui contenuto servirà all'Idp per aggiungere la nostra fantastica applicazione alla sua whitelist.
Altro non è che una route da configurare, in modo da poterla visitare al suo indirizzo:

app.get(
    '/Shibboleth.sso/Metadata',
    function(req, res) {
        res.type('application/xml');
        res.send((shib_strategy.generateServiceProviderMetadata( https_cert, https_cert )));
    }
);

Routing

Siamo finalmente in grado di consegnare all'idp il nostro metadata.xml, la nostra app sta cominciando a prendere forma. Adesso dobbiamo configurare tutte le altre routes e, soprattutto, definire i comportamenti della callback una volta effettuata l'autenticazione, che ne so, redirect alla pagina Profile o il redirect alla risorsa protetta in generale... Sbizzarrisciti! Ahimè io invece ho poca fantasia e mi limiterò a reindirizzare su una generica pagina Profile, non è in realtà tanto differente da quanto accade autenticandoti con Spid.

app.get(
    '/', 
    (req, res) => {
        if (req.isAuthenticated()) {
            console.log('GET [/] user authenticated! req.user: %s \n', JSON.stringify(req.user));
            res.render('home', { user: req.user });
        } else {
            console.log('GET [/] user not authenticated! \n');
            res.render('home', { user: null });
        }
    }

);

app.get(
    '/login', 
    passport.authenticate('saml', { 'successRedirect': '/', 'failureRedirect': '/login' })
);


app.post(
    '/login/callback',
    passport.authenticate('saml', { 'failureRedirect': '/', 'failureFlash': true }),
    function(req, res) {
        console.log('POST [/login] \n');
        res.redirect('/profile');
    }
);

app.get(
    '/profile',
    function(req, res){
        if (req.isAuthenticated()) {
            console.log('GET [/profile] user authenticated! req.user: %s \n', JSON.stringify(req.user));
            res.render('profile', { user: req.user });
        } else {
            console.log('GET [/profile] user not authenticated! \n');
            res.redirect('/login');
        }
    }
);

app.get(
    '/logout',
    function(req, res) {
        console.log('GET [/logout] \n');
        passport._strategy('saml').logout(
            req, 
            function(err, requestUrl) {
                req.logout();
                res.redirect('/');
            }
        );
    }
);

Conclusione

Il backend funziona come ci aspettavamo, non resta che divertirsi con il frontend e mappare ogni route con react-router.

Spero sia riuscito a trovare le risposte che stavi cercando e che tu sia riuscito ad autenticare la tua app in Shibboleth con l'utilizzo di Node.js, Passport e Passport-SAML, in caso di problemi non esitare a contattarmi o guarda pure la repository in github.

Grazie per essere arrivato a leggere fino qui e alla prossima.

Spotify è spento
Enjoy the Silencetorna presto!