2017-02-25 02:45:47 +02:00
package authboss
import (
"context"
2018-01-29 21:35:47 +02:00
"fmt"
2017-02-25 02:45:47 +02:00
"net/http"
)
const (
// SessionKey is the primarily used key by authboss.
SessionKey = "uid"
// SessionHalfAuthKey is used for sessions that have been authenticated by
// the remember module. This serves as a way to force full authentication
// by denying half-authed users acccess to sensitive areas.
SessionHalfAuthKey = "halfauth"
// SessionLastAction is the session key to retrieve the last action of a user.
SessionLastAction = "last_action"
2018-08-23 06:34:38 +02:00
// Session2FA is set when a user has been authenticated with a second factor
Session2FA = "twofactor"
2017-02-25 02:45:47 +02:00
// SessionOAuth2State is the xsrf protection key for oauth.
SessionOAuth2State = "oauth2_state"
// SessionOAuth2Params is the additional settings for oauth like redirection/remember.
SessionOAuth2Params = "oauth2_params"
// CookieRemember is used for cookies and form input names.
CookieRemember = "rm"
// FlashSuccessKey is used for storing sucess flash messages on the session
FlashSuccessKey = "flash_success"
// FlashErrorKey is used for storing sucess flash messages on the session
FlashErrorKey = "flash_error"
)
// ClientStateEventKind is an enum.
type ClientStateEventKind int
2018-02-01 03:07:11 +02:00
// ClientStateEvent kinds
2017-02-25 02:45:47 +02:00
const (
ClientStateEventPut ClientStateEventKind = iota
ClientStateEventDel
)
// ClientStateEvent are the different events that can be recorded during
type ClientStateEvent struct {
Kind ClientStateEventKind
Key string
Value string
}
// ClientStateReadWriter is used to create a cookie storer from an http request.
// Keep in mind security considerations for your implementation, Secure,
// HTTP-Only, etc flags.
//
// There's two major uses for this. To create session storage, and remember me
// cookies.
type ClientStateReadWriter interface {
2018-02-05 09:28:31 +02:00
// ReadState should return a map like structure allowing it to look up
// any values in the current session, or any cookie in the request
2018-03-08 02:21:37 +02:00
ReadState ( * http . Request ) ( ClientState , error )
2018-02-05 09:28:31 +02:00
// WriteState can sometimes be called with a nil ClientState in the event
2018-03-08 02:41:58 +02:00
// that no ClientState was read in from LoadClientState
2017-02-25 02:45:47 +02:00
WriteState ( http . ResponseWriter , ClientState , [ ] ClientStateEvent ) error
}
2018-01-29 21:35:47 +02:00
// UnderlyingResponseWriter retrieves the response
// writer underneath the current one. This allows us
// to wrap and later discover the particular one that we want.
// Keep in mind this should not be used to call the normal methods
// of a responsewriter, just additional ones particular to that type
// because it's possible to introduce subtle bugs otherwise.
type UnderlyingResponseWriter interface {
UnderlyingResponseWriter ( ) http . ResponseWriter
}
2017-02-25 02:45:47 +02:00
// ClientState represents the client's current state and can answer queries
// about it.
type ClientState interface {
Get ( key string ) ( string , bool )
}
2018-02-01 03:07:11 +02:00
// ClientStateResponseWriter is used to write out the client state at the last
2017-02-25 02:45:47 +02:00
// moment before the response code is written.
type ClientStateResponseWriter struct {
http . ResponseWriter
2018-03-08 02:21:37 +02:00
cookieStateRW ClientStateReadWriter
sessionStateRW ClientStateReadWriter
cookieState ClientState
sessionState ClientState
2018-02-05 09:28:31 +02:00
2017-02-25 02:45:47 +02:00
hasWritten bool
cookieStateEvents [ ] ClientStateEvent
2018-03-08 02:21:37 +02:00
sessionStateEvents [ ] ClientStateEvent
2017-02-25 02:45:47 +02:00
}
2018-02-15 01:16:44 +02:00
// LoadClientStateMiddleware wraps all requests with the ClientStateResponseWriter
// as well as loading the current client state into the context for use.
func ( a * Authboss ) LoadClientStateMiddleware ( h http . Handler ) http . Handler {
2018-02-05 09:28:31 +02:00
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2018-03-08 02:21:37 +02:00
writer := a . NewResponse ( w )
request , err := a . LoadClientState ( writer , r )
2018-02-05 09:28:31 +02:00
if err != nil {
2018-08-16 18:34:25 +02:00
logger := a . RequestLogger ( r )
logger . Errorf ( "failed to load client state %+v" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
2018-02-05 09:28:31 +02:00
}
h . ServeHTTP ( writer , request )
} )
}
2018-02-01 03:07:11 +02:00
// NewResponse wraps the ResponseWriter with a ClientStateResponseWriter
2018-03-08 02:21:37 +02:00
func ( a * Authboss ) NewResponse ( w http . ResponseWriter ) * ClientStateResponseWriter {
2017-02-25 02:45:47 +02:00
return & ClientStateResponseWriter {
ResponseWriter : w ,
2018-03-08 02:21:37 +02:00
cookieStateRW : a . Config . Storage . CookieState ,
sessionStateRW : a . Config . Storage . SessionState ,
2017-02-25 02:45:47 +02:00
}
}
2018-03-08 02:21:37 +02:00
// LoadClientState loads the state from sessions and cookies into the ResponseWriter
// for later use.
2017-02-25 02:45:47 +02:00
func ( a * Authboss ) LoadClientState ( w http . ResponseWriter , r * http . Request ) ( * http . Request , error ) {
2018-02-02 01:42:48 +02:00
if a . Storage . SessionState != nil {
2018-03-08 02:21:37 +02:00
state , err := a . Storage . SessionState . ReadState ( r )
2017-02-25 02:45:47 +02:00
if err != nil {
return nil , err
2018-08-16 18:34:25 +02:00
} else if state != nil {
c := MustClientStateResponseWriter ( w )
c . sessionState = state
r = r . WithContext ( context . WithValue ( r . Context ( ) , CTXKeySessionState , state ) )
2017-02-25 02:45:47 +02:00
}
}
2018-02-02 01:42:48 +02:00
if a . Storage . CookieState != nil {
2018-03-08 02:21:37 +02:00
state , err := a . Storage . CookieState . ReadState ( r )
2017-02-25 02:45:47 +02:00
if err != nil {
return nil , err
2018-08-16 18:34:25 +02:00
} else if state != nil {
c := MustClientStateResponseWriter ( w )
c . cookieState = state
r = r . WithContext ( context . WithValue ( r . Context ( ) , CTXKeyCookieState , state ) )
2017-02-25 02:45:47 +02:00
}
}
return r , nil
}
2018-01-29 21:35:47 +02:00
// MustClientStateResponseWriter tries to find a csrw inside the response
// writer by using the UnderlyingResponseWriter interface.
func MustClientStateResponseWriter ( w http . ResponseWriter ) * ClientStateResponseWriter {
for {
if c , ok := w . ( * ClientStateResponseWriter ) ; ok {
return c
}
if u , ok := w . ( UnderlyingResponseWriter ) ; ok {
w = u . UnderlyingResponseWriter ( )
continue
}
2018-08-16 17:01:29 +02:00
panic ( fmt . Sprintf ( "ResponseWriter must be a ClientStateResponseWriter or UnderlyingResponseWriter in (see: authboss.LoadClientStateMiddleware): %T" , w ) )
2018-01-29 21:35:47 +02:00
}
}
2017-02-25 02:45:47 +02:00
// WriteHeader writes the header, but in order to handle errors from the
// underlying ClientStateReadWriter, it has to panic.
func ( c * ClientStateResponseWriter ) WriteHeader ( code int ) {
if ! c . hasWritten {
if err := c . putClientState ( ) ; err != nil {
panic ( err )
}
}
c . ResponseWriter . WriteHeader ( code )
}
// Header retrieves the underlying headers
func ( c ClientStateResponseWriter ) Header ( ) http . Header {
return c . ResponseWriter . Header ( )
}
2018-02-15 01:16:44 +02:00
// Write ensures that the client state is written before any writes
// to the body occur (before header flush to http client)
2017-02-25 02:45:47 +02:00
func ( c * ClientStateResponseWriter ) Write ( b [ ] byte ) ( int , error ) {
if ! c . hasWritten {
if err := c . putClientState ( ) ; err != nil {
return 0 , err
}
}
return c . ResponseWriter . Write ( b )
}
2018-02-15 01:16:44 +02:00
// UnderlyingResponseWriter for this instance
2018-01-29 21:35:47 +02:00
func ( c * ClientStateResponseWriter ) UnderlyingResponseWriter ( ) http . ResponseWriter {
return c . ResponseWriter
}
2017-02-25 02:45:47 +02:00
func ( c * ClientStateResponseWriter ) putClientState ( ) error {
if c . hasWritten {
panic ( "should not call putClientState twice" )
}
c . hasWritten = true
2018-02-05 09:28:31 +02:00
if len ( c . cookieStateEvents ) == 0 && len ( c . sessionStateEvents ) == 0 {
return nil
2017-02-25 02:45:47 +02:00
}
2018-03-08 02:21:37 +02:00
if c . sessionStateRW != nil && len ( c . sessionStateEvents ) > 0 {
err := c . sessionStateRW . WriteState ( c , c . sessionState , c . sessionStateEvents )
2017-02-25 02:45:47 +02:00
if err != nil {
return err
}
}
2018-03-08 02:21:37 +02:00
if c . cookieStateRW != nil && len ( c . cookieStateEvents ) > 0 {
err := c . cookieStateRW . WriteState ( c , c . cookieState , c . cookieStateEvents )
2017-02-25 02:45:47 +02:00
if err != nil {
return err
}
}
return nil
}
2018-08-23 06:34:38 +02:00
// IsFullyAuthed returns false if the user has a SessionHalfAuth
2018-07-17 16:09:38 +02:00
// in his session.
func IsFullyAuthed ( r * http . Request ) bool {
_ , hasHalfAuth := GetSession ( r , SessionHalfAuthKey )
return ! hasHalfAuth
}
2018-08-23 06:34:38 +02:00
// IsTwoFactored returns false if the user doesn't have a Session2FA
// in his session.
func IsTwoFactored ( r * http . Request ) bool {
_ , has2fa := GetSession ( r , Session2FA )
return has2fa
}
2018-03-09 23:11:08 +02:00
// DelKnownSession deletes all known session variables, effectively
// logging a user out.
func DelKnownSession ( w http . ResponseWriter ) {
DelSession ( w , SessionKey )
DelSession ( w , SessionHalfAuthKey )
2018-05-14 23:27:33 +02:00
DelSession ( w , SessionLastAction )
2018-03-09 23:11:08 +02:00
}
// DelKnownCookie deletes all known cookie variables, which can be used
// to delete remember me pieces.
func DelKnownCookie ( w http . ResponseWriter ) {
DelCookie ( w , CookieRemember )
}
2017-02-25 02:45:47 +02:00
// PutSession puts a value into the session
func PutSession ( w http . ResponseWriter , key , val string ) {
2018-02-15 00:18:03 +02:00
putState ( w , CTXKeySessionState , key , val )
2017-02-25 02:45:47 +02:00
}
// DelSession deletes a key-value from the session.
func DelSession ( w http . ResponseWriter , key string ) {
2018-02-15 00:18:03 +02:00
delState ( w , CTXKeySessionState , key )
2017-02-25 02:45:47 +02:00
}
// GetSession fetches a value from the session
func GetSession ( r * http . Request , key string ) ( string , bool ) {
2018-02-15 00:18:03 +02:00
return getState ( r , CTXKeySessionState , key )
2017-02-25 02:45:47 +02:00
}
// PutCookie puts a value into the session
func PutCookie ( w http . ResponseWriter , key , val string ) {
2018-02-15 00:18:03 +02:00
putState ( w , CTXKeyCookieState , key , val )
2017-02-25 02:45:47 +02:00
}
// DelCookie deletes a key-value from the session.
func DelCookie ( w http . ResponseWriter , key string ) {
2018-02-15 00:18:03 +02:00
delState ( w , CTXKeyCookieState , key )
2017-02-25 02:45:47 +02:00
}
// GetCookie fetches a value from the session
func GetCookie ( r * http . Request , key string ) ( string , bool ) {
2018-02-15 00:18:03 +02:00
return getState ( r , CTXKeyCookieState , key )
2017-02-25 02:45:47 +02:00
}
2018-02-15 00:18:03 +02:00
func putState ( w http . ResponseWriter , CTXKey contextKey , key , val string ) {
setState ( w , CTXKey , ClientStateEventPut , key , val )
2017-02-25 02:45:47 +02:00
}
2018-02-15 00:18:03 +02:00
func delState ( w http . ResponseWriter , CTXKey contextKey , key string ) {
setState ( w , CTXKey , ClientStateEventDel , key , "" )
2017-02-25 02:45:47 +02:00
}
2018-03-08 02:21:37 +02:00
func setState ( w http . ResponseWriter , ctxKey contextKey , op ClientStateEventKind , key , val string ) {
2018-01-29 21:35:47 +02:00
csrw := MustClientStateResponseWriter ( w )
2017-02-25 02:45:47 +02:00
ev := ClientStateEvent {
Kind : op ,
Key : key ,
}
if op == ClientStateEventPut {
ev . Value = val
}
2018-03-08 02:21:37 +02:00
switch ctxKey {
2018-02-15 00:18:03 +02:00
case CTXKeySessionState :
2017-02-25 02:45:47 +02:00
csrw . sessionStateEvents = append ( csrw . sessionStateEvents , ev )
2018-02-15 00:18:03 +02:00
case CTXKeyCookieState :
2017-02-25 02:45:47 +02:00
csrw . cookieStateEvents = append ( csrw . cookieStateEvents , ev )
}
}
2018-03-08 02:21:37 +02:00
func getState ( r * http . Request , ctxKey contextKey , key string ) ( string , bool ) {
val := r . Context ( ) . Value ( ctxKey )
2017-02-25 02:45:47 +02:00
if val == nil {
return "" , false
}
state := val . ( ClientState )
return state . Get ( key )
}
// FlashSuccess returns FlashSuccessKey from the session and removes it.
func FlashSuccess ( w http . ResponseWriter , r * http . Request ) string {
str , ok := GetSession ( r , FlashSuccessKey )
if ! ok {
return ""
}
DelSession ( w , FlashSuccessKey )
return str
}
// FlashError returns FlashError from the session and removes it.
func FlashError ( w http . ResponseWriter , r * http . Request ) string {
str , ok := GetSession ( r , FlashErrorKey )
if ! ok {
return ""
}
DelSession ( w , FlashErrorKey )
return str
}