Skip to content
This repository has been archived by the owner on Mar 22, 2023. It is now read-only.

adds support for JWT access tokens #923

Merged
merged 22 commits into from Sep 7, 2019
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b9553af
adds implementation of jwt authenticator
shamsimam Aug 14, 2019
723d6ca
adds jwt authenticator settings
shamsimam Aug 16, 2019
395e6f4
adds integration test
shamsimam Aug 16, 2019
2884eee
addresses PR feedback
shamsimam Aug 21, 2019
051e3f5
addresses PR feedback
shamsimam Aug 21, 2019
6c91e19
triggers JWT authentication if auth header is deemed to be an access …
shamsimam Aug 22, 2019
b9da937
adds support for access tokens generated using rs256
shamsimam Aug 22, 2019
113133a
makes the supported algorithms configurable
shamsimam Aug 22, 2019
af02dde
passes control downstream even if JWT auth fails
shamsimam Aug 23, 2019
411c826
allows spnego auth to choose correct authorization header
shamsimam Aug 23, 2019
4141560
resolves the cheshire dependency used by buddy and jet
shamsimam Aug 24, 2019
e159b12
formatting changes
shamsimam Aug 29, 2019
79e0335
makes jet authentication disabled by default
shamsimam Aug 31, 2019
0e81345
addresses PR feedback
shamsimam Aug 31, 2019
171e3ca
introduces jwks server to enable jwt testing
shamsimam Sep 1, 2019
6eeed01
removes access-token creation from integration tests
shamsimam Sep 1, 2019
8045580
updates ci scripts for jwt tests
shamsimam Sep 1, 2019
a69c4eb
addresses PR feedback
shamsimam Sep 2, 2019
8ce48d2
avoids hacky use of private buddy.sign.jwt/validate-claims
shamsimam Sep 4, 2019
aaed111
adds integration tests to check the 401 unauthorized and 403 forbidde…
shamsimam Sep 4, 2019
f1f6116
corrects test - 403 responses do not content www-authenticate header
shamsimam Sep 4, 2019
b295e97
adds sources of the provided keys
shamsimam Sep 4, 2019
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
19 changes: 19 additions & 0 deletions containers/test-apps/jwks-server/.gitignore
@@ -0,0 +1,19 @@
/target
/classes
/checkouts
pom.xml
pom.xml.asc
lib/
*.class
/.lein-*
/.nrepl-port
log/

# Intellij specific files
.idea
*.iml
*.ipr

# test2junit output files
build.xml
test2junit/
32 changes: 32 additions & 0 deletions containers/test-apps/jwks-server/README.md
@@ -0,0 +1,32 @@
The jwks-server is a server to help with JWT access token authentication testing.
It supports two endpoints: `get-token` and `keys`.

# Usage

To run the server:
```bash
$ lein run <port> <key-file> <settings-file>
```

## Example:

```bash
$ lein run 8080 resources/jwks.json resources/settings.edn
...
jwks-server.main - command-line arguments: [8080 resources/jwks.json resources/settings.edn]
jwks-server.main - port: 8080
jwks-server.main - jwks file: resources/jwks.json
jwks-server.main - settings file: resources/settings.edn
jwks-server.main - starting server on port 8080
...
eclipse.jetty.server.Server - Started @10846ms
```

# Build Uberjar

```bash
$ lein uberjar
...
Created /path-to-waiter-jwks-server/target/uberjar/jwks-server-0.1.0-SNAPSHOT.jar
Created /path-to-waiter-jwks-server/target/uberjar/jwks-server-0.1.0-SNAPSHOT-standalone.jar
```
42 changes: 42 additions & 0 deletions containers/test-apps/jwks-server/project.clj
@@ -0,0 +1,42 @@
;;
;; Copyright (c) Two Sigma Open Source, LLC
;;
;; Licensed under the Apache License, Version 2.0 (the "License");
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;; http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.
;;
(defproject jwks-server "0.1.0-SNAPSHOT"
:dependencies [[buddy/buddy-sign "3.1.0"]
[cheshire "5.9.0"]
[org.clojure/clojure "1.10.1"]
[org.clojure/core.async "0.4.500"]
[org.clojure/data.json "0.2.6"]
[org.clojure/tools.logging "0.5.0"]
[org.slf4j/slf4j-log4j12 "1.7.28"]
[prismatic/plumbing "0.5.5"]
[twosigma/jet "0.7.10-20190831_055713-gf193d34"
:exclusions [org.eclipse.jetty/jetty-client
org.eclipse.jetty.alpn/alpn-api
org.eclipse.jetty.http2/http2-client
org.eclipse.jetty.websocket/websocket-client
org.eclipse.jetty/jetty-alpn-openjdk8-client
org.mortbay.jetty.alpn/alpn-boot]]]
:jvm-opts ["-server"
"-XX:+UseG1GC"
"-XX:MaxGCPauseMillis=50"]
:main ^:skip-aot jwks-server.main
:profiles {:uberjar {:aot :all}}
:resource-paths ["resources"]
:target-path "target/%s"
:test-selectors {:default (every-pred (complement :dev) (complement :integration))
:dev :dev
:integration (every-pred :integration (complement :explicit))}
:uberjar-name ~(System/getenv "UBERJAR_NAME"))
90 changes: 90 additions & 0 deletions containers/test-apps/jwks-server/resources/jwks.json
@@ -0,0 +1,90 @@
{
sradack marked this conversation as resolved.
Show resolved Hide resolved
"keys": [
{
"alg": "RS256",
"e": "AQAB",
"kid": "7c368fc914ce6cb181fa0d670f63bd5df6db7b25",
"kty": "RSA",
"n": "vWmir2ZdXeMZkfsg0GTPfQw7CKmDNu50Sc76pndZPNyLf5JeR39JueHIPVXJ",
"use": "enc"
},
{
"alg": "RS256",
"d": "Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97IjlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYTCBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLhBOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ",
"e": "AQAB",
"kid": "wwjcyqculjybrlzo0tzwjjniusfb4p4fakdotbf6",
"kty": "RSA",
"n": "ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ",
"use": "sig"
},
{
"crv": "Ed25519",
"d": "8OSgWqVALOqZyLTLBcVJSgRZ6yxo0Wk_FkNG7uRCV28",
"kid": "f8f78b86-af1b-46c5-8849-006020f34d83",
"kty": "OKP",
"use": "sig",
"x": "H5UiUvUNr0OcxjEXpGCfFkYig-63Cs2SV_q6Y_5uDck"
},
{
"crv": "Ed25519",
"d": "NIX7-o9XrTLAYNanapB9_fIeOnZUHug2jhwbZ6zs6dE",
"kid": "b66fd551-5e49-46e3-8c3b-06b953a16f6b",
"kty": "OKP",
"use": "sig",
"x": "gLYQSUzOu4T0xq12F_RCc51YgSc8w9GZK3WIyYRlH44"
},
{
"crv": "Ed25519",
"d": "SXbPPwVHzPYYTwwRG_27RuoTN15Xn8zctwND727B2no",
"kid": "3106d31a-87a2-482f-a8dd-b727a043de5a",
"kty": "OKP",
"use": "sig",
"x": "qE4PwqfWP9pQ8iVpPhvQY433hiAn_rzjH-CPTnY_8yQ"
},
{
"crv": "Ed25519",
"d": "dx_kwm1Kg47VwP-4DZHBO9bLdGgSu-w20VYwKyqHOw0",
"kid": "73711a87-eeb6-42be-b537-3fdeaa4872bb",
"kty": "OKP",
"use": "sig",
"x": "SycUT4WVy3AKTR_FxBjkAa468LocX8QrM480EK2EOoc"
},
{
"crv": "Ed25519",
"d": "kdE8RVHsC87AdRSOA9jrJct75r-oZtggCgPQmpZHQzg",
"kid": "bfc58a16-8e07-42af-a632-2d3165dbf859",
"kty": "OKP",
"use": "sig",
"x": "nkKQj8rkxfvGFRcXNCeVz236ePxBZMtuxAp_F2PeIvk"
},
{
"crv": "Ed25519",
"d": "oyi29vqepPO-ZCQLTnthB6-IWGME_88E3beFPwXK4Xs",
"kid": "e787c763-ffe1-405d-bdee-bc50ea0e3aa5",
"kty": "OKP",
"use": "sig",
"x": "nv4W45wfSuWBAKeY9hEJ_SLp1o3d0kdViCs9TpbsEWk"
},
{
"crv": "Ed25519",
"d": "sH8QTNekLsfdXA29vipsLHiC8xQHqwteWcFtjM5gexs",
"kid": "9bf169b7-d8cc-4f60-82bd-2d060b11653b",
"kty": "OKP",
"use": "sig",
"x": "Tw32CCAlFNn2jz6wwVAQOgABuuA4pyLfaSnwzTEVhoY"
},
{
"crv": "P-256",
"kid": "e787c763-ffe1-d8cc-b537-bc50ea0e3aa5",
"kty": "EC",
"x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
"y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
},
{
"e": "AQAB",
"kid": "73711a87-eeb6-46e3-b537-2d060b11653b",
"kty": "RSA",
"n": "kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd"
}
]
}
10 changes: 10 additions & 0 deletions containers/test-apps/jwks-server/resources/log4j.properties
@@ -0,0 +1,10 @@
log4j.rootLogger=DEBUG, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{4} - %m%n
log4j.appender.stdout.Threshold=DEBUG

# DEBUG is way too noisy for some categories
log4j.logger.org.eclipse.jetty=INFO
3 changes: 3 additions & 0 deletions containers/test-apps/jwks-server/resources/settings.edn
@@ -0,0 +1,3 @@
{:issuer "test.com"
:subject-key :sub
:token-type "JWT"}
37 changes: 37 additions & 0 deletions containers/test-apps/jwks-server/src/jwks_server/config.clj
@@ -0,0 +1,37 @@
;;
;; Copyright (c) Two Sigma Open Source, LLC
;;
;; Licensed under the Apache License, Version 2.0 (the "License");
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;; http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.
;;
(ns jwks-server.config
(:require [clojure.tools.logging :as log]))

(let [settings-promise (promise)]
(defn initialize-settings
[settings]
(log/info "settings:" settings)
(deliver settings-promise settings))

(defn retrieve-settings
[]
(deref settings-promise 0 {})))

(let [jwks-promise (promise)]
(defn initialize-jwks
[jwks]
(log/info "num jwks entries:" (-> jwks :keys count))
(deliver jwks-promise jwks))

(defn retrieve-jwks
[]
(deref jwks-promise 0 {})))
112 changes: 112 additions & 0 deletions containers/test-apps/jwks-server/src/jwks_server/handler.clj
@@ -0,0 +1,112 @@
;;
;; Copyright (c) Two Sigma Open Source, LLC
;;
;; Licensed under the Apache License, Version 2.0 (the "License");
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;; http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.
;;
(ns jwks-server.handler
(:require [buddy.core.keys :as buddy-keys]
[buddy.sign.jwt :as jwt]
[clojure.data.json :as json]
[clojure.tools.logging :as log]
[clojure.string :as str]
[jwks-server.config :as config]
[plumbing.core :as pc]))

(defn prepare-response
"Prepares and returns a standard response"
([{:keys [query-string request-method uri]} message status]
(prepare-response (cond-> {"message" message
"request-method" request-method
"uri" uri}
(not (str/blank? query-string))
(assoc "query-string" query-string))
status))
([data-map status]
{:body (json/write-str data-map)
:headers {"content-type" "application/json"
"server" "jwks-server"}
:status status}))

(defn generate-jwt-access-token
"Generates the JWT access token using the provided private key."
[alg jwk-entry payload header]
(let [private-key (-> jwk-entry pc/keywordize-map buddy-keys/jwk->private-key)
options {:alg alg :header header}]
(jwt/sign payload private-key options)))

(defn process-get-token-request
"Retrieves an JWT access token generated using a random EdDSA key."
[{:keys [query-string]}]
(log/info "query string:" query-string)
(let [{:strs [host]} (try
(when-not (str/blank? query-string)
(->> (str/split query-string #"&")
shamsimam marked this conversation as resolved.
Show resolved Hide resolved
(map #(str/split % #"="))
(into {})))
(catch Throwable th
(throw (ex-info "unable to parse query string" {:status 400} th))))
_ (when (str/blank? host)
(throw (ex-info "host query parameter not provided" {:status 400})))
{:keys [keys]} (config/retrieve-jwks)
eddsa-keys (filter (fn [{:keys [crv]}] (= "Ed25519" crv)) keys)
{:keys [kid] :as entry} (rand-nth eddsa-keys)
_ (log/info "selected kid:" kid)
principal (System/getProperty "user.name")
_ (log/info "principal:" principal)
{:keys [issuer subject-key token-type]} (config/retrieve-settings)
subject-key (keyword subject-key)
expiry-time-secs (+ (long (/ (System/currentTimeMillis) 1000)) 600)
payload (cond-> {:aud (str host) :exp expiry-time-secs :iss issuer :sub principal}
(not= :sub subject-key) (assoc subject-key principal))
header {:kid kid :typ token-type}
access-token (generate-jwt-access-token :eddsa entry payload header)]
(prepare-response
{"access_token" access-token
"kid" kid
"issuer" issuer
"principal" principal
"realm" host
"subject-key" subject-key
"token-type" token-type}
200)))

(defn process-get-keys-request
"Returns the JWKS managed bu this server."
[_]
(prepare-response
(update (config/retrieve-jwks)
:keys
(fn [keys]
(map (fn [entry] (dissoc entry :d)) keys)))
200))

(defn request-handler
"Factory for the ring request handler."
[{:keys [headers request-method uri] :as request}]
(log/info "received" request-method "request as path" uri)
(log/info "request headers:" headers)
(try
(cond
(= uri "/get-token")
(if (= request-method :get)
(process-get-token-request request)
(prepare-response request "method not allowed" 405))
(= uri "/keys")
(if (= request-method :get)
(process-get-keys-request request)
(prepare-response request "method not allowed" 405))
:else
(prepare-response request "unsupported endpoint" 404))
(catch Throwable th
(log/error th "error in processing request")
(prepare-response request (.getMessage th) (:status (ex-data th) 500)))))