WIP: providers and user storage
This commit is contained in:
parent
0358eeac53
commit
90825fa01b
7 changed files with 179 additions and 57 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
39
pkg/auth/provider/provider.go
Normal file
39
pkg/auth/provider/provider.go
Normal 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"`
|
||||
}
|
||||
|
||||
|
|
@ -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{
|
||||
|
|
51
pkg/auth/store.go
Normal file
51
pkg/auth/store.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue