WIP: providers and user storage

This commit is contained in:
Daniel Ponte 2022-11-12 15:56:17 -05:00
parent 0358eeac53
commit 90825fa01b
7 changed files with 179 additions and 57 deletions

View file

@ -10,6 +10,8 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"dynatron.me/x/blasphem/pkg/auth/provider"
"dynatron.me/x/blasphem/pkg/auth/provider/hass"
"dynatron.me/x/blasphem/pkg/frontend" "dynatron.me/x/blasphem/pkg/frontend"
"dynatron.me/x/blasphem/pkg/storage" "dynatron.me/x/blasphem/pkg/storage"
) )
@ -23,7 +25,7 @@ type Authenticator struct {
store AuthStore store AuthStore
flows FlowStore flows FlowStore
sessions SessionStore sessions SessionStore
providers map[string]AuthProvider providers map[string]provider.AuthProvider
} }
type AuthError struct { type AuthError struct {
@ -42,26 +44,32 @@ func (a *Authenticator) InstallRoutes(e *echo.Echo) {
loginFlow := authG.Group("/login_flow") // TODO: add IP address affinity middleware loginFlow := authG.Group("/login_flow") // TODO: add IP address affinity middleware
loginFlow.POST("/:flow_id", a.LoginFlowHandler) loginFlow.POST("/:flow_id", a.LoginFlowHandler)
loginFlow.DELETE("/:flow_id", a.LoginFlowDeleteHandler) loginFlow.DELETE("/:flow_id", a.LoginFlowDeleteHandler)
} }
func (a *Authenticator) InitAuth(s storage.Store) error { func (a *Authenticator) InitAuth(s storage.Store) error {
a.flows = make(FlowStore) a.flows = make(FlowStore)
a.sessions.init() a.sessions.init()
hap, err := NewHAProvider(s) hap, err := hass.NewHAProvider(s)
if err != nil { if err != nil {
return err return err
} }
// XXX: yuck. use init with a registry or something // XXX: yuck. use init with a registry or something
a.providers = map[string]AuthProvider{ a.providers = map[string]provider.AuthProvider{
hap.ProviderType(): hap, hap.ProviderType(): hap,
} }
a.store, err = a.newAuthStore(s)
if err != nil {
return err
}
return nil return nil
} }
func (a *Authenticator) Provider(name string) AuthProvider { func (a *Authenticator) Provider(name string) provider.AuthProvider {
p, ok := a.providers[name] p, ok := a.providers[name]
if !ok { if !ok {
return nil return nil
@ -70,42 +78,18 @@ func (a *Authenticator) Provider(name string) AuthProvider {
return p return p
} }
type AuthProvider interface { // TODO: this should include stepping
ProviderName() string
ProviderID() *string
ProviderType() string
ProviderBase() AuthProviderBase
FlowSchema() []FlowSchemaItem
ValidateCreds(reqMap map[string]interface{}) (user ProviderUser, success bool)
}
type ProviderUser interface {
ProviderUsername() string
}
type AuthProviderBase struct {
Name string `json:"name"`
ID *string `json:"id"`
Type string `json:"type"`
}
func (bp *AuthProviderBase) ProviderName() string { return bp.Name }
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
var HomeAssistant = "homeassistant" var HomeAssistant = "homeassistant"
// TODO: make this configurable // TODO: make this configurable
func (a *Authenticator) ProvidersHandler(c echo.Context) error { func (a *Authenticator) ProvidersHandler(c echo.Context) error {
providers := []AuthProviderBase{ providers := []provider.AuthProviderBase{
a.Provider(HomeAssistant).ProviderBase(), a.Provider(HomeAssistant).ProviderBase(),
} }
return c.JSON(http.StatusOK, providers) return c.JSON(http.StatusOK, providers)
} }
func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) (ProviderUser, error) { func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) (provider.ProviderUser, error) {
cID, hasCID := rm["client_id"] cID, hasCID := rm["client_id"]
cIDStr, cidIsStr := cID.(string) cIDStr, cidIsStr := cID.(string)
if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.request.ClientID) { if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.request.ClientID) {
@ -125,7 +109,7 @@ func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) (ProviderUser,
user, success := p.ValidateCreds(rm) user, success := p.ValidateCreds(rm)
if success { if success {
log.Info().Str("user", user.ProviderUsername()).Msg("Login success") log.Info().Interface("user", user.ProviderUserData()).Msg("Login success")
return user, nil return user, nil
} }
} }

View file

@ -9,6 +9,7 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"dynatron.me/x/blasphem/internal/common" "dynatron.me/x/blasphem/internal/common"
"dynatron.me/x/blasphem/pkg/auth/provider"
) )
type FlowStore map[FlowID]*Flow type FlowStore map[FlowID]*Flow
@ -19,12 +20,6 @@ type FlowRequest struct {
RedirectURI string `json:"redirect_uri"` RedirectURI string `json:"redirect_uri"`
} }
type FlowSchemaItem struct {
Type string `json:"type"`
Name string `json:"name"`
Required bool `json:"required"`
}
type FlowType string type FlowType string
const ( const (
@ -44,7 +39,7 @@ type Flow struct {
ID FlowID `json:"flow_id"` ID FlowID `json:"flow_id"`
Handler []*string `json:"handler"` Handler []*string `json:"handler"`
StepID *Step `json:"step_id,omitempty"` StepID *Step `json:"step_id,omitempty"`
Schema []FlowSchemaItem `json:"data_schema"` Schema []provider.FlowSchemaItem `json:"data_schema"`
Errors interface{} `json:"errors"` Errors interface{} `json:"errors"`
DescPlace *string `json:"description_placeholders"` DescPlace *string `json:"description_placeholders"`
LastStep *string `json:"last_step"` LastStep *string `json:"last_step"`
@ -86,7 +81,7 @@ func (fs FlowStore) Get(id FlowID) *Flow {
} }
func (a *Authenticator) NewFlow(r *FlowRequest) *Flow { func (a *Authenticator) NewFlow(r *FlowRequest) *Flow {
var sch []FlowSchemaItem var sch []provider.FlowSchemaItem
for _, h := range r.Handler { for _, h := range r.Handler {
if h == nil { if h == nil {

View file

@ -1,4 +1,4 @@
package auth package hass
import ( import (
"encoding/base64" "encoding/base64"
@ -6,6 +6,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"dynatron.me/x/blasphem/pkg/auth/provider"
"dynatron.me/x/blasphem/pkg/storage" "dynatron.me/x/blasphem/pkg/storage"
) )
@ -16,18 +17,32 @@ const (
type HAUser struct { type HAUser struct {
Password string `json:"password"` Password string `json:"password"`
Username string `json:"username"` Username string `json:"username"`
provider.AuthProvider `json:"-"`
} }
func (h *HAUser) ProviderUsername() string { return h.Username } func (hau *HAUser) UserData() interface{} {
return UserData{
Username: hau.Username,
}
}
type UserData struct {
Username string `json:"username"`
}
const HomeAssistant = "homeassistant"
func (h *HAUser) ProviderUserData() interface{} { return h.UserData() }
type HomeAssistantProvider struct { type HomeAssistantProvider struct {
AuthProviderBase `json:"-"` provider.AuthProviderBase `json:"-"`
Users []HAUser `json:"users"` Users []HAUser `json:"users"`
} }
func NewHAProvider(s storage.Store) (*HomeAssistantProvider, error) { func NewHAProvider(s storage.Store) (*HomeAssistantProvider, error) {
hap := &HomeAssistantProvider{ hap := &HomeAssistantProvider{
AuthProviderBase: AuthProviderBase{ AuthProviderBase: provider.AuthProviderBase{
Name: "Home Assistant Local", Name: "Home Assistant Local",
Type: HomeAssistant, Type: HomeAssistant,
}, },
@ -38,6 +53,10 @@ func NewHAProvider(s storage.Store) (*HomeAssistantProvider, error) {
return hap, err return hap, err
} }
for i := range hap.Users {
hap.Users[i].AuthProvider = hap
}
return hap, nil return hap, nil
} }
@ -45,7 +64,7 @@ func (hap *HomeAssistantProvider) hashPass(p string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost) return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)
} }
func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) (ProviderUser, bool) { func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) (provider.ProviderUser, bool) {
usernameE, hasU := rm["username"] usernameE, hasU := rm["username"]
passwordE, hasP := rm["password"] passwordE, hasP := rm["password"]
username, unStr := usernameE.(string) username, unStr := usernameE.(string)
@ -84,8 +103,12 @@ func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) (Prov
return nil, false return nil, false
} }
func (hap *HomeAssistantProvider) FlowSchema() []FlowSchemaItem { func (hap *HomeAssistantProvider) NewCredData() interface{} {
return []FlowSchemaItem{ return &UserData{}
}
func (hap *HomeAssistantProvider) FlowSchema() []provider.FlowSchemaItem {
return []provider.FlowSchemaItem{
{ {
Type: "string", Type: "string",
Name: "username", Name: "username",

View file

@ -0,0 +1,39 @@
package provider
type AuthProvider interface { // TODO: this should include stepping
AuthProviderMetadata
ProviderBase() AuthProviderBase
FlowSchema() []FlowSchemaItem
NewCredData() interface{}
ValidateCreds(reqMap map[string]interface{}) (user ProviderUser, success bool)
}
type ProviderUser interface {
AuthProviderMetadata
ProviderUserData() interface{}
}
type AuthProviderBase struct {
Name string `json:"name"`
ID *string `json:"id"`
Type string `json:"type"`
}
type AuthProviderMetadata interface {
ProviderName() string
ProviderID() *string
ProviderType() string
}
func (bp *AuthProviderBase) ProviderName() string { return bp.Name }
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
type FlowSchemaItem struct {
Type string `json:"type"`
Name string `json:"name"`
Required bool `json:"required"`
}

View file

@ -1,10 +1,13 @@
package auth package auth
import ( import (
"encoding/json"
"net/http" "net/http"
"time" "time"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"dynatron.me/x/blasphem/pkg/auth/provider"
) )
type SessionStore struct { type SessionStore struct {
@ -25,7 +28,7 @@ type Token struct { // TODO: jwt bro
Expires time.Time Expires time.Time
Addr string Addr string
user ProviderUser `json:"-"` user provider.ProviderUser `json:"-"`
} }
func (ss *SessionStore) init() { func (ss *SessionStore) init() {
@ -49,7 +52,7 @@ func (ss *SessionStore) register(t *Token) {
ss.s[t.ID] = t ss.s[t.ID] = t
} }
func (ss *SessionStore) verify(tr *TokenRequest, r *http.Request) (ProviderUser, bool) { func (ss *SessionStore) verify(tr *TokenRequest, r *http.Request) (provider.ProviderUser, bool) {
if t, hasToken := ss.s[tr.Code]; hasToken { if t, hasToken := ss.s[tr.Code]; hasToken {
// TODO: JWT // TODO: JWT
if t.Expires.After(time.Now()) { if t.Expires.After(time.Now()) {
@ -63,7 +66,28 @@ func (ss *SessionStore) verify(tr *TokenRequest, r *http.Request) (ProviderUser,
} }
type Credential struct { type Credential struct {
user ProviderUser ID CredID `json:"id"`
UserID UserID `json:"user_id"`
AuthProviderType string `json:"auth_provider_type"`
AuthProviderID *string `json:"auth_provider_id"`
DataRaw json.RawMessage `json:"data,omitempty"`
user provider.ProviderUser
}
func (cred *Credential) MarshalJSON() ([]byte, error) {
rm := map[string]interface{}{
"id": cred.ID,
"user_id": cred.UserID,
"auth_provider_type": cred.user.ProviderType(),
"auth_provider_id": cred.user.ProviderID(),
}
providerData := cred.user.ProviderUserData()
if providerData != nil {
rm["data"] = providerData
}
return json.Marshal(rm)
} }
func (ss *SessionStore) verifyAndGetCredential(tr *TokenRequest, r *http.Request) *Credential { func (ss *SessionStore) verifyAndGetCredential(tr *TokenRequest, r *http.Request) *Credential {
@ -77,7 +101,7 @@ func (ss *SessionStore) verifyAndGetCredential(tr *TokenRequest, r *http.Request
const defaultExpiration = 2 * time.Hour const defaultExpiration = 2 * time.Hour
func (a *Authenticator) NewToken(r *http.Request, user ProviderUser, f *Flow) TokenID { func (a *Authenticator) NewToken(r *http.Request, user provider.ProviderUser, f *Flow) TokenID {
id := TokenID(genUUID()) id := TokenID(genUUID())
t := &Token{ t := &Token{

51
pkg/auth/store.go Normal file
View file

@ -0,0 +1,51 @@
package auth
import (
"fmt"
"encoding/json"
"dynatron.me/x/blasphem/pkg/storage"
)
const (
AuthStoreKey = "auth"
)
type AuthStore interface {
}
type authStore struct {
Users []User `json:"users"`
Groups interface {} `json:"groups"`
Credentials []Credential `json:"credentials"`
userMap map[UserID]*User
}
func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error) {
as = &authStore{}
err = s.Get(AuthStoreKey, as)
as.userMap = make(map[UserID]*User)
for _, u := range as.Users {
as.userMap[u.ID] = &u
}
for _, c := range as.Credentials {
prov := a.Provider(c.AuthProviderType)
if prov == nil {
return nil, fmt.Errorf("no such provider %s", c.AuthProviderType)
}
pd := prov.NewCredData()
err := json.Unmarshal(c.DataRaw, pd)
if err != nil {
return nil, err
}
}
return
}

View file

@ -6,17 +6,23 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
const ( type UserID string
AuthKey = "auth" type GroupID string
) type CredID string
type User struct { type User struct {
Username string ID UserID `json:"id"`
GroupIDs []GroupID `json:"group_ids"`
Data interface{} `json:"data,omitempty"`
UserMetadata UserMetadata
} }
type UserMetadata struct { type UserMetadata struct {
Active bool Active bool `json:"is_active"`
Owner bool `json:"is_owner"`
LocalOnly bool `json:"local_only"`
SystemGenerated bool `json:"system_generated"`
Name string `json:"name"`
} }
func (u *User) allowedToAuth() error { func (u *User) allowedToAuth() error {
@ -28,7 +34,7 @@ func (u *User) allowedToAuth() error {
} }
func (a *Authenticator) getOrCreateUser(c *Credential) (*User, error) { func (a *Authenticator) getOrCreateUser(c *Credential) (*User, error) {
log.Debug().Str("user", c.user.ProviderUsername()).Msg("getOrCreateUser") log.Debug().Interface("userdata", c.user.ProviderUserData()).Msg("getOrCreateUser")
panic("not implemented") panic("not implemented")
return &User{}, nil return &User{}, nil
} }