Back-End

24 ago, 2015

Autenticação customizada em Clojure com Friend

Publicidade

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:

friend-1

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:

  1. Apenas seja processado ​​na página de login em uma solicitação POST,
  2. Use um par contendo nome de usuário e senha, e
  3. 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/