github.com/blend/go-sdk@v1.20220411.3/web/auth_manager.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package web 9 10 //github:codeowner @blend/infosec 11 12 import ( 13 "context" 14 "net/http" 15 "net/url" 16 "time" 17 18 "github.com/blend/go-sdk/webutil" 19 ) 20 21 // MustNewAuthManager returns a new auth manager with a given set of options but panics on error. 22 func MustNewAuthManager(options ...AuthManagerOption) AuthManager { 23 am, err := NewAuthManager(options...) 24 if err != nil { 25 panic(err) 26 } 27 return am 28 } 29 30 // NewAuthManager returns a new auth manager from a given config. 31 // For remote mode, you must provide a fetch, persist, and remove handler, and optionally a login redirect handler. 32 func NewAuthManager(options ...AuthManagerOption) (manager AuthManager, err error) { 33 manager.CookieDefaults.Name = DefaultCookieName 34 manager.CookieDefaults.Path = DefaultCookiePath 35 manager.CookieDefaults.Secure = DefaultCookieSecure 36 manager.CookieDefaults.HttpOnly = DefaultCookieHTTPOnly 37 manager.CookieDefaults.SameSite = DefaultCookieSameSiteMode 38 39 for _, opt := range options { 40 if err = opt(&manager); err != nil { 41 return 42 } 43 } 44 return 45 } 46 47 // NewLocalAuthManager returns a new locally cached session manager. 48 // It saves sessions to a local store. 49 func NewLocalAuthManager(options ...AuthManagerOption) (AuthManager, error) { 50 return NewLocalAuthManagerFromCache(NewLocalSessionCache(), options...) 51 } 52 53 // NewLocalAuthManagerFromCache returns a new locally cached session manager that saves sessions to the cache provided 54 func NewLocalAuthManagerFromCache(cache *LocalSessionCache, options ...AuthManagerOption) (manager AuthManager, err error) { 55 manager, err = NewAuthManager(options...) 56 if err != nil { 57 return 58 } 59 manager.PersistHandler = cache.PersistHandler 60 manager.FetchHandler = cache.FetchHandler 61 manager.RemoveHandler = cache.RemoveHandler 62 return 63 } 64 65 // AuthManagerOption is a variadic option for auth managers. 66 type AuthManagerOption func(*AuthManager) error 67 68 // OptAuthManagerFromConfig returns an auth manager from a config. 69 func OptAuthManagerFromConfig(cfg Config) AuthManagerOption { 70 return func(am *AuthManager) (err error) { 71 opts := []AuthManagerOption{ 72 OptAuthManagerCookieSecure(cfg.CookieSecureOrDefault()), 73 OptAuthManagerCookieHTTPOnly(cfg.CookieHTTPOnlyOrDefault()), 74 OptAuthManagerCookieName(cfg.CookieNameOrDefault()), 75 OptAuthManagerCookiePath(cfg.CookiePathOrDefault()), 76 OptAuthManagerCookieDomain(cfg.CookieDomainOrDefault()), 77 OptAuthManagerCookieSameSite(cfg.CookieSameSiteOrDefault()), 78 OptAuthManagerSessionTimeoutProvider(SessionTimeoutProvider(!cfg.SessionTimeoutIsRelative, cfg.SessionTimeoutOrDefault())), 79 } 80 for _, opt := range opts { 81 // NOTE(wc): none of the options above produce an error 82 // it is safe to ignore the error produced 83 // by the option call. 84 _ = opt(am) 85 } 86 return 87 } 88 } 89 90 // OptAuthManagerCookieDefaults sets a field on an auth manager 91 func OptAuthManagerCookieDefaults(cookie http.Cookie) AuthManagerOption { 92 return func(am *AuthManager) (err error) { 93 am.CookieDefaults = cookie 94 return nil 95 } 96 } 97 98 // OptAuthManagerCookieSecure sets a field on an auth manager 99 func OptAuthManagerCookieSecure(secure bool) AuthManagerOption { 100 return func(am *AuthManager) (err error) { 101 am.CookieDefaults.Secure = secure 102 return nil 103 } 104 } 105 106 // OptAuthManagerCookieHTTPOnly sets a field on an auth manager 107 func OptAuthManagerCookieHTTPOnly(httpOnly bool) AuthManagerOption { 108 return func(am *AuthManager) (err error) { 109 am.CookieDefaults.HttpOnly = httpOnly 110 return nil 111 } 112 } 113 114 // OptAuthManagerCookieName sets a field on an auth manager 115 func OptAuthManagerCookieName(cookieName string) AuthManagerOption { 116 return func(am *AuthManager) (err error) { 117 am.CookieDefaults.Name = cookieName 118 return nil 119 } 120 } 121 122 // OptAuthManagerCookiePath sets a field on an auth manager 123 func OptAuthManagerCookiePath(cookiePath string) AuthManagerOption { 124 return func(am *AuthManager) (err error) { 125 am.CookieDefaults.Path = cookiePath 126 return nil 127 } 128 } 129 130 // OptAuthManagerCookieDomain sets a field on an auth manager 131 func OptAuthManagerCookieDomain(domain string) AuthManagerOption { 132 return func(am *AuthManager) (err error) { 133 am.CookieDefaults.Domain = domain 134 return nil 135 } 136 } 137 138 // OptAuthManagerCookieSameSite sets a field on an auth manager 139 func OptAuthManagerCookieSameSite(sameSite http.SameSite) AuthManagerOption { 140 return func(am *AuthManager) (err error) { 141 am.CookieDefaults.SameSite = sameSite 142 return nil 143 } 144 } 145 146 // OptAuthManagerSerializeHandler sets a field on an auth manager 147 func OptAuthManagerSerializeHandler(handler AuthManagerSerializeSessionHandler) AuthManagerOption { 148 return func(am *AuthManager) (err error) { 149 am.SerializeHandler = handler 150 return nil 151 } 152 } 153 154 // OptAuthManagerPersistHandler sets a field on an auth manager 155 func OptAuthManagerPersistHandler(handler AuthManagerPersistSessionHandler) AuthManagerOption { 156 return func(am *AuthManager) (err error) { 157 am.PersistHandler = handler 158 return nil 159 } 160 } 161 162 // OptAuthManagerFetchHandler sets a field on an auth manager 163 func OptAuthManagerFetchHandler(handler AuthManagerFetchSessionHandler) AuthManagerOption { 164 return func(am *AuthManager) (err error) { 165 am.FetchHandler = handler 166 return nil 167 } 168 } 169 170 // OptAuthManagerRemoveHandler sets a field on an auth manager 171 func OptAuthManagerRemoveHandler(handler AuthManagerRemoveSessionHandler) AuthManagerOption { 172 return func(am *AuthManager) (err error) { 173 am.RemoveHandler = handler 174 return nil 175 } 176 } 177 178 // OptAuthManagerValidateHandler sets a field on an auth manager 179 func OptAuthManagerValidateHandler(handler AuthManagerValidateSessionHandler) AuthManagerOption { 180 return func(am *AuthManager) (err error) { 181 am.ValidateHandler = handler 182 return nil 183 } 184 } 185 186 // OptAuthManagerSessionTimeoutProvider sets a field on an auth manager 187 func OptAuthManagerSessionTimeoutProvider(handler AuthManagerSessionTimeoutProvider) AuthManagerOption { 188 return func(am *AuthManager) (err error) { 189 am.SessionTimeoutProvider = handler 190 return nil 191 } 192 } 193 194 // OptAuthManagerLoginRedirectHandler sets a field on an auth manager 195 func OptAuthManagerLoginRedirectHandler(handler AuthManagerRedirectHandler) AuthManagerOption { 196 return func(am *AuthManager) (err error) { 197 am.LoginRedirectHandler = handler 198 return nil 199 } 200 } 201 202 // AuthManagerSerializeSessionHandler serializes a session as a string. 203 type AuthManagerSerializeSessionHandler func(context.Context, *Session) (string, error) 204 205 // AuthManagerPersistSessionHandler saves the session to a stable store. 206 type AuthManagerPersistSessionHandler func(context.Context, *Session) error 207 208 // AuthManagerFetchSessionHandler restores a session based on a session value. 209 type AuthManagerFetchSessionHandler func(context.Context, string) (*Session, error) 210 211 // AuthManagerRemoveSessionHandler removes a session based on a session value. 212 type AuthManagerRemoveSessionHandler func(context.Context, string) error 213 214 // AuthManagerValidateSessionHandler validates a session. 215 type AuthManagerValidateSessionHandler func(context.Context, *Session) error 216 217 // AuthManagerSessionTimeoutProvider provides a new timeout for a session. 218 type AuthManagerSessionTimeoutProvider func(*Session) time.Time 219 220 // AuthManagerRedirectHandler is a redirect handler. 221 type AuthManagerRedirectHandler func(*Ctx) *url.URL 222 223 // AuthManager is a manager for sessions. 224 type AuthManager struct { 225 CookieDefaults http.Cookie 226 227 // PersistHandler is called to both create and to update a session in a persistent store. 228 PersistHandler AuthManagerPersistSessionHandler 229 // SerializeSessionHandler if set, is called to serialize the session 230 // as a session cookie value. 231 SerializeHandler AuthManagerSerializeSessionHandler 232 // FetchSessionHandler is called if set to restore a session from a string session identifier. 233 FetchHandler AuthManagerFetchSessionHandler 234 // Remove handler is called on logout to remove a session from a persistent store. 235 // It is called during `Logout` to remove logged out sessions. 236 RemoveHandler AuthManagerRemoveSessionHandler 237 // ValidateHandler is called after a session is retored to make sure it's still valid. 238 ValidateHandler AuthManagerValidateSessionHandler 239 // SessionTimeoutProvider is called to create a variable session expiry. 240 SessionTimeoutProvider AuthManagerSessionTimeoutProvider 241 242 // LoginRedirectHandler redirects an unauthenticated user to the login page. 243 LoginRedirectHandler AuthManagerRedirectHandler 244 } 245 246 // -------------------------------------------------------------------------------- 247 // Methods 248 // -------------------------------------------------------------------------------- 249 250 // Login logs a userID in. 251 func (am AuthManager) Login(userID string, ctx *Ctx) (session *Session, err error) { 252 // create a new session value 253 sessionValue := NewSessionID() 254 // userID and sessionID are required 255 session = NewSession(userID, sessionValue) 256 if am.SessionTimeoutProvider != nil { 257 session.ExpiresUTC = am.SessionTimeoutProvider(session) 258 } 259 session.UserAgent = webutil.GetUserAgent(ctx.Request) 260 session.RemoteAddr = webutil.GetRemoteAddr(ctx.Request) 261 262 // call the perist handler if one's been provided 263 if am.PersistHandler != nil { 264 err = am.PersistHandler(ctx.Context(), session) 265 if err != nil { 266 return nil, err 267 } 268 } 269 270 // call the serialize handler if one's been provided 271 if am.SerializeHandler != nil { 272 sessionValue, err = am.SerializeHandler(ctx.Context(), session) 273 if err != nil { 274 return nil, err 275 } 276 } 277 278 // inject cookies into the response 279 am.injectCookie(ctx, sessionValue, session.ExpiresUTC) 280 return session, nil 281 } 282 283 // Logout unauthenticates a session. 284 func (am AuthManager) Logout(ctx *Ctx) error { 285 sessionValue := am.readSessionValue(ctx) 286 // validate the sessionValue isn't unset 287 if sessionValue == "" { 288 return nil 289 } 290 // zero out the context session as a precaution 291 ctx.Session = nil 292 // issue the expiration cookies to the response 293 // and call the remove handler 294 return am.expire(ctx, sessionValue) 295 } 296 297 // VerifySession pulls the session cookie off the request, and validates 298 // it represents a valid session. 299 func (am AuthManager) VerifySession(ctx *Ctx) (sessionValue string, session *Session, err error) { 300 sessionValue = am.readSessionValue(ctx) 301 // validate the sessionValue is set 302 if len(sessionValue) == 0 { 303 return 304 } 305 306 // if we have a restore handler, call it. 307 if am.FetchHandler != nil { 308 session, err = am.FetchHandler(ctx.Context(), sessionValue) 309 if err != nil { 310 if IsErrSessionInvalid(err) { 311 _ = am.expire(ctx, sessionValue) 312 } 313 return 314 } 315 } 316 317 // if the session is invalid, expire the cookie(s) 318 if session == nil || session.IsZero() || session.IsExpired() { 319 // return nil whenever the session is invalid 320 session = nil 321 err = am.expire(ctx, sessionValue) 322 return 323 } 324 325 // call a custom validate handler if one's been provided. 326 if am.ValidateHandler != nil { 327 err = am.ValidateHandler(ctx.Context(), session) 328 if err != nil { 329 session = nil 330 return 331 } 332 } 333 return 334 } 335 336 // VerifyOrExtendSession reads a session value from a request and checks if it's valid. 337 // It also handles updating a rolling expiry. 338 func (am AuthManager) VerifyOrExtendSession(ctx *Ctx) (session *Session, err error) { 339 var sessionValue string 340 sessionValue, session, err = am.VerifySession(ctx) 341 if session == nil || err != nil { 342 return 343 } 344 345 if am.SessionTimeoutProvider != nil { 346 existingExpiresUTC := session.ExpiresUTC 347 session.ExpiresUTC = am.SessionTimeoutProvider(session) 348 349 // if session expiry has changed 350 if existingExpiresUTC != session.ExpiresUTC { 351 // if we have a persist handler 352 // call it to reflect the updated session timeout. 353 if am.PersistHandler != nil { 354 err = am.PersistHandler(ctx.Context(), session) 355 if err != nil { 356 return nil, err 357 } 358 } 359 360 // inject the (updated) cookie 361 am.injectCookie(ctx, sessionValue, session.ExpiresUTC) 362 } 363 } 364 return 365 } 366 367 // LoginRedirect returns a redirect result for when auth fails and you need to 368 // send the user to a login page. 369 func (am AuthManager) LoginRedirect(ctx *Ctx) Result { 370 if am.LoginRedirectHandler != nil { 371 redirectTo := am.LoginRedirectHandler(ctx) 372 if redirectTo != nil { 373 return Redirect(redirectTo.String()) 374 } 375 } 376 return ctx.DefaultProvider.NotAuthorized() 377 } 378 379 // -------------------------------------------------------------------------------- 380 // Utility Methods 381 // -------------------------------------------------------------------------------- 382 383 func (am AuthManager) expire(ctx *Ctx, sessionValue string) error { 384 // issue the cookie expiration. 385 am.expireCookie(ctx) 386 387 // if we have a remove handler and the sessionID is set 388 if am.RemoveHandler != nil { 389 err := am.RemoveHandler(ctx.Context(), sessionValue) 390 if err != nil { 391 return err 392 } 393 } 394 return nil 395 } 396 397 // InjectCookie injects a session cookie into the context. 398 func (am AuthManager) injectCookie(ctx *Ctx, value string, expire time.Time) { 399 http.SetCookie(ctx.Response, &http.Cookie{ 400 Value: value, 401 Expires: expire, 402 Name: am.CookieDefaults.Name, 403 Path: am.CookieDefaults.Path, 404 Domain: am.CookieDefaults.Domain, 405 HttpOnly: am.CookieDefaults.HttpOnly, 406 Secure: am.CookieDefaults.Secure, 407 SameSite: am.CookieDefaults.SameSite, 408 }) 409 } 410 411 // expireCookie expires the session cookie. 412 func (am AuthManager) expireCookie(ctx *Ctx) { 413 http.SetCookie(ctx.Response, &http.Cookie{ 414 Value: NewSessionID(), 415 // MaxAge<0 means delete cookie now, and is equivalent to 416 // the literal cookie header content 'Max-Age: 0' 417 MaxAge: -1, 418 Name: am.CookieDefaults.Name, 419 Path: am.CookieDefaults.Path, 420 Domain: am.CookieDefaults.Domain, 421 HttpOnly: am.CookieDefaults.HttpOnly, 422 Secure: am.CookieDefaults.Secure, 423 SameSite: am.CookieDefaults.SameSite, 424 }) 425 426 } 427 428 // cookieValue reads a param from a given request context from either the cookies or headers. 429 func (am AuthManager) cookieValue(name string, ctx *Ctx) (output string) { 430 if cookie := ctx.Cookie(name); cookie != nil { 431 output = cookie.Value 432 } 433 return 434 } 435 436 // ReadSessionID reads a session id from a given request context. 437 func (am AuthManager) readSessionValue(ctx *Ctx) string { 438 return am.cookieValue(am.CookieDefaults.Name, ctx) 439 }