diff --git a/pkg/auth/authenticator.go b/pkg/auth/authenticator.go index 321b42a..d1e30e5 100644 --- a/pkg/auth/authenticator.go +++ b/pkg/auth/authenticator.go @@ -10,6 +10,8 @@ import ( "github.com/labstack/echo/v4" "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/storage" ) @@ -23,7 +25,7 @@ type Authenticator struct { store AuthStore flows FlowStore sessions SessionStore - providers map[string]AuthProvider + providers map[string]provider.AuthProvider } 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.POST("/:flow_id", a.LoginFlowHandler) loginFlow.DELETE("/:flow_id", a.LoginFlowDeleteHandler) - } func (a *Authenticator) InitAuth(s storage.Store) error { a.flows = make(FlowStore) a.sessions.init() - hap, err := NewHAProvider(s) + hap, err := hass.NewHAProvider(s) if err != nil { return err } // XXX: yuck. use init with a registry or something - a.providers = map[string]AuthProvider{ + a.providers = map[string]provider.AuthProvider{ hap.ProviderType(): hap, } + a.store, err = a.newAuthStore(s) + if err != nil { + return err + } + + + return nil } -func (a *Authenticator) Provider(name string) AuthProvider { +func (a *Authenticator) Provider(name string) provider.AuthProvider { p, ok := a.providers[name] if !ok { return nil @@ -70,42 +78,18 @@ func (a *Authenticator) Provider(name string) AuthProvider { 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" // TODO: make this configurable func (a *Authenticator) ProvidersHandler(c echo.Context) error { - providers := []AuthProviderBase{ + providers := []provider.AuthProviderBase{ a.Provider(HomeAssistant).ProviderBase(), } 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"] cIDStr, cidIsStr := cID.(string) 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) if success { - log.Info().Str("user", user.ProviderUsername()).Msg("Login success") + log.Info().Interface("user", user.ProviderUserData()).Msg("Login success") return user, nil } } diff --git a/pkg/auth/flow.go b/pkg/auth/flow.go index 485d365..af0a83e 100644 --- a/pkg/auth/flow.go +++ b/pkg/auth/flow.go @@ -9,6 +9,7 @@ import ( "github.com/labstack/echo/v4" "dynatron.me/x/blasphem/internal/common" + "dynatron.me/x/blasphem/pkg/auth/provider" ) type FlowStore map[FlowID]*Flow @@ -19,12 +20,6 @@ type FlowRequest struct { RedirectURI string `json:"redirect_uri"` } -type FlowSchemaItem struct { - Type string `json:"type"` - Name string `json:"name"` - Required bool `json:"required"` -} - type FlowType string const ( @@ -44,7 +39,7 @@ type Flow struct { ID FlowID `json:"flow_id"` Handler []*string `json:"handler"` StepID *Step `json:"step_id,omitempty"` - Schema []FlowSchemaItem `json:"data_schema"` + Schema []provider.FlowSchemaItem `json:"data_schema"` Errors interface{} `json:"errors"` DescPlace *string `json:"description_placeholders"` LastStep *string `json:"last_step"` @@ -86,7 +81,7 @@ func (fs FlowStore) Get(id FlowID) *Flow { } func (a *Authenticator) NewFlow(r *FlowRequest) *Flow { - var sch []FlowSchemaItem + var sch []provider.FlowSchemaItem for _, h := range r.Handler { if h == nil { diff --git a/pkg/auth/provider.go b/pkg/auth/provider/hass/provider.go similarity index 71% rename from pkg/auth/provider.go rename to pkg/auth/provider/hass/provider.go index 696565e..b1090c2 100644 --- a/pkg/auth/provider.go +++ b/pkg/auth/provider/hass/provider.go @@ -1,4 +1,4 @@ -package auth +package hass import ( "encoding/base64" @@ -6,6 +6,7 @@ import ( "github.com/rs/zerolog/log" "golang.org/x/crypto/bcrypt" + "dynatron.me/x/blasphem/pkg/auth/provider" "dynatron.me/x/blasphem/pkg/storage" ) @@ -16,18 +17,32 @@ const ( type HAUser struct { Password string `json:"password"` 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 { - AuthProviderBase `json:"-"` + provider.AuthProviderBase `json:"-"` Users []HAUser `json:"users"` } func NewHAProvider(s storage.Store) (*HomeAssistantProvider, error) { hap := &HomeAssistantProvider{ - AuthProviderBase: AuthProviderBase{ + AuthProviderBase: provider.AuthProviderBase{ Name: "Home Assistant Local", Type: HomeAssistant, }, @@ -38,6 +53,10 @@ func NewHAProvider(s storage.Store) (*HomeAssistantProvider, error) { return hap, err } + for i := range hap.Users { + hap.Users[i].AuthProvider = hap + } + return hap, nil } @@ -45,7 +64,7 @@ func (hap *HomeAssistantProvider) hashPass(p string) ([]byte, error) { 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"] passwordE, hasP := rm["password"] username, unStr := usernameE.(string) @@ -84,8 +103,12 @@ func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) (Prov return nil, false } -func (hap *HomeAssistantProvider) FlowSchema() []FlowSchemaItem { - return []FlowSchemaItem{ +func (hap *HomeAssistantProvider) NewCredData() interface{} { + return &UserData{} +} + +func (hap *HomeAssistantProvider) FlowSchema() []provider.FlowSchemaItem { + return []provider.FlowSchemaItem{ { Type: "string", Name: "username", diff --git a/pkg/auth/provider/provider.go b/pkg/auth/provider/provider.go new file mode 100644 index 0000000..6f5b109 --- /dev/null +++ b/pkg/auth/provider/provider.go @@ -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"` +} + + diff --git a/pkg/auth/session.go b/pkg/auth/session.go index 2b6f1bc..5705826 100644 --- a/pkg/auth/session.go +++ b/pkg/auth/session.go @@ -1,10 +1,13 @@ package auth import ( + "encoding/json" "net/http" "time" "github.com/labstack/echo/v4" + + "dynatron.me/x/blasphem/pkg/auth/provider" ) type SessionStore struct { @@ -25,7 +28,7 @@ type Token struct { // TODO: jwt bro Expires time.Time Addr string - user ProviderUser `json:"-"` + user provider.ProviderUser `json:"-"` } func (ss *SessionStore) init() { @@ -49,7 +52,7 @@ func (ss *SessionStore) register(t *Token) { 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 { // TODO: JWT if t.Expires.After(time.Now()) { @@ -63,7 +66,28 @@ func (ss *SessionStore) verify(tr *TokenRequest, r *http.Request) (ProviderUser, } 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 { @@ -77,7 +101,7 @@ func (ss *SessionStore) verifyAndGetCredential(tr *TokenRequest, r *http.Request 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()) t := &Token{ diff --git a/pkg/auth/store.go b/pkg/auth/store.go new file mode 100644 index 0000000..b52f119 --- /dev/null +++ b/pkg/auth/store.go @@ -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 +} diff --git a/pkg/auth/user.go b/pkg/auth/user.go index c1b475d..820c2b9 100644 --- a/pkg/auth/user.go +++ b/pkg/auth/user.go @@ -6,17 +6,23 @@ import ( "github.com/rs/zerolog/log" ) -const ( - AuthKey = "auth" -) +type UserID string +type GroupID string +type CredID string type User struct { - Username string + ID UserID `json:"id"` + GroupIDs []GroupID `json:"group_ids"` + Data interface{} `json:"data,omitempty"` UserMetadata } 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 { @@ -28,7 +34,7 @@ func (u *User) allowedToAuth() 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") return &User{}, nil }