Tenho usado o Friend para fornecer um método de autenticação em meus projetos, e algumas vezes o considero muito mais antes de recorrer aos meus próprios scripts feitos à mão. A maior parte da razão para isso é que o Friend apenas parece complicado. Os fluxos de trabalho? As credenciais? Eu quero apenas verificar uma senha e esconder um objeto de usuário na sessão, fim da história. Depois de alguma investigação, no entanto, descobri que pode ser útil implantar o Friend, mesmo para fluxos de trabalho mais simples, caso você entenda como ele funciona.
O Friend tem suas próprias funções correspondente a esses dois pontos: authenticate é um middleware que envolve um manipulador Ring, enquanto authorize pode ser implementado em qualquer lugar no seu código para lidar com a autenticação do seu programa. Vamos examinar o authenticate neste artigo, uma vez que o outro é muito simples de usar. Uma maneira ilustrativa para aprender sobre como a autenticação funciona no Friend é escrever um pequeno sistema de autenticação, sem usar nenhum componente extra, escrevendo seu próprio componente.
Antes disso, entenda os termos
Mapa de autenticação: um mapa de hash que revela informações sobre um usuário autenticado que estará acessível a partir da solicitação Ring. A chave :identity é necessária, e a chave :roles é fortemente recomendada.
Fluxo de trabalho: uma função que aceita um mapa de solicitação e retorna um mapa de autenticação (por um usuário recém-autenticado) ou null (se não houver nenhuma alteração no estado da autenticação). Também podem devolver uma resposta Ring que será devolvida imediatamente, abreviando o resto do pedido.
Função credencial: uma função que aceita um conjunto de argumentos dependentes de workflow e retorna um mapa de autenticação.
Diga “Friend” e digite
Vamos escrever um sistema de autenticação realmente simples; em vez de um nome de usuário e senha, ele irá procurar por um parâmetro chamado speak na request e autenticar o usuário o parâmetro speak for friend. Para fazer isso, vamos precisar definir um fluxo de trabalho, que vamos usar com a função authenticate para fazer o nosso middleware de autenticação.
Um fluxo de trabalho é apenas uma função com uma assinatura específica e comportamento, que está documentada neste diagrama que eu espero que não seja protegido por direitos autorais:
Em suma, o fluxo de trabalho aceita um pedido do Ring, e pode retornar uma de três coisas:
- Um Ring de resposta, que será retornado diretamente.
- nil, que resultará em outros fluxos de trabalho que estejam sendo verificados (ou resulta em um erro 403 se nenhum for retornado).
- Um mapa de autenticação, que significa que a autenticação foi bem sucedida.
O termo “mapa autenticação” é mal definido, mas acaba sendo qualquer mapa com a chave :identity e com os metadados {:type :cemerick.friend/auth}. Com folga, a função cemerick.friend.workflows/make-auth irá adicionar metadados para você.
Então vamos começar com um fluxo de trabalho simples:
(defn fun-workflow [req]
(let [speak (get-in req [:params :speak])]
(when (= speak "friend")
(make-auth {:identity "friend" :roles #{::user}}))))
Quando uma solicitação Ring é recebida, o friend/authenticate vai passá-la para esse fluxo de trabalho. O fluxo de trabalho irá verificar o valor do parâmetro “speak” no mapa de parâmetros. Se “speak” == “friend”, ele chama make-auth no mapa de autenticação, que o anota com os metadados necessários para o Friend reconhecê-lo como tal.
Para ligar o seu workflow, você precisa passá-lo para :workflow no friend/authenticate. Aqui está um exemplo pequeno, mas completo, de um aplicativo utilizando esse workflow de autenticação:
(require '[compojure.core :refer [defroutes GET POST]]
'[cemerick.friend :as friend]
'[cemerick.friend.workflows :refer [make-auth]]
'[ring.middleware.session :refer [wrap-session]]
'[ring.middleware.params :refer [wrap-params]]
'[ring.middleware.keyword-params :refer [wrap-keyword-params]]
'[ring.adapter.jetty :refer [run-jetty]])
(defroutes app-routes
(GET "/" [] "Hello everyone <form action=\"logout\" method=\"post\"><button>Submit</button></form>")
(GET "/authorized" [] (friend/authorize #{::user} "Hello authorized"))
(friend/logout (POST "/logout" [] "logging out")))
(defn fun-workflow [req]
(let [speak (get-in req [:params :speak])]
(when (= speak "friend")
(make-auth {:identity "friend" :roles #{::user}}))))
(def app
(-> app-routes
(friend/authenticate {:workflows [fun-workflow]})
(wrap-keyword-params)
(wrap-params)
(wrap-session)
))
(defn -main []
(run-jetty #'app {:port 8080}))
Se você disparar um servidor, aqui está o que poderá observar:
- Se acessar o diretório /, você verá a página, independentemente de estar logado.
- Se acessar /authorized, será redirecionado para /login (a url de login padrão, que nós nunca mudamos, pois não é necessário).
- Se acessar /authorized?speak=friend, você vai acabar caindo no diretório /authorized. Agora você pode visitar livremente o diretório /authorized sem adicionar os parâmetros de consulta.
- Se clicar no botão em /, você enviará um POST para /logout, você será desconectado e redirecionado caso tente ir para /authorized.
Observe que nada disso vai funcionar sem os três middlewares Ring que eu adicionei: wrap-params, wrap-keyword-params e wrap-session.
Qual é a senha?
Você pode definitivamente escrever seus próprios fluxos de trabalho dessa forma, e isso não lhe dará problemas. No entanto, recomenda-se para termos de configuração que você divida a parte de seu fluxo de trabalho que verifica a autenticação e recupera qualquer informação extra em uma função de credencial separada.
Para usar uma função de credencial, você só precisa fornecer uma (função) para o middleware e certificar-se de que o workflow o usará. Veja como você pode quebrar o exemplo acima em um fluxo e uma função:
(defn fun-workflow [req]
(let [speak (get-in req [:params :speak])
credential-fn (get-in req [::friend/auth-config :credential-fn])]
(make-auth (credential-fn speak))))
(defn fun-credential-fn [word]
(if (= word "friend")
{:identity word :roles #{::user}}))
;And in the middleware...
(friend/authenticate {:workflows [fun-workflow]
:credential-fn fun-credential-fn})
Faça uma nota especial que retornando nil é o padrão. if na credencial-fn retorna nil se a palavra estiver incorreta, que por sua vez é retornada do fluxo de trabalho, o que significa que nenhuma alteração no estado de autenticação ocorre.
Agora, vamos imaginar que, em vez de uma palavra mágica codificada, queremos que a autenticação:
- Apenas seja processado na página de login em uma solicitação POST,
- Use um par contendo nome de usuário e senha, e
- Busque o objeto de usuário do banco de dados e preveja que a cada função esse objeto será utilizado, em vez de apenas as credenciais vazias.
Está aqui um script caseiro que usa um namespace db cujas funções já estão escritas:
(defn do-login [req]
(let [credential-fn (get-in req [::friend/auth-config :credential-fn])]
(make-auth (credential-fn (select-keys (:params req) [:username :password])))))
(defn password-workflow [req]
(when (and (= (:request-method req) :post)
(= (:uri req) "/login"))
(do-login req)))
(defn password-credential-fn [{:keys [username password] :as creds}]
(when-let [user (get-user-by-username username)]
(when (= (:hashed_password user) (some-strong-hash password))
{:identity (:id user) :roles #{::user} :user user})))
;And in the middleware...
(friend/authenticate {:workflows [password-workflow]
:credential-fn credential-fn-user})
Não é realmente muito mais complicado do que o outro script, não é?
Você pode substituir password-workflow pelo fluxo de trabalho (interactive-form) que vem com o Friend e ele ainda funcionará, uma vez que “/login” é o endereço de login padrão do Friend. Eu também tive a certeza de combinar a assinatura com credential-fn.
Você também pode descarregar a maior parte de credential-fn na função embutida bcrypt-credential-fn, embora seja necessário que seu mapa de usuário tenha uma chave :password que é criptografada com a função de hash bcrypt incluída também no Friend. O cemerick.friend.credentials contém as funções hash-bcrypt, bcrypt-verificar para esses fins também.
Outros esquemas de autenticação
A intenção do modelo de credential-fn + fluxo de trabalho é ser o mais flexível possível, e uma vez que o Friend é um middleware, você pode até mesmo criar fluxos de trabalho totalmente abstratos com OAuth. Quero dizer, ainda é um pouco doloroso para testes, mas, pelo menos, o fluxo de trabalho não é muito ruim. Aqui está o código que venho escrevendo como um invólucro Friend em torno do clj-OAuth no Twitter:
(require '[oauth.client :as oauth])
(def consumer
(oauth/make-consumer
(:twitter-consumer-key env)
(:twitter-consumer-secret env)
"https://api.twitter.com/oauth/request_token"
"https://api.twitter.com/oauth/access_token"
"https://api.twitter.com/oauth/authorize"
:hmac-sha1))
(defn twitter-auth-workflow [& {:keys [oauth-callback-uri]}]
(fn [req]
(let [credential-fn (get-in req [::friend/auth-config :credential-fn])]
(cond
;; Login url -- redirect to twitter approval page
(= (:uri req) (get-in req [::friend/auth-config :login-uri]))
(let [request-token (oauth/request-token consumer (:uri oauth-callback-uri))
approval-url (oauth/user-approval-uri consumer (:oauth_token request-token))
resp (resp/redirect approval-url)]
;; Stash the request-token in the session for later
(update-in resp [:session] assoc :oauth-request-token request-token))
; Oauth callback
(= (:path oauth-callback-uri) (:uri req))
(when-let [request-token (get-in req [:session :oauth-request-token])]
(when-let [access-token (oauth/access-token
consumer
request-token
(get-in req [:params :oauth_verifier]))]
(make-auth (credential-fn access-token))))))))
;...
(def oauth-callback-uri {:url "http://localhost/oauth/callback"
:path "/oauth/callback"})
(friend/authenticate
{:workflows [(twitter-auth-workflow :oauth-callback-uri oauth-callback-uri)]
:credential-fn #(hash-map :identity % :roles #{::user})})
Se você já tentou escrever qualquer sistema de autenticação baseado em OAuth, saberá que esse trabalho com o Friend não é ruim! Como o middleware de autenticação recebe toda a solicitação, ele pode fazer o OAuth redirecionar a URL de login e lidar com o retorno de chamada, bem como obter o caminho de todo o resto. Depois de tudo isso, o Friend lida com a autorização para você, sem ter que passar por isso de novo!
É por isto que vale a pena usar o Friend: a autenticação é um problema tão difícil como sempre foi, mas pelo menos o Friend resolve a questão de autorização para você com quase nenhum esforço, e faz um trabalho muito melhor do que se você tentar fazê-lo sozinho.
***
Adam Bard faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://adambard.com/blog/easy-auth-with-friend/




