Skip to content

Commit

Permalink
Use jwt_signing_profile for grant_type jwt-bearer (#619)
Browse files Browse the repository at this point in the history
* simplified checking for error cases

* check for set assertion attribute differently

* rename error messages

* grant type jwt-bearer: use inline jwt_signing_profile in the absence of assertion attribute

* add 'grant type jwt-bearer' use case to jwt_signing_profile block documentation

* move reading key bytes

* move config checks

* use JWTSigningConfig for grant type jwt-bearer

* use JWTSigningConfig for client_secret_jwt/private_key_jwt

* store TTL as int seconds

* don't expose functions only used internally

* removed unused property Name

* less copying of maps; fewer golang-jwt dependencies

* added information about use of nested jwt_signing_profile blocks

* more quotes around attribute values in documentation

* changelog entry

Co-authored-by: Alex Schneider <alex.schneider@avenga.com>
  • Loading branch information
johakoch and Alex Schneider authored Nov 30, 2022
1 parent 2c22d10 commit fe6998d
Show file tree
Hide file tree
Showing 16 changed files with 403 additions and 141 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

Unreleased changes are available as `avenga/couper:edge` container.

* **Changed**
* Use nested `jwt_signing_profile` block in [`oauth2` block](https://docs.couper.io/configuration/block/oauth2) for `grant_type` `"urn:ietf:params:oauth:grant-type:jwt-bearer"` in absence of `assertion` attribute ([#619](https://github.com/avenga/couper/pull/619))

---

## [1.11.0](https://github.com/avenga/couper/releases/tag/v1.11.0)
Expand Down
4 changes: 2 additions & 2 deletions config/ac_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ type OAuth2AC struct {
BackendName string `hcl:"backend,optional" docs:"[{backend} block](backend) reference."`
ClientID string `hcl:"client_id" docs:"The client identifier."`
ClientSecret string `hcl:"client_secret,optional" docs:"The client password."`
GrantType string `hcl:"grant_type" docs:"The grant type. Required, to be set to: {authorization_code}"`
GrantType string `hcl:"grant_type" docs:"The grant type. Required, to be set to: {\"authorization_code\"}"`
JWTSigningProfile *JWTSigningProfile `hcl:"jwt_signing_profile,block"`
Name string `hcl:"name,label"`
RedirectURI string `hcl:"redirect_uri" docs:"The Couper endpoint for receiving the authorization code. Relative URL references are resolved against the origin of the current request URL. The origin can be changed with the [{accept_forwarded_url} attribute](settings) if Couper is running behind a proxy."`
Remain hcl.Body `hcl:",remain"`
Scope string `hcl:"scope,optional" docs:"A space separated list of requested scope values for the access token."`
TokenEndpoint string `hcl:"token_endpoint" docs:"The authorization server endpoint URL used for requesting the token."`
TokenEndpointAuthMethod *string `hcl:"token_endpoint_auth_method,optional" docs:"Defines the method to authenticate the client at the token 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"`
VerifierMethod string `hcl:"verifier_method" docs:"The method to verify the integrity of the authorization code flow. Available values: {ccm_s256} ({code_challenge} parameter with {code_challenge_method} {S256}), {state} ({state} parameter)"`
VerifierMethod string `hcl:"verifier_method" docs:"The method to verify the integrity of the authorization code flow. Available values: {\"ccm_s256\"} ({code_challenge} parameter with {code_challenge_method} {S256}), {\"state\"} ({state} parameter)"`

// internally used
Backend *hclsyntax.Body
Expand Down
8 changes: 1 addition & 7 deletions config/configload/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,12 +337,6 @@ func LoadConfig(body *hclsyntax.Body) (*config.Couper, error) {
return nil, errors.Configuration.Label(profile.Name).With(fmt.Errorf(`"alg" cannot be set via "headers"`))
}
}

key, err := reader.ReadFromAttrFile("jwt_signing_profile key", profile.Key, profile.KeyFile)
if err != nil {
return nil, errors.Configuration.Label(profile.Name).With(err)
}
profile.KeyBytes = key
}

for _, saml := range helper.config.Definitions.SAML {
Expand All @@ -355,7 +349,7 @@ func LoadConfig(body *hclsyntax.Body) (*config.Couper, error) {

jwtSigningConfigs := make(map[string]*lib.JWTSigningConfig)
for _, profile := range helper.config.Definitions.JWTSigningProfile {
signConf, err := lib.NewJWTSigningConfigFromJWTSigningProfile(profile)
signConf, err := lib.NewJWTSigningConfigFromJWTSigningProfile(profile, nil)
if err != nil {
return nil, errors.Configuration.Label(profile.Name).With(err)
}
Expand Down
3 changes: 0 additions & 3 deletions config/jwt_signing_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,4 @@ type JWTSigningProfile struct {
Name string `hcl:"name,label,optional"`
SignatureAlgorithm string `hcl:"signature_algorithm" docs:"algorithm used for signing: {\"RS256\"}, {\"RS384\"}, {\"RS512\"}, {\"HS256\"}, {\"HS384\"}, {\"HS512\"}, {\"ES256\"}, {\"ES384\"}, {\"ES512\"}"`
TTL string `hcl:"ttl" docs:"The token's time-to-live, creates the {exp} claim"`

// internally used
KeyBytes []byte
}
12 changes: 6 additions & 6 deletions config/oauth2ra.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,19 @@ var (

// OAuth2ReqAuth represents the oauth2 block in a backend block.
type OAuth2ReqAuth struct {
AssertionExpr hcl.Expression `hcl:"assertion,optional" docs:"The assertion (JWT for jwt-bearer flow). Required if {grant_type} is {urn:ietf:params:oauth:grant-type:jwt-bearer}." type:"string"`
AssertionExpr hcl.Expression `hcl:"assertion,optional" docs:"The assertion (JWT for jwt-bearer flow). Required if {grant_type} is {\"urn:ietf:params:oauth:grant-type:jwt-bearer\"} and no nested {jwt_signing_profile} block is present." type:"string"`
BackendName string `hcl:"backend,optional" docs:"[{backend} block](backend) reference."`
ClientID string `hcl:"client_id,optional" docs:"The client identifier. Required unless the {grant_type} is {urn:ietf:params:oauth:grant-type:jwt-bearer}."`
ClientSecret string `hcl:"client_secret,optional" docs:"The client password. Required unless the {grant_type} is {urn:ietf:params:oauth:grant-type:jwt-bearer}."`
GrantType string `hcl:"grant_type" docs:"Required, valid values: {client_credentials}, {password}, {urn:ietf:params:oauth:grant-type:jwt-bearer}"`
ClientID string `hcl:"client_id,optional" docs:"The client identifier. Required unless the {grant_type} is {\"urn:ietf:params:oauth:grant-type:jwt-bearer\"}."`
ClientSecret string `hcl:"client_secret,optional" docs:"The client password. Required unless the {grant_type} is {\"urn:ietf:params:oauth:grant-type:jwt-bearer\"}."`
GrantType string `hcl:"grant_type" docs:"Required, valid values: {\"client_credentials\"}, {\"password\"}, {\"urn:ietf:params:oauth:grant-type:jwt-bearer\"}"`
JWTSigningProfile *JWTSigningProfile `hcl:"jwt_signing_profile,block"`
Password string `hcl:"password,optional" docs:"The (service account's) password (for password flow). Required if grant_type is {password}."`
Password string `hcl:"password,optional" docs:"The (service account's) password (for password flow). Required if grant_type is {\"password\"}."`
Remain hcl.Body `hcl:",remain"`
Retries *uint8 `hcl:"retries,optional" default:"1" docs:"The number of retries to get the token and resource, if the resource-request responds with {401 Unauthorized} HTTP status code."`
Scope string `hcl:"scope,optional" docs:"A space separated list of requested scope values for the access token."`
TokenEndpoint string `hcl:"token_endpoint,optional" docs:"URL of the token endpoint at the authorization server."`
TokenEndpointAuthMethod *string `hcl:"token_endpoint_auth_method,optional" docs:"Defines the method to authenticate the client at the token 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"`
Username string `hcl:"username,optional" docs:"The (service account's) username (for password flow). Required if grant_type is {password}."`
Username string `hcl:"username,optional" docs:"The (service account's) username (for password flow). Required if grant_type is {\"password\"}."`
}

// Reference implements the <BackendReference> interface.
Expand Down
6 changes: 4 additions & 2 deletions docs/website/content/2.configuration/4.block/beta_oauth2.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Like all [access control](/configuration/access-control) types, the `beta_oauth2
|:--------------|:----------------------------------------|:-----------------|:-----------------------------------------------------------------------------------------------------------------|
| `beta_oauth2` | [Definitions Block](/configuration/block/definitions) | &#9888; required | [Backend Block](/configuration/block/backend), [Error Handler Block](/configuration/block/error_handler), [JWT Signing Profile Block](jwt_signing_profile) |

A nested `jwt_signing_profile` block is used to create a client assertion if `token_endpoint_auth_method` is either `"client_secret_jwt"` or `"private_key_jwt"`.

::attributes
---
values: [
Expand Down Expand Up @@ -43,7 +45,7 @@ values: [
},
{
"default": "",
"description": "The grant type. Required, to be set to: `authorization_code`",
"description": "The grant type. Required, to be set to: `\"authorization_code\"`",
"name": "grant_type",
"type": "string"
},
Expand Down Expand Up @@ -73,7 +75,7 @@ values: [
},
{
"default": "",
"description": "The method to verify the integrity of the authorization code flow. Available values: `ccm_s256` (`code_challenge` parameter with `code_challenge_method` `S256`), `state` (`state` parameter)",
"description": "The method to verify the integrity of the authorization code flow. Available values: `\"ccm_s256\"` (`code_challenge` parameter with `code_challenge_method` `S256`), `\"state\"` (`state` parameter)",
"name": "verifier_method",
"type": "string"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ by its required _label_.

It can also be used (without _label_) in [`oauth2`](oauth2), [`oidc`](oidc) or
[`beta_oauth2`](beta_oauth2) blocks for `token_endpoint_auth_method`s `"client_secret_jwt"`
or `"private_key_jwt"`.
or `"private_key_jwt"` or in [`oauth2`](oauth2) blocks with
`grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"`, in the absence of an
`assertion` attribute, for configuring a self-signed JWT assertion.

| Block name | Context | Label | Nested block(s) |
|:----------------------|:--------------------------------------------------------------------------------------------------------------------|:-----------------------------------|:----------------|
Expand Down
16 changes: 10 additions & 6 deletions docs/website/content/2.configuration/4.block/oauth2.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ The `oauth2` block in the [Backend Block](/configuration/block/backend) context
|:-----------|:-------------------------|:---------|:---------------------------------------------------------------------------|
| `oauth2` | [Backend Block](/configuration/block/backend) | no label | [Backend Block](/configuration/block/backend), [JWT Signing Profile Block](jwt_signing_profile) |

A nested `jwt_signing_profile` block is used in two cases:
* to create a client assertion if `token_endpoint_auth_method` is either `"client_secret_jwt"` or `"private_key_jwt"`; or
* to create an assertion if `grant_type` is `"urn:ietf:params:oauth:grant-type:jwt-bearer"` and no `assertion` attribute is set.

::attributes
---
values: [
{
"default": "",
"description": "The assertion (JWT for jwt-bearer flow). Required if `grant_type` is `urn:ietf:params:oauth:grant-type:jwt-bearer`.",
"description": "The assertion (JWT for jwt-bearer flow). Required if `grant_type` is `\"urn:ietf:params:oauth:grant-type:jwt-bearer\"` and no nested `jwt_signing_profile` block is present.",
"name": "assertion",
"type": "string"
},
Expand All @@ -25,25 +29,25 @@ values: [
},
{
"default": "",
"description": "The client identifier. Required unless the `grant_type` is `urn:ietf:params:oauth:grant-type:jwt-bearer`.",
"description": "The client identifier. Required unless the `grant_type` is `\"urn:ietf:params:oauth:grant-type:jwt-bearer\"`.",
"name": "client_id",
"type": "string"
},
{
"default": "",
"description": "The client password. Required unless the `grant_type` is `urn:ietf:params:oauth:grant-type:jwt-bearer`.",
"description": "The client password. Required unless the `grant_type` is `\"urn:ietf:params:oauth:grant-type:jwt-bearer\"`.",
"name": "client_secret",
"type": "string"
},
{
"default": "",
"description": "Required, valid values: `client_credentials`, `password`, `urn:ietf:params:oauth:grant-type:jwt-bearer`",
"description": "Required, valid values: `\"client_credentials\"`, `\"password\"`, `\"urn:ietf:params:oauth:grant-type:jwt-bearer\"`",
"name": "grant_type",
"type": "string"
},
{
"default": "",
"description": "The (service account's) password (for password flow). Required if grant_type is `password`.",
"description": "The (service account's) password (for password flow). Required if grant_type is `\"password\"`.",
"name": "password",
"type": "string"
},
Expand Down Expand Up @@ -73,7 +77,7 @@ values: [
},
{
"default": "",
"description": "The (service account's) username (for password flow). Required if grant_type is `password`.",
"description": "The (service account's) username (for password flow). Required if grant_type is `\"password\"`.",
"name": "username",
"type": "string"
}
Expand Down
2 changes: 2 additions & 0 deletions docs/website/content/2.configuration/4.block/oidc.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Like all [access control](/configuration/access-control) types, the `oidc` block

> any `backend` attributes: Do not disable the peer certificate validation with `disable_certificate_validation = true`.
A nested `jwt_signing_profile` block is used to create a client assertion if `token_endpoint_auth_method` is either `"client_secret_jwt"` or `"private_key_jwt"`.

::attributes
---
values: [
Expand Down
63 changes: 32 additions & 31 deletions eval/lib/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,28 @@ type JWTSigningConfig struct {
Claims config.Claims
Headers hcl.Expression
Key interface{}
Name string
SignatureAlgorithm string
TTL time.Duration
TTL int64
}

func CheckData(ttl, signatureAlgorithm string) (time.Duration, acjwt.Algorithm, error) {
var (
dur time.Duration
parseErr error
)

func checkData(ttl, signatureAlgorithm string) (int64, acjwt.Algorithm, error) {
alg := acjwt.NewAlgorithm(signatureAlgorithm)
if alg == acjwt.AlgorithmUnknown {
return dur, alg, fmt.Errorf("algorithm is not supported")
return 0, alg, fmt.Errorf("algorithm is not supported")
}

if ttl != "0" {
dur, parseErr = time.ParseDuration(ttl)
if parseErr != nil {
return dur, alg, parseErr
dur, err := time.ParseDuration(ttl)
if err != nil {
return 0, alg, err
}
return int64(dur.Seconds()), alg, nil
}

return dur, alg, nil
return 0, alg, nil
}

func GetKey(keyBytes []byte, signatureAlgorithm string) (interface{}, error) {
func getKey(keyBytes []byte, signatureAlgorithm string) (interface{}, error) {
var (
key interface{}
parseErr error
Expand All @@ -68,13 +63,24 @@ func GetKey(keyBytes []byte, signatureAlgorithm string) (interface{}, error) {
return key, parseErr
}

func NewJWTSigningConfigFromJWTSigningProfile(j *config.JWTSigningProfile) (*JWTSigningConfig, error) {
ttl, _, err := CheckData(j.TTL, j.SignatureAlgorithm)
func NewJWTSigningConfigFromJWTSigningProfile(j *config.JWTSigningProfile, algCheckFunc func(alg acjwt.Algorithm) error) (*JWTSigningConfig, error) {
ttl, alg, err := checkData(j.TTL, j.SignatureAlgorithm)
if err != nil {
return nil, err
}

if algCheckFunc != nil {
if err = algCheckFunc(alg); err != nil {
return nil, err
}
}

keyBytes, err := reader.ReadFromAttrFile("jwt_signing_profile key", j.Key, j.KeyFile)
if err != nil {
return nil, err
}

key, err := GetKey(j.KeyBytes, j.SignatureAlgorithm)
key, err := getKey(keyBytes, j.SignatureAlgorithm)
if err != nil {
return nil, err
}
Expand All @@ -83,7 +89,6 @@ func NewJWTSigningConfigFromJWTSigningProfile(j *config.JWTSigningProfile) (*JWT
Claims: j.Claims,
Headers: j.Headers,
Key: key,
Name: j.Name,
SignatureAlgorithm: j.SignatureAlgorithm,
TTL: ttl,
}
Expand All @@ -95,7 +100,7 @@ func NewJWTSigningConfigFromJWT(j *config.JWT) (*JWTSigningConfig, error) {
return nil, nil
}

ttl, alg, err := CheckData(j.SigningTTL, j.SignatureAlgorithm)
ttl, alg, err := checkData(j.SigningTTL, j.SignatureAlgorithm)
if err != nil {
return nil, err
}
Expand All @@ -114,15 +119,14 @@ func NewJWTSigningConfigFromJWT(j *config.JWT) (*JWTSigningConfig, error) {
return nil, err
}

key, err := GetKey(keyBytes, j.SignatureAlgorithm)
key, err := getKey(keyBytes, j.SignatureAlgorithm)
if err != nil {
return nil, err
}

c := &JWTSigningConfig{
Claims: j.Claims,
Key: key,
Name: j.Name,
SignatureAlgorithm: j.SignatureAlgorithm,
TTL: ttl,
}
Expand Down Expand Up @@ -154,8 +158,7 @@ func NewJwtSignFunction(ctx *hcl.EvalContext, jwtSigningConfigs map[string]*JWTS
return cty.StringVal(""), fmt.Errorf("missing jwt_signing_profile or jwt (with signing_ttl) for given label %q", label)
}

mapClaims := jwt.MapClaims{}
var defaultClaims, argumentClaims, headers map[string]interface{}
var claims, argumentClaims, headers map[string]interface{}

if signingConfig.Headers != nil {
h, diags := evalFn(ctx, signingConfig.Headers)
Expand All @@ -171,15 +174,13 @@ func NewJwtSignFunction(ctx *hcl.EvalContext, jwtSigningConfigs map[string]*JWTS
if diags != nil {
return cty.StringVal(""), err
}
defaultClaims = seetie.ValueToMap(v)
}

for k, v := range defaultClaims {
mapClaims[k] = v
claims = seetie.ValueToMap(v)
} else {
claims = make(map[string]interface{})
}

if signingConfig.TTL != 0 {
mapClaims["exp"] = time.Now().Unix() + int64(signingConfig.TTL.Seconds())
claims["exp"] = time.Now().Unix() + signingConfig.TTL
}

// get claims from function argument
Expand All @@ -194,10 +195,10 @@ func NewJwtSignFunction(ctx *hcl.EvalContext, jwtSigningConfigs map[string]*JWTS
}

for k, v := range argumentClaims {
mapClaims[k] = v
claims[k] = v
}

tokenString, err := CreateJWT(signingConfig.SignatureAlgorithm, signingConfig.Key, mapClaims, headers)
tokenString, err := CreateJWT(signingConfig.SignatureAlgorithm, signingConfig.Key, claims, headers)
if err != nil {
return cty.StringVal(""), err
}
Expand Down
Loading

0 comments on commit fe6998d

Please sign in to comment.