2018-09-12

Authentication with Amplify in an Electron app

I’ve recently had to implement the authentication page in an Electron app. We’ve decided to go with Amplify. A lot of below code is based on this excellent article: https://blog.ecliptic.io/google-auth-in-electron-a47b773940ae

(def Auth js/amplify.Auth) 
(def electron (js/require "electron")) 
(def remote (.-remote electron)) 
(def browser-window (.-BrowserWindow remote)) 
 
(.configure Auth #js{:Auth #js{:identityPoolId "<aws-identity-pool-id>" 
                               :region "eu-west-1" 
                               :mandatorySignIn true}}) 
                               
;; I'm using https://github.com/r0man/cljs-http to make requests below:
(defn <auth-token 
  "Returns a channel with oAuth access_token" 
  [code] 
  (http/post "https://www.googleapis.com/oauth2/v4/token" 
             {:with-credentials? false 
              :json-params {:code code 
                            ;; If your app is running on a `file://` protocol,
                            ;; don't worry. We don't care about where the user
                            ;; will be redirected too, as we are closing the window
                            ;; before that redirect happens.
                            ;; We only want to get the `code` from the resulting
                            ;; redirect's url.
                            :redirect_uri "http://localhost:3000" 
                            :client_id "<google-client-id>" 
                            :grant_type "authorization_code"}})) 
 
(defn <google-profile 
  "Returns a channel with authenticated user's profile" 
  [access_token] 
  (http/get "https://www.googleapis.com/userinfo/v2/me" 
            {:with-credentials? false 
             :oauth-token access_token})) 
 
(defn- handle-redirect [url callback] 
  (let [{:keys [code id_token] :as ks} (parse-url-params url)] 
    ;; When a user has been redirected to a page with `code`, that means they authenticated
    ;; in the separate Electron window, and that window will now be closed.
    ;; we have to now authenticate the user with Amplify, by fetching the required
    ;; information using the `code` we just got
    (when code 
      (go (let [;; first, we need the `access_token`:
                {auth-response :body} (<! (<auth-token code)) 
                {:keys [access_token expires_in]} auth-response 
                ;; then we need to fetch the user profile from Google, using 
                ;; the access_token:
                {user-profile :body} (<! (when access_token 
                                           (<google-profile access_token)))] 
            ;; once we have both, we should use `federatedSignIn` function from 
            ;; Amplify to authenticate the user using the AWS IdentityPool
            (when (and user-profile access_token) 
              (-> (.federatedSignIn Auth "google" 
                                    (clj->js {:token id_token 
                                              :expires_at expires_in}) 
                                    (clj->js user-profile)) 
                  (.then 
                   (fn [identity] 
                     ;; at this point with have the Identity from AWS as well
                     ;; as `user-profile` with information from Google
                     (callback user-profile)))))))))) 
 
(defn sign-in 
  "Opens a new Electron window with Google Sign-in/Consent process. After a user
  has signed in, the callback will be called with user's profile." 
  [callback] 
  (let [win (browser-window.) 
        web-contents (.-webContents win) 
        url (google-auth-url (:google_client_id core/federated-identities-config) 
                             "http://localhost:3000")] 
    ;; We open a new Electron window and watch for redirects. When the redirect happens
    ;; the result URL will have the required information once the user has signed-in
    (.on web-contents "will-navigate" (fn [url] 
                                        (handle-redirect url callback))) 
    (.on web-contents "did-get-redirect-request" (fn [event old-url new-url] 
                                                   (handle-redirect new-url callback))) 
    (.loadURL win url)))

After the above sign-in process, you can get the auth information at any point by using Amplify’s Cache function:

(.getItem Cache "federatedInfo")

For example to sign AppSync communication using the authorization token:

(.configure API #js{:aws_appsync_graphqlEndpoint "<appsync-url>" 
                    :aws_appsync_region "eu-west-1" 
                    :aws_appsync_authenticationType "OPENID_CONNECT" 
                    :graphql_headers (fn [] 
                                       (let [token (.-token (.getItem Cache "federatedInfo"))] 
                                         #js{"Authorization" token}))})