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/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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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",
|
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
|
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
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"
|
"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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue