Skip to content

Commit

Permalink
use client authenticator in token introspector
Browse files Browse the repository at this point in the history
  • Loading branch information
Johannes Koch committed Jan 4, 2023
1 parent d48ee53 commit 6034084
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 15 deletions.
33 changes: 24 additions & 9 deletions accesscontrol/introspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,34 @@ import (
"sync"
"time"

"github.com/hashicorp/hcl/v2"

"github.com/avenga/couper/cache"
"github.com/avenga/couper/config"
"github.com/avenga/couper/config/request"
"github.com/avenga/couper/eval"
"github.com/avenga/couper/oauth2"
)

type Introspector struct {
conf *config.Introspection
memStore *cache.MemoryStore
mu sync.Mutex
transport http.RoundTripper
authenticator *oauth2.ClientAuthenticator
conf *config.Introspection
memStore *cache.MemoryStore
mu sync.Mutex
transport http.RoundTripper
}

func NewIntrospector(conf *config.Introspection, transport http.RoundTripper, memStore *cache.MemoryStore) *Introspector {
return &Introspector{
conf: conf,
memStore: memStore,
transport: transport,
func NewIntrospector(evalCtx *hcl.EvalContext, conf *config.Introspection, transport http.RoundTripper, memStore *cache.MemoryStore) (*Introspector, error) {
authenticator, err := oauth2.NewClientAuthenticator(evalCtx, conf.EndpointAuthMethod, "endpoint_auth_method", conf.ClientID, conf.ClientSecret, "", conf.JWTSigningProfile)
if err != nil {
return nil, err
}
return &Introspector{
authenticator: authenticator,
conf: conf,
memStore: memStore,
transport: transport,
}, nil
}

type IntrospectionResponse map[string]interface{}
Expand Down Expand Up @@ -70,6 +79,12 @@ func (i *Introspector) Introspect(ctx context.Context, token string, exp, nbf in

formParams := &url.Values{}
formParams.Add("token", token)

err := i.authenticator.Authenticate(formParams, req)
if err != nil {
return nil, err
}

eval.SetBody(req, []byte(formParams.Encode()))

req = req.WithContext(outCtx)
Expand Down
12 changes: 8 additions & 4 deletions config/introspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ var (
)

type Introspection struct {
BackendName string `hcl:"backend,optional" docs:"References a [backend](/configuration/block/backend) in [definitions](/configuration/block/definitions) for introspection requests. Mutually exclusive with {backend} block."`
Endpoint string `hcl:"endpoint" docs:"The authorization server's {introspection_endpoint}."`
Remain hcl.Body `hcl:",remain"`
TTL string `hcl:"ttl" docs:"The time-to-live of a cached introspection response. With a non-positive value the introspection endpoint is called each time a token is validated." type:"duration"`
BackendName string `hcl:"backend,optional" docs:"References a [backend](/configuration/block/backend) in [definitions](/configuration/block/definitions) for introspection requests. Mutually exclusive with {backend} block."`
ClientID string `hcl:"client_id" docs:"The client identifier."`
ClientSecret string `hcl:"client_secret,optional" docs:"The client password. Required unless the {endpoint_auth_method} is {\"private_key_jwt\"}."`
Endpoint string `hcl:"endpoint" docs:"The authorization server's {introspection_endpoint}."`
EndpointAuthMethod *string `hcl:"endpoint_auth_method,optional" docs:"Defines the method to authenticate the client at the introspection endpoint. If set to {\"client_secret_post\"}, the client credentials are transported in the request body. If set to {\"client_secret_basic\"}, the client credentials are transported via Basic Authentication. If set to {\"client_secret_jwt\"}, the client is authenticated via a JWT signed with the {client_secret}. If set to {\"private_key_jwt\"}, the client is authenticated via a JWT signed with its private key (see {jwt_signing_profile} block)." default:"client_secret_basic"`
JWTSigningProfile *JWTSigningProfile `hcl:"jwt_signing_profile,block" docs:"Configures a [JWT signing profile](/configuration/block/jwt_signing_profile) to create a client assertion if {endpoint_auth_method} is either {\"client_secret_jwt\"} or {\"private_key_jwt\"}."`
Remain hcl.Body `hcl:",remain"`
TTL string `hcl:"ttl" docs:"The time-to-live of a cached introspection response. With a non-positive value the introspection endpoint is called each time a token is validated." type:"duration"`

// Internally used
Backend *hclsyntax.Body
Expand Down
2 changes: 1 addition & 1 deletion config/runtime/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ func configureIntrospector(jwtConf *config.JWT, confContext *hcl.EvalContext, lo
return nil, err
}

return ac.NewIntrospector(jwtConf.Introspection, backend, memStore), nil
return ac.NewIntrospector(confContext, jwtConf.Introspection, backend, memStore)
}

type protectedOptions struct {
Expand Down
22 changes: 22 additions & 0 deletions docs/website/content/2.configuration/4.block/introspection.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,30 @@ values: [
"name": "backend",
"type": "string"
},
{
"default": "",
"description": "The client identifier.",
"name": "client_id",
"type": "string"
},
{
"default": "",
"description": "The client password. Required unless the `endpoint_auth_method` is `\"private_key_jwt\"`.",
"name": "client_secret",
"type": "string"
},
{
"default": "",
"description": "The authorization server's `introspection_endpoint`.",
"name": "endpoint",
"type": "string"
},
{
"default": "\"client_secret_basic\"",
"description": "Defines the method to authenticate the client at the introspection endpoint. If set to `\"client_secret_post\"`, the client credentials are transported in the request body. If set to `\"client_secret_basic\"`, the client credentials are transported via Basic Authentication. If set to `\"client_secret_jwt\"`, the client is authenticated via a JWT signed with the `client_secret`. If set to `\"private_key_jwt\"`, the client is authenticated via a JWT signed with its private key (see `jwt_signing_profile` block).",
"name": "endpoint_auth_method",
"type": "string"
},
{
"default": "",
"description": "The time-to-live of a cached introspection response. With a non-positive value the introspection endpoint is called each time a token is validated.",
Expand All @@ -38,6 +56,10 @@ values: [
{
"description": "Configures a [backend](/configuration/block/backend) for introspection requests (zero or one). Mutually exclusive with `backend` attribute.",
"name": "backend"
},
{
"description": "Configures a [JWT signing profile](/configuration/block/jwt_signing_profile) to create a client assertion if `endpoint_auth_method` is either `\"client_secret_jwt\"` or `\"private_key_jwt\"`.",
"name": "jwt_signing_profile"
}
]

Expand Down
42 changes: 42 additions & 0 deletions server/http_oauth2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2394,3 +2394,45 @@ func Test_OAuth2_Introspection_NonCaching(t *testing.T) {
t.Errorf("Expected error type\nWant:\t%s\nGot:\t%s", expErrorType, et)
}
}

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

shutdown, _ := newCouper("testdata/oauth2/25_couper.hcl", test.New(t))
defer shutdown()

client := newClient()

// get token
req, err := http.NewRequest(http.MethodGet, "http://1.1.1.1:9999/token", nil)
helper.Must(err)
res, err := client.Do(req)
helper.Must(err)
token := res.Header.Get("access-token")

type testCase struct {
name string
path string
}

for _, tc := range []testCase{
{"client_secret_basic", "/csb"},
{"client_secret_post", "/csp"},
{"client_secret_jwt", "/csj"},
{"private_key_jwt", "/pkj"},
} {
t.Run(tc.name, func(subT *testing.T) {
h := test.New(subT)

// get resource
req, err = http.NewRequest(http.MethodGet, "http://anyserver:8080"+tc.path, nil)
h.Must(err)
req.Header.Set("Authorization", "Bearer "+token)
res, err = client.Do(req)
h.Must(err)
if res.StatusCode != http.StatusNoContent {
subT.Errorf("Expected status code 204, got %d", res.StatusCode)
}
})
}
}
4 changes: 3 additions & 1 deletion server/testdata/oauth2/24_couper.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ server {
api "as" {
base_path = "/as"

endpoint "/token" {
endpoint "/token" { # not proper OAuth2 token endpoint, but token is easier to extract from header
response {
headers = {
access-token = jwt_sign("at", {})
Expand Down Expand Up @@ -31,6 +31,8 @@ definitions {
introspection {
endpoint = "{{.asOrigin}}/introspect"
ttl = "{{.ttl}}"
client_id = "the_rs"
client_secret = "the_rs_asdf"
}
}
}
180 changes: 180 additions & 0 deletions server/testdata/oauth2/25_couper.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
server "rs" {
hosts = ["*:8080"]

api {
endpoint "/csb" {
access_control = ["at_in_csb"]

response {
status = 204
}
}

endpoint "/csp" {
access_control = ["at_in_csp"]

response {
status = 204
}
}

endpoint "/csj" {
access_control = ["at_in_csj"]

response {
status = 204
}
}

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

response {
status = 204
}
}
}
}

definitions {
# for rs
jwt "at_in_csb" {
signature_algorithm = "HS256"
key = "asdf"

introspection {
endpoint = "http://1.1.1.1:9999/introspect/csb"
ttl = "0s"
client_id = "the_rs"
client_secret = "the_rs_asdf"
}
}

jwt "at_in_csp" {
signature_algorithm = "HS256"
key = "asdf"

introspection {
endpoint = "http://1.1.1.1:9999/introspect/csp"
endpoint_auth_method = "client_secret_post"
ttl = "0s"
client_id = "the_rs"
client_secret = "the_rs_asdf"
}
}

jwt "at_in_csj" {
signature_algorithm = "HS256"
key = "asdf"

introspection {
endpoint = "http://1.1.1.1:9999/introspect/csj"
endpoint_auth_method = "client_secret_jwt"
ttl = "0s"
client_id = "the_rs"
client_secret = "the_rs_asdf"

jwt_signing_profile {
signature_algorithm = "HS256"
ttl = "10s"
}
}
}

jwt "at_in_pkj" {
signature_algorithm = "HS256"
key = "asdf"

introspection {
endpoint = "http://1.1.1.1:9999/introspect/pkj"
endpoint_auth_method = "private_key_jwt"
ttl = "0s"
client_id = "the_rs"

jwt_signing_profile {
signature_algorithm = "RS256"
key_file = "./pkcs8.key"
ttl = "10s"
}
}
}

# for as
jwt_signing_profile "at" {
signature_algorithm = "HS256"
key = "asdf"
ttl = "60s"
}

basic_auth "ba_csb" {
user = "the_rs"
password = "the_rs_asdf"
}

jwt "jwt_csj" {
signature_algorithm = "HS256"
key = "the_rs_asdf"
token_value = request.form_body.client_assertion[0]
claims = {
iss = "the_rs"
sub = "the_rs"
}
required_claims = ["exp", "iat", "jti"]
}

jwt "jwt_pkj" {
signature_algorithm = "RS256"
key_file = "./certificate.pem"
token_value = request.form_body.client_assertion[0]
claims = {
iss = "the_rs"
sub = "the_rs"
}
required_claims = ["exp", "iat", "jti"]
}
}

server "as" {
hosts = ["*:9999"]

api {
endpoint "/token" { # not proper OAuth2 token endpoint, but token is easier to extract from header
response {
headers = {
access-token = jwt_sign("at", {})
}
}
}

endpoint "/introspect/csb" {
access_control = ["ba_csb"]

response {
json_body = {active: true}
}
}

endpoint "/introspect/csp" {
response {
status = request.form_body.client_id[0] == "the_rs" && request.form_body.client_secret[0] == "the_rs_asdf" ? 200 : 401
json_body = {active: true}
}
}

endpoint "/introspect/csj" {
access_control = ["jwt_csj"]

response {
json_body = {active: true}
}
}

endpoint "/introspect/pkj" {
access_control = ["jwt_pkj"]

response {
json_body = {active: true}
}
}
}
}

0 comments on commit 6034084

Please sign in to comment.