dilema de design da API: para REST ou não REST

4

Gostaria de criar uma API REST que suporte:

  • Login
  • Geração temporária de tokens

O motivo é que existem várias bibliotecas REST do lado do cliente que acelerariam o desenvolvimento se usadas, já que elas cuidam da serialização, da conexão etc. Se não fossem usadas, teríamos que codificar essas partes separadamente. Eu não preciso de REST para desempenho, não haverá balanceamento de carga ou cache envolvido no lado do servidor.

Login e geração de token são bastante comuns. E ainda estou tendo dificuldades tentando descobrir como fazê-las no REST. Por exemplo:

Login

Eu li muito sobre o login no REST, e aparentemente não há resposta certa aqui. No final, muitas pessoas acabam usando o OAuth apenas porque é mainstream. Na verdade, só preciso verificar se o usuário existe e a senha está correta. Qualquer outra operação na API receberá o nome de usuário e a senha para fins de autorização, portanto, por enquanto, ainda estamos no lado sem estado.

O problema aqui é que eu imaginei o login como uma consulta contra a coleção de usuários:

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

O servidor responderá com uma lista filtrada de um único usuário.

Mas não gosto de passar a senha na string de consulta. Não parece bom para mim. A conexão será feita usando HTTPS , mas há uma etapa adicional de codificar todos os caracteres ímpares na URL e decodificá-los no servidor, o que não teremos em um POST. E também URLs são registrados com mais freqüência do que cargas úteis.

Eu também consegui a senha na resposta do servidor:

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

O objeto JSON retornado conteria todos os campos para o usuário (id, senha, etc), para que eu pudesse verificar a senha no cliente.

Qual é o melhor? Alguma alternativa?

Geração de token

Um usuário registrado é capaz de gerar tokens temporários. Eu estou lutando tentando forçar tokens em ser um recurso. Eles são gerados em tempo real e a operação não é idempotente, pois cada solicitação de token subseqüente retornará um token diferente. Para piorar as coisas, esta operação é stateful: haverá uma tabela temporária no backend onde os tokens serão armazenados por um período de tempo. Então qual seria a versão REST? Poderia ser um PUT se o cliente fosse o gerador, mas é o servidor.

TL; DR Isso é tão difícil. Se eu conseguir criar uma versão REST desta API, o código do cliente será mais curto e mais claro, já que estaremos usando bibliotecas conhecidas que foram extensivamente testadas. Mas honestamente parece quase impossível forçar o stateful a ficar sem estado. Talvez eu deva desistir e fornecer uma API simples e stateful? Como eu poderia explicar o tempo extra que levaria para a gerência? Eles provavelmente argumentarão que hoje em dia tudo é REST.

    
por Mister Smith 18.01.2016 / 12:03
fonte

5 respostas

2

Primeiro, deixe-me dizer que concordo 100% com @MvdD , que uma API REST deve não inclui semântica de login / logout. No entanto, como tenho certeza de que você já ouviu / leu OAuth é uma dor de cabeça que você realmente não quer se não for "obrigatório" para ter autenticação federada!

Como outras respostas apontaram, não há um padrão para conseguir isso, com vários sendo aceitos como respostas em questões semelhantes anteriores (principalmente do SO). Também presumo que você tenha analisado perguntas relacionadas anteriores e tal. Há também esta pergunta (Autenticação RESTful) em SO , que detalha 4 técnicas (algumas mais RESTful do que outras). Se você está (aparentemente?) Feliz em transmitir a senha como texto puro em HTTPS, a opção nº 1 dessa resposta (autenticação básica HTTP) funcionaria com praticamente qualquer biblioteca HTTP que suas ferramentas REST estivessem envolvendo.

Se você precisa ...

Desde que há uma recompensa e tudo (e porque é um desafio interessante), eu pensei sobre isso, e (depois de cerca de 1,5 xícaras de café) tive a seguinte idéia: POST para um recurso "desafio", que então redireciona para um identificador de recurso para esse desafio, (e não, isso não é idempotente).

Do que estou falando?

Bem, como apontado aqui (Tabela 1, página 3), recurso < em> identificadores são diferentes para recursos . Ergo, você pode RESTful-ly POST para um URI mas estar causando uma alteração ( ie alteração RESTful, como CREATE, UPDATE) em um recurso não necessariamente localizado lá . Deixe-me explicar em ação ...

Processo de autenticação :

  1. O cliente envia POST /challenges com carga útil de username . Isso aciona o servidor para criar a entrada de backend necessária na tabela de banco de dados, alocar recursos, etc.
  2. O servidor responde com um 303 See Other (que é apenas uma nova versão 302 Moved temporarily ), direcionando o cliente para /challenges/<some random request code> .
  3. GET do cliente /challenges/<some random request code> (Observação, será ser GET, pois 303 é usado em vez de 302 ). Que retorna (com 200 OK ) um desafio codificado em JSON (mais sobre isso depois).
  4. O cliente responde ao desafio, desta vez com POST a /challenges/<some random request code> com um hash que atende ao desafio como carga útil.
  5. O servidor responde com 403 Forbidden (se o desafio falhou , ergo, senha incorreta etc.) ou 200 OK (opcionalmente com uma carga útil JSON contendo suas informações da sessão ou perfil do usuário ou o que você tem).

Uma palavra sobre Desafios : (Conforme prometido na etapa 3)

O desafio dado pelo servidor é basicamente apenas uma tarefa (geralmente hashing, mas possivelmente uma criptografia / descriptografia bidirecional, etc) dada pelo servidor ao cliente. O servidor sabe (calcula) o resultado (daí porque os hashes são bons, já que são baratos) antes do tempo, e compara isso com o desafio-resposta dado pelo cliente.

Isto pode ser tão simples quanto algum texto aleatório (ou mesmo texto estático, como o nome do seu aplicativo, etc) enviado ao cliente em texto puro (ou hash, não importante) com o servidor tendo trabalhado com o hash desse texto (essencialmente um salt ) concatenado com a senha (hash, já que está no banco de dados - e nós não armazenamos senhas em texto simples em bancos de dados). Se o cliente puder concatenar este salt com a senha (depois de fazer o hashing da senha localmente) e depois fazer o hash dessa concatenação, ele poderá responder o desafio.

Exemplo:

  • servidor: salt = md5 ("MaryHadALittleLamb") - > 8c20828418ca489f5b949f25f35abaa0 (envia para o cliente na etapa 3).
  • cliente: hashes a senha " password " ( 'nome de usuário' é um seletor de senha notoriamente inseguro ) para produzir 5f4dcc3b5aa765d61d8327deb882cf99 , isso é concatenado com 8c20828418ca489f5b949f25f35abaa0 para produzir " 5f4dcc3b5aa765d61d8327deb882cf998c20828418ca489f5b949f25f35abaa0 ".
  • cliente: hashes este md5("5f4dcc3b5aa765d61d8327deb882cf998c20828418ca489f5b949f25f35abaa0") - > 09f3bb66a44153c4053857d4b57fdf3b e envia para o servidor (etapa 4).
  • servidor: (tendo já feito o acima mencionado) compara 09f3bb66a44153c4053857d4b57fdf3b com seu próprio resultado e (se eles corresponderem) permite que o nome de usuário faça o login (ou inicie uma sessão, etc.).

Nota : Esta é uma maneira bastante ingênua de implementar desafios, já que não há nenhum componente temporal do salt que é vulnerável a ataques de repetição, etc. Mas você pode perguntar mais sobre isso no Security.SO se precisa ser.

    
por 05.02.2016 / 01:08
fonte
9

Para senhas, enviar uma cópia hash da senha parece ser sua melhor alternativa. Observe que isso evita dois dos problemas que você está considerando: o hash da senha disfarça o "segredo" que você está transmitindo e o resultado do hash pode ser expresso em um conjunto de caracteres razoável para que você não precise se preocupar sobre codificar / decodificar. (Nota: na verdade, não adicionamos nenhuma segurança ao fazer isso).

Suspeito de usar o URL para comunicar as credenciais. Por que não usar o cabeçalho de autorização? Você escreveu:

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

Você esperava incorporar as credenciais em todas as chamadas de API?

the operation is not idempotent

Então a operação é um POST. A resposta mais comum que vi é expor um recurso de "coleção de tokens" e adicionar um novo token postando a solicitação nesse recurso. Princípio da menor surpresa talvez deva inclinar você nessa direção.

Mas você pode reexaminar sua suposição de que a operação não é idempotente. Obviamente, você não deseja ter vários tokens em sua tabela - mas se o banco de dados rejeitar o comando para injetar um token duplicado, você terá o comportamento idempotente de que precisa. Você deseja ser consistente com a semântica de colocação de HTTP, o que significa que você desejará que o recurso que você está colocando seja exclusivo para o token.

Observe que o recurso de token e a entidade de token são duas coisas diferentes. Você pode "criar" primeiro o recurso e, em seguida (se o aplicativo cliente seguir esse link), criar o token. REST não se importa - desde que o cliente esteja seguindo os links fornecidos na hipermídia, tudo está bem.

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

Não deve ser muito difícil, os recursos são baratos.

Por exemplo, se o seu token for um análogo de um cookie HTTP (uma das partes da Web que o Fielding chama como não sendo restful), então um fluxo de solicitações do cliente pode parecer

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.

Outra alternativa seria separar a reserva do identificador de token da criação do 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 você quiser ser mais cuidadoso, pode implementar recursos que distinguem um token reservado de um token persistido .

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.

O cliente está apenas escolhendo entre os controles de hipermídia fornecidos pelo servidor. Então, enquanto essa história se parece com

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

Mais tarde, você pode decidir que, se esses identificadores de recursos não estiverem em conformidade com seus padrões de codificação, ou se forem difíceis de seguir, ou simplesmente que você está entediado com eles, poderá alterá-los todos

GET /A
GET /W?reservedToken=54321
POST /X?reservedToken=54321
GET /Y?persistedToken=54321
GET /Z?persistedToken=54321
    
por 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.

Parabéns, você quebrou as três regras fundamentais da segurança básica de uma só vez!

  • nunca armazena senhas claras
  • nunca os envia por http, mas apenas https
  • nunca confia em clientes

Por que não criar apenas /login e /logout consultas?

Todas as chamadas REST subseqüentes enviarão o ID da sessão como cookie para "provar" que estão registradas. O tratamento da sessão é algo básico na maioria das estruturas da web. BEIJO.

... ou o seu argumento seria "porque tudo deveria ser REST e stateless" ?! Bem, você realmente acha que isso faz sentido? ... há muitas coisas que não são REST e nunca serão. Por exemplo /send_email , ou /generate_token ... Eu acho que você interpreta erroneamente as coisas sendo muito dogmáticas que tudo deve ser REST, etc. REST é para acessar seus recursos, não fazer ações, etc.

Por fim, enviar seu nome de usuário / senha toda vez não é muito diferente de enviar sua id de sessão em um cookie. Um é realmente mais sem estado ou com estado do que o outro? Sim. Isso faz diferença? Não.

Quanto à parte OAuth, ouvi dizer que é tão complexo que os autores da especificação original quiseram remover o nome dela: link

Eu duvido que seja mais fácil e exija um id de sessão exatamente como o login normal.

    
por 29.01.2016 / 11:44
fonte
6

Normalmente, sua API REST não deve suportar a funcionalidade de login. Basta exigir que o chamador forneça um token de portador de um emissor confiável no Authorization e ser feito com ele. Use um provedor de autorização bem conhecido, como o Google , Facebook ou AD do Azure como seu servidor de autorização.

As pessoas não usam o OAuth porque é mainstream. É mainstream porque permite que os desenvolvedores deleguem a autenticação e não precisem se preocupar com o armazenamento de senhas e a implementação segura de protocolos.

Se, por algum motivo, você precisar implementar essa funcionalidade em um serviço REST, exporia apenas um /token resource via HTTPS para o qual você pode POST um nome de usuário e senha. A partir dessa solicitação de token, retorne um token assinado digitalmente que contenha as informações sobre o usuário que a API precisa, seja válido por um período limitado tempo e que sua API confia.

    
por 29.01.2016 / 17:25
fonte
2

O envio de uma senha nos parâmetros não é apenas "não parece certo para mim", é a pior coisa que você poderia fazer, e eu (e todo mundo percebendo isso) pararia de usar seu serviço imediatamente devido a um violação de segurança brutal.

Se você enviar a senha de volta ao cliente, trata-se de uma violação de segurança ainda pior: seu servidor NUNCA deve saber a senha. Nunca deve ser capaz de devolver a senha ao cliente.

Um método comum é o seguinte: você cria um certificado para criptografia. A chave pública é distribuída com o cliente, a chave privada está no servidor. O login criptografa o nome de usuário e a senha com a chave pública e os envia para você. Você pode descriptografar o nome de usuário e senha no servidor. Em seguida, você transforma a senha em um hash salgado e verifica se corresponde ao nome de usuário. Se esse for o caso, você cria um token aleatório e o envia ao cliente. O cliente lembra o token e o envia a cada solicitação. Você decide quanto tempo o token permanece válido.

Basicamente, cada solicitação requer o token (com exceções óbvias, como login, criar conta, redefinir senha).

Parece que você se meteu em um estado com as fichas não sendo apátridas. REST não é dogma. Tokens são para autenticação, eles estão fora do resto da API REST. Eles devem ser stateful. Nada de errado com isso.

    
por 29.01.2016 / 09:52
fonte