package auth import ( "net/http" "strings" "time" "github.com/jinzhu/copier" "github.com/labstack/echo/v4" "dynatron.me/x/blasphem/internal/common" ) type FlowStore map[FlowID]*Flow type FlowRequest struct { ClientID string `json:"client_id"` Handler []*string `json:"handler"` RedirectURI string `json:"redirect_uri"` } type FlowSchemaItem struct { Type string `json:"type"` Name string `json:"name"` Required bool `json:"required"` } 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 []FlowSchemaItem `json:"data_schema"` Errors interface{} `json:"errors"` DescPlace *string `json:"description_placeholders"` LastStep *string `json:"last_step"` request *FlowRequest ctime time.Time } func (f *Flow) touch() { f.ctime = time.Now() } 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 } return nil } func (a *Authenticator) NewFlow(r *FlowRequest) *Flow { var sch []FlowSchemaItem for _, h := range r.Handler { if h == nil { break } if hand := a.Provider(*h); hand != nil { sch = hand.FlowSchema() break } } if sch == nil { return nil } flow := &Flow{ Type: TypeForm, ID: FlowID(genUUID()), StepID: stepPtr(StepInit), Schema: sch, Handler: r.Handler, Errors: []string{}, request: r, } flow.touch() a.Flows.register(flow) return flow } func stepPtr(s Step) *Step { return &s } func (f *Flow) redirect(c echo.Context) { c.Request().Header.Set("Location", f.request.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: rm := make(map[string]interface{}) err := c.Bind(&rm) if err != nil { 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 = a.Check(f, rm) switch err { case nil: var finishedFlow struct { ID FlowID `json:"flow_id"` Handler []*string `json:"handler"` Result TokenID `json:"result"` Title string `json:"title"` Type FlowType `json:"type"` Version int `json:"version"` } // TODO: setup the session. delete the flow. a.Flows.Remove(f) copier.Copy(&finishedFlow, f) finishedFlow.Type = TypeCreateEntry finishedFlow.Title = common.AppName finishedFlow.Version = 1 finishedFlow.Result = a.NewToken(c.Request(), f) f.redirect(c) return c.JSON(http.StatusCreated, &finishedFlow) case ErrInvalidHandler: return c.String(http.StatusNotFound, err.Error()) case ErrInvalidAuth: fallthrough default: f.Errors = map[string]interface{}{ "base": "invalid_auth", } return c.JSON(http.StatusOK, f) } default: return c.String(http.StatusBadRequest, "unknown flow step") } } func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error { flowID := c.Param("flow_id") if flowID == "" { return c.String(http.StatusBadRequest, "empty flow ID") } delete(a.Flows, FlowID(flowID)) return c.String(http.StatusOK, "deleted") } func setJSON(c echo.Context) { if c.Request().Method == http.MethodPost && strings.HasPrefix(c.Request().Header.Get(echo.HeaderContentType), "text/plain") { // hack around the content-type, Context.JSON refuses to work otherwise c.Request().Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) } } func (a *Authenticator) BeginLoginFlowHandler(c echo.Context) error { setJSON(c) var flowReq FlowRequest err := c.Bind(&flowReq) if err != nil { return c.String(http.StatusBadRequest, err.Error()) } resp := a.NewFlow(&flowReq) if resp == nil { return c.String(http.StatusBadRequest, "no such handler") } return c.JSON(http.StatusOK, resp) } func (a *Authenticator) LoginFlowHandler(c echo.Context) error { setJSON(c) flowID := c.Param("flow_id") flow := a.Flows.Get(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) }