🤖🚫 AI-free content. This post is 100% written by a human, as is everything on my blog. Enjoy!

OAuth2 is easy - illustrated in 50 lines of Clojure

July 30, 2015 in Clojure

I have recently released a product that uses OAuth2 in a way that is somewhat off the beaten path. Besides, it’s written in Clojure. After a weekend of hunting for a working library, I had a revelation - which I’m happy to share - that OAuth2 is super easy to understand and to support without any third-party libraries.

State of OAuth2 in Clojure

A quick search for oauth2 in Clojars finds a formidable number of libraries, starting with the legit sounding clj-oauth2. Unfortunately, it hasn’t been updated in three years, has a dozen forks and, of course, it’s hard to choose one that works. The other libraries didn’t work for my providers (Automatic and Fitbit), either. (Clojars is a bit inconvenient, but that is another topic.)

Then there’s the Scribe Java library which has stellar OAuth 1 support and, formally, supports OAuth2, but I had to hack around to support the aforementioned providers and even the official wiki says that OAuth2 isn’t a priority for Scribe.

OAuth2 explained in 50 lines of code

OAuth 1 is a complicated protocol, mostly because of the request signing. Implementing an OAuth 1 client is far from trivial.

OAuth2, on the other hand, relies upon HTTPS: all API requests, including authentication, always go over HTTPS. As HTTPS already verifies the server and provides a secure, encrypted connection, the only thing remaining to do is user identification.

Here is a full annotated implementation of OAuth2 authorization for Fitbit.

  1. There are some settings that specify every OAuth2 client: the API client key and secret, URIs, and authentication scope. Scope is the set of permissions your app needs.

    (def oauth2-params
      {:client-id (System/getenv "FITBIT_CLIENT_ID")
       :client-secret (System/getenv "FITBIT_CLIENT_SECRET")
       :authorize-uri  "https://www.fitbit.com/oauth2/authorize"
       :redirect-uri (str (System/getenv "APP_HOST") "/connect/fitbit/success")
       :access-token-uri "https://api.fitbit.com/oauth2/token"
       :scope "activity profile"})
    
  2. To authorize, you redirect the user to the sign in / grant page. It can be a simple redirect to a constant URL, but ideally you should include a CSRF token (called “state”) and check it upon redirect.

    (defn authorize-uri [client-params csrf-token]
      (str
        (:authorize-uri client-params)
        "?response_type=code"
        "&client_id="
        (url-encode (:client-id client-params))
        "&redirect_uri="
        (url-encode (:redirect-uri client-params))
        "&scope="
        (url-encode (:scope client-params))
        "&state="
        (url-encode csrf-token)))
    
  3. After the user signs in to the provider server, and grants your application access, he is redirected back to your application with a unique code in a GET parameter. To avoid GETting the code into wrong hands, the redirect URL is explicitly predefined in the application settings rather than in the authentication request. Ideally your redirect page should also use HTTPS, and some providers require it so.

  4. The application checks the CSRF token, and then sends the code, along with the application token and secret, through a back channel to the provider server, and receives an access token and usually a refresh token. None of the parameters are hashed or encrypted because, again, we are using HTTPS.

    (defn get-authentication-response [csrf-token response-params]
      (if (= csrf-token (:state response-params))
        (try
          (-> (http/post (:access-token-uri oauth2-params)
                         {:form-params {:code         (:code response-params)
                                        :grant_type   "authorization_code"
                                        :client_id    (:client-id oauth2-params)
                                        :redirect_uri (:redirect-uri oauth2-params)}
                          :basic-auth [(:client-id oauth2-params) (:client-secret oauth2-params)]
                          :as          :json
                          })
              :body)
          (catch Exception _ nil))
        nil))
    
  5. Once you have the “access token”, you send it as a client identifier in API requests. You don’t have to sign the request or hash anything - it’s just a GET parameter or an HTTP header - in plain text, again, thanks to HTTPS. In clj-http, the ability to send a token header is built in.

    (defn get-user-info
      "User info API call"
      [access-token]
      (let [url (str api-base "/1/user/-/profile.json")]
        (-> (http/get url {:oauth-token access-token, :as :json})
            :body
            :user))))
    
  6. Because the access token could be exposed, e.g. on a browser-based application page, it is customarily short lived. This is why you also have a “refresh token” which is more closely kept, and is only used to request a new access token from the provider when the current access token expires. With some providers, you receive a new refresh token every time as well.

    (defn get-fresh-tokens
      "Returns current token pair if they have not expired, or a refreshed token pair otherwise"
      [access-token refresh-token]
      (try+
        (and (get-user-info access-token)
             [access-token refresh-token])
        (catch [:status 401] _ (refresh-tokens refresh-token))))
    
    (defn- refresh-tokens
      "Request a new token pair"
      [refresh-token]
      (try+
        (let [{{access-token :access_token refresh-token :refresh_token} :body}
              (http/post (:access-token-uri oauth2-params)
                         {:form-params {:grant_type       "refresh_token"
                                        :refresh_token    refresh-token}
                          :basic-auth [(:client-id oauth2-params) (:client-secret oauth2-params)]
                          :as          :json})]
          [access-token refresh-token])
        (catch [:status 401] _ nil)))
    

The problem with OAuth2

The problem with OAuth2 is that it’s more of a set of guidelines than a rigid protocol. Thusly, different vendors have subtle differences that make implementing a generic OAuth2 client complicated and rife with options and configurations.

However, making a specific OAuth2 client takes less than 50 lines of code, with hash and crypto functions nowhere to be seen. Truly a breath of fresh air in the age of complicated authentication libraries.

Buy me a coffee Liked the post? Treat me to a coffee