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

Use jwt block for signing #309

Merged
merged 13 commits into from
Sep 10, 2021
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Unreleased changes are available as `avenga/couper:edge` container.
* **Added**
* `Accept: application/json` request header to the OAuth2 token request, in order to make the Github token endpoint respond with a JSON token response ([#307](https://github.com/avenga/couper/pull/307))
* Documentation of [logs](docs/LOGS.md) ([#310](https://github.com/avenga/couper/pull/310))
* `signing_ttl` and `signing_key`/`signing_key_file` to [`jwt` block](./docs/REFERENCE.md#jwt-block) for use with [`jwt_sign()` function](#functions) ([#309](https://github.com/avenga/couper/pull/309))

* **Changed**
* Organized log format fields for uniform access and upstream log ([#300](https://github.com/avenga/couper/pull/300))
Expand Down
14 changes: 7 additions & 7 deletions accesscontrol/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/dgrijalva/jwt-go/v4"

acjwt "github.com/avenga/couper/accesscontrol/jwt"
"github.com/avenga/couper/config/request"
"github.com/avenga/couper/errors"
)
Expand All @@ -25,7 +26,6 @@ const (
var _ AccessControl = &JWT{}

type (
Algorithm int
JWTSourceType uint8
JWTSource struct {
Name string
Expand All @@ -34,7 +34,7 @@ type (
)

type JWT struct {
algorithm Algorithm
algorithm acjwt.Algorithm
claims map[string]interface{}
claimsRequired []string
source JWTSource
Expand Down Expand Up @@ -76,7 +76,7 @@ func NewJWTSource(cookie, header string) JWTSource {
// NewJWT parses the key and creates Validation obj which can be referenced in related handlers.
func NewJWT(options *JWTOptions) (*JWT, error) {
jwtAC := &JWT{
algorithm: NewAlgorithm(options.Algorithm),
algorithm: acjwt.NewAlgorithm(options.Algorithm),
claims: options.Claims,
claimsRequired: options.ClaimsRequired,
name: options.Name,
Expand All @@ -87,7 +87,7 @@ func NewJWT(options *JWTOptions) (*JWT, error) {
return nil, fmt.Errorf("token source is invalid")
}

if jwtAC.algorithm == AlgorithmUnknown {
if jwtAC.algorithm == acjwt.AlgorithmUnknown {
return nil, fmt.Errorf("algorithm is not supported")
}

Expand Down Expand Up @@ -169,9 +169,9 @@ func (j *JWT) Validate(req *http.Request) error {

func (j *JWT) getValidationKey(_ *jwt.Token) (interface{}, error) {
switch j.algorithm {
case AlgorithmRSA256, AlgorithmRSA384, AlgorithmRSA512:
case acjwt.AlgorithmRSA256, acjwt.AlgorithmRSA384, acjwt.AlgorithmRSA512:
return j.pubKey, nil
case AlgorithmHMAC256, AlgorithmHMAC384, AlgorithmHMAC512:
case acjwt.AlgorithmHMAC256, acjwt.AlgorithmHMAC384, acjwt.AlgorithmHMAC512:
return j.hmacSecret, nil
default: // this error case gets normally caught on configuration level
return nil, errors.Configuration.Message("algorithm is not supported")
Expand Down Expand Up @@ -220,7 +220,7 @@ func getBearer(val string) (string, error) {
return "", errors.JwtTokenExpired.Message("bearer required with authorization header")
}

func newParser(algo Algorithm, claims map[string]interface{}) (*jwt.Parser, error) {
func newParser(algo acjwt.Algorithm, claims map[string]interface{}) (*jwt.Parser, error) {
options := []jwt.ParserOption{
jwt.WithValidMethods([]string{algo.String()}),
jwt.WithLeeway(time.Second),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
package accesscontrol
package jwt

type (
Algorithm int
)

const (
AlgorithmUnknown Algorithm = iota - 1
Expand Down
5 changes: 3 additions & 2 deletions accesscontrol/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/dgrijalva/jwt-go/v4"

ac "github.com/avenga/couper/accesscontrol"
acjwt "github.com/avenga/couper/accesscontrol/jwt"
"github.com/avenga/couper/config/reader"
"github.com/avenga/couper/config/request"
"github.com/avenga/couper/errors"
Expand Down Expand Up @@ -134,7 +135,7 @@ QolLGgj3tz4NbDEitq+zKMr0uTHvP1Vyu1mXAflcpYcJA4ZmuB3Oj39e0U0gnmr/

func Test_JWT_Validate(t *testing.T) {
type fields struct {
algorithm ac.Algorithm
algorithm acjwt.Algorithm
claims map[string]interface{}
claimsRequired []string
source ac.JWTSource
Expand All @@ -155,7 +156,7 @@ func Test_JWT_Validate(t *testing.T) {
var token string
var tokenErr error

algo := ac.NewAlgorithm(signingMethod.Alg())
algo := acjwt.NewAlgorithm(signingMethod.Alg())

if algo.IsHMAC() {
pubKeyBytes = []byte("mySecretK3y")
Expand Down
3 changes: 3 additions & 0 deletions config/ac_jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type JWT struct {
PostParam string `hcl:"post_param,optional"`
QueryParam string `hcl:"query_param,optional"`
SignatureAlgorithm string `hcl:"signature_algorithm"`
SigningKey string `hcl:"signing_key,optional"`
SigningKeyFile string `hcl:"signing_key_file,optional"`
SigningTTL string `hcl:"signing_ttl,optional"`

// Internally used for 'error_handler'.
Remain hcl.Body `hcl:",remain"`
Expand Down
27 changes: 26 additions & 1 deletion config/configload/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/avenga/couper/config/reader"
"github.com/avenga/couper/errors"
"github.com/avenga/couper/eval"
"github.com/avenga/couper/eval/lib"
)

const (
Expand Down Expand Up @@ -290,8 +291,32 @@ func LoadConfig(body hcl.Body, src []byte, filename string) (*config.Couper, err
saml.MetadataBytes = metadata
}

jwtSigningConfigs := make(map[string]*lib.JWTSigningConfig, 0)
for _, profile := range couperConfig.Definitions.JWTSigningProfile {
if _, exists := jwtSigningConfigs[profile.Name]; exists {
return nil, errors.Configuration.Messagef("jwt_signing_profile block with label %s already defined", profile.Name)
}
config, err := lib.NewJWTSigningConfigFromJWTSigningProfile(profile)
if err != nil {
return nil, errors.Configuration.Label(profile.Name).With(err)
}
jwtSigningConfigs[profile.Name] = config
}
for _, jwt := range couperConfig.Definitions.JWT {
config, err := lib.NewJWTSigningConfigFromJWT(jwt)
if err != nil {
return nil, errors.Configuration.Label(jwt.Name).With(err)
}
if config != nil {
if _, exists := jwtSigningConfigs[jwt.Name]; exists {
return nil, errors.Configuration.Messagef("jwt_signing_profile or jwt with label %s already defined", jwt.Name)
}
jwtSigningConfigs[jwt.Name] = config
}
}

couperConfig.Context = evalContext.
WithJWTProfiles(couperConfig.Definitions.JWTSigningProfile).
WithJWTSigningConfigs(jwtSigningConfigs).
WithOAuth2AC(couperConfig.Definitions.OAuth2AC).
WithSAML(couperConfig.Definitions.SAML)

Expand Down
18 changes: 14 additions & 4 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,10 +346,20 @@ required _label_.
| `header` |string|-|-|⚠ Implies `Bearer` if `Authorization` (case-insensitive) is used, otherwise any other header name can be used.|`header = "Authorization"` |
| `key` |string|-|Public key (in PEM format) for `RS*` variants or the secret for `HS*` algorithm.|-|-|
| `key_file` |string|-|Optional file reference instead of `key` usage.|-|-|
| `signature_algorithm` |string|-|-|⚠ required. Valid values are: `RS256` `RS384` `RS512` `HS256` `HS384` `HS512`.|-|
| `signature_algorithm` |string|-|-|⚠ required. Valid values are: `RS256` `RS384` `RS512` `HS256` `HS384` `HS512`.|-|
| `claims` |string|-|Equals/in comparison with JWT payload.|-|-|
| `required_claims` | string|-|list of claims that must be given for a valid token |-|-|

The `jwt` block may also be referenced by the [`jwt_sign()` function](#functions), if it has a `signing_ttl` defined. For `HS*` algorithms the signing key is taken from `key`/`key_file`, for `RS*` algorithms, `signing_key` or `signing_key_file` have to be specified.

*Note:* A `jwt` block with `signing_ttl` cannot have the same label as a `jwt_signing_profile` block.
johakoch marked this conversation as resolved.
Show resolved Hide resolved
alex-schneider marked this conversation as resolved.
Show resolved Hide resolved

| Attribute(s) | Type |Default|Description|Characteristic(s)| Example|
| :-------- | :--------------- | :--------------- | :--------------- | :--------------- | :--------------- |
| `signing_key` |string|-|Private key (in PEM format) for `RS*` variants.|-|-|
| `signing_key_file` |string|-|Optional file reference instead of `signing_key` usage.|-|-|
| `signing_ttl` |[duration](#duration)|-|The token's time-to-live (creates the `exp` claim).|-|-|

### JWT Signing Profile Block

The `jwt_signing_profile` block lets you configure a JSON Web Token signing
Expand All @@ -368,7 +378,7 @@ An example can be found
| `key` |string|-|Private key (in PEM format) for `RS*` variants or the secret for `HS*` algorithm.|-|-|
| `key_file` |string|-|Optional file reference instead of `key` usage.|-|-|
| `signature_algorithm`|-|-|-|⚠ required. Valid values are: `RS256` `RS384` `RS512` `HS256` `HS384` `HS512`.|-|
|`ttl` |string|-|The token's time-to-live (creates the `exp` claim).|-|-|
|`ttl` |[duration](#duration)|-|The token's time-to-live (creates the `exp` claim).|-|-|
| `claims` |string|-|Default claims for the JWT payload.|-|-|

### OAuth2 AC Block (Beta)
Expand Down Expand Up @@ -413,7 +423,7 @@ Like all [Access Control](#access-control) types, the `beta_oidc` block is defin
| :------------------------------ | :--------------- | :--------------- | :--------------- | :--------------- | :--------------- |
| `backend` |string|-|[Backend Block Reference](#backend-block)| ⚠ Do not disable the peer certificate validation with `disable_certificate_validation = true`! |-|
| `configuration_url` | string |-| The OpenID configuration URL. |⚠ required|-|
| `configuration_ttl` | duration | `1h` | The duration to cache the OpenID configuration located at `configuration_url`. | - | `configuration_ttl = "1d"` |
| `configuration_ttl` | [duration](#duration) | `1h` | The duration to cache the OpenID configuration located at `configuration_url`. | - | `configuration_ttl = "1d"` |
| `token_endpoint_auth_method` |string|`client_secret_basic`|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.|-|
| `redirect_uri` | string |-| The Couper endpoint for receiving the authorization code. |⚠ required. Relative URL references are resolved against the origin of the current request URL.|-|
| `client_id`| string|-|The client identifier.|⚠ required|-|
Expand Down Expand Up @@ -636,7 +646,7 @@ To access the HTTP status code of the `default` response use `backend_responses.
| `coalesce` | | Returns the first of the given arguments that is not null. | `arg...` (various) | `coalesce(request.cookies.foo, "bar")` |
| `json_decode` | various | Parses the given JSON string and, if it is valid, returns the value it represents. | `encoded` (string) | `json_decode("{\"foo\": 1}")` |
| `json_encode` | string | Returns a JSON serialization of the given value. | `val` (various) | `json_encode(request.context.myJWT)` |
| `jwt_sign` | string | jwt_sign creates and signs a JSON Web Token (JWT) from information from a referenced [JWT Signing Profile Block](#jwt-signing-profile-block) and additional claims provided as a function parameter. | `label` (string), `claims` (object) | `jwt_sign("myJWT")` |
| `jwt_sign` | string | jwt_sign creates and signs a JSON Web Token (JWT) from information from a referenced [JWT Signing Profile Block](#jwt-signing-profile-block) (or [JWT Block](#jwt-block) with `signing_ttl`) and additional claims provided as a function parameter. | `label` (string), `claims` (object) | `jwt_sign("myJWT")` |
| `merge` | object or tuple | Deep-merges two or more of either objects or tuples. `null` arguments are ignored. A `null` attribute value in an object removes the previous attribute value. An attribute value with a different type than the current value is set as the new value. `merge()` with no parameters returns `null`. | `arg...` (object or tuple) | `merge(request.headers, { x-additional = "myval" })` |
| `beta_oauth_authorization_url` | string | Creates an OAuth2 authorization URL from a referenced [OAuth2 AC Block](#oauth2-ac-block-beta) or [OIDC Block](#oidc-block-beta). | `label` (string) | `beta_oauth_authorization_url("myOAuth2")` |
| `beta_oauth_verifier` | string | Creates a cryptographically random key as specified in RFC 7636, applicable for all verifier methods; e.g. to be set as a cookie and read into `verifier_value`. Multiple calls of this function in the same client request context return the same value. | | `beta_oauth_verifier()` |
Expand Down
54 changes: 27 additions & 27 deletions eval/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ func (m ContextMap) Merge(other ContextMap) ContextMap {
}

type Context struct {
bufferOption BufferOption
eval *hcl.EvalContext
inner context.Context
memorize map[string]interface{}
oauth2 []config.OAuth2Authorization
profiles []*config.JWTSigningProfile
saml []*config.SAML
bufferOption BufferOption
eval *hcl.EvalContext
inner context.Context
memorize map[string]interface{}
oauth2 []config.OAuth2Authorization
jwtSigningConfigs map[string]*lib.JWTSigningConfig
saml []*config.SAML
}

func NewContext(src []byte, defaults *config.Defaults) *Context {
Expand Down Expand Up @@ -94,13 +94,13 @@ func (c *Context) Value(key interface{}) interface{} {

func (c *Context) WithClientRequest(req *http.Request) *Context {
ctx := &Context{
bufferOption: c.bufferOption,
eval: c.cloneEvalContext(),
inner: c.inner,
memorize: make(map[string]interface{}),
oauth2: c.oauth2[:],
profiles: c.profiles[:],
saml: c.saml[:],
bufferOption: c.bufferOption,
eval: c.cloneEvalContext(),
inner: c.inner,
memorize: make(map[string]interface{}),
oauth2: c.oauth2[:],
jwtSigningConfigs: c.jwtSigningConfigs,
saml: c.saml[:],
}

if rc := req.Context(); rc != nil {
Expand Down Expand Up @@ -158,13 +158,13 @@ func (c *Context) WithClientRequest(req *http.Request) *Context {

func (c *Context) WithBeresps(beresps ...*http.Response) *Context {
ctx := &Context{
bufferOption: c.bufferOption,
eval: c.cloneEvalContext(),
inner: c.inner,
memorize: c.memorize,
oauth2: c.oauth2[:],
profiles: c.profiles[:],
saml: c.saml[:],
bufferOption: c.bufferOption,
eval: c.cloneEvalContext(),
inner: c.inner,
memorize: c.memorize,
oauth2: c.oauth2[:],
jwtSigningConfigs: c.jwtSigningConfigs,
saml: c.saml[:],
}
ctx.inner = context.WithValue(c.inner, request.ContextType, ctx)

Expand Down Expand Up @@ -230,11 +230,11 @@ func (c *Context) WithBeresps(beresps ...*http.Response) *Context {
return ctx
}

// WithJWTProfiles initially setup the lib.FnJWTSign function.
func (c *Context) WithJWTProfiles(profiles []*config.JWTSigningProfile) *Context {
c.profiles = profiles
if c.profiles == nil {
c.profiles = make([]*config.JWTSigningProfile, 0)
// WithJWTSigningConfigs initially sets up the lib.FnJWTSign function.
func (c *Context) WithJWTSigningConfigs(configs map[string]*lib.JWTSigningConfig) *Context {
c.jwtSigningConfigs = configs
if c.jwtSigningConfigs == nil {
c.jwtSigningConfigs = make(map[string]*lib.JWTSigningConfig, 0)
}
c.updateFunctions()
return c
Expand Down Expand Up @@ -294,7 +294,7 @@ func (c *Context) getCodeVerifier() (*pkce.CodeVerifier, error) {

// updateFunctions recreates the listed functions with the current evaluation context.
func (c *Context) updateFunctions() {
jwtfn := lib.NewJwtSignFunction(c.profiles, c.eval)
jwtfn := lib.NewJwtSignFunction(c.jwtSigningConfigs, c.eval)
c.eval.Functions[lib.FnJWTSign] = jwtfn
}

Expand Down
Loading