-
Notifications
You must be signed in to change notification settings - Fork 33
/
security.go
399 lines (336 loc) · 13.8 KB
/
security.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm)
// Source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package aah
import (
"net/http"
"net/url"
"strings"
"aahframe.work/ahttp"
ess "aahframe.work/essentials"
"aahframe.work/internal/util"
"aahframe.work/security"
"aahframe.work/security/anticsrf"
"aahframe.work/security/authc"
"aahframe.work/security/authz"
"aahframe.work/security/scheme"
)
const (
// KeyViewArgAuthcInfo key name is used to store `AuthenticationInfo` instance into `ViewArgs`.
KeyViewArgAuthcInfo = "_aahAuthcInfo"
// KeyViewArgSubject key name is used to store `Subject` instance into `ViewArgs`.
KeyViewArgSubject = "_aahSubject"
// KeyOAuth2Token key name is used to store OAuth2 Access Token into `aah.Context`.
KeyOAuth2Token = "_aahOAuth2Token"
keyAntiCSRF = "_aahAntiCSRF"
keyOAuth2StateKey = "_aahOAuth2State"
keyAuthScheme = "_aahAuthScheme"
)
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// app Unexported methods
//______________________________________________________________________________
func (a *Application) initSecurity() error {
asecmgr := security.New()
asecmgr.IsSSLEnabled = a.IsSSLEnabled()
if err := asecmgr.Init(a.Config()); err != nil {
return err
}
a.securityMgr = asecmgr
a.settings.AuthSchemeExists = len(a.securityMgr.AuthSchemes()) > 0
return nil
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Authentication and Authorization Middleware
//______________________________________________________________________________
// AuthcAuthzMiddleware is aah Authentication and Authorization Middleware.
func AuthcAuthzMiddleware(ctx *Context, m *Middleware) {
// Continue with the flow, if -
// - Auth scheme is not defined in `security.conf`
// - Route auth is `anonymous`
if !ctx.a.settings.AuthSchemeExists || ctx.route.Auth == "anonymous" {
m.Next(ctx)
return
}
// If session is authenticated then populate subject and continue the request flow.
if ctx.Subject().IsAuthenticated() {
if key := ctx.Session().GetString(keyAuthScheme); key != "" {
populateAuthorizationInfo(ctx.a.SecurityManager().AuthScheme(key), ctx)
if hasAccess(ctx) == flowCont {
m.Next(ctx)
}
return
}
} else if ctx.route.Auth == "authenticated" {
// If route auth is `authenticated` then denied request with 401
ctx.Reply().Unauthorized().Error(newError(ErrNotAuthenticated, http.StatusUnauthorized))
return
}
// Supports one or more auth scheme on route
var result flowResult
for _, s := range strings.Split(ctx.route.Auth, ",") {
authScheme := ctx.a.SecurityManager().AuthScheme(strings.TrimSpace(s))
ctx.Log().Debugf("Processing route auth scheme: %s", authScheme.Key())
switch authScheme.Scheme() {
case "form":
result = doFormAuth(authScheme, ctx)
case "oauth2":
result = doOAuth2(authScheme, ctx)
default:
result = doAuthScheme(authScheme, ctx)
}
if result == flowCont {
break
}
}
if result == flowCont && hasAccess(ctx) == flowCont {
m.Next(ctx)
}
}
// doFormAuth method does Form Authentication and Authorization.
func doFormAuth(authScheme scheme.Schemer, ctx *Context) flowResult {
formAuth := authScheme.(*scheme.FormAuth)
// Check route is login submit URL otherwise send it login URL.
// Since session is not authenticated.
if formAuth.LoginSubmitURL != ctx.route.Path && ctx.Req.Method != ahttp.MethodPost {
loginURL := formAuth.LoginURL
if formAuth.LoginURL != ctx.Req.Path {
loginURL = util.AddQueryString(loginURL, "_rt", ctx.Req.URL().String())
}
ctx.Reply().Redirect(loginURL)
return flowAbort
}
ctx.e.publishOnPreAuthEvent(ctx)
if doAuthentication(authScheme, ctx) == flowAbort {
return flowAbort
}
populateAuthorizationInfo(authScheme, ctx)
debugLogSubjectInfo(ctx)
ctx.e.publishOnPostAuthEvent(ctx)
rt := ctx.Req.FormValue("_rt") // redirect to requested URL
if formAuth.IsAlwaysToDefaultTarget || len(rt) == 0 {
ctx.Reply().Redirect(formAuth.DefaultTargetURL)
} else {
ctx.Log().Debugf("Redirecting to URL found in param '_rt': %s", rt)
ctx.Reply().Redirect(rt)
}
return flowAbort
}
// doOAuth2 method does 3-legged OAuth2 authentication with provider
// and adds the Token into Context. It bit different from FormAuth,
// BasicAuth and Generic (basically it does not have support for
// interface authenticator and authorizer, since its not appliable in the
// OAuth2 flow).
func doOAuth2(authScheme scheme.Schemer, ctx *Context) flowResult {
ctx.e.publishOnPreAuthEvent(ctx)
oauth := authScheme.(*scheme.OAuth2)
// OAuth2 provider login
if ctx.Req.Path == oauth.LoginURL {
state, authURL := oauth.ProviderAuthURL(ctx.Req)
ctx.Session().Set(keyOAuth2StateKey, state)
ctx.Reply().Redirect(authURL)
return flowAbort
}
// OAuth2 provider callback handling
if ctx.Req.Path == oauth.RedirectURL {
defer ctx.Session().Del(keyOAuth2StateKey)
// Validate OAuth2 callback
ctx.Log().Debug(ctx.Req.URL().String())
token, err := oauth.ValidateCallback(ctx.Session().GetString(keyOAuth2StateKey), ctx.Req)
if err != nil {
ctx.Log().Error(err)
ctx.Reply().Unauthorized().Error(newError(err, http.StatusUnauthorized))
return flowAbort
}
// Set successful access token into aah.Context
ctx.Log().Info("oauth2: Token obtained from provider")
ctx.Set(KeyOAuth2Token, token)
if doAuthentication(authScheme, ctx) == flowAbort {
return flowAbort
}
populateAuthorizationInfo(authScheme, ctx)
debugLogSubjectInfo(ctx)
ctx.e.publishOnPostAuthEvent(ctx)
// Redirect to success URL
ctx.Reply().Redirect(oauth.SuccessURL)
return flowAbort
}
// typically it should not reach here
ctx.Log().Trace("OAuth2 flow; typically it should not reach here")
return flowAbort
}
// doAuthScheme method does generic and basic (Authentication and Authorization).
func doAuthScheme(authScheme scheme.Schemer, ctx *Context) flowResult {
ctx.e.publishOnPreAuthEvent(ctx)
if doAuthentication(authScheme, ctx) == flowAbort {
if ctx.Reply().err == nil {
ctx.Reply().Unauthorized().Error(newError(ErrAuthenticationFailed, http.StatusUnauthorized))
}
return flowAbort
}
populateAuthorizationInfo(authScheme, ctx)
debugLogSubjectInfo(ctx)
ctx.e.publishOnPostAuthEvent(ctx)
return flowCont
}
type principalProviderNoInit interface {
Principal(keyName string, v ess.Valuer) ([]*authc.Principal, error)
}
func doAuthentication(authScheme scheme.Schemer, ctx *Context) flowResult {
var authcInfo *authc.AuthenticationInfo
if c, ok := authScheme.(principalProviderNoInit); ok {
// Call Subject principals provider
principals, err := c.Principal(authScheme.Key(), ctx)
if err != nil {
ctx.Log().Error(ErrUnableToGetPrincipal)
ctx.Reply().Unauthorized().Error(newError(ErrUnableToGetPrincipal, http.StatusUnauthorized))
return flowAbort
}
ctx.Log().Debugf("%s: Subject principals obtained", authScheme.Key())
authcInfo = authc.NewAuthenticationInfo()
authcInfo.Principals = append(authcInfo.Principals, principals...)
} else {
// Call Authentication Info provider
var err error
authcInfo, err = authScheme.DoAuthenticate(authScheme.ExtractAuthenticationToken(ctx.Req))
if err != nil || authcInfo == nil {
switch sa := authScheme.(type) {
case *scheme.FormAuth:
ctx.Log().Infof("%s: Authentication is failed, sending to login failure URL", authScheme.Key())
ctx.Reply().Redirect(util.AddQueryString(sa.LoginFailureURL, "_rt", ctx.Req.FormValue("_rt")))
case *scheme.BasicAuth:
ctx.Log().Infof("%s: Authentication is failed", authScheme.Key())
ctx.Reply().Header(ahttp.HeaderWWWAuthenticate, `Basic realm="`+sa.RealmName+`"`)
ctx.Reply().Unauthorized().Error(newError(ErrAuthenticationFailed, http.StatusUnauthorized))
case *scheme.GenericAuth:
switch err {
case authc.ErrAuthenticationFailed, authc.ErrAuthenticatorIsNil, authc.ErrPrincipalIsNil, authc.ErrSubjectNotExists:
ctx.Log().Infof("%s: Authentication is failed", authScheme.Key())
ctx.Reply().Unauthorized().Error(newError(ErrAuthenticationFailed, http.StatusUnauthorized))
case authc.ErrInternalServerError:
ctx.Log().Errorf("%s: Internal Server Error", authScheme.Key())
ctx.Reply().InternalServerError().Error(newError(err, http.StatusInternalServerError))
case authc.ErrServiceUnavailable:
ctx.Log().Errorf("%s: Service Unavailable", authScheme.Key())
ctx.Reply().ServiceUnavailable().Error(newError(err, http.StatusServiceUnavailable))
}
}
return flowAbort
}
}
populateAuthenticationInfo(authcInfo, ctx)
ctx.Session().IsAuthenticated = true
ctx.Session().Set(keyAuthScheme, authScheme.Key())
ctx.Log().Infof("%s: Authentication successful", authScheme.Key())
// Add to session its stateful
if ctx.a.SessionManager().IsStateful() {
ctx.Session().Set(KeyViewArgAuthcInfo, ctx.Subject().AuthenticationInfo)
}
if ctx.a.ViewEngine() != nil {
// Change the Anti-CSRF token in use for a request after login for security purposes.
ctx.Log().Info("Change Anti-CSRF secret after successful authentication for security purpose")
ctx.AddViewArg(keyAntiCSRF, ctx.a.SecurityManager().AntiCSRF.GenerateSecret())
}
return flowCont
}
func populateAuthenticationInfo(authcInfo *authc.AuthenticationInfo, ctx *Context) {
ctx.Subject().AuthenticationInfo = authcInfo
ctx.logger = ctx.Log().WithField("principal", ctx.Subject().PrimaryPrincipal().Value)
ctx.Subject().AuthenticationInfo.Credential = nil // Remove the credential
}
func populateAuthorizationInfo(authScheme scheme.Schemer, ctx *Context) {
ctx.Subject().AuthorizationInfo = authScheme.DoAuthorizationInfo(ctx.Subject().AuthenticationInfo)
}
func hasAccess(ctx *Context) flowResult {
result, reasons := ctx.hasAccess()
if result {
return flowCont
}
ctx.Log().Warnf("Authorization failed:%s", reason2String(reasons))
ctx.Reply().Forbidden().Error(newErrorWithData(ErrAuthorizationFailed, http.StatusForbidden, reasons))
return flowAbort
}
func debugLogSubjectInfo(ctx *Context) {
ctx.Log().Debug(ctx.Subject().AuthenticationInfo)
ctx.Log().Debug(ctx.Subject().AuthorizationInfo)
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Anti-CSRF Middleware
//______________________________________________________________________________
// AntiCSRFMiddleware provides feature to prevent Cross-Site Request Forgery (CSRF)
// attacks.
func AntiCSRFMiddleware(ctx *Context, m *Middleware) {
ac := ctx.a.SecurityManager().AntiCSRF
// If Anti-CSRF is not enabled, move on.
// It is highly recommended to enable it for web application.
if !ac.Enabled || !ctx.route.IsAntiCSRFCheck || ctx.a.ViewEngine() == nil {
ac.ClearCookie(ctx.Res, ctx.Req)
m.Next(ctx)
return
}
// Get cipher secret from anti-csrf cookie
secret := ac.CipherSecret(ctx.Req)
ctx.AddViewArg(keyAntiCSRF, secret)
// HTTP Method is safe per defined in
// https://tools.ietf.org/html/rfc7231#section-4.2.1
if anticsrf.IsSafeHTTPMethod(ctx.Req.Method) {
ctx.Log().Tracef("HTTP %s is safe method per RFC7231", ctx.Req.Method)
m.Next(ctx)
if err := ac.SetCookie(ctx.Res, secret); err != nil {
ctx.Log().Error("anticsrf: Unable to write cookie")
}
return
}
// Below comment graciously borrowed from django
// Suppose user visits http://example.com/
// An active network attacker (man-in-the-middle, MITM) sends a
// POST form that targets https://example.com/detonate-bomb/ and
// submits it via JavaScript.
//
// The attacker will need to provide a CSRF cookie and token, but
// that's no problem for a MITM and the session-independent
// secret we're using. So the MITM can circumvent the CSRF
// protection. This is true for any HTTP connection, but anyone
// using HTTPS expects better! For this reason, for
// https://example.com/ we need additional protection that treats
// http://example.com/ as completely untrusted. Under HTTPS,
// Barth et al. found that the Referer header is missing for
// same-domain requests in only about 0.2% of cases or less, so
// we can use strict Referer checking.
if ctx.Req.Scheme == ahttp.SchemeHTTPS {
referer, err := url.Parse(ctx.Req.Referer())
if err != nil {
ctx.Log().Warnf("anticsrf: Malformed referer %s", ctx.Req.Referer())
ctx.Reply().Forbidden().Error(newError(anticsrf.ErrMalformedReferer, http.StatusForbidden))
return
}
if len(referer.String()) == 0 {
ctx.Log().Warnf("anticsrf: No referer %s", ctx.Req.Referer())
ctx.Reply().Forbidden().Error(newError(anticsrf.ErrNoReferer, http.StatusForbidden))
return
}
if !anticsrf.IsSameOrigin(ctx.Req.URL(), referer) && !ac.IsTrustedOrigin(referer) {
ctx.Log().Warnf("anticsrf: Bad referer %s", ctx.Req.Referer())
ctx.Reply().Forbidden().Error(newError(anticsrf.ErrBadReferer, http.StatusForbidden))
return
}
}
// Get request cipher secret from HTTP header or Form
requestSecret := ac.RequestCipherSecret(ctx.Req)
if requestSecret == nil || !ac.IsAuthentic(secret, requestSecret) {
ctx.Log().Warn("anticsrf: Verification failed, invalid cipher secret")
ctx.Reply().Forbidden().Error(newError(anticsrf.ErrNoCookieFound, http.StatusForbidden))
return
}
ctx.Log().Info("anticsrf: Cipher secret verification passed")
m.Next(ctx)
if err := ac.SetCookie(ctx.Res, secret); err != nil {
ctx.Log().Error("anticsrf: Unable to write cookie")
}
}
func reason2String(reasons []*authz.Reason) string {
var str string
for _, r := range reasons {
str += " " + r.String()
}
return str
}