WIP: binding issues
This commit is contained in:
parent
46df74226f
commit
a65c6bc394
6 changed files with 144 additions and 34 deletions
5
Makefile
5
Makefile
|
@ -4,11 +4,16 @@ all: build
|
|||
build:
|
||||
go build -o blas ./cmd/blas/
|
||||
|
||||
serve:
|
||||
go run ./cmd/blas/ serve ${BLAS_ARGS}
|
||||
|
||||
# pkg/frontend/frontend/hass_frontend:
|
||||
frontend:
|
||||
${FE}/script/setup
|
||||
${FE}/script/build_frontend
|
||||
|
||||
todo:
|
||||
rg -g '!Makefile' -g '!pkg/frontend/frontend/**' -A3 -C3 'XXX:|TODO:' .
|
||||
|
||||
debug:
|
||||
dlv debug ./cmd/blas/ ${ARGS}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
@ -24,6 +25,11 @@ type Authenticator struct {
|
|||
providers map[string]AuthProvider
|
||||
}
|
||||
|
||||
type AuthError struct {
|
||||
Error string `json:"error"`
|
||||
Description string `json:"error_description"`
|
||||
}
|
||||
|
||||
func (a *Authenticator) InstallRoutes(e *echo.Echo) {
|
||||
authG := e.Group("/auth")
|
||||
authG.GET("/authorize", frontend.AliasHandler("authorize.html"))
|
||||
|
@ -69,7 +75,11 @@ type AuthProvider interface { // TODO: this should include stepping
|
|||
ProviderType() string
|
||||
ProviderBase() AuthProviderBase
|
||||
FlowSchema() []FlowSchemaItem
|
||||
ValidateCreds(reqMap map[string]interface{}) bool
|
||||
ValidateCreds(reqMap map[string]interface{}) (user ProviderUser, success bool)
|
||||
}
|
||||
|
||||
type ProviderUser interface {
|
||||
ProviderUsername() string
|
||||
}
|
||||
|
||||
type AuthProviderBase struct {
|
||||
|
@ -94,31 +104,32 @@ func (a *Authenticator) ProvidersHandler(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, providers)
|
||||
}
|
||||
|
||||
func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) error {
|
||||
func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) (ProviderUser, error) {
|
||||
cID, hasCID := rm["client_id"]
|
||||
if !hasCID || cID != f.request.ClientID {
|
||||
return ErrInvalidAuth
|
||||
cIDStr, cidIsStr := cID.(string)
|
||||
if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.request.ClientID) {
|
||||
return nil, ErrInvalidAuth
|
||||
}
|
||||
|
||||
for _, h := range f.Handler {
|
||||
if h == nil {
|
||||
return ErrInvalidHandler
|
||||
return nil, ErrInvalidHandler
|
||||
}
|
||||
|
||||
p := a.Provider(*h)
|
||||
if p == nil {
|
||||
return ErrInvalidAuth
|
||||
return nil, ErrInvalidAuth
|
||||
}
|
||||
|
||||
success := p.ValidateCreds(rm)
|
||||
user, success := p.ValidateCreds(rm)
|
||||
|
||||
if success {
|
||||
log.Info().Str("user", rm["username"].(string)).Msg("Login success")
|
||||
return nil
|
||||
log.Info().Str("user", user.ProviderUsername()).Msg("Login success")
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
return ErrInvalidAuth
|
||||
return nil, ErrInvalidAuth
|
||||
}
|
||||
|
||||
func genUUID() string {
|
||||
|
@ -127,3 +138,12 @@ func genUUID() string {
|
|||
|
||||
return hex.EncodeToString(u[:])
|
||||
}
|
||||
|
||||
func genHex(l int) string {
|
||||
b := make([]byte, l)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
type FlowStore map[FlowID]*Flow
|
||||
|
||||
type FlowRequest struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientID ClientID `json:"client_id"`
|
||||
Handler []*string `json:"handler"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
err = a.Check(f, rm)
|
||||
user, err := a.Check(f, rm)
|
||||
switch err {
|
||||
case nil:
|
||||
var finishedFlow struct {
|
||||
|
@ -158,13 +158,13 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
|||
Type FlowType `json:"type"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
// TODO: setup the session. delete the flow.
|
||||
|
||||
a.flows.Remove(f)
|
||||
copier.Copy(&finishedFlow, f)
|
||||
finishedFlow.Type = TypeCreateEntry
|
||||
finishedFlow.Title = common.AppName
|
||||
finishedFlow.Version = 1
|
||||
finishedFlow.Result = a.NewToken(c.Request(), f)
|
||||
finishedFlow.Result = a.NewToken(c.Request(), user, f)
|
||||
|
||||
f.redirect(c)
|
||||
|
||||
|
|
|
@ -13,14 +13,16 @@ const (
|
|||
HAProviderKey = "auth_provider.homeassistant"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
type HAUser struct {
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func (h *HAUser) ProviderUsername() string { return h.Username }
|
||||
|
||||
type HomeAssistantProvider struct {
|
||||
AuthProviderBase `json:"-"`
|
||||
Users []User `json:"users"`
|
||||
Users []HAUser `json:"users"`
|
||||
}
|
||||
|
||||
func NewHAProvider(s storage.Store) (*HomeAssistantProvider, error) {
|
||||
|
@ -43,16 +45,16 @@ func (hap *HomeAssistantProvider) hashPass(p string) ([]byte, error) {
|
|||
return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)
|
||||
}
|
||||
|
||||
func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) bool {
|
||||
func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) (ProviderUser, bool) {
|
||||
usernameE, hasU := rm["username"]
|
||||
passwordE, hasP := rm["password"]
|
||||
username, unStr := usernameE.(string)
|
||||
password, paStr := passwordE.(string)
|
||||
if !hasU || !hasP || !unStr || !paStr || username == "" || password == "" {
|
||||
return false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var found *User
|
||||
var found *HAUser
|
||||
|
||||
const dummyHash = "$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO"
|
||||
|
||||
|
@ -64,22 +66,22 @@ func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) bool
|
|||
|
||||
if found == nil { // one more compare to thwart timing attacks
|
||||
bcrypt.CompareHashAndPassword([]byte("foo"), []byte(dummyHash))
|
||||
return false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var hash []byte
|
||||
hash, err := base64.StdEncoding.DecodeString(found.Password)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("b64 encode fail")
|
||||
return false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword(hash, []byte(password))
|
||||
if err == nil {
|
||||
return true
|
||||
return found, true
|
||||
}
|
||||
|
||||
return false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (hap *HomeAssistantProvider) FlowSchema() []FlowSchemaItem {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
|
@ -14,11 +15,18 @@ type SessionStore struct {
|
|||
|
||||
type TokenID string
|
||||
|
||||
func (t *TokenID) IsValid() bool {
|
||||
// TODO: more validation than this
|
||||
return *t != ""
|
||||
}
|
||||
|
||||
type Token struct { // TODO: jwt bro
|
||||
ID TokenID
|
||||
Ctime time.Time
|
||||
Expires time.Time
|
||||
Addr string
|
||||
|
||||
user ProviderUser `json:"-"`
|
||||
}
|
||||
|
||||
func (ss *SessionStore) init() {
|
||||
|
@ -42,20 +50,53 @@ func (ss *SessionStore) register(t *Token) {
|
|||
ss.s[t.ID] = t
|
||||
}
|
||||
|
||||
func (ss *SessionStore) verify(tr *TokenRequest, r *http.Request) bool {
|
||||
func (ss *SessionStore) verify(tr *TokenRequest, r *http.Request) (ProviderUser, bool) {
|
||||
if t, hasToken := ss.s[tr.Code]; hasToken {
|
||||
// TODO: JWT
|
||||
if t.Expires.After(time.Now()) {
|
||||
return true
|
||||
return t.user, true
|
||||
} else {
|
||||
delete(ss.s, t.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
type Credential struct {
|
||||
user ProviderUser
|
||||
}
|
||||
|
||||
func (ss *SessionStore) verifyAndGetCredential(tr *TokenRequest, r *http.Request) *Credential {
|
||||
user, success := ss.verify(tr, r)
|
||||
if !success {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Credential{user: user}
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Active bool
|
||||
}
|
||||
|
||||
func (u *User) allowedToAuth() error {
|
||||
if !u.Active {
|
||||
return errors.New("user disabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) getOrCreateUser(c *Credential) (*User, error) {
|
||||
panic("not implemented")
|
||||
return &User{}, nil
|
||||
}
|
||||
|
||||
const defaultExpiration = 2 * time.Hour
|
||||
|
||||
func (a *Authenticator) NewToken(r *http.Request, f *Flow) TokenID {
|
||||
func (a *Authenticator) NewToken(r *http.Request, user ProviderUser, f *Flow) TokenID {
|
||||
id := TokenID(genUUID())
|
||||
|
||||
t := &Token{
|
||||
|
@ -63,6 +104,8 @@ func (a *Authenticator) NewToken(r *http.Request, f *Flow) TokenID {
|
|||
Ctime: time.Now(),
|
||||
Expires: time.Now().Add(defaultExpiration),
|
||||
Addr: r.RemoteAddr,
|
||||
|
||||
user: user,
|
||||
}
|
||||
|
||||
a.sessions.register(t)
|
||||
|
@ -70,22 +113,61 @@ func (a *Authenticator) NewToken(r *http.Request, f *Flow) TokenID {
|
|||
return id
|
||||
}
|
||||
|
||||
type GrantType string
|
||||
|
||||
const (
|
||||
GTAuthorizationCode GrantType = "authorization_code"
|
||||
GTRefreshToken GrantType = "refresh_token"
|
||||
)
|
||||
|
||||
type ClientID string
|
||||
|
||||
func (c *ClientID) IsValid() bool {
|
||||
// TODO: || !indieauth.VerifyClientID(rq.ClientID)?
|
||||
return *c != ""
|
||||
}
|
||||
|
||||
type TokenRequest struct {
|
||||
ClientID string `query:"client_id"` // TODO: validate this?
|
||||
ClientID ClientID `query:"client_id"`
|
||||
Code TokenID `query:"code"`
|
||||
GrantType string `query:"grant_type"`
|
||||
GrantType GrantType `query:"grant_type"`
|
||||
}
|
||||
|
||||
func (a *Authenticator) TokenHandler(c echo.Context) error {
|
||||
var rq TokenRequest
|
||||
err := c.Bind(&rq)
|
||||
rq := new(TokenRequest)
|
||||
err := c.Bind(rq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.sessions.verify(&rq, c.Request()) {
|
||||
// TODO: success
|
||||
return c.String(http.StatusOK, "token good I guess")
|
||||
if *rq == (TokenRequest{}) {
|
||||
panic("it didn't bind")
|
||||
}
|
||||
|
||||
switch rq.GrantType {
|
||||
case GTAuthorizationCode:
|
||||
if !rq.ClientID.IsValid() {
|
||||
return c.JSON(http.StatusBadRequest, AuthError{Error: "invalid_request", Description: "invalid client ID"})
|
||||
}
|
||||
|
||||
if !rq.Code.IsValid() {
|
||||
return c.JSON(http.StatusBadRequest, AuthError{Error: "invalid_request", Description: "invalid code"})
|
||||
}
|
||||
|
||||
if cred := a.sessions.verifyAndGetCredential(rq, c.Request()); cred != nil {
|
||||
// TODO: success
|
||||
user, err := a.getOrCreateUser(cred)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusUnauthorized, AuthError{Error: "access_denied", Description: "bad user"})
|
||||
}
|
||||
|
||||
if err := user.allowedToAuth(); err != nil {
|
||||
return c.JSON(http.StatusUnauthorized, AuthError{Error: "access_denied", Description: err.Error()})
|
||||
}
|
||||
return c.String(http.StatusOK, "token good I guess")
|
||||
}
|
||||
case GTRefreshToken:
|
||||
return c.String(http.StatusNotImplemented, "not implemented")
|
||||
}
|
||||
|
||||
return c.String(http.StatusUnauthorized, "token bad I guess")
|
||||
|
|
|
@ -36,6 +36,7 @@ func AliasHandler(toFile string) echo.HandlerFunc {
|
|||
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
RootFS, err = fs.Sub(root, "frontend/hass_frontend")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
Loading…
Reference in a new issue