jeff

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 23, 2021 License: BSD-3-Clause Imports: 10 Imported by: 1

README

jeff

Build GoDoc Go Report Card License

A tool for managing login sessions in Go.

Motivation

I was looking for a simple session management wrapper for Go and from what I could tell there exists no simple sesssion library.

This library is requires a stateful backend to enable easy session revocation and simplify the security considerations. See the section on security for more details.

Features

  • Redirect to login
  • Middleware wrapper
  • Easy to clear sessions
  • Small, idiomatic API
  • CSRF Protection
  • Context aware
  • Fast
  • Multiple sessions under one key

Requirements

The module uses msgpack for encoding and requires a recent version of Go to function. It's recommended to have a version no older than 1 year, but there's a hard requirement to have at least Go 1.11+. Tests are only done against the latest stable version of Go.

Usage

There are three primary methods:

Set starts the session, sets the cookie on the given response, and stores the session token.

func (s Server) Login(w http.ResponseWriter, r *http.Request) {
    user = Authenticate(r)
    if user != nil {
        // Key must be unique to one user among all users
        err := s.jeff.Set(r.Context(), w, user.Email)
        // handle error
    }
    // finish login
}

Wrap authenticates every http.Handler it wraps, or redirects if authentication fails. Wrap's signature works with alice. The "Public" wrapper checks for an active session but does not call the redirect handler if there is no active session. It's a way to set the active session on the request without denying access to anonymous users.

    mux.HandleFunc("/login", loginHandler)
    mux.HandleFunc("/products", j.Public(productHandler))
    mux.HandleFunc("/users", j.Wrap(usersHandler))
    http.ListenAndServe(":8080", mux)

Clear deletes the active session from the store for the given key.

func (s Server) Revoke(w http.ResponseWriter, r *http.Request) {
    // stuff to get user: admin input form or perhaps even from current session
    err = s.jeff.Clear(r.Context(), user.Email)
    // handle err
}

The default redirect handler redirects to root. Override this behavior to set your own login route.

    sessions := jeff.New(store, jeff.Redirect(
        http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            http.Redirect(w, r, "/login", http.StatusFound)
        })))

This is primarily helpful to run custom logic on redirect:

    // customHandler gets called when authentication fails
    sessions := jeff.New(store, jeff.Redirect(customHandler))

Design

Session tokens are securely generated on Set (called after successful login). This library is unique in that the user gets to decide the session key. This is to make it easier for operators to manage sessions by not having to track/store session tokens after creating a session. Session keys don't have to be cryptographically secure, just unique per user. A good key that works for most people is the user's email.

The cookie format is as follows:

CookieName=SessionKey::SessionToken

The SessionKey is used to find the given session in the backend. If found, the client SessionToken is then constant-time compared with the stored token.

Sessions are stored in the backend as a map from the application-chosen session key to a list of active sessions. Sessions are lazily cleaned up once they expire.

Security

Most of the existing solutions use encrypted cookies for authentication. This enables you to have stateless sessions. However, this strategy has two major drawbacks:

  • Single ultra-secret key.
  • Hard to revoke sessions.

It's possible to alleviate these concerns, but in the process one will end up making a stateful framework for revocation, and a complicated key management strategy for de-risking the single point of failure key.

Why aren't we encrypting the cookie?

Encrypting the cookie implies the single secret key used to encrypt said cookie. Programs like chamber can aid in handling these secrets, but any developer can tell you that accidentally logging environment variables is commonplace. I'd rather reduce the secrets required for my service to a minimum.

CSRF Protection

This library also provides limited CSRF protection via the SameSite session cookie attribute. This attribute (implemented in modern browsers) limits a Cross Origin Request to a subset of safe HTTP methods. See the OWASP Guide for more details.

Development

Clone the repo, run docker-compose up -d, then run make test.

With the local redis instance running, you can then run the example application: go run ./cmd/example/main.go.

Limitations

Also excluded from this library are flash sessions. While useful, this is not a concern for this library. If you need this feature, please see one of the libraries below.

Race Conditions

There is a race condition inherant in how this library handles expiration and deletion of sessions. Because sessions are stored as a list for each user, to add, delete, or prune sessions, it's required to do a read, modify, write without any kind of transaction. That means that it's possible, for example, for a new session to be wiped out if it's created between reading and writing in another concurrent read-modify-write operation, or for a session which was meant to be cleared, didn't get cleared because the clear was issued during another processes' modify step in the read-modify-write cycle.

In practice, this should be quite rare but for people considering this for short-lived sessions with high numbers of concurrent sessions per user, you might want to reconsider.

Alternatives

The most popular session management tool is in the gorilla toolkit. It uses encrypted cookies by default. Has a very large API.

https://github.com/gorilla/sessions

A comprehensive session management tool. Also a very large API. Heavy use of naked interfaces.

https://github.com/kataras/go-sessions

Encrypted cookie manager by default. Has middleware feature. Big API. No easy way to clear session without storing session token elsewhere.

https://github.com/alexedwards/scs

Lightweight, server-only API. Uncertain about what the purpose of the Manager interface is. Heavy use of naked interface.

https://github.com/icza/session

Lightweight, server-only API. Includes concept of Users in library. No wrapping or middleware.

https://github.com/rivo/sessions

Batteries-included middleware for keeping track of users, login states and permissions. Very large API.

https://github.com/xyproto/permissions2

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CookieName

func CookieName(n string) func(*Jeff)

CookieName sets the name of the cookie in the browser. If you want to avoid fingerprinting, override it here. defaults to "_gosession"

func Domain

func Domain(d string) func(*Jeff)

Domain sets the domain the cookie belongs to. If unset, cookie becomes a host-only domain, meaning subdomains won't receive the cookie.

func Expires

func Expires(dur time.Duration) func(*Jeff)

Expires sets the cookie lifetime. After logging in, the session will last as long as defined here and then expire. If set to 0, then Expiration is not set and the cookie will expire when the client closes their user agent. Defaults to 30 days.

func Insecure

func Insecure(j *Jeff)

Insecure unsets the Secure flag for the cookie. This is for development only. Doing this in production is an error.

func Path

func Path(p string) func(*Jeff)

Path sets the path attribute of the cookie. Defaults to '/'. You probably don't need to change this. See the RFC for details: https://tools.ietf.org/html/rfc6265

func Redirect

func Redirect(h http.Handler) func(*Jeff)

Redirect sets the handler which gets called when authentication fails. By default, this redirects to '/'. It's recommended that you replace this with your own.

sessions := jeff.New(store, jeff.Redirect(
    http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        http.Redirect(w, r, "/login", http.StatusFound)
    })))

Setting this is particularly useful if you want to stop a redirect on an authenticated route to render a page despite the user not being authenticated. For example, say you want to display user information on the home page if they're logged in, but otherwise want to ignore the redirect and render the page for an anonymous user. You'd define that behavior using a custom handler here.

func Samesite added in v0.2.0

func Samesite(s http.SameSite) func(*Jeff)

SameSite sets the SameSite attribute for the cookie. If unset, the default behavior is to inherit the default behavior of the http package. See the docs for details. https://pkg.go.dev/net/http?tab=doc#SameSite

Types

type Jeff

type Jeff struct {
	// contains filtered or unexported fields
}

Jeff holds the metadata needed to handle session management.

func New

func New(s Storage, opts ...func(*Jeff)) *Jeff

New instantiates a Jeff, applying the options provided.

func (*Jeff) Clear

func (j *Jeff) Clear(ctx context.Context, w http.ResponseWriter) error

Clear the session in the context for the given key.

func (*Jeff) Delete

func (j *Jeff) Delete(ctx context.Context, key []byte, tokens ...[]byte) error

Delete the session for the given key.

func (*Jeff) Public

func (j *Jeff) Public(wrap http.Handler) http.Handler

Public wraps the given handler, adding the Session object (if there's an active session) to the request context before passing control to the next handler.

func (*Jeff) PublicFunc

func (j *Jeff) PublicFunc(wrap http.HandlerFunc) http.HandlerFunc

PublicFunc wraps the given handler, adding the Session object (if there's an active session) to the request context before passing control to the next handler.

func (*Jeff) SessionsForKey added in v0.2.0

func (j *Jeff) SessionsForKey(ctx context.Context, key []byte) (SessionList, error)

SessionsForKey returns the list of active sessions that exist in the backend for the given key. The result may have stale (expired) sessions.

func (*Jeff) Set

func (j *Jeff) Set(ctx context.Context, w http.ResponseWriter, key []byte, meta ...[]byte) error

Set the session cookie on the response. Call after successful authentication / login. meta optional parameter sets metadata in the session storage.

func (*Jeff) Wrap

func (j *Jeff) Wrap(wrap http.Handler) http.Handler

Wrap wraps the given handler, authenticating this route and calling the redirect handler if session is invalid.

func (*Jeff) WrapFunc

func (j *Jeff) WrapFunc(wrap http.HandlerFunc) http.HandlerFunc

WrapFunc wraps the given handler, authenticating this route and calling the redirect handler if session is invalid.

type Session

type Session struct {
	Key   []byte    `msg:"key"`
	Token []byte    `msg:"token"`
	Meta  []byte    `msg:"meta"`
	Exp   time.Time `msg:"exp"`
}

Session represents the Session as it's stored in serialized form. It's the object that gets returned to the caller when checking a session.

func ActiveSession

func ActiveSession(ctx context.Context) Session

ActiveSession returns the currently active session on the context. If there is no active session on the context, it returns an empty session object.

func (*Session) DecodeMsg

func (z *Session) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Session) EncodeMsg

func (z *Session) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Session) MarshalMsg

func (z *Session) MarshalMsg(b []byte) (o []byte, err error)

MarshalMsg implements msgp.Marshaler

func (*Session) Msgsize

func (z *Session) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

func (*Session) UnmarshalMsg

func (z *Session) UnmarshalMsg(bts []byte) (o []byte, err error)

UnmarshalMsg implements msgp.Unmarshaler

type SessionList

type SessionList []Session

SessionList is a list of active sessions for a given key

func (*SessionList) DecodeMsg

func (z *SessionList) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (SessionList) EncodeMsg

func (z SessionList) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (SessionList) MarshalMsg

func (z SessionList) MarshalMsg(b []byte) (o []byte, err error)

MarshalMsg implements msgp.Marshaler

func (SessionList) Msgsize

func (z SessionList) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

func (*SessionList) UnmarshalMsg

func (z *SessionList) UnmarshalMsg(bts []byte) (o []byte, err error)

UnmarshalMsg implements msgp.Unmarshaler

type Storage

type Storage interface {
	// Store persists the session in the backend with the given expiration
	// Implementation must return value exactly as it is received.
	// Value will be given as...
	Store(ctx context.Context, key, value []byte, exp time.Time) error
	// Fetch retrieves the session from the backend.  If err != nil or
	// value == nil, then it's assumed that the session is invalid and Jeff
	// will redirect.  Expired sessions must return nil error and nil value.
	// Unknown (not found) sessions must return nil error and nil value.
	Fetch(ctx context.Context, key []byte) (value []byte, err error)
	// Delete removes the session given by key from the store. Errors are
	// bubbled up to the caller.  Delete should not return an error on expired
	// or missing keys.
	Delete(ctx context.Context, key []byte) error
}

Storage provides the base level abstraction for implementing session storage. Typically this would be memcache, redis or a database.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL