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  }