Skip to content

Commit 0c9d6bc

Browse files
authored
Feature/original request headers (#34)
Original request headers implemented. Config structure expanded.
1 parent 7d58a95 commit 0c9d6bc

File tree

11 files changed

+420
-295
lines changed

11 files changed

+420
-295
lines changed

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77
## [Unreleased]
88
There are currently no unreleased changes.
99

10+
## [v0.0.1] - 2020-06-12
11+
### Added
12+
- Support for original request path and method specification through headers (see [nginx docs](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/)).
13+
- Server and Authentication sections to Config.
14+
15+
### Removed
16+
- JWT validation related flags moved to YAML configuration.
17+
1018
## v0.0.0 - 2020-06-01
1119
This is the first version that includes the following functionality:
1220
- YAML configuration support
@@ -16,4 +24,5 @@ This is the first version that includes the following functionality:
1624
- Claims-based authorization
1725
- Pure authorization server and reverse proxy modes
1826

19-
[Unreleased]: https://github.com/kaancfidan/bouncer/compare/v0.0.0...master
27+
[Unreleased]: https://github.com/kaancfidan/bouncer/compare/v0.0.1...master
28+
[v0.0.1]: https://github.com/kaancfidan/bouncer/compare/v0.0.0...v0.0.1

README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,6 @@ Every startup setting has an environment variable and a CLI flag counterpart.
165165
| BOUNCER_CONFIG_PATH | -p | Config YAML path. **default = /etc/bouncer/config.yaml** |
166166
| BOUNCER_LISTEN_ADDRESS | -l | TCP listen address. **default = :3512** |
167167
| BOUNCER_UPSTREAM_URL | --url | Upstream URL to be used in reverse proxy mode. If not set, Bouncer runs in pure auth server mode. |
168-
| BOUNCER_VALID_ISSUER | --iss | Valid issuer id. If set Bouncer validates **iss** claim. |
169-
| BOUNCER_VALID_AUDIENCE | --aud | Valid audience id. If set Bouncer validates **aud** claim. |
170-
| BOUNCER_REQUIRE_EXPIRATION | --exp | Require expiration (**exp**) timestamp on tokens. Unless explicitly set to **false**, **default = true** |
171-
| BOUNCER_REQUIRE_NOT_BEFORE | --nbf | Require "not before" (**nbf**) timestamp on tokens. Unless explicitly set to **false**, **default = true** |
172-
| BOUNCER_CLOCK_SKEW | --clk | Clock skew tolerance in seconds. When set **iat**, **exp**, **nbf** claims are checked with the given tolerance. |
173168

174169
## License
175170
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fkaancfidan%2Fbouncer.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fkaancfidan%2Fbouncer?ref=badge_large)

main.go

Lines changed: 8 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import (
77
"log"
88
"net/http"
99
"net/http/httputil"
10-
"net/url"
1110
"os"
12-
"strconv"
1311

1412
"github.com/kaancfidan/bouncer/services"
1513
)
@@ -20,13 +18,7 @@ type flags struct {
2018
signingKey string
2119
signingMethod string
2220
configPath string
23-
upstreamURL string
2421
listenAddress string
25-
validIssuer string
26-
validAudience string
27-
expRequired string
28-
nbfRequired string
29-
clockSkew string
3022
}
3123

3224
func main() {
@@ -53,22 +45,6 @@ func main() {
5345
}
5446

5547
func newServer(f *flags, configReader io.Reader) (*services.Server, error) {
56-
var upstream http.Handler
57-
58-
if f.upstreamURL != "" {
59-
// parse upstream URL
60-
parsedURL, err := url.Parse(f.upstreamURL)
61-
if err != nil {
62-
return nil, fmt.Errorf("upstream url could not be parsed: %w", err)
63-
}
64-
65-
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
66-
return nil, fmt.Errorf("upstream url scheme must be http or https")
67-
}
68-
69-
upstream = httputil.NewSingleHostReverseProxy(parsedURL)
70-
}
71-
7248
parser := services.YamlConfigParser{}
7349
cfg, err := parser.ParseConfig(configReader)
7450
if err != nil {
@@ -80,46 +56,34 @@ func newServer(f *flags, configReader io.Reader) (*services.Server, error) {
8056
return nil, fmt.Errorf("invalid config: %w", err)
8157
}
8258

83-
var clockSkew int
84-
if f.clockSkew != "" {
85-
clockSkew, err = strconv.Atoi(f.clockSkew)
86-
if err != nil {
87-
return nil, fmt.Errorf("clock skew flag %s cannot be converted to integer", f.clockSkew)
88-
}
89-
} else {
90-
clockSkew = 0
59+
var upstream http.Handler
60+
if cfg.Server.ParsedURL != nil {
61+
upstream = httputil.NewSingleHostReverseProxy(cfg.Server.ParsedURL)
9162
}
9263

9364
authenticator, err := services.NewAuthenticator(
9465
[]byte(f.signingKey),
9566
f.signingMethod,
96-
f.validIssuer,
97-
f.validAudience,
98-
f.expRequired != "false",
99-
f.nbfRequired != "false",
100-
clockSkew)
67+
cfg.Authentication)
10168

10269
if err != nil {
10370
return nil, fmt.Errorf("could not create authenticator: %w", err)
10471
}
10572

106-
s := services.NewServer(
73+
return services.NewServer(
10774
upstream,
10875
services.NewRouteMatcher(cfg.RoutePolicies),
10976
services.NewAuthorizer(cfg.ClaimPolicies),
110-
authenticator)
111-
112-
return s, nil
77+
authenticator,
78+
cfg.Server), nil
11379
}
11480

11581
func parseFlags() *flags {
11682
f := flags{
11783
configPath: "/etc/bouncer/config.yaml",
11884
listenAddress: ":3512",
119-
expRequired: "true",
120-
nbfRequired: "true",
12185
}
122-
86+
12387
printVersion := flag.Bool("v", false, "print version and exit")
12488
flag.StringVar(&f.signingKey, "k",
12589
lookupEnv("BOUNCER_SIGNING_KEY", ""),
@@ -137,30 +101,6 @@ func parseFlags() *flags {
137101
lookupEnv("BOUNCER_LISTEN_ADDRESS", f.listenAddress),
138102
fmt.Sprintf("listen address, default = %s", f.listenAddress))
139103

140-
flag.StringVar(&f.upstreamURL, "url",
141-
lookupEnv("BOUNCER_UPSTREAM_URL", ""),
142-
"URL to be called when the request is authorized")
143-
144-
flag.StringVar(&f.validIssuer, "iss",
145-
lookupEnv("BOUNCER_VALID_ISSUER", ""),
146-
fmt.Sprintf("valid token issuer"))
147-
148-
flag.StringVar(&f.validAudience, "aud",
149-
lookupEnv("BOUNCER_VALID_AUDIENCE", ""),
150-
fmt.Sprintf("valid token audience"))
151-
152-
flag.StringVar(&f.expRequired, "exp",
153-
lookupEnv("BOUNCER_REQUIRE_EXPIRATION", f.expRequired),
154-
fmt.Sprintf("require token expiration timestamp claims, default = %s", f.expRequired))
155-
156-
flag.StringVar(&f.nbfRequired, "nbf",
157-
lookupEnv("BOUNCER_REQUIRE_NOT_BEFORE", f.nbfRequired),
158-
fmt.Sprintf("require token not before timestamp claims, default = %s", f.nbfRequired))
159-
160-
flag.StringVar(&f.clockSkew, "clk",
161-
lookupEnv("BOUNCER_CLOCK_SKEW", f.clockSkew),
162-
fmt.Sprintf("require token not before timestamp claims, default = %s", f.nbfRequired))
163-
164104
flag.Parse()
165105

166106
if *printVersion {

main_test.go

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -30,46 +30,6 @@ func TestNewServer(t *testing.T) {
3030
cfgContent: "",
3131
wantErr: true,
3232
},
33-
{
34-
name: "reverse proxy",
35-
flags: &flags{
36-
signingKey: "SuperSecretKey123!",
37-
signingMethod: "HMAC",
38-
upstreamURL: "http://localhost:8080",
39-
},
40-
cfgContent: "claimPolicies: {}\nroutePolicies: []",
41-
wantErr: false,
42-
},
43-
{
44-
name: "clock skewed",
45-
flags: &flags{
46-
signingKey: "SuperSecretKey123!",
47-
signingMethod: "HMAC",
48-
clockSkew: "10",
49-
},
50-
cfgContent: "claimPolicies: {}\nroutePolicies: []",
51-
wantErr: false,
52-
},
53-
{
54-
name: "invalid url scheme",
55-
flags: &flags{
56-
signingKey: "SuperSecretKey123!",
57-
signingMethod: "HMAC",
58-
upstreamURL: "tcp://localhost:8080",
59-
},
60-
cfgContent: "claimPolicies: {}\nroutePolicies: []",
61-
wantErr: true,
62-
},
63-
{
64-
name: "malformed url",
65-
flags: &flags{
66-
signingKey: "SuperSecretKey123!",
67-
signingMethod: "HMAC",
68-
upstreamURL: "!!http://localhost:8080",
69-
},
70-
cfgContent: "claimPolicies: {}\nroutePolicies: []",
71-
wantErr: true,
72-
},
7333
{
7434
name: "invalid config yaml",
7535
flags: &flags{
@@ -104,16 +64,6 @@ func TestNewServer(t *testing.T) {
10464
cfgContent: "claimPolicies: {}\nroutePolicies: []",
10565
wantErr: true,
10666
},
107-
{
108-
name: "invalid clock skew flag",
109-
flags: &flags{
110-
signingKey: "SuperSecretKey123!",
111-
signingMethod: "HMAC",
112-
clockSkew: "not a number",
113-
},
114-
cfgContent: "claimPolicies: {}\nroutePolicies: []",
115-
wantErr: true,
116-
},
11767
}
11868
for _, tt := range tests {
11969
t.Run(tt.name, func(t *testing.T) {

models/config.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
package models
22

3+
import "net/url"
4+
5+
// AuthenticationConfig holds JWT validation related parameters
6+
type AuthenticationConfig struct {
7+
Issuer string `yaml:"issuer"`
8+
Audience string `yaml:"audience"`
9+
IgnoreExpiration bool `yaml:"ignoreExpiration"`
10+
IgnoreNotBefore bool `yaml:"ignoreNotBefore"`
11+
ClockSkewInSeconds int `yaml:"clockSkewInSeconds"`
12+
}
13+
14+
// OriginalRequestHeaders contains headers to lookup for original request method and path details
15+
// in the case where the auth request is a sub-request with distinct method and path
16+
type OriginalRequestHeaders struct {
17+
Method string `yaml:"method"`
18+
Path string `yaml:"path"`
19+
}
20+
21+
// ServerConfig holds operation mode (auth server / reverse proxy) related parameters
22+
type ServerConfig struct {
23+
OriginalRequestHeaders *OriginalRequestHeaders `yaml:"originalRequestHeaders"`
24+
UpstreamURL string `yaml:"upstreamUrl"`
25+
ParsedURL *url.URL `yaml:"-"`
26+
}
27+
328
// ClaimRequirement is a key-value pair for a given claim constraint.
429
// When multiple claim values are provided, these values are effectively ORed.
530
type ClaimRequirement struct {
@@ -15,8 +40,16 @@ type RoutePolicy struct {
1540
AllowAnonymous bool `yaml:"allowAnonymous"`
1641
}
1742

43+
// ClaimPolicyConfig is a type alias for claimPolicies section
44+
type ClaimPolicyConfig map[string][]ClaimRequirement
45+
46+
// RoutePolicyConfig is a type alias for routePolicies section
47+
type RoutePolicyConfig []RoutePolicy
48+
1849
// Config is the overall struct that matches the YAML structure
1950
type Config struct {
20-
ClaimPolicies map[string][]ClaimRequirement `yaml:"claimPolicies"`
21-
RoutePolicies []RoutePolicy `yaml:"routePolicies"`
51+
Server ServerConfig `yaml:"server"`
52+
Authentication AuthenticationConfig `yaml:"authentication"`
53+
ClaimPolicies ClaimPolicyConfig `yaml:"claimPolicies"`
54+
RoutePolicies RoutePolicyConfig `yaml:"routePolicies"`
2255
}

services/authenticator.go

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"time"
99

1010
"github.com/dgrijalva/jwt-go"
11+
12+
"github.com/kaancfidan/bouncer/models"
1113
)
1214

1315
// Authenticator interface
@@ -17,13 +19,9 @@ type Authenticator interface {
1719

1820
// AuthenticatorImpl is a JWT based authentication implementation
1921
type AuthenticatorImpl struct {
20-
signingKey interface{}
21-
signingMethod string
22-
validIssuer string
23-
validAudience string
24-
expirationRequired bool
25-
notBeforeRequired bool
26-
clockSkew int
22+
signingKey interface{}
23+
signingMethod string
24+
config models.AuthenticationConfig
2725
}
2826

2927
type claims struct {
@@ -69,25 +67,17 @@ func parseSigningKey(signingKey []byte, signingMethod string) (key interface{},
6967
func NewAuthenticator(
7068
signingKey []byte,
7169
signingMethod string,
72-
validIssuer string,
73-
validAudience string,
74-
expirationRequired bool,
75-
notBeforeRequired bool,
76-
clockSkew int) (*AuthenticatorImpl, error) {
70+
config models.AuthenticationConfig) (*AuthenticatorImpl, error) {
7771

7872
key, err := parseSigningKey(signingKey, signingMethod)
7973
if err != nil {
8074
return nil, fmt.Errorf("could not parse signing key: %w", err)
8175
}
8276

8377
return &AuthenticatorImpl{
84-
signingKey: key,
85-
signingMethod: signingMethod,
86-
validIssuer: validIssuer,
87-
validAudience: validAudience,
88-
expirationRequired: expirationRequired,
89-
notBeforeRequired: notBeforeRequired,
90-
clockSkew: clockSkew,
78+
signingKey: key,
79+
signingMethod: signingMethod,
80+
config: config,
9181
}, nil
9282
}
9383

@@ -114,33 +104,33 @@ func (a AuthenticatorImpl) Authenticate(authHeader string) (map[string]interface
114104
// check claims for authorization
115105
claims := claims{
116106
MapClaims: jwt.MapClaims{},
117-
ClockSkew: a.clockSkew,
107+
ClockSkew: a.config.ClockSkewInSeconds,
118108
}
119109
_, err := jwt.ParseWithClaims(tokenString, &claims, a.keyFactory)
120110

121111
if err != nil {
122112
return nil, fmt.Errorf("error occurred while parsing claims: %w", err)
123113
}
124114

125-
if _, ok := claims.MapClaims["exp"]; !ok && a.expirationRequired {
115+
if _, ok := claims.MapClaims["exp"]; !ok && !a.config.IgnoreExpiration {
126116
return nil, fmt.Errorf("required expiration timestamp not found")
127117
}
128118

129-
if _, ok := claims.MapClaims["nbf"]; !ok && a.notBeforeRequired {
119+
if _, ok := claims.MapClaims["nbf"]; !ok && !a.config.IgnoreNotBefore {
130120
return nil, fmt.Errorf("required not before timestamp not found")
131121
}
132122

133123
// verify audience
134-
if a.validAudience != "" {
135-
checkAud := claims.VerifyAudience(a.validAudience, true)
124+
if a.config.Audience != "" {
125+
checkAud := claims.VerifyAudience(a.config.Audience, true)
136126
if !checkAud {
137127
return nil, fmt.Errorf("invalid audience")
138128
}
139129
}
140130

141131
// verify issuer
142-
if a.validIssuer != "" {
143-
checkIss := claims.VerifyIssuer(a.validIssuer, true)
132+
if a.config.Issuer != "" {
133+
checkIss := claims.VerifyIssuer(a.config.Issuer, true)
144134
if !checkIss {
145135
return nil, fmt.Errorf("invalid issuer")
146136
}

0 commit comments

Comments
 (0)