pre-model
This commit is contained in:
parent
3038750206
commit
9224b21db7
7 changed files with 117 additions and 66 deletions
|
@ -25,7 +25,7 @@ var (
|
||||||
type Authenticator struct {
|
type Authenticator struct {
|
||||||
store AuthStore
|
store AuthStore
|
||||||
flows *AuthFlowManager
|
flows *AuthFlowManager
|
||||||
sessions AccessSessionStore
|
authCodes authCodeStore
|
||||||
providers map[string]provider.AuthProvider
|
providers map[string]provider.AuthProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ func (a *Authenticator) InitAuth(s storage.Store) error {
|
||||||
|
|
||||||
a.flows = NewAuthFlowManager()
|
a.flows = NewAuthFlowManager()
|
||||||
|
|
||||||
a.sessions.init()
|
a.authCodes.init()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
a.store, err = a.newAuthStore(s)
|
a.store, err = a.newAuthStore(s)
|
||||||
|
@ -91,24 +91,24 @@ 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 *LoginFlow, req *http.Request, rm map[string]interface{}) (provider.ProviderUser, error) {
|
func (a *Authenticator) Check(f *LoginFlow, req *http.Request, rm map[string]interface{}) (user provider.ProviderUser, clientID string, err error) {
|
||||||
cID, hasCID := rm["client_id"]
|
cID, hasCID := rm["client_id"]
|
||||||
cIDStr, cidIsStr := cID.(string)
|
clientID, cidIsStr := cID.(string)
|
||||||
if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.ClientID) {
|
if !hasCID || !cidIsStr || clientID == "" || clientID != string(f.ClientID) {
|
||||||
return nil, ErrInvalidAuth
|
return nil, clientID, ErrInvalidAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
p := a.Provider(f.Handler.String())
|
p := a.Provider(f.Handler.String())
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil, ErrInvalidAuth
|
return nil, clientID, ErrInvalidAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
user, success := p.ValidateCreds(req, rm)
|
user, success := p.ValidateCreds(req, rm)
|
||||||
|
|
||||||
if success {
|
if success {
|
||||||
log.Info().Interface("user", user.UserData()).Msg("Login success")
|
log.Info().Interface("user", user.UserData()).Msg("Login success")
|
||||||
return user, nil
|
return user, clientID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrInvalidAuth
|
return nil, clientID, ErrInvalidAuth
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,17 +103,19 @@ func (f *LoginFlow) progress(a *Authenticator, c echo.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusBadRequest, f.ShowForm([]string{err.Error()}))
|
return c.JSON(http.StatusBadRequest, f.ShowForm([]string{err.Error()}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
user, err := a.Check(f, c.Request(), rm)
|
user, clientID, err := a.Check(f, c.Request(), rm)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
|
creds := a.store.Credential(user)
|
||||||
finishedFlow := flow.Result{}
|
finishedFlow := flow.Result{}
|
||||||
a.flows.Remove(f)
|
a.flows.Remove(f)
|
||||||
copier.Copy(&finishedFlow, f)
|
copier.Copy(&finishedFlow, f)
|
||||||
finishedFlow.Type = flow.TypeCreateEntry
|
finishedFlow.Type = flow.TypeCreateEntry
|
||||||
finishedFlow.Title = common.AppNamePtr()
|
finishedFlow.Title = common.AppNamePtr()
|
||||||
finishedFlow.Version = common.IntPtr(1)
|
finishedFlow.Version = common.IntPtr(1)
|
||||||
finishedFlow.Result = a.NewAccessToken(c.Request(), user)
|
finishedFlow.Result = a.NewAuthCode(ClientID(clientID), creds)
|
||||||
|
|
||||||
f.redirect(c)
|
f.redirect(c)
|
||||||
|
|
||||||
|
|
|
@ -24,9 +24,39 @@ func Register(providerName string, f func(storage.Store) (AuthProvider, error))
|
||||||
Providers[providerName] = f
|
Providers[providerName] = f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Credentials struct {
|
||||||
|
ID CredID `json:"id"`
|
||||||
|
UserID UserID `json:"user_id"`
|
||||||
|
AuthProviderType string `json:"auth_provider_type"`
|
||||||
|
AuthProviderID *string `json:"auth_provider_id"`
|
||||||
|
DataRaw *json.RawMessage `json:"data,omitempty"`
|
||||||
|
user ProviderUser `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cred *Credentials) MarshalJSON() ([]byte, error) {
|
||||||
|
type CredAlias Credentials // alias so ø method set and we don't recurse
|
||||||
|
nCd := (*CredAlias)(cred)
|
||||||
|
|
||||||
|
if cred.user != nil {
|
||||||
|
providerData := cred.user.UserData()
|
||||||
|
if providerData != nil {
|
||||||
|
b, err := json.Marshal(providerData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dr := json.RawMessage(b)
|
||||||
|
nCd.DataRaw = &dr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(nCd)
|
||||||
|
}
|
||||||
|
|
||||||
type ProviderUser interface {
|
type ProviderUser interface {
|
||||||
// TODO: make sure this is sane with all the ProviderUser and UserData type stuff
|
// TODO: make sure this is sane with all the ProviderUser and UserData type stuff
|
||||||
UserData() ProviderUser
|
UserData() ProviderUser
|
||||||
|
Credentials() *Credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthProviderBase struct {
|
type AuthProviderBase struct {
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"dynatron.me/x/blasphem/internal/common"
|
"dynatron.me/x/blasphem/internal/common"
|
||||||
"dynatron.me/x/blasphem/internal/generate"
|
"dynatron.me/x/blasphem/internal/generate"
|
||||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -38,62 +40,78 @@ func (rt *RefreshToken) IsValid() bool {
|
||||||
return rt.JWTKey != ""
|
return rt.JWTKey != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessSessionStore struct {
|
type authCodeStore struct {
|
||||||
s map[AccessTokenID]*AccessToken
|
s map[authCodeTuple]flowResult
|
||||||
lastCull time.Time
|
lastCull time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessTokenID string
|
type authCodeTuple struct {
|
||||||
|
ClientID ClientID
|
||||||
|
Code AuthCode
|
||||||
|
}
|
||||||
|
|
||||||
func (t *AccessTokenID) IsValid() bool {
|
func (t *authCodeTuple) IsValid() bool {
|
||||||
// TODO: more validation than this
|
// TODO: more validation than this
|
||||||
return *t != ""
|
return t.Code != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessToken struct { // TODO: jwt bro
|
type flowResult struct {
|
||||||
ID AccessTokenID
|
Time time.Time
|
||||||
Ctime time.Time
|
Cred *Credentials
|
||||||
Expires time.Time
|
// TODO: remove this comment below \/
|
||||||
Addr string
|
//user provider.ProviderUser `json:"-"`
|
||||||
|
|
||||||
user provider.ProviderUser `json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *AccessSessionStore) init() {
|
// OAuth 4.2.1 spec recommends 10 minutes
|
||||||
ss.s = make(map[AccessTokenID]*AccessToken)
|
const authCodeExpire = 10 * time.Minute
|
||||||
|
|
||||||
|
func (f *flowResult) IsValid(now time.Time) bool {
|
||||||
|
if now.After(f.Time.Add(authCodeExpire)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *authCodeStore) init() {
|
||||||
|
ss.s = make(map[authCodeTuple]flowResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cullInterval = 5 * time.Minute
|
const cullInterval = 5 * time.Minute
|
||||||
|
|
||||||
func (ss *AccessSessionStore) cull() {
|
func (ss *authCodeStore) cull() {
|
||||||
if now := time.Now(); now.Sub(ss.lastCull) > cullInterval {
|
if now := time.Now(); now.Sub(ss.lastCull) > cullInterval {
|
||||||
for k, v := range ss.s {
|
for k, v := range ss.s {
|
||||||
if now.After(v.Expires) {
|
if !v.IsValid(now) {
|
||||||
delete(ss.s, k)
|
delete(ss.s, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *AccessSessionStore) register(t *AccessToken) {
|
func (ss *authCodeStore) store(clientID ClientID, cred *Credentials) string {
|
||||||
|
log.Info().Msgf("store cred is %+v", cred)
|
||||||
ss.cull()
|
ss.cull()
|
||||||
ss.s[t.ID] = t
|
code := generate.UUID()
|
||||||
|
ss.s[authCodeTuple{clientID, AuthCode(code)}] = flowResult{Time: time.Now(), Cred: cred}
|
||||||
|
|
||||||
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *AccessSessionStore) verify(tr *TokenRequest, r *http.Request) (provider.ProviderUser, bool) {
|
func (ss *authCodeStore) verify(tr *TokenRequest, r *http.Request) (*Credentials, bool) {
|
||||||
if t, hasToken := ss.s[tr.Code]; hasToken {
|
key := authCodeTuple{tr.ClientID, tr.Code}
|
||||||
|
if t, hasCode := ss.s[key]; hasCode {
|
||||||
|
defer delete(ss.s, key)
|
||||||
// TODO: JWT
|
// TODO: JWT
|
||||||
if t.Expires.After(time.Now()) {
|
if t.IsValid(time.Now()) {
|
||||||
return t.user, true
|
return t.Cred, true
|
||||||
} else {
|
|
||||||
delete(ss.s, t.ID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
type Credential struct {
|
type Credentials struct {
|
||||||
ID CredID `json:"id"`
|
ID CredID `json:"id"`
|
||||||
UserID UserID `json:"user_id"`
|
UserID UserID `json:"user_id"`
|
||||||
AuthProviderType string `json:"auth_provider_type"`
|
AuthProviderType string `json:"auth_provider_type"`
|
||||||
|
@ -102,8 +120,8 @@ type Credential struct {
|
||||||
user provider.ProviderUser `json:"-"`
|
user provider.ProviderUser `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cred *Credential) MarshalJSON() ([]byte, error) {
|
func (cred *Credentials) MarshalJSON() ([]byte, error) {
|
||||||
type CredAlias Credential // alias so ø method set and we don't recurse
|
type CredAlias Credentials // alias so ø method set and we don't recurse
|
||||||
nCd := (*CredAlias)(cred)
|
nCd := (*CredAlias)(cred)
|
||||||
|
|
||||||
if cred.user != nil {
|
if cred.user != nil {
|
||||||
|
@ -122,35 +140,19 @@ func (cred *Credential) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(nCd)
|
return json.Marshal(nCd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest, r *http.Request) *Credential {
|
func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest, r *http.Request) *Credentials {
|
||||||
user, success := a.sessions.verify(tr, r)
|
cred, success := a.authCodes.verify(tr, r)
|
||||||
if !success {
|
if !success {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cred := a.store.Credential(user)
|
|
||||||
|
|
||||||
return cred
|
return cred
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultExpiration = 15 * time.Minute
|
const defaultExpiration = 15 * time.Minute
|
||||||
|
|
||||||
func (a *Authenticator) NewAccessToken(r *http.Request, user provider.ProviderUser) AccessTokenID {
|
func (a *Authenticator) NewAuthCode(clientID ClientID, cred *Credentials) string {
|
||||||
id := AccessTokenID(generate.UUID())
|
return a.authCodes.store(clientID, cred)
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
t := &AccessToken{
|
|
||||||
ID: id,
|
|
||||||
Ctime: now,
|
|
||||||
Expires: now.Add(defaultExpiration),
|
|
||||||
Addr: r.RemoteAddr,
|
|
||||||
|
|
||||||
user: user,
|
|
||||||
}
|
|
||||||
|
|
||||||
a.sessions.register(t)
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GrantType string
|
type GrantType string
|
||||||
|
@ -167,9 +169,14 @@ func (c *ClientID) IsValid() bool {
|
||||||
return *c != ""
|
return *c != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthCode string
|
||||||
|
func (ac *AuthCode) IsValid() bool {
|
||||||
|
return *ac != ""
|
||||||
|
}
|
||||||
|
|
||||||
type TokenRequest struct {
|
type TokenRequest struct {
|
||||||
ClientID ClientID `form:"client_id"`
|
ClientID ClientID `form:"client_id"`
|
||||||
Code AccessTokenID `form:"code"`
|
Code AuthCode `form:"code"`
|
||||||
GrantType GrantType `form:"grant_type"`
|
GrantType GrantType `form:"grant_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||||
"dynatron.me/x/blasphem/pkg/storage"
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -14,20 +16,20 @@ const (
|
||||||
|
|
||||||
type AuthStore interface {
|
type AuthStore interface {
|
||||||
User(UserID) *User
|
User(UserID) *User
|
||||||
Credential(provider.ProviderUser) *Credential
|
Credential(provider.ProviderUser) *Credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
type authStore struct {
|
type authStore struct {
|
||||||
Users []User `json:"users"`
|
Users []User `json:"users"`
|
||||||
Groups []Group `json:"groups"`
|
Groups []Group `json:"groups"`
|
||||||
Credentials []Credential `json:"credentials"`
|
Credentials []Credentials `json:"credentials"`
|
||||||
Refresh []RefreshToken `json:"refresh_tokens"`
|
Refresh []RefreshToken `json:"refresh_tokens"`
|
||||||
|
|
||||||
userMap map[UserID]*User
|
userMap map[UserID]*User
|
||||||
providerUsers map[provider.ProviderUser]*Credential
|
providerUsers map[provider.ProviderUser]*Credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *authStore) Credential(p provider.ProviderUser) *Credential {
|
func (as *authStore) Credential(p provider.ProviderUser) *Credentials {
|
||||||
c, have := as.providerUsers[p]
|
c, have := as.providerUsers[p]
|
||||||
if !have {
|
if !have {
|
||||||
return nil
|
return nil
|
||||||
|
@ -41,7 +43,7 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error)
|
||||||
err = s.Get(AuthStoreKey, as)
|
err = s.Get(AuthStoreKey, as)
|
||||||
|
|
||||||
as.userMap = make(map[UserID]*User)
|
as.userMap = make(map[UserID]*User)
|
||||||
as.providerUsers = make(map[provider.ProviderUser]*Credential)
|
as.providerUsers = make(map[provider.ProviderUser]*Credentials)
|
||||||
|
|
||||||
for _, u := range as.Users {
|
for _, u := range as.Users {
|
||||||
as.userMap[u.ID] = &u
|
as.userMap[u.ID] = &u
|
||||||
|
@ -67,6 +69,14 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error)
|
||||||
}
|
}
|
||||||
as.providerUsers[c.user] = &c
|
as.providerUsers[c.user] = &c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u, hasUser := as.userMap[c.UserID]
|
||||||
|
if !hasUser {
|
||||||
|
log.Error().Str("userid", string(c.UserID)).Msg("no such userid in map")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Creds = append(u.Creds, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove invalid RefreshTokens
|
// remove invalid RefreshTokens
|
||||||
|
|
|
@ -18,6 +18,8 @@ type User struct {
|
||||||
GroupIDs []GroupID `json:"group_ids"`
|
GroupIDs []GroupID `json:"group_ids"`
|
||||||
Data interface{} `json:"data,omitempty"`
|
Data interface{} `json:"data,omitempty"`
|
||||||
UserMetadata
|
UserMetadata
|
||||||
|
|
||||||
|
Creds []Credentials `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserMetadata struct {
|
type UserMetadata struct {
|
||||||
|
@ -36,7 +38,7 @@ func (u *User) allowedToAuth() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) getOrCreateUser(c *Credential) (*User, error) {
|
func (a *Authenticator) getOrCreateUser(c *Credentials) (*User, error) {
|
||||||
log.Debug().Interface("userdata", c).Msg("getOrCreateUser")
|
log.Debug().Interface("userdata", c).Msg("getOrCreateUser")
|
||||||
u := a.store.User(c.UserID)
|
u := a.store.User(c.UserID)
|
||||||
if u == nil {
|
if u == nil {
|
||||||
|
|
|
@ -181,7 +181,7 @@ func (fs *FlowManager) Remove(f Handler) {
|
||||||
delete(fs.flows, f.FlowID())
|
delete(fs.flows, f.FlowID())
|
||||||
}
|
}
|
||||||
|
|
||||||
const cullAge = time.Minute * 30
|
const cullAge = time.Minute * 10
|
||||||
|
|
||||||
func (fs FlowStore) cull() {
|
func (fs FlowStore) cull() {
|
||||||
for k, v := range fs {
|
for k, v := range fs {
|
||||||
|
|
Loading…
Reference in a new issue