blasphem/pkg/auth/flow.go
2022-12-19 19:24:01 -05:00

198 lines
4.2 KiB
Go

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.FlowHandler
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{
FlowHandler: 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, clientID, err := a.Check(f, c.Request(), rm)
switch err {
case nil:
creds := a.store.GetCredential(user)
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.NewAuthCode(ClientID(clientID), creds)
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 {
a.Lock()
defer a.Unlock()
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 {
a.Lock()
defer a.Unlock()
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 {
a.Lock()
defer a.Unlock()
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)
}