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 } 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.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: // TODO: setup the session. delete the flow. a.Flows.Remove(f) 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 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") } return flow.progress(a, c) } func (a *Authenticator) InstallRoutes(e *echo.Echo) { authG := e.Group("/auth") authG.GET("/authorize", a.AuthorizeHandler) authG.GET("/providers", a.ProvidersHandler) authG.POST("/login_flow", a.BeginLoginFlowHandler) loginFlow := authG.Group("/login_flow") // TODO: add IP address affinity middleware loginFlow.POST("/:flow_id", a.LoginFlowHandler) loginFlow.DELETE("/:flow_id", a.LoginFlowDeleteHandler) }