@@ -4,30 +4,88 @@ import (
4
4
"context"
5
5
"net/http"
6
6
7
+ jwt "github.com/dgrijalva/jwt-go"
7
8
"github.com/netlify/git-gateway/conf"
8
9
"github.com/sirupsen/logrus"
9
10
"github.com/okta/okta-jwt-verifier-golang"
10
11
)
11
12
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
+
12
25
type Auth struct {
13
26
config * conf.GlobalConfiguration
27
+ authenticator Authenticator
28
+ authorizer Authorizer
14
29
version string
15
30
}
16
31
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
+
17
56
// check both authentication and authorization
18
57
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 )
20
60
if err != nil {
21
61
return nil , err
22
62
}
23
63
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
25
84
}
26
85
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 ) {
29
87
logrus .Info ("Getting auth token" )
30
- token , err := a .extractBearerToken (w , r )
88
+ token , err := a .auth . extractBearerToken (w , r )
31
89
if err != nil {
32
90
return nil , err
33
91
}
@@ -36,61 +94,36 @@ func (a *Auth) authenticate(w http.ResponseWriter, r *http.Request) (context.Con
36
94
return a .parseJWTClaims (token , r )
37
95
}
38
96
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
+ })
53
103
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 )
65
106
}
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
68
109
}
69
110
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
74
113
}
75
114
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
85
120
}
86
121
87
- return matches [1 ], nil
122
+ logrus .Infof ("Parsing JWT claims: %v" , token )
123
+ return a .parseOktaJWTClaims (token , r )
88
124
}
89
125
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 ) {
94
127
config := getConfig (r .Context ())
95
128
96
129
toValidate := map [string ]string {}
@@ -106,17 +139,47 @@ func (a *Auth) parseJWTClaims(bearer string, r *http.Request) (context.Context,
106
139
107
140
_ , err := verifier .VerifyAccessToken (bearer )
108
141
109
- // @TODO? WARNING: Should be roles and other claims be checked here?
110
-
111
142
if err != nil {
112
143
return nil , unauthorizedError ("Invalid token: %v" , err )
113
144
}
114
145
146
+ claims := GatewayClaims {Email : "e" , StandardClaims : jwt.StandardClaims {Audience : "a" }}
147
+
115
148
logrus .Infof ("parseJWTClaims passed" )
149
+ return withClaims (r .Context (), & claims ), nil
150
+ }
116
151
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" )
122
185
}
0 commit comments