Protezione servizi rest su BTP mediante XSUAA

Nella scorsa puntata abbiamo visto come deployare un servizio REST sulla BTP basato su una immagine Docker. Questo servizio però è accessibile liberamente da internet, senza alcun tipo di protezione. Ovviamente sarebbe facile implementare qualche protezione casereccia, ma la BTP dispone di un sofisticato sistema di autenticazione e gestione permessi utente, quindi perchè non usare quello?

NOTA: il codice con cui ho sperimentato è nello stesso repository GitHub di quello del post precedente, ma in un diverso branch "xsuaa".

Un sistema di questo genere prevede 3 componenti:

  • l'applicazione da proteggere;
  • una istanza del servizio XSUAA della BTP, che gestisce autenticazione nonchè ruoli/permessi specifici per l'applicazione;
  • un application router, ovvero una applicazione nodejs che fa da proxy all'applicazione protetta, utilizzando il servizio XSUAA per effettuare il login con il protocollo OAUTH.

Istanza XSUAA

Prima di creare l'istanza XSUAA dobbiamo predisporre un file  json (convenzionalmente denominato "xs-security.json") che ne descrive i parametri. In questo file andremo a impostare il nome dell'applicazione che andremo a proteggere, la modalità ("dedicated" nel nostro caso) e la gerarchia dei ruoli relativi alla nostra applicazione.

La gerarchia dei ruoli della btp è composta da tre elementi:

  • scope: una singola funzionalità permessa nella nostra applicazione, come ad esempio la lettura o scrittura di una singola tipologia di oggetto o l'accesso a una specifica funzionalità.
  • role-template: un ruolo, che contiene una collezione di scopes.
  • role-collection: l'oggetto che potremo finalmente associare a un utente, che contiene una serie di ruoli.

Nel nostro esempio molto semplice ci servirà un solo oggetto per categoria.

Il nostro file avrà questo aspetto:

{
  "xsappname": "barcode-docker",
  "tenant-mode": "dedicated",
  "scopes": [
    {
      "name": "$XSAPPNAME.hello",
      "description": "Hello scope."
    }
  ],

  "role-templates": [
    {
      "name": "HelloRole",
      "description": "Role to say hello.",
      "scope-references": [
        "$XSAPPNAME.hello"
      ]
    }
  ],
  "role-collections": [
    {
      "name": "HelloCollection",
      "description": "Role collection to say hello.",
      "role-template-references": [
        "$XSAPPNAME.HelloRole"
      ]
    }
  ],
  "oauth2-configuration": {
    "redirect-uris": ["https://*.us10-001.hana.ondemand.com/**"]
  }
}

Nota: l'ultima parte (oauth2-configuration) è necessaria al funzionamento del login mediante oauth e deve riportare il dominio corrispondente alla zona in cui si trova il nostro account BTP (us10-001 è la zona geografica).

A questo punto dobbiamo creare l'istanza XSUAA. Entriamo nella BTP, nel nostro subaccount, e nello spazio su cui vogliamo installare l'applicazione. Dopodichè entriamo nel marketplace..

..cerchiamo l'applicazione..

..e creiamo una nuova istanza di tipo Application. Come nome per convenzione gli possiamo dare il nome della nostra applicazione principale seguito da "-xsuaa":

Nella schermata successiva possiamo caricare il json che abbiamo creato in precedenza.


Deployment applicazione

Per questo esempio utilizzeremo l'applicazione creata in un precedente post. L'unica differenza sarà che al momento del deployment dobbiamo specificare che questa applicazione ha come dipendenza il servizio XSUAA creato prima. Un modo comodo per ottenere questo effetto, nonchè semplificare il deployment, è utilizzare un file chiamato "manifest.yaml"; questo file conterrà tutte le informazioni necesssarie al deployment della nostra applicazione, e ci risparmierà la fatica di ricordarcele. Usiamo un manifest.yaml come questo:

---
applications:
  - name: barcode-docker
    memory: 512MB
    disk_quota: 512MB
    routes:
      - route: barcode-docker-ottimismo.cfapps.us10-001.hana.ondemand.com
    docker:
      image: piccimario/barcode-cf:latest
      username: piccimario
    services:
      - barcode-docker-xsuaa

Il file è autoesplicativo, si noti che riporta le stesse informazioni che prima dovevamo specificare a riga di comando durante il deployment. La "route" è usata per definire un dominio per la nostra applicazione diverso dal nome dell'applicazione stessa, nel caso questo nome sia già in uso. Unico aspetto degno di nota la sezione "services" con il nome della nostra istanza XSUAA.

A questo punto basta dare il comando:

cf push

..e per magia la nostra applicazione è già deployata sulla BPT (prima bisogna comunque fare il login con "cf login", naturalmente).


Application Router

L'approuter è una applicazione NodeJS di SAP installabile mediante package NPM, che tra le altre cose fa da proxy alla nostra applicazione principale limitandone l'accesso agli utenti autenticati.

Per crearla ci spostiamo in una cartella vuota e creiamo un file "package.json":

{
    "name": "barcode-docker-approuter",
    "scripts": {
        "start": "node node_modules/@sap/approuter/approuter.js"
    },
    "dependencies": {
        "@sap/approuter": "^12.0.0"
    }
}

Come nome abbiamo scelto "barcode-docker-approuter". Si noti la singola dipendenza da @sap/approuter. A questo punto lanciamo:

npm install

...e gli lasciamo scaricare le librerie. Nel frattempo creiamo un file "xs-app.json", che contiene la configurazione dell'approuter:

{
    "routes": [
        {
            "source": "/",
            "target": "/",
            "destination": "dest-barcode-docker"
        }
    ]
}

Come destination andiamo a mettere un nome qualunque, che poi nel successivo file andremo a mappare sull'url della nostra applicazione principale.

Infine creiamo un file "manifest.yaml" che contiene i parametri di configurazione/deployment. Usiamo il buildpack nodejs_buildpack, specifico per le applicazioni nodejs. Nella sezione destinations andiamo a riportare il nome destinazione scelto prima e la route della nostra applicazione principale. Importante il parametro forwardAuthToken, necessario per far si che il token di autenticazione, una volta verificato dal router, sia poi inoltrato alla nostra applicazione che lo può ulteriormente verificare. Anche in questo caso associamo tra i services la nostra istanza xsuaa.

---
applications:
- name: barcode-docker-approuter
  path: /
  memory: 128M
  routes:
    - route: barcode-docker-approuter.cfapps.us10-001.hana.ondemand.com
  buildpacks:
    - nodejs_buildpack
  env:
    destinations: > 
      [
        {
          "name":"dest-barcode-docker", 
          "url" :"https://barcode-docker-ottimismo.cfapps.us10-001.hana.ondemand.com", 
          "forwardAuthToken": true
        }
      ]
  services:
    - barcode-docker-xsuaa

A questo punto abbiamo tutto e possiamo deployare anche l'approuter con il solito

cf push

Le attività di cui sopra non hanno influito direttamente sulla sicurezza della nostra nostra applicazione (che può sempre essere acceduta mediante la sua route). Se però utilizziamo la route dell'approuter in una finestra anonima, noteremo che al primo accesso viene chiesto di effettuare l'autenticazione col nostro account BTP, e solo a quel punto potremo utilizzare l'endpoint.

Dietro le quinte, l'approuter nota che l'utente non è autenticato e lo redirige verso le pagine di autenticazione; a quel punto nel browser dell'utente viene salvato un token crittografico che sarà inviato durante tutti i successivi accessi, e sarà questo a identificare il login effettuato.


Il passo successivo è quello di proteggere la nostra applicazione dall'interno. Come abbiamo visto, si può ancora accedere senza passare dall'application router, e quindi bypassando questo controllo. E' necessario che anche l'applicazione verifichi la presenza (e regolarità) del token crittografico descritto sopra. Per fortuna ci sono delle comode librerie allo scopo.

Per prima cosa dobbiamo installare le seguenti librerie (nel nostro esempio le dovremo mettere nel Dockerfile, nel punto in cui vengono installate con pip anche le altre librerie).

cfenv 
sap-xssec

La libreria CFENV consente di interagire con la BTP per ottenere informazioni in merito all'applicazione in esecuzione. In questo esempio la useremo per farci dare il nome e i parametri del servizio XSUAA connesso all'applicazione.

La libreria SAP-XSSEC effettua le verifiche vere e proprie, come ad esempio verificare che l'utente loggato abbia a disposizione lo scope "hello" che abbiamo descritto quando abbiamo creato l'istanza XSUAA.

Un semplice esempio di codice, adattato dal nostro tutorial precedente:

...
from cfenv import AppEnv
from sap import xssec

env = AppEnv()
uaa_service = env.get_service(name='barcode-docker-xsuaa').credentials


@app.route('/')
def hello():

	auth_header = request.headers.get('Authorization')

	if not auth_header:
		return "Errore: nessun header autorizzativo!", 403

	auth_token = auth_header.split(" ")[1]
	security_context = xssec.create_security_context(auth_token, uaa_service)

	isAuthorized = security_context.check_scope(f"{uaa_service['xsappname']}.hello")

	return f"Benvenuto {security_context.get_logon_name()}, {isAuthorized}"

Il codice prima della route serve ad accedere alla configurazione del servizio xsuaa (barcode-docker-xsuaa).

All'interno della route, andiamo ad estrarre il token autorizzativo dall'header "Authorization". Se non c'è, restituiamo errore. Se c'è, estraiamo la seconda parte (il token vero e proprio) e lo diamo in pasto a XSSEC. A quel punto usiamo il metodo "check_scope" per verificare la presenza del nostro scope nell'utente corrente.

Ricreiamo l'immagine docker e la ricarichiamo sulla btp seguendo i passaggi descritti nel post precedente.

A quel punto proviamo ad accedere all'applicazione SENZA usare il router, e saremo correttamente bloccati da un messaggio di errore.

Adesso proviamo ad accedere passando dal router. Dopo aver effettuato il login, avremo questo risultato:

Il login è andato a buon fine e abbiamo potuto accedere all'endpoint, ma quel "false" alla fine indica che abbiamo fallito la verifica dello scope. Questo perchè ovviamente non abbiamo assegnato la role collection al nostro utente.

Dell'interno del nostro subaccount andiamo nel menu users e selezioniamo il nostro utente. Nella sezione che si aprirà a destra dello schermo, cerchiamo le role collections e aggiungiamo la nostra.

A questo punto proviamo ad aprire una nuova finestra in incognito (per cancellare le cache) e accedere di nuovo al nostro approuter. Dopo aver effettuato il login ci troviamo davanti a questo risultato:

Funziona!