From a65c6bc3949aa1c500a9834922a34be0907c07bb Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Fri, 11 Nov 2022 18:02:52 -0500 Subject: [PATCH] WIP: binding issues --- Makefile | 5 ++ pkg/auth/authenticator.go | 40 +++++++++++---- pkg/auth/flow.go | 8 +-- pkg/auth/provider.go | 20 ++++---- pkg/auth/session.go | 104 ++++++++++++++++++++++++++++++++++---- pkg/frontend/frontend.go | 1 + 6 files changed, 144 insertions(+), 34 deletions(-) diff --git a/Makefile b/Makefile index 935ae05..9842ec9 100644 --- a/Makefile +++ b/Makefile @@ -4,11 +4,16 @@ all: build build: go build -o blas ./cmd/blas/ +serve: + go run ./cmd/blas/ serve ${BLAS_ARGS} + # pkg/frontend/frontend/hass_frontend: frontend: ${FE}/script/setup ${FE}/script/build_frontend +todo: + rg -g '!Makefile' -g '!pkg/frontend/frontend/**' -A3 -C3 'XXX:|TODO:' . debug: dlv debug ./cmd/blas/ ${ARGS} diff --git a/pkg/auth/authenticator.go b/pkg/auth/authenticator.go index c59d1df..fe54cba 100644 --- a/pkg/auth/authenticator.go +++ b/pkg/auth/authenticator.go @@ -1,6 +1,7 @@ package auth import ( + "crypto/rand" "encoding/hex" "errors" "net/http" @@ -24,6 +25,11 @@ type Authenticator struct { providers map[string]AuthProvider } +type AuthError struct { + Error string `json:"error"` + Description string `json:"error_description"` +} + func (a *Authenticator) InstallRoutes(e *echo.Echo) { authG := e.Group("/auth") authG.GET("/authorize", frontend.AliasHandler("authorize.html")) @@ -69,7 +75,11 @@ type AuthProvider interface { // TODO: this should include stepping ProviderType() string ProviderBase() AuthProviderBase FlowSchema() []FlowSchemaItem - ValidateCreds(reqMap map[string]interface{}) bool + ValidateCreds(reqMap map[string]interface{}) (user ProviderUser, success bool) +} + +type ProviderUser interface { + ProviderUsername() string } type AuthProviderBase struct { @@ -94,31 +104,32 @@ func (a *Authenticator) ProvidersHandler(c echo.Context) error { return c.JSON(http.StatusOK, providers) } -func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) error { +func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) (ProviderUser, error) { cID, hasCID := rm["client_id"] - if !hasCID || cID != f.request.ClientID { - return ErrInvalidAuth + cIDStr, cidIsStr := cID.(string) + if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.request.ClientID) { + return nil, ErrInvalidAuth } for _, h := range f.Handler { if h == nil { - return ErrInvalidHandler + return nil, ErrInvalidHandler } p := a.Provider(*h) if p == nil { - return ErrInvalidAuth + return nil, ErrInvalidAuth } - success := p.ValidateCreds(rm) + user, success := p.ValidateCreds(rm) if success { - log.Info().Str("user", rm["username"].(string)).Msg("Login success") - return nil + log.Info().Str("user", user.ProviderUsername()).Msg("Login success") + return user, nil } } - return ErrInvalidAuth + return nil, ErrInvalidAuth } func genUUID() string { @@ -127,3 +138,12 @@ func genUUID() string { return hex.EncodeToString(u[:]) } + +func genHex(l int) string { + b := make([]byte, l) + if _, err := rand.Read(b); err != nil { + panic(err) + } + + return hex.EncodeToString(b) +} diff --git a/pkg/auth/flow.go b/pkg/auth/flow.go index f94b1e7..84c7b7a 100644 --- a/pkg/auth/flow.go +++ b/pkg/auth/flow.go @@ -14,7 +14,7 @@ import ( type FlowStore map[FlowID]*Flow type FlowRequest struct { - ClientID string `json:"client_id"` + ClientID ClientID `json:"client_id"` Handler []*string `json:"handler"` RedirectURI string `json:"redirect_uri"` } @@ -147,7 +147,7 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error { } } } - err = a.Check(f, rm) + user, err := a.Check(f, rm) switch err { case nil: var finishedFlow struct { @@ -158,13 +158,13 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error { Type FlowType `json:"type"` Version int `json:"version"` } - // TODO: setup the session. delete the flow. + a.flows.Remove(f) copier.Copy(&finishedFlow, f) finishedFlow.Type = TypeCreateEntry finishedFlow.Title = common.AppName finishedFlow.Version = 1 - finishedFlow.Result = a.NewToken(c.Request(), f) + finishedFlow.Result = a.NewToken(c.Request(), user, f) f.redirect(c) diff --git a/pkg/auth/provider.go b/pkg/auth/provider.go index 895ad6e..696565e 100644 --- a/pkg/auth/provider.go +++ b/pkg/auth/provider.go @@ -13,14 +13,16 @@ const ( HAProviderKey = "auth_provider.homeassistant" ) -type User struct { +type HAUser struct { Password string `json:"password"` Username string `json:"username"` } +func (h *HAUser) ProviderUsername() string { return h.Username } + type HomeAssistantProvider struct { AuthProviderBase `json:"-"` - Users []User `json:"users"` + Users []HAUser `json:"users"` } func NewHAProvider(s storage.Store) (*HomeAssistantProvider, error) { @@ -43,16 +45,16 @@ func (hap *HomeAssistantProvider) hashPass(p string) ([]byte, error) { return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost) } -func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) bool { +func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) (ProviderUser, bool) { usernameE, hasU := rm["username"] passwordE, hasP := rm["password"] username, unStr := usernameE.(string) password, paStr := passwordE.(string) if !hasU || !hasP || !unStr || !paStr || username == "" || password == "" { - return false + return nil, false } - var found *User + var found *HAUser const dummyHash = "$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO" @@ -64,22 +66,22 @@ func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) bool if found == nil { // one more compare to thwart timing attacks bcrypt.CompareHashAndPassword([]byte("foo"), []byte(dummyHash)) - return false + return nil, false } var hash []byte hash, err := base64.StdEncoding.DecodeString(found.Password) if err != nil { log.Error().Err(err).Msg("b64 encode fail") - return false + return nil, false } err = bcrypt.CompareHashAndPassword(hash, []byte(password)) if err == nil { - return true + return found, true } - return false + return nil, false } func (hap *HomeAssistantProvider) FlowSchema() []FlowSchemaItem { diff --git a/pkg/auth/session.go b/pkg/auth/session.go index 799b2ae..79106ac 100644 --- a/pkg/auth/session.go +++ b/pkg/auth/session.go @@ -1,6 +1,7 @@ package auth import ( + "errors" "net/http" "time" @@ -14,11 +15,18 @@ type SessionStore struct { type TokenID string +func (t *TokenID) IsValid() bool { + // TODO: more validation than this + return *t != "" +} + type Token struct { // TODO: jwt bro ID TokenID Ctime time.Time Expires time.Time Addr string + + user ProviderUser `json:"-"` } func (ss *SessionStore) init() { @@ -42,20 +50,53 @@ func (ss *SessionStore) register(t *Token) { ss.s[t.ID] = t } -func (ss *SessionStore) verify(tr *TokenRequest, r *http.Request) bool { +func (ss *SessionStore) verify(tr *TokenRequest, r *http.Request) (ProviderUser, bool) { if t, hasToken := ss.s[tr.Code]; hasToken { // TODO: JWT if t.Expires.After(time.Now()) { - return true + return t.user, true + } else { + delete(ss.s, t.ID) } } - return false + return nil, false +} + +type Credential struct { + user ProviderUser +} + +func (ss *SessionStore) verifyAndGetCredential(tr *TokenRequest, r *http.Request) *Credential { + user, success := ss.verify(tr, r) + if !success { + return nil + } + + return &Credential{user: user} +} + +type User struct { + Username string + Active bool +} + +func (u *User) allowedToAuth() error { + if !u.Active { + return errors.New("user disabled") + } + + return nil +} + +func (a *Authenticator) getOrCreateUser(c *Credential) (*User, error) { + panic("not implemented") + return &User{}, nil } const defaultExpiration = 2 * time.Hour -func (a *Authenticator) NewToken(r *http.Request, f *Flow) TokenID { +func (a *Authenticator) NewToken(r *http.Request, user ProviderUser, f *Flow) TokenID { id := TokenID(genUUID()) t := &Token{ @@ -63,6 +104,8 @@ func (a *Authenticator) NewToken(r *http.Request, f *Flow) TokenID { Ctime: time.Now(), Expires: time.Now().Add(defaultExpiration), Addr: r.RemoteAddr, + + user: user, } a.sessions.register(t) @@ -70,22 +113,61 @@ func (a *Authenticator) NewToken(r *http.Request, f *Flow) TokenID { return id } +type GrantType string + +const ( + GTAuthorizationCode GrantType = "authorization_code" + GTRefreshToken GrantType = "refresh_token" +) + +type ClientID string + +func (c *ClientID) IsValid() bool { + // TODO: || !indieauth.VerifyClientID(rq.ClientID)? + return *c != "" +} + type TokenRequest struct { - ClientID string `query:"client_id"` // TODO: validate this? + ClientID ClientID `query:"client_id"` Code TokenID `query:"code"` - GrantType string `query:"grant_type"` + GrantType GrantType `query:"grant_type"` } func (a *Authenticator) TokenHandler(c echo.Context) error { - var rq TokenRequest - err := c.Bind(&rq) + rq := new(TokenRequest) + err := c.Bind(rq) if err != nil { return err } - if a.sessions.verify(&rq, c.Request()) { - // TODO: success - return c.String(http.StatusOK, "token good I guess") + if *rq == (TokenRequest{}) { + panic("it didn't bind") + } + + switch rq.GrantType { + case GTAuthorizationCode: + if !rq.ClientID.IsValid() { + return c.JSON(http.StatusBadRequest, AuthError{Error: "invalid_request", Description: "invalid client ID"}) + } + + if !rq.Code.IsValid() { + return c.JSON(http.StatusBadRequest, AuthError{Error: "invalid_request", Description: "invalid code"}) + } + + if cred := a.sessions.verifyAndGetCredential(rq, c.Request()); cred != nil { + // TODO: success + user, err := a.getOrCreateUser(cred) + if err != nil { + return c.JSON(http.StatusUnauthorized, AuthError{Error: "access_denied", Description: "bad user"}) + } + + if err := user.allowedToAuth(); err != nil { + return c.JSON(http.StatusUnauthorized, AuthError{Error: "access_denied", Description: err.Error()}) + } + return c.String(http.StatusOK, "token good I guess") + } + case GTRefreshToken: + return c.String(http.StatusNotImplemented, "not implemented") } return c.String(http.StatusUnauthorized, "token bad I guess") diff --git a/pkg/frontend/frontend.go b/pkg/frontend/frontend.go index 782bddd..8961164 100644 --- a/pkg/frontend/frontend.go +++ b/pkg/frontend/frontend.go @@ -36,6 +36,7 @@ func AliasHandler(toFile string) echo.HandlerFunc { func init() { var err error + RootFS, err = fs.Sub(root, "frontend/hass_frontend") if err != nil { panic(err)