Problema di progettazione dell'API: per REST o non per REST

4

Mi piacerebbe progettare un API REST che supporti:

  • Accedi
  • Generazione di token temporanei

La ragione è che ci sono un certo numero di librerie REST lato client che accelererebbero lo sviluppo se usate, poiché si occupano della serializzazione, della connessione, ecc. Se non utilizzate, dovremmo codificare queste parti separatamente. Non ho bisogno di REST per le prestazioni, non ci sarà il bilanciamento del carico o il caching nel lato server.

Il login e la generazione di token sono abbastanza comuni. Eppure sto attraversando un periodo difficile cercando di capire come eseguirli in REST. Ad esempio:

Accedi

Ho letto molto sul login in REST e apparentemente non c'è una risposta giusta qui. Alla fine molte persone finiscono per usare OAuth solo perché è mainstream. In realtà ho solo bisogno di verificare se l'utente esiste e la password è corretta. Qualsiasi altra operazione nell'API passerà il nome utente e la password per scopi di autorizzazione, quindi per ora siamo ancora in parte senza stato.

Il problema qui è che ho immaginato l'accesso come una query contro la raccolta degli utenti:

GET https://api.example.com/users?usr=username&psw=1234

Il server risponderà con un elenco filtrato di un singolo utente.

Ma non mi piace passare la password nella stringa di query. Non mi sembra giusto. La connessione verrà effettuata utilizzando HTTPS , ma c'è un ulteriore passaggio di dover codificare URL tutti i caratteri dispari e decodificarli nel server, cosa che non avremo in un POST. E anche gli URL sono registrati più frequentemente dei payload.

Potrei anche ottenere la password nella risposta del server:

GET https://api.example.com/users?usr=username

L'oggetto JSON restituito conterrebbe tutti i campi per l'utente (id, password, ecc.), quindi potrei verificare la password nel client.

Quale è meglio? Qualche alternativa?

Generazione token

Un utente registrato è in grado di generare token temporanei. Sto lottando cercando di forzare le pedine ad essere una risorsa. Vengono generati al volo e l'operazione non è idempotente, poiché ogni richiesta di token successiva restituirà un token diverso. A peggiorare le cose, questa operazione è stateful: ci sarà una tabella temporanea nel back-end in cui i token saranno memorizzati per un periodo di tempo. Quindi quale sarebbe la versione REST? Potrebbe essere un PUT se il client fosse quello che lo genera, ma è il server.

TL; DR Questo è così difficile. Se riesco a creare una versione REST di questa API, il codice client sarà più breve e più chiaro, poiché utilizzeremo librerie note ampiamente testate. Ma onestamente sembra quasi impossibile forzare lo stato in apolidi. Forse dovrei semplicemente rinunciare e fornire una semplice API di stato? Come potrei spiegare i tempi supplementari necessari alla direzione? Probabilmente sosterranno che oggigiorno tutto è REST.

    
posta Mister Smith 18.01.2016 - 12:03
fonte

5 risposte

2

Prima di tutto, lasciatemi dire che sono d'accordo al 100% con @MvdD , che un'API REST dovrebbe non include la semantica login / logout. Tuttavia, siccome sono sicuro che hai sentito / letto OAuth è un mal di testa che davvero non vuoi se non sei "obbligato" ad avere un'autenticazione federata!

Come hanno sottolineato altre risposte, non esiste un modello per raggiungere questo obiettivo, dato che molti sono stati accettati come risposte in precedenti domande simili (principalmente da SO). Suppongo anche che tu abbia esaminato domande precedenti correlate e simili. C'è anche questa domanda (RESTful Authentication) su SO , che dettagli 4 tecniche (alcune più resistenti di altre). Se sei (apparentemente?) Felice di trasmettere la password come testo in chiaro in HTTPS, l'opzione n. 1 di tale risposta (autenticazione di base HTTP) funzionerebbe praticamente con qualsiasi libreria HTTP che i tuoi strumenti REST stanno eseguendo.

Se devi ...

Dato che c'è una taglia e tutto (e perché è una sfida interessante), ci ho pensato e (dopo circa 1,5 tazze di caffè) ho avuto la seguente idea: POST a una risorsa "sfida", che quindi reindirizza a un identificatore di risorsa per che sfida, (e questo non è idempotente).

Di cosa sto parlando?

Bene, come sottolineato qui (Tabella 1, pagina 3), risorsa < em> identificatori sono diversi da risorse stessi. Ergo, puoi RESTful-ly POST a un URI ma stai causando una modifica ( ie modifica RESTful, come CREATE, UPDATE) in una risorsa non necessariamente localizzata lì . Lasciami spiegare in azione ...

Processo di autenticazione :

  1. Il cliente invia POST /challenges con il carico utile di username . Ciò fa sì che il server crei la voce back-end necessaria nella tabella DB, alloca risorse, ecc.
  2. Il server risponde con un 303 See Other (che è solo un nuovo versione di 302 Moved temporarily ), indirizzando il cliente a /challenges/<some random request code> .
  3. Cliente GET 's /challenges/<some random request code> (Nota, sarà essere GET, poiché 303 è usato invece di 302 ). Che restituisce (con 200 OK ) una sfida con codifica JSON (maggiori informazioni in seguito).
  4. Il cliente risponde alla sfida, questa volta con POST su /challenges/<some random request code> con un hash che soddisfa la sfida come carico utile.
  5. Il server risponde con 403 Forbidden (se sfida fallita , ergo, password errata, ecc.) o 200 OK (facoltativamente con un payload JSON contenente le informazioni sulla sessione o il profilo utente o what- hanno-te).

Una parola sulle sfide : (come promesso al passaggio n. 3)

La sfida data dal server è fondamentalmente solo un'attività (di solito hashing, ma probabilmente una crittografia / decrittografia a due vie, ecc.) fornita dal server al client. Il server conosce (calcola) il risultato (quindi perché gli hash sono buoni, dato che sono economici) in anticipo, e lo confronta con la risposta alla sfida fornita dal cliente.

Questo può essere semplice come un testo casuale (o anche un testo statico, come il nome dell'applicazione, ecc.) inviato al client in testo semplice (o hash, non importante) con il server che ha elaborato l'hash / em> di quel testo (essenzialmente un salt ) concatenato con la password (hash, dal momento che è nel DB - e non memorizziamo password in chiaro nei database). Se il client è in grado di concatenare questo sale con la password (dopo aver eseguito l'hashing della password localmente) e poi ha cancellato quella concatenazione, può rispondere alla sfida.

Esempio:

  • server: salt = md5 ("MaryHadALittleLamb") - > 8c20828418ca489f5b949f25f35abaa0 (invia al client al punto # 3).
  • client: hash la password " password " ( "username" è un notoriamente non sicuro selettore di password ) per produrre 5f4dcc3b5aa765d61d8327deb882cf99 , questo viene poi concatenato con 8c20828418ca489f5b949f25f35abaa0 per produrre " 5f4dcc3b5aa765d61d8327deb882cf998c20828418ca489f5b949f25f35abaa0 ".
  • client: blocca questo md5("5f4dcc3b5aa765d61d8327deb882cf998c20828418ca489f5b949f25f35abaa0") - > 09f3bb66a44153c4053857d4b57fdf3b e invia al server (passo # 4).
  • server: (avendo già eseguito la stessa operazione) confronta 09f3bb66a44153c4053857d4b57fdf3b con il proprio risultato e (se corrispondono) consente al nome utente di accedere (o iniziare una sessione, ecc.)

Nota : questo è un modo abbastanza ingenuo per implementare le sfide, in quanto non c'è alcuna componente temporale del sale è vulnerabile agli attacchi di riproduzione, ecc. Ma puoi chiedere di più su Security.SO se bisogno di essere.

    
risposta data 05.02.2016 - 01:08
fonte
9

Per le password, l'invio di una copia hash della password sembra essere la tua migliore alternativa. Nota che questo evita due dei problemi che stai considerando: l'hash della password nasconde il "segreto" che stai trasmettendo, e il risultato dell'hash può essere espresso in un set di caratteri ragionevole in modo da non doverti preoccupare su codifica / decodifica. (Nota: non abbiamo effettivamente aggiunto alcuna sicurezza in questo modo).

Sono sospettoso nell'usare l'url per comunicare le credenziali - perché non usare l'intestazione Authorization? Hai scritto:

Any other operation in the API will be passed the user name and password for authorization purposes

Ti aspettavi di incorporare le credenziali in ogni chiamata API?

the operation is not idempotent

Quindi l'operazione è un POST. La risposta più comune che ho visto è quella di esporre una risorsa "raccolta token" e aggiungere un nuovo token pubblicando la richiesta su quella risorsa. Il principio della sorpresa minima dovrebbe forse inclinarti in questa direzione.

Ma potresti riesaminare la tua ipotesi che l'operazione non sia idempotente. Chiaramente, non vuoi avere più token nella tua tabella, ma se il database rifiuta il comando di iniettare un token duplicato, hai il comportamento idempotenziale di cui hai bisogno. Dovrai essere coerente con la semantica HTTP put, il che significa che vorrai che la risorsa che stai PUTting sia univoca per il token.

Si noti che la risorsa token e l'entità token sono due cose diverse. È possibile "creare" prima la risorsa, quindi (se l'applicazione client segue quel collegamento) creare il token. REST non si cura - finché il client sta seguendo i collegamenti forniti nell'ipermedia, tutto è buono.

I'm struggling trying to force tokens into being a resource.

Non dovrebbe essere troppo difficile, le risorse sono economiche.

Ad esempio, se il tuo token è un analogo di un cookie HTTP (una delle parti del web che Fielding chiama non essere riposante), un flusso di richieste dal client potrebbe essere simile a

POST /A
-- the server does its magic here, generating and storing a new token, which
-- it wants the client to reference in its subsequent requests
redirect: /B?token=54321

GET /B?token=54321
-- Now the server knows that this is a request for resource /B within the
-- context of the specific token.  The representation of this resource
-- includes links to things that are also in the context of the token
returns: [representation including link /C?token=54321]

GET /C?token=54321
-- Subsequent calls stay in token=54321 space, until the server expires the
-- token and redirects the caller to some other representation of state.

Un'altra alternativa sarebbe quella di separare la prenotazione dell'identificatore token dalla creazione del token

GET /A
-- here, the server generates a unique identifier for the token, without
-- doing any of the persistence work.
redirect: /B?token=54321

POST /B?token=54321
-- The client passes back to the server the id of the token, and the server
-- can choose to go create it and store it.  
-- Notice: we aren't posting to the token resource, we are posting to 
-- the resource identified by /B?token=12345; you get to decide what
-- that resource is.  If the representations are suitable, and you can make
-- the operation idempotent now that the token identifier is fixed, you
-- might be able to use PUT rather than POST
returns: [representation including link /C?token=54321]

GET /C?token=54321
-- Subsequent calls stay in token=54321 space, until the server expires the
-- token and redirects the caller to some other representation of state.

Se si desidera essere più attenti, è possibile implementare risorse che distinguono un token riservato da un token persistito .

GET /A
-- here, the server generates a unique identifier for the token, without
-- doing any of the persistence work.  We're not making any changes to
-- the server state right now, so this is a reserved token.
redirect: /A?reservedToken=54321

GET /A?reservedToken=54321
-- this resource knows the token identifier, so it can now produce the
-- hypermedia control(s) that constrain the client to the reservedToken=54321 space.
-- We still haven't changed anything; this GET request is safe.
-- It's also potentially cacheable.
returns: [representation including link /B?reservedToken=54321]

POST /B?reservedToken=54321
-- The client passes back to the server the id of the token, and the server
-- can choose to go create it and store it.  
-- Notice: we aren't posting to the token resource, we are posting to 
-- the resource identified by /B?reservedToken=12345; you get to decide what
-- that resource is.  If the representations are suitable, and you can make
-- the operation idempotent now that the token identifier is fixed, you
-- might be able to use PUT rather than POST.
redirect: /B?persistedToken=54321

GET /B?persistedToken=54321
-- Now this resource knows that the token has already been reserved, and
-- can generate additional hypermedia controls that constrain the 
-- client to the persistedToken=54321 space.  Once again, this GET
-- request is safe and cacheable.
returns: [representation including link /C?persistedToken=54321]

GET /C?persistedToken=54321
-- Subsequent calls stay in persistedToken=54321 space, until the server expires the
-- token and redirects the caller to some other representation of state.

Il client sta semplicemente scegliendo tra i controlli hypermedia forniti dal server. Quindi, mentre questa storia sarebbe simile a

GET /A
GET /A?reservedToken=54321
POST /B?reservedToken=54321
GET /B?persistedToken=54321
GET /C?persistedToken=54321

In seguito puoi decidere che se questi identificatori di risorse non sono conformi agli standard di codifica, o che sono difficili da seguire, o semplicemente che sei annoiato da loro, puoi cambiarli tutti

GET /A
GET /W?reservedToken=54321
POST /X?reservedToken=54321
GET /Y?persistedToken=54321
GET /Z?persistedToken=54321
    
risposta data 18.01.2016 - 15:37
fonte
8

GET http://api.example.com/users?usr=username

The returned JSON object would contain all the fields for the user (id, password, etc), so I could check the password in the client.

Congratulazioni, hai rotto subito le 3 regole fondamentali della sicurezza di base!

  • non memorizza mai password chiare
  • non inviarli mai su http, ma solo su https
  • non fidarti mai dei clienti

Perché non creare semplicemente /login e /logout query?

Tutte le chiamate REST successive invieranno quindi l'id di sessione come cookie per "proove" l'accesso. La gestione delle sessioni è qualcosa di fondamentale nella maggior parte dei framework web. BACIO.

... o la tua argomentazione sarebbe "perché tutto dovrebbe essere REST e apolide" ?! Beh, pensi davvero che abbia senso? ... ci sono molte cose che non sono REST e non lo saranno mai. Ad esempio /send_email o /generate_token ... Penso che tu abbia interpretato erroneamente le cose in modo troppo dogmatico che tutto dovrebbe essere REST, ecc. REST è per accedere alle tue risorse, non fare azioni, ecc.

Infine, l'invio di username / password ogni volta non è molto diverso rispetto all'invio del tuo ID di sessione in un cookie. Uno è davvero più apolide o statico dell'altro? Sì. Fa la differenza? No.

Per quanto riguarda la parte OAuth, ho sentito che è così complesso che gli autori delle specifiche originali volevano rimuovere il loro nome da esso: link

Dubito che sia più semplice e richiede un ID di sessione esattamente come il solito login.

    
risposta data 29.01.2016 - 11:44
fonte
6

In genere, l'API REST non dovrebbe supportare alcuna funzionalità di accesso. Basta richiedere al chiamante di fornire un token al portatore da un emittente fidato nel Authorization header e sii fatto con esso. Utilizza un fornitore di autorizzazioni noto come Google , Facebook o Microsoft Azure AD come server di autorizzazione.

Le persone non usano OAuth perché è mainstream. È mainstream perché consente agli sviluppatori di delegare l'autenticazione e non devono preoccuparsi dell'archiviazione della password e dell'implementazione del protocollo sicuro.

Se per qualche motivo devi implementare questa funzionalità in un servizio REST stesso, vorrei solo esporre una risorsa /token su HTTPS a cui puoi POST un nome utente e una password. Da tale richiesta di token, restituisci un token con firma digitale che contiene le informazioni sull'utente di cui l'API ha bisogno, è valido per un numero limitato tempo e che la tua API si fida.

    
risposta data 29.01.2016 - 17:25
fonte
2

L'invio di una password nei parametri non è solo "non mi sembra giusto", è la cosa peggiore che potresti fare, e io (e chiunque altro se ne accorge) smetterebbe di usare il tuo servizio immediatamente a causa di un brutale violazione della sicurezza.

Se si restituisce la password al client, si tratta di una violazione della sicurezza ancora peggiore: il server non deve MAI conoscere la password. Non deve MAI essere in grado di restituire la password al client.

Un metodo comune è questo: si crea un certificato per la crittografia. La chiave pubblica è distribuita con il client, la chiave privata è sul server. Il login crittografa il nome utente e la password con la chiave pubblica e li invia a te. È possibile decodificare il nome utente e la password sul server. Quindi si trasforma la password in un hash salato e si controlla che corrisponda al nome utente. In tal caso, si crea un token casuale e lo si invia al client. Il client ricorda il token e lo invia ad ogni richiesta. Decidi per quanto tempo il token rimane valido.

Fondamentalmente ogni richiesta richiede il token (con evidenti eccezioni come login, creare account, reimpostare la password).

Sembra che tu ti ritrovi in un pò di stato con token che non sono apolidi. REST non è un dogma. I token sono per l'autenticazione, sono al di fuori del resto dell'API REST. Dovrebbero essere stateful. Niente di male in tutto ciò.

    
risposta data 29.01.2016 - 09:52
fonte

Leggi altre domande sui tag