Skip to content

Commit 163a708

Browse files
committed
ORGANIC-467. Added Okta JWT parser "properly", mostly.
1 parent 5f0f6a4 commit 163a708

File tree

3 files changed

+128
-78
lines changed

3 files changed

+128
-78
lines changed

api/auth.go

Lines changed: 120 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,88 @@ import (
44
"context"
55
"net/http"
66

7+
jwt "github.com/dgrijalva/jwt-go"
78
"github.com/netlify/git-gateway/conf"
89
"github.com/sirupsen/logrus"
910
"github.com/okta/okta-jwt-verifier-golang"
1011
)
1112

13+
type Authenticator interface {
14+
// authenticate checks incoming requests for tokens presented using the Authorization header
15+
authenticate(w http.ResponseWriter, r *http.Request) (context.Context, error)
16+
getName() string
17+
}
18+
19+
type Authorizer interface {
20+
// authorize checks incoming requests for roles data in tokens that is parsed and verified by prior authentication step
21+
authorize(w http.ResponseWriter, r *http.Request) (context.Context, error)
22+
getName() string
23+
}
24+
1225
type Auth struct {
1326
config *conf.GlobalConfiguration
27+
authenticator Authenticator
28+
authorizer Authorizer
1429
version string
1530
}
1631

32+
type JWTAuthenticator struct {
33+
name string
34+
auth Auth
35+
}
36+
37+
type OktaJWTAuthenticator struct {
38+
name string
39+
auth Auth
40+
}
41+
42+
type RolesAuthorizer struct {
43+
name string
44+
auth Auth
45+
}
46+
47+
func NewAuthWithVersion(ctx context.Context, globalConfig *conf.GlobalConfiguration, version string) *Auth {
48+
auth := &Auth{config: globalConfig, version: version}
49+
50+
auth.authenticator = &OktaJWTAuthenticator{name: "bearer-jwt-token", auth: *auth}
51+
auth.authorizer = &RolesAuthorizer{name: "bearer-jwt-token-roles", auth: *auth}
52+
53+
return auth
54+
}
55+
1756
// check both authentication and authorization
1857
func (a *Auth) accessControl(w http.ResponseWriter, r *http.Request) (context.Context, error) {
19-
_, err := a.authenticate(w, r)
58+
logrus.Infof("Authenticate with: %v", a.authenticator.getName())
59+
ctx, err := a.authenticator.authenticate(w, r)
2060
if err != nil {
2161
return nil, err
2262
}
2363

24-
return a.authorize(w, r)
64+
logrus.Infof("Authorizing with: %v", a.authorizer.getName())
65+
return a.authorizer.authorize(w, r.WithContext(ctx))
66+
}
67+
68+
func (a *Auth) extractBearerToken(w http.ResponseWriter, r *http.Request) (string, error) {
69+
authHeader := r.Header.Get("Authorization")
70+
if authHeader == "" {
71+
return "", unauthorizedError("This endpoint requires a Bearer token")
72+
}
73+
74+
matches := bearerRegexp.FindStringSubmatch(authHeader)
75+
if len(matches) != 2 {
76+
return "", unauthorizedError("This endpoint requires a Bearer token")
77+
}
78+
79+
return matches[1], nil
80+
}
81+
82+
func (a *JWTAuthenticator) getName() string {
83+
return a.name
2584
}
2685

27-
// authenticate checks incoming requests for tokens presented using the Authorization header
28-
func (a *Auth) authenticate(w http.ResponseWriter, r *http.Request) (context.Context, error) {
86+
func (a *JWTAuthenticator) authenticate(w http.ResponseWriter, r *http.Request) (context.Context, error) {
2987
logrus.Info("Getting auth token")
30-
token, err := a.extractBearerToken(w, r)
88+
token, err := a.auth.extractBearerToken(w, r)
3189
if err != nil {
3290
return nil, err
3391
}
@@ -36,61 +94,36 @@ func (a *Auth) authenticate(w http.ResponseWriter, r *http.Request) (context.Con
3694
return a.parseJWTClaims(token, r)
3795
}
3896

39-
// authorize checks incoming requests for roles data in tokens that is parsed and verified by prior authentication step
40-
func (a *Auth) authorize(w http.ResponseWriter, r *http.Request) (context.Context, error) {
41-
ctx := r.Context()
42-
claims := getClaims(ctx)
43-
config := getConfig(ctx)
44-
45-
logrus.Infof("authenticate url: %v+", r.URL)
46-
if claims == nil {
47-
return nil, unauthorizedError("Access to endpoint not allowed: no claims found in Bearer token")
48-
}
49-
50-
if len(config.Roles) == 0 {
51-
return ctx, nil
52-
}
97+
func (a *JWTAuthenticator) parseJWTClaims(bearer string, r *http.Request) (context.Context, error) {
98+
config := getConfig(r.Context())
99+
p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
100+
token, err := p.ParseWithClaims(bearer, &GatewayClaims{}, func(token *jwt.Token) (interface{}, error) {
101+
return []byte(config.JWT.Secret), nil
102+
})
53103

54-
roles, ok := claims.AppMetaData["roles"]
55-
if ok {
56-
roleStrings, _ := roles.([]interface{})
57-
for _, data := range roleStrings {
58-
role, _ := data.(string)
59-
for _, adminRole := range config.Roles {
60-
if role == adminRole {
61-
return ctx, nil
62-
}
63-
}
64-
}
104+
if err != nil {
105+
return nil, unauthorizedError("Invalid token: %v", err)
65106
}
66-
67-
return nil, unauthorizedError("Access to endpoint not allowed: your role doesn't allow access")
107+
claims := token.Claims.(GatewayClaims)
108+
return withClaims(r.Context(), &claims), nil
68109
}
69110

70-
func NewAuthWithVersion(ctx context.Context, globalConfig *conf.GlobalConfiguration, version string) *Auth {
71-
auth := &Auth{config: globalConfig, version: version}
72-
73-
return auth
111+
func (a *OktaJWTAuthenticator) getName() string {
112+
return a.name
74113
}
75114

76-
func (a *Auth) extractBearerToken(w http.ResponseWriter, r *http.Request) (string, error) {
77-
authHeader := r.Header.Get("Authorization")
78-
if authHeader == "" {
79-
return "", unauthorizedError("This endpoint requires a Bearer token")
80-
}
81-
82-
matches := bearerRegexp.FindStringSubmatch(authHeader)
83-
if len(matches) != 2 {
84-
return "", unauthorizedError("This endpoint requires a Bearer token")
115+
func (a *OktaJWTAuthenticator) authenticate(w http.ResponseWriter, r *http.Request) (context.Context, error) {
116+
logrus.Info("Getting auth token")
117+
token, err := a.auth.extractBearerToken(w, r)
118+
if err != nil {
119+
return nil, err
85120
}
86121

87-
return matches[1], nil
122+
logrus.Infof("Parsing JWT claims: %v", token)
123+
return a.parseOktaJWTClaims(token, r)
88124
}
89125

90-
func (a *Auth) parseJWTClaims(bearer string, r *http.Request) (context.Context, error) {
91-
// Reimplemented to use Okta lib
92-
// Original validation only work for HS256 algo,
93-
// Okta supports RS256 only which requires public key downloading and caching (key rotation)
126+
func (a *OktaJWTAuthenticator) parseOktaJWTClaims(bearer string, r *http.Request) (context.Context, error) {
94127
config := getConfig(r.Context())
95128

96129
toValidate := map[string]string{}
@@ -106,17 +139,47 @@ func (a *Auth) parseJWTClaims(bearer string, r *http.Request) (context.Context,
106139

107140
_, err := verifier.VerifyAccessToken(bearer)
108141

109-
// @TODO? WARNING: Should be roles and other claims be checked here?
110-
111142
if err != nil {
112143
return nil, unauthorizedError("Invalid token: %v", err)
113144
}
114145

146+
claims := GatewayClaims{Email: "e", StandardClaims: jwt.StandardClaims{Audience: "a"}}
147+
115148
logrus.Infof("parseJWTClaims passed")
149+
return withClaims(r.Context(), &claims), nil
150+
}
116151

117-
// return nil, because the `github.go` is coded to send personal token
118-
// both github oauth generates its own id, so oauth pass-thru is impossible
119-
// we can improve the gateway to talk oauth with github.com, but we will
120-
// still return nil here.
121-
return nil, nil
152+
func (a *RolesAuthorizer) getName() string {
153+
return a.name
154+
}
155+
156+
func (a *RolesAuthorizer) authorize(w http.ResponseWriter, r *http.Request) (context.Context, error) {
157+
ctx := r.Context()
158+
claims := getClaims(ctx)
159+
config := getConfig(ctx)
160+
161+
logrus.Infof("authenticate url: %v+", r.URL)
162+
logrus.Infof("claims: %v+", claims)
163+
if claims == nil {
164+
return nil, unauthorizedError("Access to endpoint not allowed: no claims found in Bearer token")
165+
}
166+
167+
if len(config.Roles) == 0 {
168+
return ctx, nil
169+
}
170+
171+
roles, ok := claims.AppMetaData["roles"]
172+
if ok {
173+
roleStrings, _ := roles.([]interface{})
174+
for _, data := range roleStrings {
175+
role, _ := data.(string)
176+
for _, adminRole := range config.Roles {
177+
if role == adminRole {
178+
return ctx, nil
179+
}
180+
}
181+
}
182+
}
183+
184+
return nil, unauthorizedError("Access to endpoint not allowed: your role doesn't allow access")
122185
}

api/context.go

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"net/url"
66

7-
jwt "github.com/dgrijalva/jwt-go"
87
"github.com/netlify/git-gateway/conf"
98
"github.com/netlify/git-gateway/models"
109
)
@@ -20,8 +19,8 @@ func (c contextKey) String() string {
2019
}
2120

2221
const (
23-
accessTokenKey = contextKey("token")
24-
tokenKey = contextKey("jwt")
22+
accessTokenKey = contextKey("access_token")
23+
tokenClaimsKey = contextKey("jwt_claims")
2524
requestIDKey = contextKey("request_id")
2625
configKey = contextKey("config")
2726
instanceIDKey = contextKey("instance_id")
@@ -31,27 +30,17 @@ const (
3130
netlifyIDKey = contextKey("netlify_id")
3231
)
3332

34-
// withToken adds the JWT token to the context.
35-
func withToken(ctx context.Context, token *jwt.Token) context.Context {
36-
return context.WithValue(ctx, tokenKey, token)
37-
}
38-
39-
// getToken reads the JWT token from the context.
40-
func getToken(ctx context.Context) *jwt.Token {
41-
obj := ctx.Value(tokenKey)
42-
if obj == nil {
43-
return nil
44-
}
45-
46-
return obj.(*jwt.Token)
33+
// withTokenClaims adds the JWT token claims to the context.
34+
func withClaims(ctx context.Context, claims *GatewayClaims) context.Context {
35+
return context.WithValue(ctx, tokenClaimsKey, claims)
4736
}
4837

4938
func getClaims(ctx context.Context) *GatewayClaims {
50-
token := getToken(ctx)
51-
if token == nil {
39+
claims := ctx.Value(tokenClaimsKey)
40+
if claims == nil {
5241
return nil
5342
}
54-
return token.Claims.(*GatewayClaims)
43+
return claims.(*GatewayClaims)
5544
}
5645

5746
func withRequestID(ctx context.Context, id string) context.Context {

api/github.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ func (gh *GitHubGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
7474
ctx = withProxyTarget(ctx, target)
7575
ctx = withAccessToken(ctx, config.GitHub.AccessToken)
7676

77-
log := getLogEntry(r)
78-
log.Infof("proxy.ServeHTTP: %+v\n", r.WithContext(ctx))
7977
gh.proxy.ServeHTTP(w, r.WithContext(ctx))
8078
}
8179

0 commit comments

Comments
 (0)