Skip to content

Commit

Permalink
Merge branch 'master' into scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
johakoch authored Sep 28, 2021
2 parents 372cfa9 + 86f0c7b commit ee01f04
Show file tree
Hide file tree
Showing 1,152 changed files with 205,492 additions and 20,419 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Unreleased changes are available as `avenga/couper:edge` container.

* **Changed**
* Organized log format fields for uniform access and upstream log ([#300](https://github.com/avenga/couper/pull/300))
* `claims` in a [`jwt` block](./docs/REFERENCE.md#jwt-block) are now evaluated per request, so that [`request` properties](./docs/REFERENCE.md#request) can be used as required claim values ([#314](https://github.com/avenga/couper/pull/314))

* **Fixed**
* Key for storing and reading [OpenID configuration](./docs/REFERENCE.md#oidc-block-beta) ([#319](https://github.com/avenga/couper/pull/319))

---

Expand Down
6 changes: 2 additions & 4 deletions accesscontrol/ac.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package accesscontrol
import (
"net/http"

"github.com/avenga/couper/config/request"
"github.com/avenga/couper/errors"
"github.com/avenga/couper/eval"
)
Expand Down Expand Up @@ -48,9 +47,8 @@ func (i ListItem) Validate(req *http.Request) error {
return errors.AccessControl.Label(i.label).Kind(i.kind).With(err)
}

if evalCtx, ok := req.Context().Value(request.ContextType).(*eval.Context); ok {
*req = *req.WithContext(evalCtx.WithClientRequest(req))
}
evalCtx := eval.ContextFromRequest(req)
*req = *req.WithContext(evalCtx.WithClientRequest(req))

return nil
}
Expand Down
39 changes: 23 additions & 16 deletions accesscontrol/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ import (
"time"

"github.com/dgrijalva/jwt-go/v4"
"github.com/hashicorp/hcl/v2"

acjwt "github.com/avenga/couper/accesscontrol/jwt"
"github.com/avenga/couper/config/request"
"github.com/avenga/couper/errors"
"github.com/avenga/couper/eval/content"
"github.com/avenga/couper/internal/seetie"
)

const (
Expand All @@ -35,19 +38,18 @@ type (

type JWT struct {
algorithm acjwt.Algorithm
claims map[string]interface{}
claims hcl.Expression
claimsRequired []string
source JWTSource
hmacSecret []byte
name string
parser *jwt.Parser
pubKey *rsa.PublicKey
scopeClaim string
}

type JWTOptions struct {
Algorithm string
Claims map[string]interface{}
Claims hcl.Expression
ClaimsRequired []string
Name string // TODO: more generic (validate)
ScopeClaim string
Expand Down Expand Up @@ -94,12 +96,6 @@ func NewJWT(options *JWTOptions) (*JWT, error) {
return nil, fmt.Errorf("algorithm is not supported")
}

parser, err := newParser(jwtAC.algorithm, jwtAC.claims)
if err != nil {
return nil, err
}
jwtAC.parser = parser

if jwtAC.algorithm.IsHMAC() {
jwtAC.hmacSecret = options.Key
return jwtAC, nil
Expand All @@ -116,6 +112,14 @@ func NewJWT(options *JWTOptions) (*JWT, error) {

// Validate reading the token from configured source and validates against the key.
func (j *JWT) Validate(req *http.Request) error {
ctx := req.Context()
cctx := ctx.Value(request.ContextType).(content.Context)
evalCtx := cctx.HCLContext()
claims, diags := seetie.ExpToMap(evalCtx, j.claims)
if diags != nil {
return diags
}

var tokenValue string
var err error

Expand All @@ -142,7 +146,12 @@ func (j *JWT) Validate(req *http.Request) error {
return errors.JwtTokenMissing.Message("token required")
}

token, err := j.parser.ParseWithClaims(tokenValue, jwt.MapClaims{}, j.getValidationKey)
parser, err := newParser(j.algorithm, claims)
if err != nil {
return err
}

token, err := parser.Parse(tokenValue, j.getValidationKey)
if err != nil {
switch err.(type) {
case *jwt.TokenExpiredError:
Expand All @@ -152,13 +161,11 @@ func (j *JWT) Validate(req *http.Request) error {
}
}

tokenClaims, err := j.validateClaims(token)
tokenClaims, err := j.validateClaims(token, claims)
if err != nil {
return err
}

ctx := req.Context()

acMap, ok := ctx.Value(request.AccessControls).(map[string]interface{})
if !ok {
acMap = make(map[string]interface{})
Expand Down Expand Up @@ -198,7 +205,7 @@ func (j *JWT) getValidationKey(_ *jwt.Token) (interface{}, error) {
}
}

func (j *JWT) validateClaims(token *jwt.Token) (map[string]interface{}, error) {
func (j *JWT) validateClaims(token *jwt.Token, claims map[string]interface{}) (map[string]interface{}, error) {
var tokenClaims jwt.MapClaims
if tc, ok := token.Claims.(jwt.MapClaims); ok {
tokenClaims = tc
Expand All @@ -214,7 +221,7 @@ func (j *JWT) validateClaims(token *jwt.Token) (map[string]interface{}, error) {
}
}

for k, v := range j.claims {
for k, v := range claims {

if k == "iss" || k == "aud" { // gets validated during parsing
continue
Expand All @@ -226,7 +233,7 @@ func (j *JWT) validateClaims(token *jwt.Token) (map[string]interface{}, error) {
}

if val != v {
return nil, errors.JwtTokenInvalid.Messagef("unexpected value for claim %s: %s", k, val)
return nil, errors.JwtTokenInvalid.Messagef("unexpected value for claim %s: %q, expected %q", k, val, v)
}
}
return tokenClaims, nil
Expand Down
41 changes: 27 additions & 14 deletions accesscontrol/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ import (
"testing"

"github.com/dgrijalva/jwt-go/v4"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"

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"
"github.com/avenga/couper/eval"
"github.com/avenga/couper/internal/test"
)

Expand All @@ -26,7 +29,7 @@ func Test_JWT_NewJWT_RSA(t *testing.T) {

type fields struct {
algorithm string
claims map[string]interface{}
claims hcl.Expression
claimsRequired []string
pubKey []byte
pubKeyPath string
Expand Down Expand Up @@ -136,7 +139,7 @@ QolLGgj3tz4NbDEitq+zKMr0uTHvP1Vyu1mXAflcpYcJA4ZmuB3Oj39e0U0gnmr/
func Test_JWT_Validate(t *testing.T) {
type fields struct {
algorithm acjwt.Algorithm
claims map[string]interface{}
claims map[string]string
claimsRequired []string
source ac.JWTSource
pubKey []byte
Expand Down Expand Up @@ -179,61 +182,65 @@ func Test_JWT_Validate(t *testing.T) {
algorithm: algo,
source: ac.NewJWTSource("", "Authorization"),
pubKey: pubKeyBytes,
}, httptest.NewRequest(http.MethodGet, "/", nil), true},
}, setContext(httptest.NewRequest(http.MethodGet, "/", nil)), true},
{"src: header /w valid bearer", fields{
algorithm: algo,
source: ac.NewJWTSource("", "Authorization"),
pubKey: pubKeyBytes,
}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token), false},
}, setContext(setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token)), false},
{"src: header /w no cookie", fields{
algorithm: algo,
source: ac.NewJWTSource("token", ""),
pubKey: pubKeyBytes,
}, httptest.NewRequest(http.MethodGet, "/", nil), true},
}, setContext(httptest.NewRequest(http.MethodGet, "/", nil)), true},
{"src: header /w empty cookie", fields{
algorithm: algo,
source: ac.NewJWTSource("token", ""),
pubKey: pubKeyBytes,
}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "token", ""), true},
}, setContext(setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "token", "")), true},
{"src: header /w valid cookie", fields{
algorithm: algo,
source: ac.NewJWTSource("token", ""),
pubKey: pubKeyBytes,
}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "token", token), false},
}, setContext(setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "token", token)), false},
{"src: header /w valid bearer & claims", fields{
algorithm: algo,
claims: map[string]interface{}{
claims: map[string]string{
"aud": "peter",
"test123": "value123",
},
claimsRequired: []string{"aud"},
source: ac.NewJWTSource("", "Authorization"),
pubKey: pubKeyBytes,
}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token), false},
}, setContext(setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token)), false},
{"src: header /w valid bearer & w/o claims", fields{
algorithm: algo,
claims: map[string]interface{}{
claims: map[string]string{
"aud": "peter",
"cptn": "hook",
},
source: ac.NewJWTSource("", "Authorization"),
pubKey: pubKeyBytes,
}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token), true},
}, setContext(setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token)), true},
{"src: header /w valid bearer & w/o required claims", fields{
algorithm: algo,
claims: map[string]interface{}{
claims: map[string]string{
"aud": "peter",
},
claimsRequired: []string{"exp"},
source: ac.NewJWTSource("", "Authorization"),
pubKey: pubKeyBytes,
}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token), true},
}, setContext(setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token)), true},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%v_%s", signingMethod, tt.name), func(t *testing.T) {
claimValMap := make(map[string]cty.Value)
for k, v := range tt.fields.claims {
claimValMap[k] = cty.StringVal(v)
}
j, err := ac.NewJWT(&ac.JWTOptions{
Algorithm: tt.fields.algorithm.String(),
Claims: tt.fields.claims,
Claims: hcl.StaticExpr(cty.ObjectVal(claimValMap), hcl.Range{}),
ClaimsRequired: tt.fields.claimsRequired,
Name: "test_ac",
Source: tt.fields.source,
Expand Down Expand Up @@ -395,3 +402,9 @@ func setCookieAndHeader(req *http.Request, key, value string) *http.Request {
req.Header.Set("Cookie", key+"="+value)
return req
}

func setContext(req *http.Request) *http.Request {
evalCtx := eval.ContextFromRequest(req)
*req = *req.WithContext(evalCtx.WithClientRequest(req))
return req
}
21 changes: 20 additions & 1 deletion command/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"strings"
"sync"
"time"

"github.com/sirupsen/logrus"

Expand All @@ -18,6 +19,7 @@ import (
"github.com/avenga/couper/errors"
"github.com/avenga/couper/server"
"github.com/avenga/couper/server/writer"
"github.com/avenga/couper/telemetry"
)

var _ Cmd = &Run{}
Expand All @@ -43,11 +45,17 @@ func NewRun(ctx context.Context) *Run {
set.Var(&AcceptForwardedValue{settings: &settings}, "accept-forwarded-url", "-accept-forwarded-url [proto][,host][,port]")
set.Var(&settings.TLSDevProxy, "https-dev-proxy", "-https-dev-proxy 8443:8080,9443:9000")
set.BoolVar(&settings.NoProxyFromEnv, "no-proxy-from-env", settings.NoProxyFromEnv, "-no-proxy-from-env")
set.StringVar(&settings.RequestIDFormat, "request-id-format", settings.RequestIDFormat, "-request-id-format uuid4")
set.StringVar(&settings.RequestIDAcceptFromHeader, "request-id-accept-from-header", settings.RequestIDAcceptFromHeader, "-request-id-accept-from-header X-UID")
set.StringVar(&settings.RequestIDBackendHeader, "request-id-backend-header", settings.RequestIDBackendHeader, "-request-id-backend-header Couper-Request-ID")
set.StringVar(&settings.RequestIDClientHeader, "request-id-client-header", settings.RequestIDClientHeader, "-request-id-client-header Couper-Request-ID")
set.StringVar(&settings.RequestIDFormat, "request-id-format", settings.RequestIDFormat, "-request-id-format uuid4")
set.StringVar(&settings.SecureCookies, "secure-cookies", settings.SecureCookies, "-secure-cookies strip")
set.BoolVar(&settings.TelemetryMetrics, "beta-metrics", settings.TelemetryMetrics, "-metrics")
set.IntVar(&settings.TelemetryMetricsPort, "beta-metrics-port", settings.TelemetryMetricsPort, "-metrics-port 9090")
set.StringVar(&settings.TelemetryMetricsEndpoint, "beta-metrics-endpoint", settings.TelemetryMetricsEndpoint, "-metrics-endpoint [host:port]")
set.StringVar(&settings.TelemetryMetricsExporter, "beta-metrics-exporter", settings.TelemetryMetricsExporter, "-metrics-exporter [name]")
set.BoolVar(&settings.TelemetryTraces, "beta-traces", settings.TelemetryTraces, "-traces")
set.StringVar(&settings.TelemetryTracesEndpoint, "beta-traces-endpoint", settings.TelemetryTracesEndpoint, "-traces-endpoint [host:port]")
return &Run{
context: ctx,
flagSet: set,
Expand Down Expand Up @@ -109,6 +117,16 @@ func (r *Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry)
timings := runtime.DefaultTimings
env.Decode(&timings)

telemetry.InitExporter(r.context, &telemetry.Options{
MetricsCollectPeriod: time.Second * 2,
Metrics: r.settings.TelemetryMetrics,
MetricsEndpoint: r.settings.TelemetryMetricsEndpoint,
MetricsExporter: r.settings.TelemetryMetricsExporter,
MetricsPort: r.settings.TelemetryMetricsPort,
Traces: r.settings.TelemetryTraces,
TracesEndpoint: r.settings.TelemetryTracesEndpoint,
}, logEntry)

// logEntry has still the 'daemon' type which can be used for config related load errors.
srvConf, err := runtime.NewServerConfiguration(config, logEntry, cache.New(logEntry, r.context.Done()))
if err != nil {
Expand Down Expand Up @@ -158,6 +176,7 @@ func (r *Run) Execute(args Args, config *config.Couper, logEntry *logrus.Entry)
_ = s.Close()
logEntry.Infof("Server closed: %s", s.Addr)
}

return nil
}

Expand Down
Loading

0 comments on commit ee01f04

Please sign in to comment.