package auth import ( "net/http" "strings" "github.com/jinzhu/copier" "github.com/labstack/echo/v4" "dynatron.me/x/blasphem/internal/common" "dynatron.me/x/blasphem/pkg/auth/provider" "dynatron.me/x/blasphem/pkg/flow" ) type AuthFlowManager struct { *flow.FlowManager } type LoginFlow struct { flow.FlowHandlerBase ClientID common.ClientID FlowContext } type FlowContext struct { IPAddr string CredentialOnly bool RedirectURI string } type LoginFlowRequest struct { ClientID common.ClientID `json:"client_id"` Handler []*string `json:"handler"` RedirectURI string `json:"redirect_uri"` Type *string `json:"type"` ip string `json:"-"` } func (r *LoginFlowRequest) FlowContext() FlowContext { return FlowContext{ IPAddr: r.ip, RedirectURI: r.RedirectURI, CredentialOnly: r.Type != nil && *r.Type == "link_user", } } func NewAuthFlowManager() *AuthFlowManager { return &AuthFlowManager{FlowManager: flow.NewFlowManager()} } func (afm *AuthFlowManager) NewLoginFlow(req *LoginFlowRequest, prov provider.AuthProvider) *LoginFlow { lf := &LoginFlow{ FlowHandlerBase: flow.NewFlowHandlerBase(prov, prov.ProviderType()), ClientID: req.ClientID, FlowContext: req.FlowContext(), } afm.Register(lf) return lf } func (a *Authenticator) NewFlow(r *LoginFlowRequest) *flow.Result { var prov provider.AuthProvider for _, h := range r.Handler { if h == nil { break } prov = a.Provider(*h) if prov != nil { break } } if prov == nil { return nil } flow := a.flows.NewLoginFlow(r, prov) return flow.ShowForm(nil) } func (f *LoginFlow) redirect(c echo.Context) { c.Request().Header.Set("Location", f.RedirectURI) } 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) if err != nil { return c.String(http.StatusBadRequest, err.Error()) } 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: finishedFlow := flow.Result{} a.flows.Remove(f) copier.Copy(&finishedFlow, f) finishedFlow.Type = flow.TypeCreateEntry finishedFlow.Title = common.AppNamePtr() finishedFlow.Version = common.IntPtr(1) finishedFlow.Result = a.NewAccessToken(c.Request(), user) f.redirect(c) return c.JSON(http.StatusCreated, &finishedFlow) case ErrInvalidHandler: return c.String(http.StatusNotFound, err.Error()) case ErrInvalidAuth: fallthrough default: return c.JSON(http.StatusOK, f.ShowForm(map[string]interface{}{ "base": "invalid_auth", })) } default: return c.JSON(http.StatusOK, f.ShowForm(map[string]interface{}{ "base": "unknown_flow_step", })) } } func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error { flowID := flow.FlowID(c.Param("flow_id")) if flowID == "" { return c.String(http.StatusBadRequest, "empty flow ID") } a.flows.Delete(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 LoginFlowRequest err := c.Bind(&flowReq) if err != nil { return c.String(http.StatusBadRequest, err.Error()) } flowReq.ip = c.Request().RemoteAddr 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(flow.FlowID(flowID)) if flow == nil { return c.String(http.StatusNotFound, "no such flow") } return flow.(*LoginFlow).progress(a, c) }