Skip to content

Instantly share code, notes, and snippets.

@jwhitlark
Created March 25, 2015 20:16
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jwhitlark/5dc423485548c31d2c72 to your computer and use it in GitHub Desktop.
Save jwhitlark/5dc423485548c31d2c72 to your computer and use it in GitHub Desktop.
Combine friend, oauth2, and JWT token processing.
(ns foo.auth.google
(:require [clojure.string :as str]
[clojure.java.io :as io]
[clojure.data.json :as json]
[clojure.logging :refer :all]
[friend-oauth2.util :refer [format-config-uri]]
[friend-oauth2.workflow :as oauth2])
(:import [java.security.cert CertificateFactory]
[org.apache.commons.codec.binary Base64]))
(def client-config
{:client-id <client-id>
:client-secret <client-secret>
:callback {:domain "http://localhost:8090" :path "/oauth2callback"}})
(defn credential-fn [token]
(debug "in oauth cred-fn: " token)
(let [username (get-in token [:access-token :email])
bare-token (get-in token [:access-token :token])
expires (get-in token [:expires])]
(info "User authenticated:" username )
{:identity bare-token
:username username
:expires expires
:roles #{::user}}))
(let [cert-factory (CertificateFactory/getInstance "X.509")
gen-cert #(.generateCertificates cert-factory %)]
(defn load-x509-cert "Load an X.509 cert from a string."
[s]
(->> s
.getBytes
io/input-stream
gen-cert
first)))
(defn load-certs "Load googles certs, put them in the format we need,
and map them to their KIDs"
[]
(let [raw-certs (-> "https://www.googleapis.com/oauth2/v1/certs" slurp json/read-str)]
(zipmap (keys raw-certs) (mapv load-x509-cert (vals raw-certs)))))
(defn- rsa-verify
"Function to verify data and signature with RSA algorithm."
[alg key body signature & {:keys [charset] :or {charset "UTF-8"}}]
(let [sig (doto (java.security.Signature/getInstance alg)
(.initVerify key)
(.update (.getBytes body charset)))]
(.verify sig signature)))
(defn- decode-piece [piece]
(->> (Base64/decodeBase64 piece)
(map char)
(apply str)
(#(json/read-str % :key-fn keyword))))
(defn parse-jwt [jwt-str]
(let [[header, claims, sig] (str/split jwt-str #"\." )]
{:header (decode-piece header)
:claims (decode-piece claims)
:sig (Base64/decodeBase64 sig)
:payload (str/join "." [header, claims])}))
(defn valid-signature?
"Validates a JWT against its signature, downloading the google certs
EACH TIME."
([jwt-map] (valid-signature? jwt-map (load-certs)))
([jwt-map certs]
(let [kid (get-in jwt-map [:header :kid])
alg (get-in jwt-map [:headers :alg])
cert (get certs kid)]
(if (nil? cert)
(throw (Exception. "No matching cert.")))
(rsa-verify alg cert (:payload jwt-map) (:sig jwt-map)))))
(defn process-jwt [{body :body}]
(let [resp (json/read-str body :key-fn keyword)
access-token (:access_token resp)
jwt-map (parse-jwt (:id_token resp))
validated? (valid-signature? jwt-map)
token {:token access-token
:validated? validated?
:email (get-in jwt-map [:claims :email])
:expires (get-in jwt-map [:claims :exp])}]
(debug "token: " token)
token))
(def uri-config
{:authentication-uri {:url "https://accounts.google.com/o/oauth2/auth"
:query {:client_id (:client-id client-config)
:response_type "code"
:redirect_uri (format-config-uri client-config)
:hd <your-domain>
:scope "openid email profile"}}
:access-token-uri {:url "https://accounts.google.com/o/oauth2/token"
:query {:client_id (:client-id client-config)
:client_secret (:client-secret client-config)
:grant_type "authorization_code"
:redirect_uri (format-config-uri client-config)}}})
(defn workflow []
(oauth2/workflow
{:client-config client-config
:uri-config uri-config
:credential-fn credential-fn
:access-token-parsefn process-jwt
}))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment