package auth import ( "crypto/rand" "encoding/hex" "net/http" "strings" "time" "github.com/labstack/echo/v4" ) 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" ) 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"` Schema []FlowSchemaItem `json:"data_schema"` Errors interface{} `json:"errors"` DescPlace *string `json:"description_placeholders"` LastStep *string `json:"last_step"` request *FlowRequest age time.Time } func (f *Flow) touch() { f.age = time.Now() } func genFlowID() FlowID { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { panic(err) } return FlowID(hex.EncodeToString(b)) } func (fs FlowStore) register(f *Flow) { fs.cull() fs[f.ID] = f } const cullAge = time.Minute * 30 func (fs FlowStore) cull() { for k, v := range fs { if time.Now().Sub(v.age) > 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: genFlowID(), StepID: StepInit, Schema: sch, Handler: r.Handler, Errors: []string{}, request: r, } flow.touch() a.Flows.register(flow) return flow } func (f *Flow) progress(a *Authenticator, c echo.Context) error { 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: return c.String(http.StatusOK, "login success!") 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 (a *Authenticator) LoginFlowHandler(c echo.Context) error { 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) } flowID := c.Param("flow_id") switch flowID { case "": // new 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) default: flow := a.Flows.Get(FlowID(flowID)) if flow == nil { return c.String(http.StatusNotFound, "no such flow") } return flow.progress(a, c) } }