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

Claims as query param #3

Merged
merged 2 commits into from
Sep 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ validationKeys:
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
claimsSource: static
claims:
- group:
- developers
Expand All @@ -24,7 +25,11 @@ claims:

With this configuration, a JWT will be validated against the given public key, and the claims are then matched against the given structure, meaning there has to be a `group` claim, with either a `developers` or `administrators` value.

Multiple alternative allowed claims can be configured, for example:
Claims can either be statically set, as in the above example, or passed via query string parameters. The `claimsSource` configuration parameter controls which mode the server operates in, and can be either `static` or `queryString`. Further examples of the two modes are given below.

## Static

Multiple alternative allowed sets of claims can be configured, for example:

```yaml
validationKeys:
Expand Down Expand Up @@ -63,13 +68,28 @@ claims:

Here, the token claims must **both** have the groups as before, **and** a `location` of `hq`.

## Query string
In query string mode, the allowed claims are passed via query string parameters to the /validate endpoint. For example, with `/validate?claims_group=developers&claims_group=administrators&claims_location=hq`, the token claims must **both** have a `group` claim of **either** `developers` or `administrators`, **and** a `location` claim of `hq`.

Each claim must be prefixed with `claims_`. Giving the same claim multiple time results in any value being accepted.

In this mode, in contrast to static mode, only a single set of acceptable claims can be passed at a time (but different NGINX server blocks can pass different sets).

If no claims are passed in this mode, the request will be denied.

# NGINX Ingress Controller integration
To use with the NGINX Ingress Controller, first create a deployment and a service for this endpoint. See the [kubernetes/](kubernetes/) directory for example manifests. Then on the ingress object you wish to authenticate, add this annotation:
To use with the NGINX Ingress Controller, first create a deployment and a service for this endpoint. See the [kubernetes/](kubernetes/) directory for example manifests. Then on the ingress object you wish to authenticate, add this annotation for a server in static claims source mode:

```yaml
nginx.ingress.kubernetes.io/auth-url: http:https://nginx-subrequest-auth-jwt.default.svc.cluster.local:8080/validate
```

Or, in query string mode:

```yaml
nginx.ingress.kubernetes.io/auth-url: http:https://nginx-subrequest-auth-jwt.default.svc.cluster.local:8080/validate?claims_group=developers
```

Change the url to match the name of the service and namespace you chose when deploying. All requests will now have their JWTs validated before getting passed to the upstream service.

# Metrics
Expand Down
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ go 1.12

require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/prometheus/client_golang v1.1.0
go.uber.org/atomic v1.4.0 // indirect
go.uber.org/multierr v1.1.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdO
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
74 changes: 63 additions & 11 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io/ioutil"
"net/http"
"os"
"strings"
"time"

"github.com/carlpett/nginx-subrequest-auth-jwt/logger"
Expand All @@ -29,6 +30,11 @@ var (
})
)

const (
claimsSourceStatic = "static"
claimsSourceQueryString = "queryString"
)

func init() {
requestsTotal.WithLabelValues("200")
requestsTotal.WithLabelValues("401")
Expand All @@ -42,9 +48,10 @@ func init() {
}

type server struct {
PublicKey *ecdsa.PublicKey
Logger logger.Logger
ValidClaims []map[string][]string
PublicKey *ecdsa.PublicKey
Logger logger.Logger
ClaimsSource string
StaticClaims []map[string][]string
}

func newServer(logger logger.Logger) (*server, error) {
Expand All @@ -65,14 +72,19 @@ func newServer(logger logger.Logger) (*server, error) {
return nil, err
}

if len(config.Claims) == 0 {
if !contains([]string{"static", "queryString"}, config.ClaimsSource) {
return nil, fmt.Errorf("claimsSource parameter must be set and either 'static' or 'queryString'")
}

if config.ClaimsSource == claimsSourceStatic && len(config.StaticClaims) == 0 {
return nil, fmt.Errorf("Claims configuration is empty")
}

return &server{
PublicKey: pubkey,
Logger: logger,
ValidClaims: config.Claims,
PublicKey: pubkey,
Logger: logger,
ClaimsSource: config.ClaimsSource,
StaticClaims: config.StaticClaims,
}, nil
}

Expand All @@ -83,7 +95,8 @@ type validationKey struct {

type config struct {
ValidationKeys []validationKey `yaml:"validationKeys"`
Claims []map[string][]string `yaml:"claims"`
ClaimsSource string `yaml:"claimsSource"`
StaticClaims []map[string][]string `yaml:"claims"`
}

func main() {
Expand Down Expand Up @@ -174,8 +187,20 @@ func (s *server) validateDeviceToken(r *http.Request) bool {
return false
}

switch s.ClaimsSource {
case claimsSourceStatic:
return s.staticClaimValidator(claims)
case claimsSourceQueryString:
return s.queryStringClaimValidator(claims, r)
default:
s.Logger.Errorw("Configuration error: Unhandled claims source", "claimsSource", s.ClaimsSource)
return false
}
}

func (s *server) staticClaimValidator(claims jwt.MapClaims) bool {
var valid bool
for _, claimSet := range s.ValidClaims {
for _, claimSet := range s.StaticClaims {
valid = true
for claimName, validValues := range claimSet {
if !contains(validValues, claims[claimName].(string)) {
Expand All @@ -186,12 +211,39 @@ func (s *server) validateDeviceToken(r *http.Request) bool {
break
}
}

if !valid {
s.Logger.Debugw("Token claims did not match required values", "validClaims", s.ValidClaims, "actualClaims", claims)
s.Logger.Debugw("Token claims did not match required values", "validClaims", s.StaticClaims, "actualClaims", claims)
}
return valid
}

func (s *server) queryStringClaimValidator(claims jwt.MapClaims, r *http.Request) bool {
validClaims := r.URL.Query()
hasClaimsPrefixedKey := false
for key := range validClaims {
if strings.HasPrefix(key, "claims_") {
hasClaimsPrefixedKey = true
}
}
if len(validClaims) == 0 || !hasClaimsPrefixedKey {
s.Logger.Warnw("No claims requirements sent, rejecting", "queryParams", validClaims)
return false
}
s.Logger.Debugw("Validating claims from query string", "validClaims", validClaims)

return true
passedValidation := true
for claimName, validValues := range validClaims {
actual, ok := claims[strings.TrimPrefix(claimName, "claims_")].(string)
if !ok || !contains(validValues, actual) {
passedValidation = false
}
}

if !passedValidation {
s.Logger.Debugw("Token claims did not match required values", "validClaims", validClaims, "actualClaims", claims)
}
return passedValidation
}

func contains(haystack []string, needle string) bool {
Expand Down