Skip to content

Commit

Permalink
integration test for client_secret_jwt and private_key_jwt: the created
Browse files Browse the repository at this point in the history
JWT is validated at the AS's token endpoint; some claims are passed in
the access token to the client to further check them at the RS; the x5t
header is not tested (yet)
  • Loading branch information
Johannes Koch committed Sep 20, 2022
1 parent f6643f0 commit 949da26
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 2 deletions.
97 changes: 95 additions & 2 deletions server/http_oauth2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"net/http/httptest"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
Expand Down Expand Up @@ -875,7 +877,6 @@ definitions {
`,
"configuration error: be: client authentication key: read error: open ",
},

} {
var errMsg string
conf, err := configload.LoadBytes([]byte(tc.hcl), "couper.hcl")
Expand Down Expand Up @@ -906,6 +907,98 @@ definitions {
}
}

func TestOAuth2_AuthnJWT(t *testing.T) {
helper := test.New(t)
jtiRE, err := regexp.Compile("^[a-zA-Z0-9]{43}$")
helper.Must(err)

rsOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
authz := req.Header.Get("Authorization")
if !strings.HasPrefix(authz, "Bearer ") {
helper.Must(fmt.Errorf("wrong authz: %q", authz))
}
token := strings.TrimPrefix(authz, "Bearer ")
parts := strings.Split(token, " ")
if len(parts) != 3 {
helper.Must(fmt.Errorf("wrong token: %q", token))
}
exp, err := strconv.Atoi(parts[1])
helper.Must(err)
iat, err := strconv.Atoi(parts[0])
helper.Must(err)
if exp-iat != 10 {
helper.Must(fmt.Errorf("wrong token: %q", token))
}
if !jtiRE.MatchString(parts[2]) {
helper.Must(fmt.Errorf("wrong jti: %q", parts[2]))
}
rw.WriteHeader(http.StatusNoContent)
}))
defer rsOrigin.Close()

type testCase struct {
name string
path string
wantStatus int
wantErrLog string
}

for _, tc := range []testCase{
{
"client_secret_jwt",
"/csj",
http.StatusNoContent,
"",
},
{
"client_secret_jwt error",
"/csj_error",
http.StatusBadGateway,
"access control error: csj_error: token signature is invalid",
},
{
"private_key_jwt",
"/pkj",
http.StatusNoContent,
"",
},
{
"private_key_jwt error",
"/pkj_error",
http.StatusBadGateway,
"access control error: pkj_error: token is unverifiable: signing method RS256 is invalid",
},
} {
t.Run(tc.name, func(subT *testing.T) {
h := test.New(subT)

shutdown, hook := newCouperWithTemplate("testdata/oauth2/20_couper.hcl", h, map[string]interface{}{"rsOrigin": rsOrigin.URL})
defer shutdown()

req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080"+tc.path, nil)
h.Must(err)

hook.Reset()

res, err := newClient().Do(req)
h.Must(err)

if res.StatusCode != tc.wantStatus {
t.Errorf("expected status %d, got: %d", tc.wantStatus, res.StatusCode)
}

message := getFirstAccessLogMessage(hook)
if message != tc.wantErrLog {
t.Errorf("error log\nwant: %q\ngot: %q", tc.wantErrLog, message)
}

shutdown()
})
}

rsOrigin.Close()
}

func TestOAuth2_Runtime_Errors(t *testing.T) {
helper := test.New(t)

Expand Down Expand Up @@ -953,7 +1046,7 @@ func TestOAuth2_Runtime_Errors(t *testing.T) {
h.Must(err)

if res.StatusCode != http.StatusBadGateway {
t.Errorf("expected status NoContent, got: %d", res.StatusCode)
t.Errorf("expected status StatusBadGateway, got: %d", res.StatusCode)
}

message := getFirstAccessLogMessage(hook)
Expand Down
174 changes: 174 additions & 0 deletions server/testdata/oauth2/20_couper.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
server {
hosts = ["*:8080"]

api {
endpoint "/csj" {
proxy {
backend = "csj"
}
}

endpoint "/csj_error" {
proxy {
backend = "csj_error"
}
}

endpoint "/pkj" {
proxy {
backend = "pkj"
}
}

endpoint "/pkj_error" {
proxy {
backend = "pkj_error"
}
}
}
}

definitions {
backend "csj" {
origin = "{{.rsOrigin}}"

oauth2 {
token_endpoint = "http://1.1.1.1:9999/token/csj"
grant_type = "client_credentials"
client_id = "my_clid"
client_secret = "my_cls"
token_endpoint_auth_method = "client_secret_jwt"
authn_signature_algorithm = "HS256"
authn_ttl = "10s"
}
}

backend "csj_error" {
origin = "{{.rsOrigin}}"

oauth2 {
token_endpoint = "http://1.1.1.1:9999/token/csj/error"
grant_type = "client_credentials"
client_id = "my_clid"
client_secret = "my_cls"
token_endpoint_auth_method = "client_secret_jwt"
authn_signature_algorithm = "HS256"
authn_ttl = "10s"
}
}

backend "pkj" {
origin = "{{.rsOrigin}}"

oauth2 {
token_endpoint = "http://1.1.1.1:9999/token/pkj"
grant_type = "client_credentials"
client_id = "my_clid"
token_endpoint_auth_method = "private_key_jwt"
authn_key_file = "./testdata/oauth2/pkcs8.key"
authn_signature_algorithm = "RS256"
authn_ttl = "10s"
authn_aud_claim = "some explicit value"
}
}

backend "pkj_error" {
origin = "{{.rsOrigin}}"

oauth2 {
token_endpoint = "http://1.1.1.1:9999/token/pkj/error"
grant_type = "client_credentials"
client_id = "my_clid"
token_endpoint_auth_method = "private_key_jwt"
authn_key_file = "./testdata/oauth2/pkcs8.key"
authn_signature_algorithm = "RS256"
authn_ttl = "10s"
}
}

jwt "csj" {
token_value = request.form_body.client_assertion[0]
signature_algorithm = "HS256"
key = "my_cls"
claims = {
iss = "my_clid"
sub = "my_clid"
aud = "http://1.1.1.1:9999/token/csj"
}
required_claims = ["iat", "exp", "jti"]
}

jwt "csj_error" {
token_value = request.form_body.client_assertion[0]
signature_algorithm = "HS256"
key = "wrong key"
}

jwt "pkj" {
token_value = request.form_body.client_assertion[0]
signature_algorithm = "RS256"
key_file = "./testdata/oauth2/certificate.pem"
claims = {
iss = "my_clid"
sub = "my_clid"
aud = "some explicit value"
}
required_claims = ["iat", "exp", "jti"]
}

jwt "pkj_error" {
token_value = request.form_body.client_assertion[0]
signature_algorithm = "HS256"
key = "wrong key"
}
}

server {
hosts = ["*:9999"]

api {
endpoint "/token/csj" {
access_control = ["csj"]

response {
json_body = {
access_token = "${request.context.csj.iat} ${request.context.csj.exp} ${request.context.csj.jti}"
expires_in = 60
}
}
}

endpoint "/token/csj/error" {
access_control = ["csj_error"]

response {
json_body = {
access_token = "qoebnqeb"
expires_in = 60
}
}
}

endpoint "/token/pkj" {
access_control = ["pkj"]

response {
json_body = {
access_token = "${request.context.pkj.iat} ${request.context.pkj.exp} ${request.context.pkj.jti}"
expires_in = 60
}
}
}

endpoint "/token/pkj/error" {
access_control = ["pkj_error"]

response {
json_body = {
access_token = "qoebnqeb"
expires_in = 60
}
}
}
}
}
21 changes: 21 additions & 0 deletions server/testdata/oauth2/certificate.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUUc60oTARAfBxsH/kWM1V/hwVvWEwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA5MDYwOTUyMzFaFw00OTAx
MjIwOTUyMzFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDE65urxA3yAFUTAg0J9U2+ZkCE75kt4plqlPfw7JmS
qb7wFSz3xwEWbNQKK43ZwWuO6GhEBePZhX/eW7RLL8beScsVsZJ+4n6hm7Bg0MDe
VqwYk1midESwBQl17WEjS4ltx4nNwkOJ9THbMZHl4JFAqXHGJX+KsTL7Bn79833G
u57XwHKQvPodAFe7iaFzXur2oBm3HeEsFHFDCRkZvG6aBgZ09e6HZhXzZp3DXzCJ
kheBU69rCBdC+VUOeNJouoTdET9uRWZaQThNFC4ViGqgQVXnREEgsRaXBQeo2CAv
3NTDb9F1bW5PJAm0QxGLPpaCJZqrSX1KSaWtIUDZIMDnAgMBAAGjUzBRMB0GA1Ud
DgQWBBTB0Uww2YLLyIq7GkUXgIK3prhOIDAfBgNVHSMEGDAWgBTB0Uww2YLLyIq7
GkUXgIK3prhOIDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC4
M1e8o0qGN51yn5yAoiFB68+g45XhGr8ebFI66nSs35/f38xbjG4RQVLiJHdztOS1
axaUORssN5KycvNGZUv89LEd7hw9a8H9Nj/T80M4u3e5kGYwFHc/1su/CoT20tmT
VkXOnjnOV++5xK8feLt9lN9d5PFeSE+RnByfdtN3GeKE2JWr5h8Ld6gVVIepilSG
jw7pmfSZvtCcnAHY4jpyW6Qfujp/m1XczLnzcDxE6DF9M7aVePH2dQW7t1zq5J1A
S7O+snPtz4NKLfRM+1xtVH7zig9zlc/gnqCJMy72howXP0daUmcJfEsPh7Eos50a
uc7Gh5RWuHDesWBWtAJF
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions server/testdata/oauth2/pkcs8.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDE65urxA3yAFUT
Ag0J9U2+ZkCE75kt4plqlPfw7JmSqb7wFSz3xwEWbNQKK43ZwWuO6GhEBePZhX/e
W7RLL8beScsVsZJ+4n6hm7Bg0MDeVqwYk1midESwBQl17WEjS4ltx4nNwkOJ9THb
MZHl4JFAqXHGJX+KsTL7Bn79833Gu57XwHKQvPodAFe7iaFzXur2oBm3HeEsFHFD
CRkZvG6aBgZ09e6HZhXzZp3DXzCJkheBU69rCBdC+VUOeNJouoTdET9uRWZaQThN
FC4ViGqgQVXnREEgsRaXBQeo2CAv3NTDb9F1bW5PJAm0QxGLPpaCJZqrSX1KSaWt
IUDZIMDnAgMBAAECggEBAKNQAzrgze/19phdCxNHLcLKapfVXeSAGVwbT8Wvc23+
+SuDZFfZ4z0F5JTKqkn974YFmPNRLzYnUXTH+S6h4PxZluPW8PfqP7sns4+XkVzT
5MY87gmdA5o3kzEKPZVYABHbep36jqjLgR2YbreHFu/Zl1INp0kOzIkDSi6y0Y0O
eIEsRjbfp1/JWk+Tf8HiAAEiMf65M1r4LPHrPzW6ly5lCTXuMRS6WBdMxT9VgPgp
hu6FvzspG2MCYoh8jD/ITrbcf9Amp4hCoiBC6K35kzzGv4c06D5IBfZB7+8BCXqw
0d3T+JqOdAEL0MEYhlNzflBeP1dSoTsPVmt3rnoKL4ECgYEA5+LlF1DgF043iZ/H
dKvDy5ZLATI5+pcsOFOC9heRVTaZFKL/HGGykEN139Ts0hDwQiIbLZ3gr8d1jb02
WWWhz27fpH7//lqKnapfChsckKe3UJJ2zg67q9PZYrq2IU9FFZTkZyelNm2bebM4
vLkPacmQ+/CEc8+CAyRrah1Yg00CgYEA2WXgEcIO7wTQhQq8wIlZjMj/eKzHVEl+
AISzC95AI6uji5RcEpEclwMw49st3WO1NJcA03zuU03bUk55w9zKt7H3K1djlvQi
n0tXRkNcGGo2FiV0hsyzdN5p8DYFsgeUv6PtzlB2O5N18KGDxxYwS/o8X8b4BBCE
yHcCbwd6kwMCgYEAzsPFbL3jo0RORzweeIz0ICOaK63ifuyvNGZavx6Sq33sj7cr
bN48f2B3yactp74MzZtlyo4dG//pdQJDZQE3gCQn7KCFmQKY2S9iYTt9hArYbVK7
9s6yTuuuydccsaTiP/UsmEKEkXy4hpLlQ3psIPLngY6cPDvKfQzLbqpOE30CgYEA
g8+5JAs9cr7Aj2oLN9IPccUM4OYhlYFZ3IaY6MFAsmAHMUIq8Tb61rUgl4b1MB6c
Z96GqbQ97FRwfl8GhTMB1o8ZBjZeM9CijWLo77k3xbXgRV1AYdsLk/im0vZuTZs7
HVcPgOBYT5cBE31aoQNbFTYMFSZWimBZohJGb3thXnECgYEAn3My4uCRLK8i6bjl
XewerYfTbSF5DKVWmDKHFBDUFj1IvgW8nn+29s4kTOcZWlzPMLlcZXCBIMcWNpPn
Wpd0ZHmpKn8xIgiIYIWrsAI0QZNSgZpOKqwK0ve3tXAYCL/NMbYDsvDS09vAdPNu
I/W9l5w3VVeLiSW1mKdwq0EOJ1s=
-----END PRIVATE KEY-----

0 comments on commit 949da26

Please sign in to comment.