package auth import ( "encoding/hex" "errors" "io" "net/http" "github.com/dgrijalva/jwt-go" "github.com/go-oauth2/oauth2/models" "github.com/go-oauth2/oauth2/server" "github.com/go-oauth2/oauth2/v4/generates" "github.com/go-oauth2/oauth2/v4/manage" "github.com/go-oauth2/oauth2/v4/store" "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/rs/zerolog/log" "dynatron.me/x/blasphem/pkg/frontend" "dynatron.me/x/blasphem/pkg/storage" ) var ( ErrInvalidAuth = errors.New("invalid auth") ErrInvalidHandler = errors.New("no such handler") ) type Authenticator struct { manager *manage.Manager flows FlowStore sessions SessionStore providers map[string]AuthProvider } func (a *Authenticator) InstallRoutes(e *echo.Echo) { authG := e.Group("/auth") authG.GET("/authorize", a.AuthorizeHandler) authG.GET("/providers", a.ProvidersHandler) authG.POST("/token", a.TokenHandler) 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) } func (a *Authenticator) InitAuth(s storage.Store) error { a.manager = manage.NewDefaultManager() a.manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) // token store a.manager.MustTokenStorage(store.NewMemoryTokenStore()) // generate jwt access token a.manager.MapAccessGenerate(generates.NewJWTAccessGenerate("", []byte("00000000"), jwt.SigningMethodHS512)) clientStore := store.NewClientStore() clientStore.Set(idvar, &models.Client{ ID: idvar, Secret: secretvar, Domain: domainvar, }) manager.MapClientStorage(clientStore) srv := server.NewServer(server.NewConfig(), manager) a.flows = make(FlowStore) a.sessions.init() hap, err := NewHAProvider(s) if err != nil { return err } // XXX: yuck a.providers = map[string]AuthProvider{ hap.ProviderType(): hap, } return nil } func (a *Authenticator) Provider(name string) AuthProvider { p, ok := a.providers[name] if !ok { return nil } return p } type AuthProvider interface { // TODO: this should include stepping ProviderName() string ProviderID() *string ProviderType() string ProviderBase() AuthProviderBase FlowSchema() []FlowSchemaItem ValidateCreds(reqMap map[string]interface{}) bool } type AuthProviderBase struct { Name string `json:"name"` ID *string `json:"id"` Type string `json:"type"` } func (bp *AuthProviderBase) ProviderName() string { return bp.Name } func (bp *AuthProviderBase) ProviderID() *string { return bp.ID } func (bp *AuthProviderBase) ProviderType() string { return bp.Type } func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp } var HomeAssistant = "homeassistant" // TODO: make this configurable func (a *Authenticator) ProvidersHandler(c echo.Context) error { providers := []AuthProviderBase{ a.Provider(HomeAssistant).ProviderBase(), } return c.JSON(http.StatusOK, providers) } func (s *Authenticator) AuthorizeHandler(c echo.Context) error { authContents, err := frontend.RootFS.Open("authorize.html") if err != nil { return err } defer authContents.Close() b, err := io.ReadAll(authContents) if err != nil { return err } return c.HTML(http.StatusOK, string(b)) } func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) error { cID, hasCID := rm["client_id"] if !hasCID || cID != f.request.ClientID { return ErrInvalidAuth } for _, h := range f.Handler { if h == nil { return ErrInvalidHandler } p := a.Provider(*h) if p == nil { return ErrInvalidAuth } success := p.ValidateCreds(rm) if success { log.Info().Str("user", rm["username"].(string)).Msg("Login success") return nil } } return ErrInvalidAuth } func genUUID() string { // must be addressable u := uuid.New() return hex.EncodeToString(u[:]) }