Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalize JWT-based authentication #1992

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 11 additions & 5 deletions common-lib/src/cmr/common/util.clj
Original file line number Diff line number Diff line change
Expand Up @@ -997,9 +997,8 @@

(defn is-jwt-token?
"Check if a token matches the JWT pattern (Base64.Base64.Base64) and if it
does, try to look inside the header section and verify that the token is JWT
and it came from EarthDataLogin (EDL). Tokens may start with Bearer and end
with with a client-id section.
does, try to look inside the header section and verify that the token is JWT.
Tokens may start with Bearer and end with with a client-id section.
Note: Similar code exists at gov.nasa.echo.kernel.service.authentication."
[raw-token]
(let [BEARER "Bearer "
Expand All @@ -1018,8 +1017,8 @@
(string/ends-with? header-raw "}"))
(try
(if-let [header-data (json/parse-string header-raw true)]
(and (= "JWT" (:typ header-data))
(= "Earthdata Login" (:origin header-data)))
(and (contains? header-data :kid)
(contains? header-data :alg))
false)
(catch com.fasterxml.jackson.core.JsonParseException e false))
false))
Expand Down Expand Up @@ -1130,3 +1129,10 @@
[anything]
(println anything)
anything)

(defn first-or-throw
"Take the first element of a supplied list, throw an exception if it fails"
[l]
(cond
(empty? l) (throw (ex-info "Can't take first of empty list" {:cause :empty-list}))
:else (first l)))
21 changes: 15 additions & 6 deletions transmit-lib/src/cmr/transmit/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,24 @@
"Defines the password that is sent from the CMR to URS to authenticate the CMR."
{})

(defconfig local-edl-verification
"Controls when cmr uses the EDL public key to locally verify JWT tokens."
(defconfig local-jwt-verification
"Controls when cmr uses a locally-defined JWKS (public key) to verify JWT tokens."
{:type Boolean
:default true})

(defconfig edl-public-key
"Defines the EDL public key which is used to validate EDL JWT tokens locally. Default is set to
a locally generated EDL test jwk and is used in token unit tests"
{:default "\n {\n \"kty\": \"RSA\",\n \"n\": \"xSxiOkM8m8oCyWn-sNNZxBVTUcPAlhXRjKpLTYIM21epMC9rqEnrgL7iuntmp3UcffOIKtFHOtCG-jWUkyzxZHPPMo0kYZVHKRjGj-AVAy3FA-d2AtUc1dPlrQ0TpdDoTzew_6-48BcbdFEQI3161wcMoy40unYYYfzo3KuUeNcCY3cmHzSkYn4iQHaBy5zTAzKTIcYCTpaBGDk4_IyuysvaYmgwdeNO26hNV9dmPx_rWgYZYlashXZ_kRLirDaGpnJJHyPrYaEJpMIWuIfsh_UoMjsyuoQGe4XU6pG8uNnUd31mHa4VU78cghGZGrCz_YkPydfFlaX65LBp9aLdCyKkA66pDdnCkm8odVMgsH2x_kGM7sNlQ6ELTsT-dtJoiEDI_z3fSZehLw469QpTGQjfsfXUCYm8QrGckJF4bJc935TfGU86qr2Ik2YoipP_L4K_oqUf8i6bwO0iomo_C7Ukr4l-dh4D5O7szAb9Ga804OZusFk3JENlc1-RlB20S--dWrrO-v_L8WI2d72gizOKky0Xwzd8sseEqfMWdktyeKoaW0ANkBJHib4E0QxgedeTca0DH_o0ykMjOZLihOFtvDuCsbHG3fv41OQr4qRoX97QO2Hj1y3EBYtUEypan46g-fUyLCt-sYP66RkBYzCJkikCbzF_ECBDgX314_0\",\n \"e\": \"AQAB\",\n \"kid\": \"edljwtpubkey_development\"\n}"})
(defconfig jwt-web-key-set
"Defines the JWKS (public key) which is used to validate JWT tokens locally. Default is set to
a locally-generated test JWKS and is used in token unit tests. Must be a JSON-formatted array."
{:default "[{\n \"kty\": \"RSA\",\n \"n\": \"xSxiOkM8m8oCyWn-sNNZxBVTUcPAlhXRjKpLTYIM21epMC9rqEnrgL7iuntmp3UcffOIKtFHOtCG-jWUkyzxZHPPMo0kYZVHKRjGj-AVAy3FA-d2AtUc1dPlrQ0TpdDoTzew_6-48BcbdFEQI3161wcMoy40unYYYfzo3KuUeNcCY3cmHzSkYn4iQHaBy5zTAzKTIcYCTpaBGDk4_IyuysvaYmgwdeNO26hNV9dmPx_rWgYZYlashXZ_kRLirDaGpnJJHyPrYaEJpMIWuIfsh_UoMjsyuoQGe4XU6pG8uNnUd31mHa4VU78cghGZGrCz_YkPydfFlaX65LBp9aLdCyKkA66pDdnCkm8odVMgsH2x_kGM7sNlQ6ELTsT-dtJoiEDI_z3fSZehLw469QpTGQjfsfXUCYm8QrGckJF4bJc935TfGU86qr2Ik2YoipP_L4K_oqUf8i6bwO0iomo_C7Ukr4l-dh4D5O7szAb9Ga804OZusFk3JENlc1-RlB20S--dWrrO-v_L8WI2d72gizOKky0Xwzd8sseEqfMWdktyeKoaW0ANkBJHib4E0QxgedeTca0DH_o0ykMjOZLihOFtvDuCsbHG3fv41OQr4qRoX97QO2Hj1y3EBYtUEypan46g-fUyLCt-sYP66RkBYzCJkikCbzF_ECBDgX314_0\",\n \"e\": \"AQAB\",\n \"kid\": \"edljwtpubkey_development\"\n}]"})

(defconfig jwt-user-id-claims
"Defines a list of JWT claim fields where the identifier for the current user may
be found. This is a list to allow for tokens of different provenance to be
handled identically. For example, most OAuth-compatible providers use the
\"username\" claim, but EDL/URS will use \"uid\"; tokens generated by the client
credentials grant may have no extra claims, and so the \"sub\" claim will need
to be used. The first field name in the list that matches will be used."
{:default "[\"uid\"]"})

(defn mins->ms
"Returns the number of minutes in milliseconds"
Expand Down
25 changes: 17 additions & 8 deletions transmit-lib/src/cmr/transmit/tokens.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
(:require
[buddy.core.keys :as buddy-keys]
[buddy.sign.jwt :as jwt]
[buddy.sign.jws :as jws]
[cheshire.core :as json]
[clj-time.core :as t]
[clj-http.client :as client]
Expand All @@ -17,17 +18,25 @@
[cmr.transmit.connection :as conn]
[cmr.transmit.launchpad-user-cache :as launchpad-user-cache]))

(defn verify-edl-token-locally
"Uses the EDL public key to verify jwt tokens locally."
(defn verify-json-web-token-locally
"Uses a known public JWKS to verify JWT tokens locally."
[token]
(try
(let [public-key (buddy-keys/jwk->public-key (json/parse-string (transmit-config/edl-public-key) true))
bearer-stripped-token (string/replace token #"Bearer\W+" "")
(let [bearer-stripped-token (string/replace token #"Bearer\W+" "")
token-kid (get (jws/decode-header bearer-stripped-token) :kid)
jwks-list (json/parse-string (transmit-config/jwt-web-key-set) true)
matching-key (common-util/first-or-throw (filter #(= token-kid (get % :kid)) jwks-list))
public-key (buddy-keys/jwk->public-key matching-key)
decrypted-token (jwt/unsign bearer-stripped-token public-key {:alg :rs256})]
(:uid decrypted-token))
(first (remove nil? (map #((keyword %) decrypted-token) (json/parse-string (transmit-config/jwt-user-id-claims) true)))))
(catch clojure.lang.ExceptionInfo ex
(let [error-data (ex-data ex)]
(cond
(= :empty-list (:cause error-data))
(errors/throw-service-error
:unauthorized
(format "Token [%s] was not issued by recognized provider. Note the token value has been partially redacted."
(common-util/scrub-token token)))
(= :exp (:cause error-data))
(errors/throw-service-error
:unauthorized
Expand Down Expand Up @@ -123,14 +132,14 @@
[status parsed body])))

(defn get-user-id
"Get the user-id from EDL or Launchpad for the given token"
"Get the user identifier from Launchpad or extract from the JWT bearer token"
[context token]
(if (transmit-config/echo-system-token? token)
;; Short circuit a lookup when we already know who this is.
(transmit-config/echo-system-username)
(if (and (common-util/is-jwt-token? token)
(transmit-config/local-edl-verification))
(verify-edl-token-locally token)
(transmit-config/local-jwt-verification))
(verify-json-web-token-locally token)
(if (common-util/is-launchpad-token? token)
(:uid (launchpad-user-cache/get-launchpad-user context token))
;; Legacy services has been shut down, we still use a mock for tests, we will leave this here and handle the 301.
Expand Down