Skip to content

Commit 46e6467

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

File tree

2 files changed

+128
-76
lines changed

2 files changed

+128
-76
lines changed

api/auth.go

Lines changed: 121 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,73 +4,65 @@ 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

17-
// check both authentication and authorization
18-
func (a *Auth) accessControl(w http.ResponseWriter, r *http.Request) (context.Context, error) {
19-
_, err := a.authenticate(w, r)
20-
if err != nil {
21-
return nil, err
22-
}
23-
24-
return a.authorize(w, r)
32+
type JWTAuthenticator struct {
33+
name string
34+
auth Auth
2535
}
2636

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) {
29-
logrus.Info("Getting auth token")
30-
token, err := a.extractBearerToken(w, r)
31-
if err != nil {
32-
return nil, err
33-
}
34-
35-
logrus.Infof("Parsing JWT claims: %v", token)
36-
return a.parseJWTClaims(token, r)
37+
type OktaJWTAuthenticator struct {
38+
name string
39+
auth Auth
3740
}
3841

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-
}
42+
type RolesAuthorizer struct {
43+
name string
44+
auth Auth
45+
}
4946

50-
if len(config.Roles) == 0 {
51-
return ctx, nil
52-
}
47+
func NewAuthWithVersion(ctx context.Context, globalConfig *conf.GlobalConfiguration, version string) *Auth {
48+
auth := &Auth{config: globalConfig, version: version}
5349

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-
}
65-
}
50+
auth.authenticator = &OktaJWTAuthenticator{name: "bearer-jwt-token", auth: *auth}
51+
auth.authorizer = &RolesAuthorizer{name: "bearer-jwt-token-roles", auth: *auth}
6652

67-
return nil, unauthorizedError("Access to endpoint not allowed: your role doesn't allow access")
53+
return auth
6854
}
6955

70-
func NewAuthWithVersion(ctx context.Context, globalConfig *conf.GlobalConfiguration, version string) *Auth {
71-
auth := &Auth{config: globalConfig, version: version}
56+
// check both authentication and authorization
57+
func (a *Auth) accessControl(w http.ResponseWriter, r *http.Request) (context.Context, error) {
58+
logrus.Infof("Authenticate with: %v", a.authenticator.getName())
59+
ctx, err := a.authenticator.authenticate(w, r)
60+
if err != nil {
61+
return nil, err
62+
}
7263

73-
return auth
64+
logrus.Infof("Authorizing with: %v", a.authorizer.getName())
65+
return a.authorizer.authorize(w, r.WithContext(ctx))
7466
}
7567

7668
func (a *Auth) extractBearerToken(w http.ResponseWriter, r *http.Request) (string, error) {
@@ -87,10 +79,51 @@ func (a *Auth) extractBearerToken(w http.ResponseWriter, r *http.Request) (strin
8779
return matches[1], nil
8880
}
8981

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)
82+
func (a *JWTAuthenticator) getName() string {
83+
return a.name
84+
}
85+
86+
func (a *JWTAuthenticator) authenticate(w http.ResponseWriter, r *http.Request) (context.Context, error) {
87+
logrus.Info("Getting auth token")
88+
token, err := a.auth.extractBearerToken(w, r)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
logrus.Infof("Parsing JWT claims: %v", token)
94+
return a.parseJWTToken(token, r)
95+
}
96+
97+
func (a *JWTAuthenticator) parseJWTToken(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+
})
103+
104+
if err != nil {
105+
return nil, unauthorizedError("Invalid token: %v", err)
106+
}
107+
claims := token.Claims.(GatewayClaims)
108+
return withClaims(r.Context(), &claims), nil
109+
}
110+
111+
func (a *OktaJWTAuthenticator) getName() string {
112+
return a.name
113+
}
114+
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
120+
}
121+
122+
logrus.Infof("Parsing JWT claims: %v", token)
123+
return a.parseOktaJWTToken(token, r)
124+
}
125+
126+
func (a *OktaJWTAuthenticator) parseOktaJWTToken(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: 7 additions & 18 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
)
@@ -21,7 +20,7 @@ func (c contextKey) String() string {
2120

2221
const (
2322
accessTokenKey = contextKey("token")
24-
tokenKey = contextKey("jwt")
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 {

0 commit comments

Comments
 (0)