Skip to content

Commit

Permalink
actually implement session revocation (WIP)
Browse files Browse the repository at this point in the history
  • Loading branch information
equinox0815 committed Nov 18, 2023
1 parent b977833 commit fa36c98
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 94 deletions.
52 changes: 43 additions & 9 deletions cmd/whawty-nginx-sso/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,25 +46,29 @@ import (
"gitlab.com/go-box/pongo2gin/v6"
)

type WebError struct {
Error error `json:"error"`
}

type HandlerContext struct {
conf *WebConfig
cookies *cookie.Store
auth auth.Backend
}

func (h *HandlerContext) verifyCookie(c *gin.Context) (*cookie.Session, error) {
func (h *HandlerContext) verifyCookie(c *gin.Context) (string, *cookie.Session, error) {
cookie, err := c.Cookie(h.cookies.Options().Name)
if err != nil {
return nil, err
return "", nil, err
}
if cookie == "" {
return nil, errors.New("no cookie found")
return "", nil, errors.New("no cookie found")
}
session, err := h.cookies.Verify(cookie)
id, session, err := h.cookies.Verify(cookie)
if err != nil {
return nil, err
return "", nil, err
}
return &session, nil
return id, &session, nil
}

func (h *HandlerContext) getBasePath(c *gin.Context) string {
Expand All @@ -79,7 +83,7 @@ func (h *HandlerContext) getBasePath(c *gin.Context) string {
}

func (h *HandlerContext) handleAuth(c *gin.Context) {
session, err := h.verifyCookie(c)
_, session, err := h.verifyCookie(c)
if err != nil {
c.Data(http.StatusUnauthorized, "text/plain", []byte(err.Error()))
return
Expand All @@ -92,8 +96,8 @@ func (h *HandlerContext) handleLoginGet(c *gin.Context) {
login := h.conf.Login
login.BasePath = h.getBasePath(c)

session, err := h.verifyCookie(c)
if err == nil && session != nil {
_, session, err := h.verifyCookie(c)
if err == nil {
// TODO: follow redir?
c.HTML(http.StatusOK, "logged-in.htmpl", pongo2.Context{
"login": login,
Expand Down Expand Up @@ -160,6 +164,13 @@ func (h *HandlerContext) handleLoginPost(c *gin.Context) {
}

func (h *HandlerContext) handleLogout(c *gin.Context) {
id, _, err := h.verifyCookie(c)
if err == nil {
if err = h.cookies.Revoke(id); err != nil {
// TODO: render error page!
c.AbortWithError(http.StatusInternalServerError, err)
}
}
opts := h.cookies.Options()
c.SetCookie(opts.Name, "invalid", -1, "/", opts.Domain, opts.Secure, true)
redirect, _ := c.GetQuery("redir")
Expand All @@ -169,6 +180,27 @@ func (h *HandlerContext) handleLogout(c *gin.Context) {
c.Redirect(http.StatusSeeOther, redirect)
}

func (h *HandlerContext) handleSessions(c *gin.Context) {
_, session, err := h.verifyCookie(c)
if err != nil {
c.JSON(http.StatusUnauthorized, WebError{err})
}
sessions, err := h.cookies.ListUser(session.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, WebError{err})
}
c.JSON(http.StatusOK, sessions)
}

func (h *HandlerContext) handleRevocations(c *gin.Context) {
// TODO: add authentication based on bearer tokens!
revocations, err := h.cookies.ListRevoked()
if err != nil {
c.JSON(http.StatusInternalServerError, WebError{err})
}
c.JSON(http.StatusOK, revocations)
}

func runWeb(config *WebConfig, cookies *cookie.Store, auth auth.Backend) (err error) {
if config.Listen == "" {
config.Listen = ":http"
Expand Down Expand Up @@ -203,6 +235,8 @@ func runWeb(config *WebConfig, cookies *cookie.Store, auth auth.Backend) (err er
r.GET("/login", h.handleLoginGet)
r.POST("/login", h.handleLoginPost)
r.GET("/logout", h.handleLogout)
r.GET("/sessions", h.handleSessions)
r.GET("/revocations", h.handleRevocations)

listener, err := net.Listen("tcp", config.Listen)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion contrib/sample-cfg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ cookie:
# ## generate with `openssl pkey -in ./contrib/bar_ed25519_priv.pem -pubout -out ./contrib/bar_ed25519_pub.pem`
# public-key-file: ./contrib/bar_ed25519_pub.pem
backend:
memory: {}
in-memory: {}

auth:
static:
Expand Down
28 changes: 14 additions & 14 deletions cookie/backend_memory.go → cookie/backend_in-memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,31 +37,31 @@ import (
"github.com/oklog/ulid/v2"
)

type MemoryBackendConfig struct {
type InMemoryBackendConfig struct {
}

type MemorySessionList map[ulid.ULID]Session
type InMemorySessionList map[ulid.ULID]Session

type MemoryBackend struct {
type InMemoryBackend struct {
mutex sync.RWMutex
sessions map[string]MemorySessionList
sessions map[string]InMemorySessionList
revoked map[ulid.ULID]bool
}

func NewMemoryBackend(conf *MemoryBackendConfig) (*MemoryBackend, error) {
m := &MemoryBackend{}
m.sessions = make(map[string]MemorySessionList)
func NewInMemoryBackend(conf *InMemoryBackendConfig) (*InMemoryBackend, error) {
m := &InMemoryBackend{}
m.sessions = make(map[string]InMemorySessionList)
m.revoked = make(map[ulid.ULID]bool)
return m, nil
}

func (b *MemoryBackend) Save(username string, id ulid.ULID, session Session) error {
func (b *InMemoryBackend) Save(username string, id ulid.ULID, session Session) error {
b.mutex.Lock()
defer b.mutex.Unlock()

sessions, exists := b.sessions[username]
if !exists {
sessions = make(MemorySessionList)
sessions = make(InMemorySessionList)
b.sessions[username] = sessions
}
if _, exists = sessions[id]; exists {
Expand All @@ -72,7 +72,7 @@ func (b *MemoryBackend) Save(username string, id ulid.ULID, session Session) err
return nil
}

func (b *MemoryBackend) ListUser(username string) (list StoredSessionList, err error) {
func (b *InMemoryBackend) ListUser(username string) (list StoredSessionList, err error) {
b.mutex.RLock()
defer b.mutex.RUnlock()

Expand All @@ -86,23 +86,23 @@ func (b *MemoryBackend) ListUser(username string) (list StoredSessionList, err e
return
}

func (b *MemoryBackend) Revoke(id ulid.ULID) error {
func (b *InMemoryBackend) Revoke(id ulid.ULID) error {
b.mutex.Lock()
defer b.mutex.Unlock()

b.revoked[id] = true
return nil
}

func (b *MemoryBackend) IsRevoked(id ulid.ULID) (bool, error) {
func (b *InMemoryBackend) IsRevoked(id ulid.ULID) (bool, error) {
b.mutex.RLock()
defer b.mutex.RUnlock()

_, exists := b.revoked[id]
return exists, nil
}

func (b *MemoryBackend) ListRevoked() (list RevocationList, err error) {
func (b *InMemoryBackend) ListRevoked() (list RevocationList, err error) {
b.mutex.RLock()
defer b.mutex.RUnlock()

Expand All @@ -112,7 +112,7 @@ func (b *MemoryBackend) ListRevoked() (list RevocationList, err error) {
return
}

func (b *MemoryBackend) CollectGarbage() error {
func (b *InMemoryBackend) CollectGarbage() error {
b.mutex.RLock()
defer b.mutex.RUnlock()

Expand Down
107 changes: 73 additions & 34 deletions cookie/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type SignerVerifierConfig struct {
}

type StoreBackendConfig struct {
Memory *MemoryBackendConfig `yaml:"memory"`
InMemory *InMemoryBackendConfig `yaml:"in-memory"`
}

type Config struct {
Expand Down Expand Up @@ -131,23 +131,23 @@ func NewStore(conf *Config, infoLog, dbgLog *log.Logger) (*Store, error) {
conf.Expire = DefaultExpire
}

ctrl := &Store{conf: conf, infoLog: infoLog, dbgLog: dbgLog}
if err := ctrl.initKeys(conf); err != nil {
ctrl.infoLog.Printf("cookie-store: failed to initialize keys: %v", err)
st := &Store{conf: conf, infoLog: infoLog, dbgLog: dbgLog}
if err := st.initKeys(conf); err != nil {
st.infoLog.Printf("cookie-store: failed to initialize keys: %v", err)
return nil, err
}
if err := ctrl.initBackend(conf); err != nil {
ctrl.infoLog.Printf("cookie-store: failed to initialize backend: %v", err)
if err := st.initBackend(conf); err != nil {
st.infoLog.Printf("cookie-store: failed to initialize backend: %v", err)
return nil, err
}
ctrl.infoLog.Printf("cookie-store: successfully initialized (%d keys loaded)", len(ctrl.keys))
if ctrl.signer == nil {
ctrl.infoLog.Printf("cookie-store: no signing key has been loaded - this instance can only verify cookies")
st.infoLog.Printf("cookie-store: successfully initialized (%d keys loaded)", len(st.keys))
if st.signer == nil {
st.infoLog.Printf("cookie-store: no signing key has been loaded - this instance can only verify cookies")
}
return ctrl, nil
return st, nil
}

func (c *Store) initKeys(conf *Config) (err error) {
func (st *Store) initKeys(conf *Config) (err error) {
for _, key := range conf.Keys {
var s SignerVerifier
if key.Ed25519 != nil {
Expand All @@ -160,65 +160,68 @@ func (c *Store) initKeys(conf *Config) (err error) {
return fmt.Errorf("failed to load key '%s': no valid type-specific config found", key.Name)
}

c.keys = append(c.keys, s)
st.keys = append(st.keys, s)
mode := "(verify-only)"
if s.CanSign() && c.signer == nil {
c.signer = s
if s.CanSign() && st.signer == nil {
st.signer = s
mode = "(*sign* and verify)"
}
c.dbgLog.Printf("cookie-store: loaded %s key '%s' %s", s.Algo(), key.Name, mode)
st.dbgLog.Printf("cookie-store: loaded %s key '%s' %s", s.Algo(), key.Name, mode)
}
if len(c.keys) < 1 {
if len(st.keys) < 1 {
return fmt.Errorf("at least one key must be configured")
}
return
}

func (c *Store) initBackend(conf *Config) (err error) {
if conf.Backend.Memory != nil {
c.backend, err = NewMemoryBackend(conf.Backend.Memory)
func (st *Store) initBackend(conf *Config) (err error) {
if conf.Backend.InMemory != nil {
st.backend, err = NewInMemoryBackend(conf.Backend.InMemory)
return
}
// TODO: add garbage collector!!
err = fmt.Errorf("no valid backend configuration found")
return
}

func (c *Store) Options() (opts Options) {
opts.fromConfig(c.conf)
func (st *Store) Options() (opts Options) {
opts.fromConfig(st.conf)
return
}

func (c *Store) New(s Session) (value string, opts Options, err error) {
if c.signer == nil {
func (st *Store) New(s Session) (value string, opts Options, err error) {
if st.signer == nil {
err = fmt.Errorf("no signing key loaded")
return
}

s.SetExpiry(c.conf.Expire)
s.SetExpiry(st.conf.Expire)
id := ulid.Make()
var v *Value
if v, err = MakeValue(id, s); err != nil {
return
}
if v.signature, err = c.signer.Sign(v.payload); err != nil {
if v.signature, err = st.signer.Sign(v.payload); err != nil {
return
}

// TODO: store session
c.dbgLog.Printf("successfully generated new session('%v'): %+v", id, s)
if err = st.backend.Save(s.Username, id, s); err != nil {
return
}
st.dbgLog.Printf("successfully generated new session('%v'): %+v", id, s)

opts.fromConfig(c.conf)
opts.fromConfig(st.conf)
value = v.String()
return
}

func (c *Store) Verify(value string) (s Session, err error) {
func (st *Store) Verify(value string) (id string, s Session, err error) {
var v Value
if err = v.FromString(value); err != nil {
return
}

for _, key := range c.keys {
for _, key := range st.keys {
if err = key.Verify(v.payload, v.signature); err == nil {
break
}
Expand All @@ -228,13 +231,20 @@ func (c *Store) Verify(value string) (s Session, err error) {
return
}

var id ulid.ULID
if id, err = v.ID(); err != nil {
var _id ulid.ULID
if _id, err = v.ID(); err != nil {
err = fmt.Errorf("unable to decode cookie: %v", err)
return
}
id = _id.String()

// TODO: check if id is revoked
var revoked bool
if revoked, err = st.backend.IsRevoked(_id); err != nil {
err = fmt.Errorf("failed to check for cookie revocation: %v", err)
}
if revoked {
err = fmt.Errorf("cookie is revoked")
}

if s, err = v.Session(); err != nil {
err = fmt.Errorf("unable to decode cookie: %v", err)
Expand All @@ -245,6 +255,35 @@ func (c *Store) Verify(value string) (s Session, err error) {
return
}

c.dbgLog.Printf("successfully verified session('%v'): %+v", id, s)
st.dbgLog.Printf("successfully verified session('%v'): %+v", id, s)
return
}

func (st *Store) ListUser(username string) (StoredSessionList, error) {
return st.backend.ListUser(username)
}

func (st *Store) Revoke(id string) error {
toRevoke, err := ulid.ParseStrict(id)
if err != nil {
return err
}
return st.backend.Revoke(toRevoke)
}

func (st *Store) ListRevoked() (result SignedRevocationList, err error) {
var revoked RevocationList
if revoked, err = st.backend.ListRevoked(); err != nil {
return
}

if result.Revoked, err = json.Marshal(revoked); err != nil {
return
}
if st.signer != nil {
if result.Signature, err = st.signer.Sign(result.Revoked); err != nil {
return
}
}
return
}
Loading

0 comments on commit fa36c98

Please sign in to comment.