Skip to content

Commit

Permalink
Use jwt block for signing
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Schneider authored Sep 10, 2021
2 parents a937155 + f9b8dd4 commit 12ddf6c
Show file tree
Hide file tree
Showing 10 changed files with 507 additions and 100 deletions.
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.

| 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

0 comments on commit 12ddf6c

Please sign in to comment.