diff --git a/internal/common/common.go b/internal/common/common.go index 736879f..a8e2b20 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -15,6 +15,13 @@ type cmdOptions interface { Execute() error } +func AppNamePtr() *string { + s := AppName + return &s +} + +func IntPtr(i int) *int { return &i } + // RunE is a convenience function for use with cobra. func RunE(c cmdOptions) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { diff --git a/internal/common/types.go b/internal/common/types.go index 9ae4e24..2816850 100644 --- a/internal/common/types.go +++ b/internal/common/types.go @@ -11,6 +11,7 @@ import ( type ( // PyTimeStamp is a timestamp that marshals to python-style timestamp strings (long nano). PyTimestamp time.Time + ClientID string ) const PytTimeFormat = "2006-01-02T15:04:05.999999-07:00" diff --git a/pkg/auth/authenticator.go b/pkg/auth/authenticator.go index 1e19f49..f0ac84f 100644 --- a/pkg/auth/authenticator.go +++ b/pkg/auth/authenticator.go @@ -24,7 +24,7 @@ var ( type Authenticator struct { store AuthStore - flows FlowStore + flows *AuthFlowManager sessions AccessSessionStore providers map[string]provider.AuthProvider } @@ -58,7 +58,7 @@ func (a *Authenticator) InitAuth(s storage.Store) error { a.providers[nProv.ProviderType()] = nProv } - a.flows = make(FlowStore) + a.flows = NewAuthFlowManager() a.sessions.init() @@ -91,29 +91,23 @@ func (a *Authenticator) ProvidersHandler(c echo.Context) error { return c.JSON(http.StatusOK, providers) } -func (a *Authenticator) Check(f *Flow, req *http.Request, rm map[string]interface{}) (provider.ProviderUser, error) { +func (a *Authenticator) Check(f *LoginFlow, req *http.Request, 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) { + if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.ClientID) { return nil, ErrInvalidAuth } - for _, h := range f.Handler { - if h == nil { - return nil, ErrInvalidHandler - } + p := a.Provider(f.Handler.String()) + if p == nil { + return nil, ErrInvalidAuth + } - p := a.Provider(*h) - if p == nil { - return nil, ErrInvalidAuth - } + user, success := p.ValidateCreds(req, rm) - user, success := p.ValidateCreds(req, rm) - - if success { - log.Info().Interface("user", user.UserData()).Msg("Login success") - return user, nil - } + if success { + log.Info().Interface("user", user.UserData()).Msg("Login success") + return user, nil } return nil, ErrInvalidAuth diff --git a/pkg/auth/flow.go b/pkg/auth/flow.go index e32c381..1174aa9 100644 --- a/pkg/auth/flow.go +++ b/pkg/auth/flow.go @@ -3,132 +3,67 @@ package auth import ( "net/http" "strings" - "time" "github.com/jinzhu/copier" "github.com/labstack/echo/v4" "dynatron.me/x/blasphem/internal/common" - "dynatron.me/x/blasphem/internal/generate" "dynatron.me/x/blasphem/pkg/auth/provider" + "dynatron.me/x/blasphem/pkg/flow" ) -type FlowStore map[FlowID]*Flow - -type FlowRequest struct { - ClientID ClientID `json:"client_id"` - Handler []*string `json:"handler"` - RedirectURI string `json:"redirect_uri"` +type AuthFlowManager struct { + *flow.FlowManager } -type FlowType string - -const ( - TypeForm FlowType = "form" - TypeCreateEntry FlowType = "create_entry" -) - -type FlowID string -type Step string - -const ( - StepInit Step = "init" -) - -type Flow struct { - Type FlowType `json:"type"` - ID FlowID `json:"flow_id"` - Handler []*string `json:"handler"` - StepID *Step `json:"step_id,omitempty"` - Schema []provider.FlowSchemaItem `json:"data_schema"` - Errors interface{} `json:"errors"` - DescPlace *string `json:"description_placeholders"` - LastStep *string `json:"last_step"` - - request *FlowRequest - ctime time.Time +type LoginFlow struct { + flow.FlowHandlerBase } -func (f *Flow) touch() { - f.ctime = time.Now() +func NewAuthFlowManager() *AuthFlowManager { + return &AuthFlowManager{FlowManager: flow.NewFlowManager()} } -func (fs FlowStore) register(f *Flow) { - fs.cull() - fs[f.ID] = f -} - -func (fs FlowStore) Remove(f *Flow) { - delete(fs, f.ID) -} - -const cullAge = time.Minute * 30 - -func (fs FlowStore) cull() { - for k, v := range fs { - if time.Now().Sub(v.ctime) > cullAge { - delete(fs, k) - } - } -} - -func (fs FlowStore) Get(id FlowID) *Flow { - f, ok := fs[id] - if ok { - return f +func (afm *AuthFlowManager) NewLoginFlow(f *flow.FlowRequest, prov provider.AuthProvider) *LoginFlow { + lf := &LoginFlow{ + FlowHandlerBase: flow.NewFlowHandlerBase(f, prov, prov.ProviderType()), } - return nil + afm.Register(lf) + + return lf } -func (a *Authenticator) NewFlow(r *FlowRequest) *Flow { - var sch []provider.FlowSchemaItem +func (a *Authenticator) NewFlow(r *flow.FlowRequest) *flow.FlowResult { + var prov provider.AuthProvider for _, h := range r.Handler { if h == nil { break } - if hand := a.Provider(*h); hand != nil { - sch = hand.FlowSchema() + prov = a.Provider(h.String()) + if prov != nil { break } } - if sch == nil { + if prov == nil { return nil } - flow := &Flow{ - Type: TypeForm, - ID: FlowID(generate.UUID()), - StepID: stepPtr(StepInit), - Schema: sch, - Handler: r.Handler, - Errors: []string{}, - request: r, - } - flow.touch() + flow := a.flows.NewLoginFlow(r, prov) - a.flows.register(flow) - - return flow + return flow.ShowForm(nil) } -func stepPtr(s Step) *Step { return &s } - -func (f *Flow) redirect(c echo.Context) { - c.Request().Header.Set("Location", f.request.RedirectURI) +func (f *LoginFlow) redirect(c echo.Context) { + c.Request().Header.Set("Location", f.RedirectURI) } -func (f *Flow) progress(a *Authenticator, c echo.Context) error { - if f.StepID == nil { - c.Logger().Error("stepID is nil") - return c.String(http.StatusInternalServerError, "No Step ID") - } - - switch *f.StepID { - case StepInit: +func (f *LoginFlow) progress(a *Authenticator, c echo.Context) error { + switch f.Step() { + case flow.StepInit: rm := make(map[string]interface{}) err := c.Bind(&rm) @@ -136,31 +71,21 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error { return c.String(http.StatusBadRequest, err.Error()) } - for _, si := range f.Schema { - if si.Required { - if _, ok := rm[si.Name]; !ok { - return c.String(http.StatusBadRequest, "missing required param "+si.Name) - } - } + err = f.Schema.CheckRequired(rm) + if err != nil { + return c.JSON(http.StatusBadRequest, f.ShowForm([]string{err.Error()})) } + user, err := a.Check(f, c.Request(), rm) switch err { case nil: - var finishedFlow struct { - ID FlowID `json:"flow_id"` - Handler []*string `json:"handler"` - Result AccessTokenID `json:"result"` - Title string `json:"title"` - Type FlowType `json:"type"` - Version int `json:"version"` - } - + finishedFlow := flow.FlowResult{} a.flows.Remove(f) copier.Copy(&finishedFlow, f) - finishedFlow.Type = TypeCreateEntry - finishedFlow.Title = common.AppName - finishedFlow.Version = 1 - finishedFlow.Result = a.NewAccessToken(c.Request(), user, f) + finishedFlow.Type = flow.TypeCreateEntry + finishedFlow.Title = common.AppNamePtr() + finishedFlow.Version = common.IntPtr(1) + finishedFlow.Result = a.NewAccessToken(c.Request(), user) f.redirect(c) @@ -170,10 +95,10 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error { case ErrInvalidAuth: fallthrough default: - f.Errors = map[string]interface{}{ + fr := f.ShowForm(map[string]interface{}{ "base": "invalid_auth", - } - return c.JSON(http.StatusOK, f) + }) + return c.JSON(http.StatusOK, fr) } default: return c.String(http.StatusBadRequest, "unknown flow step") @@ -181,13 +106,13 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error { } func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error { - flowID := c.Param("flow_id") + flowID := flow.FlowID(c.Param("flow_id")) if flowID == "" { return c.String(http.StatusBadRequest, "empty flow ID") } - delete(a.flows, FlowID(flowID)) + a.flows.Delete(flowID) return c.String(http.StatusOK, "deleted") } @@ -202,7 +127,7 @@ func setJSON(c echo.Context) { func (a *Authenticator) BeginLoginFlowHandler(c echo.Context) error { setJSON(c) - var flowReq FlowRequest + var flowReq flow.FlowRequest err := c.Bind(&flowReq) if err != nil { return c.String(http.StatusBadRequest, err.Error()) @@ -222,16 +147,10 @@ func (a *Authenticator) LoginFlowHandler(c echo.Context) error { flowID := c.Param("flow_id") - flow := a.flows.Get(FlowID(flowID)) + flow := a.flows.Get(flow.FlowID(flowID)) if flow == nil { return c.String(http.StatusNotFound, "no such flow") } - if time.Now().Sub(flow.ctime) > cullAge { - a.flows.Remove(flow) - - return c.String(http.StatusGone, "flow timed out") - } - - return flow.progress(a, c) + return flow.(*LoginFlow).progress(a, c) } diff --git a/pkg/auth/provider/hass/provider.go b/pkg/auth/provider/hass/provider.go index a5cc06e..f692eec 100644 --- a/pkg/auth/provider/hass/provider.go +++ b/pkg/auth/provider/hass/provider.go @@ -9,6 +9,7 @@ import ( "golang.org/x/crypto/bcrypt" "dynatron.me/x/blasphem/pkg/auth/provider" + "dynatron.me/x/blasphem/pkg/flow" "dynatron.me/x/blasphem/pkg/storage" ) @@ -44,7 +45,7 @@ func (h *HAUser) ProviderUserData() interface{} { return h.UserData() } type HomeAssistantProvider struct { provider.AuthProviderBase `json:"-"` Users []HAUser `json:"users"` - userMap map[string]*HAUser + userMap map[string]*HAUser } func NewHAProvider(s storage.Store) (provider.AuthProvider, error) { @@ -126,8 +127,8 @@ func (hap *HomeAssistantProvider) NewCredData() interface{} { return &HAUser{} } -func (hap *HomeAssistantProvider) FlowSchema() []provider.FlowSchemaItem { - return []provider.FlowSchemaItem{ +func (hap *HomeAssistantProvider) FlowSchema() flow.FlowSchema { + return []flow.FlowSchemaItem{ { Type: "string", Name: "username", diff --git a/pkg/auth/provider/provider.go b/pkg/auth/provider/provider.go index a34bed0..2738a6c 100644 --- a/pkg/auth/provider/provider.go +++ b/pkg/auth/provider/provider.go @@ -3,6 +3,7 @@ package provider import ( "net/http" + "dynatron.me/x/blasphem/pkg/flow" "dynatron.me/x/blasphem/pkg/storage" ) @@ -13,7 +14,7 @@ var Providers = make(map[string]Constructor) type AuthProvider interface { // TODO: this should include stepping AuthProviderMetadata ProviderBase() AuthProviderBase - FlowSchema() []FlowSchemaItem + FlowSchema() flow.FlowSchema NewCredData() interface{} ValidateCreds(r *http.Request, reqMap map[string]interface{}) (user ProviderUser, success bool) Lookup(ProviderUser) ProviderUser @@ -24,7 +25,7 @@ func Register(providerName string, f func(storage.Store) (AuthProvider, error)) } type ProviderUser interface { - // TODO: make sure this is sane with all the ProviderUser and UserData type stuf + // TODO: make sure this is sane with all the ProviderUser and UserData type stuff UserData() ProviderUser } @@ -44,9 +45,3 @@ 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/provider/trustednets/trustednets.go b/pkg/auth/provider/trustednets/trustednets.go index 5c3dcb8..68108e4 100644 --- a/pkg/auth/provider/trustednets/trustednets.go +++ b/pkg/auth/provider/trustednets/trustednets.go @@ -6,6 +6,7 @@ import ( "net/http" "dynatron.me/x/blasphem/pkg/auth/provider" + "dynatron.me/x/blasphem/pkg/flow" "dynatron.me/x/blasphem/pkg/storage" ) @@ -61,8 +62,8 @@ func (hap *TrustedNetworksProvider) NewCredData() interface{} { return &UserData{} } -func (hap *TrustedNetworksProvider) FlowSchema() []provider.FlowSchemaItem { - return []provider.FlowSchemaItem{ +func (hap *TrustedNetworksProvider) FlowSchema() flow.FlowSchema { + return []flow.FlowSchemaItem{ { Type: "string", Name: "username", diff --git a/pkg/auth/session.go b/pkg/auth/session.go index 7c2fb73..a5e9f12 100644 --- a/pkg/auth/session.go +++ b/pkg/auth/session.go @@ -131,7 +131,7 @@ func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest, r *http.Request const defaultExpiration = 15 * time.Minute -func (a *Authenticator) NewAccessToken(r *http.Request, user provider.ProviderUser, f *Flow) AccessTokenID { +func (a *Authenticator) NewAccessToken(r *http.Request, user provider.ProviderUser) AccessTokenID { id := AccessTokenID(generate.UUID()) now := time.Now() @@ -156,7 +156,7 @@ const ( GTRefreshToken GrantType = "refresh_token" ) -type ClientID string +type ClientID common.ClientID func (c *ClientID) IsValid() bool { // TODO: || !indieauth.VerifyClientID(rq.ClientID)? diff --git a/pkg/auth/store.go b/pkg/auth/store.go index aa3cc40..46d1513 100644 --- a/pkg/auth/store.go +++ b/pkg/auth/store.go @@ -23,7 +23,7 @@ type authStore struct { Credentials []Credential `json:"credentials"` Refresh []RefreshToken `json:"refresh_tokens"` - userMap map[UserID]*User + userMap map[UserID]*User providerUsers map[provider.ProviderUser]*Credential } diff --git a/pkg/flow/flow.go b/pkg/flow/flow.go new file mode 100644 index 0000000..cacc76e --- /dev/null +++ b/pkg/flow/flow.go @@ -0,0 +1,211 @@ +// flow is the data entry flow. +package flow + +import ( + "fmt" + "time" + + "dynatron.me/x/blasphem/internal/common" + "dynatron.me/x/blasphem/internal/generate" +) + +type ( + FlowResultType string + FlowID string + Step string + HandlerKey string + Errors interface{} + + Context interface{} + + FlowStore map[FlowID]Handler + + FlowManager struct { + flows FlowStore + } + + FlowResult struct { + Type FlowResultType `json:"type"` + ID FlowID `json:"flow_id"` + Handler []*HandlerKey `json:"handler"` + Title *string `json:"title,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` + StepID *Step `json:"step_id,omitempty"` + Schema []FlowSchemaItem `json:"data_schema"` + Extra *string `json:"extra,omitempty"` + Required *bool `json:"required,omitempty"` + Errors interface{} `json:"errors"` + Description *string `json:"description,omitempty"` + DescPlace *string `json:"description_placeholders"` + URL *string `json:"url,omitempty"` + Reason *string `json:"reason,omitempty"` + Context *string `json:"context,omitempty"` + Result interface{} `json:"result,omitempty"` + LastStep *string `json:"last_step"` + Options map[string]interface{} `json:"options,omitempty"` + Version *int `json:"version,omitempty"` + } + + FlowSchemaItem struct { + Type string `json:"type"` + Name string `json:"name"` + Required bool `json:"required"` + } + + FlowSchema []FlowSchemaItem + + FlowRequest struct { + ClientID common.ClientID `json:"client_id"` + Handler []*HandlerKey `json:"handler"` + RedirectURI string `json:"redirect_uri"` + } +) + +type ( + Schemer interface { + FlowSchema() FlowSchema + } + + Handler interface { + Base() FlowHandlerBase + FlowID() FlowID + flowCtime() time.Time + } +) + +const ( + StepInit Step = "init" +) + +func (fs *FlowSchema) CheckRequired(rm map[string]interface{}) error { + for _, si := range *fs { + if si.Required { + if _, ok := rm[si.Name]; !ok { + return fmt.Errorf("missing required param %s", si.Name) + } + } + } + + return nil +} + +func NewFlowManager() *FlowManager { + return &FlowManager{ + flows: make(FlowStore), + } +} + +func stepPtr(s Step) *Step { return &s } + +type FlowHandlerBase struct { + ID FlowID // ID is the FlowID + Handler HandlerKey // Handler key + Context Context // flow Context + ClientID common.ClientID + RedirectURI string + Schema FlowSchema + + // curStep is the current step set by the flow manager + curStep Step + + ctime time.Time +} + +func (f *FlowHandlerBase) Step() Step { return f.curStep } + +func (f *FlowHandlerBase) Base() FlowHandlerBase { return *f } + +func (f *FlowHandlerBase) FlowID() FlowID { + return f.ID +} + +func (f *FlowHandlerBase) flowCtime() time.Time { return f.ctime } + +func NewFlowHandlerBase(f *FlowRequest, sch Schemer, hand string) FlowHandlerBase { + return FlowHandlerBase{ + ID: FlowID(generate.UUID()), + Handler: HandlerKey(hand), + ClientID: f.ClientID, + RedirectURI: f.RedirectURI, + Schema: sch.FlowSchema(), + + curStep: StepInit, + ctime: time.Now(), + } +} + +func (hk *HandlerKey) String() string { + return string(*hk) +} + +func (fm *FlowHandlerBase) Handlers() []*HandlerKey { + return []*HandlerKey{&fm.Handler, nil} +} + +func resultErrs(e Errors) Errors { + if e == nil { + return []string{} + } + + return e +} + +func (fm *FlowHandlerBase) ShowForm(errs Errors) *FlowResult { + res := &FlowResult{ + Type: TypeForm, + ID: fm.ID, + StepID: stepPtr(fm.curStep), + Schema: fm.Schema, + Handler: fm.Handlers(), + Errors: resultErrs(errs), + } + + return res +} + +func (fm *FlowManager) Delete(id FlowID) { + delete(fm.flows, id) +} + +const ( + TypeForm FlowResultType = "form" + TypeCreateEntry FlowResultType = "create_entry" + TypeAbort FlowResultType = "abort" + TypeExternalStep FlowResultType = "external" + TypeExternalStepDone FlowResultType = "external_done" + TypeShowProgress FlowResultType = "progress" + TypeShowProgressDone FlowResultType = "progress_done" + TypeMenu FlowResultType = "menu" +) + +func (f *FlowHandlerBase) touch() { + f.ctime = time.Now() +} + +func (fm *FlowManager) Register(f Handler) { + fm.flows.cull() + fm.flows[f.FlowID()] = f +} + +func (fs *FlowManager) Remove(f Handler) { + delete(fs.flows, f.FlowID()) +} + +const cullAge = time.Minute * 30 + +func (fs FlowStore) cull() { + for k, v := range fs { + if time.Now().Sub(v.flowCtime()) > cullAge { + delete(fs, k) + } + } +} + +func (fs *FlowManager) Get(id FlowID) Handler { + f, ok := fs.flows[id] + if ok { + return f + } + + return nil +}