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