OAuth in Python per testare servizi su SAP BTP

Nelle scorse puntate ho realizzato un servizio REST su BTP, e l'ho protetto utilizzando il servizio XSUAA della BTP che implementa la protezione mediante OAuth.

Se il servizio è accessibile da browser, l'approuter si occupa di gestire tutte le fasi dell'autenticazione, compreso il rimando verso la pagina di login di SAP e l'invio del token al servizio. Ma se voglio richiamare il servizio senza passare dal browser, ad esempio usando uno script Python, devo occuparmi io di chiedere al server il token da utilizzare per effettuare le chiamate.

Lo standard OAuth 2.0 prevede diversi flussi di autenticazione. In questi esperimenti ne ho provati due, i più semplici: il Resource Owner Password Flow e il Client Credentials Flow.


Il Resource Owner Password Flow è utilizzato quando il client (ovvero chi vuole accedere al servizio) è sufficientemente fidato da poter avere accesso diretto alle credenziali dell'utente. Non è usati nei normali flussi autorizzativi app-app (ad esempio, se voglio autorizzare Facebook ad accedere alle mie foto su Google Drive, non voglio certo inserire in Facebook la mia password Google!), ma è accettabile se, per esempio, il client è uno script costruito su misura per un utilizzo specifico e in esecuzione in un ambiente privato.

In questo caso, per ottenere il token autorizzativo si effettua una richiesta di tipo "password" al server XSUAA. Bisogna fornire le credenziali dell'utente e le credenziali del client, così come registrate nel server XSUAA (client id e client server). Queste informazioni sono reperibili nel servizio XSUAA nella BTP (sezione "Instances and Subscriptions"), premendo il pulsante "Show Credentials".

A questo punto, nel nostro script Python possiamo fare questa richiesta al server di autenticazione:

print("Richiesta token con grant: PASSWORD")

token_req_payload = {
  'grant_type': 'password',
  'username': username,
  'password': password,
}

token_response = requests.post(
  auth_server_url + '/oauth/token',
  data = token_req_payload,  
  allow_redirects = False,
  auth = (client_id, client_secret)
)

tokens = json.loads(token_response.text)
access_token = tokens['access_token']

Se tutto è andato per il verso giusto, ora possiamo chiamare direttamente il nostro servizio (NON l'application router) usando il token:

test_api_url = "https://XXXXXXX.cfapps.us10-001.hana.ondemand.com/"
api_call_headers = {'Authorization': 'Bearer ' + access_token}
api_call_response = requests.get(test_api_url, headers=api_call_headers)

print(api_call_response.text)

La richiesta sarà gestita con le autorizzazioni dell'utente di cui abbiamo usato le credenziali.


Il Client Credentials Flow è diverso dal precedente poichè in questo caso non si utilizzano le credenziali di un utente ma solo le credenziali dell'applicazione. Questo flusso è infatti pensato per autenticare un servizio verso un altro servizio, senza dargli per esempio accesso diretto ai dati di un utente. La chiamata da effettuare è più semplice:

print("Richiesta token con grant: CLIENT_CREDENTIALS")

token_req_payload = {
  'grant_type': 'client_credentials'
}

token_response = requests.post(
  auth_server_url + '/oauth/token',
  data = token_req_payload, 
  allow_redirects = False,
  auth = (client_id, client_secret)
)

tokens = json.loads(token_response.text)
access_token = tokens['access_token']

Una volta ottenuto il token possiamo continuare come visto in precedenza.


Gestire permessi a livello di applicazione Python su BTP

Come visto in passato, la presenza del token non è necessaria per accedere all'applicazione, che comunque è accessibile sulla rete pubblica. Il tassello fondamentale per la sicurezza è che l'applicazione stessa, alla ricezione della richiesta, verifichi (utilizzando il server XSUAA) il token ricevuto per capire se è valido e di che tipo è, e se è legato o meno a un utente (o è un client credentials).

Una modalità semplice per effettuare questa verifica è prevedere nella nostra applicazione basata su Flask un metodo decorato con "@app.before_request". Questo metodo è eseguito prima di qualunque richiesta in entrata, ed è un buon posto per analizzare le credenziali e salvare le informazioni in una variabile globale accessibile agli altri metodi. Una cosa del genere:

import ...
from sap import xssec

# Global variables
SECURITY_CONTEXT = "security_context"
globals = {}

@app.before_request
def check_credentials():

  global globals

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

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

  try:
    auth_token = auth_header.split(" ")[1]
    globals[SECURITY_CONTEXT] = xssec.create_security_context(auth_token, uaa_service)
  except:
    return "Errore: token errato!", 403

Questo metodo ha questa caratteristica: se ritorna qualcosa, quel qualcosa viene restituito e termina immediatamente la chiamata in corso. Quindi, se troviamo dei problemi possiamo semplicemente restituire un 403 Unauthorized e tutto si ferma qui.

A questo punto, i nostri singoli endpoint possono analizzare l'oggetto globale e chiamarne metodi per verificare, per esempio, la presenza di uno scope o il nome utente (o l'assenza, nel caso di client credentials). Un esempio potrebbe essere una cosa del genere:

@app.route('/', methods=['GET'])
def info():

  global globals 

  isAuthorized = globals[SECURITY_CONTEXT].check_scope(f"{uaa_service['xsappname']}.split")
  grant_type = globals[SECURITY_CONTEXT].get_grant_type()
  logon_name = globals[SECURITY_CONTEXT].get_logon_name()

  text = (
    f"<h1>Barcode services up&running!</h1>"
    f"<ul>"
    f"<li>Split scope available: {isAuthorized}</li>"
    f"<li>Grant type: {grant_type}</li>"
    f"<li>Logon name: {logon_name}</li>"
    f"</ul>"
  )

  return(text)

Sarà poi l'applicazione a decidere cosa rendere disponibile all'esterno, ad esempio differenziando per grant type o per nome utente.

Alla prossima!