Work on new auth framework based on new storage system
Some checks are pending
/ test (push) Waiting to run
Some checks are pending
/ test (push) Waiting to run
This commit is contained in:
parent
66d6299295
commit
9496ba0cc6
5 changed files with 264 additions and 0 deletions
9
auth-new/auth.go
Normal file
9
auth-new/auth.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
webauthn *webauthn.WebAuthn
|
||||
}
|
11
auth-new/errors.go
Normal file
11
auth-new/errors.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package auth
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// The provided authentication method is not known to the server
|
||||
ErrUnknownAuthMethod = errors.New("unknown authentication method")
|
||||
// The user hasn't setup the provided authentication method
|
||||
ErrUnsupportedAuthMethod = errors.New("authentication method not supported for this user")
|
||||
ErrInvalidCombination = errors.New("invalid account and token combination")
|
||||
)
|
67
auth-new/fakeUser.go
Normal file
67
auth-new/fakeUser.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||
)
|
||||
|
||||
type fakeUser struct {
|
||||
actualUser *models.User
|
||||
}
|
||||
|
||||
// Ensure that fakeUser always implements webauthn.User
|
||||
var _ webauthn.User = &fakeUser{}
|
||||
|
||||
// WebAuthnID provides the user handle of the user account. A user handle is an opaque byte sequence with a maximum
|
||||
// size of 64 bytes, and is not meant to be displayed to the user.
|
||||
//
|
||||
// To ensure secure operation, authentication and authorization decisions MUST be made on the basis of this id
|
||||
// member, not the displayName nor name members. See Section 6.1 of [RFC8266].
|
||||
//
|
||||
// It's recommended this value is completely random and uses the entire 64 bytes.
|
||||
//
|
||||
// Specification: §5.4.3. User Account Parameters for Credential Generation (https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-id)
|
||||
func (fakeuser *fakeUser) WebAuthnID() []byte {
|
||||
return fakeuser.actualUser.PasskeyId
|
||||
}
|
||||
|
||||
// WebAuthnName provides the name attribute of the user account during registration and is a human-palatable name for the user
|
||||
// account, intended only for display. For example, "Alex Müller" or "田中倫". The Relying Party SHOULD let the user
|
||||
// choose this, and SHOULD NOT restrict the choice more than necessary.
|
||||
//
|
||||
// Specification: §5.4.3. User Account Parameters for Credential Generation (https://w3c.github.io/webauthn/#dictdef-publickeycredentialuserentity)
|
||||
func (fakeuser *fakeUser) WebAuthnName() string {
|
||||
return fakeuser.actualUser.DisplayName
|
||||
}
|
||||
|
||||
// WebAuthnDisplayName provides the name attribute of the user account during registration and is a human-palatable
|
||||
// name for the user account, intended only for display. For example, "Alex Müller" or "田中倫". The Relying Party
|
||||
// SHOULD let the user choose this, and SHOULD NOT restrict the choice more than necessary.
|
||||
//
|
||||
// Specification: §5.4.3. User Account Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialuserentity-displayname)
|
||||
func (fakeuser *fakeUser) WebAuthnDisplayName() string {
|
||||
return fakeuser.actualUser.DisplayName
|
||||
}
|
||||
|
||||
// WebAuthnCredentials provides the list of Credential objects owned by the user.
|
||||
func (fakeuser *fakeUser) WebAuthnCredentials() []webauthn.Credential {
|
||||
// Basically just convert the relevant entries from the user to a credential
|
||||
return sliceutils.Map(sliceutils.Filter(
|
||||
fakeuser.actualUser.AuthMethods,
|
||||
func(t models.UserAuthMethod) bool {
|
||||
return t.AuthMethod == models.AuthMethodPasskey ||
|
||||
t.AuthMethod == models.AuthMethodPasskey2fa
|
||||
}), func(t models.UserAuthMethod) webauthn.Credential {
|
||||
var c webauthn.Credential
|
||||
if err := json.Unmarshal(t.Token, &c); err != nil {
|
||||
// TODO: Is there any better way to handle the error here?
|
||||
log.Error().Err(err).Msg("Failed to unmarshal webauthn credential")
|
||||
}
|
||||
return c
|
||||
})
|
||||
}
|
161
auth-new/login.go
Normal file
161
auth-new/login.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||
)
|
||||
|
||||
type LoginNextState uint8
|
||||
|
||||
const (
|
||||
LoginNextFailure LoginNextState = 0 // Login failed (default state)
|
||||
LoginNextSucess LoginNextState = 1 << iota // Login suceeded
|
||||
LoginUnknown // Unknown login method type, should result in failure
|
||||
LoginNext2FaTotp // Login requires a totp token next as 2fa response
|
||||
LoginNext2FaPasskey // Login requires a passkey token next as 2fa response
|
||||
LoginNext2FaMail // Login requires an email token next as 2fa response
|
||||
LoginStartPassword // Login starts with a password
|
||||
LoginStartPasskey // Login starts with a passkey
|
||||
)
|
||||
|
||||
func ConvertNewStorageAuthMethodsToLoginState(
|
||||
methods []models.AuthenticationMethodType,
|
||||
isStart bool,
|
||||
) LoginNextState {
|
||||
translatedMethods := sliceutils.Map(methods, oneStorageAuthToLoginState)
|
||||
// Filter out only the valid methods for the current request
|
||||
valids := sliceutils.Filter(translatedMethods, func(t LoginNextState) bool {
|
||||
if isStart {
|
||||
return t == LoginStartPasskey || t == LoginStartPassword
|
||||
} else {
|
||||
return t == LoginNext2FaTotp || t == LoginNext2FaPasskey || t == LoginNext2FaMail
|
||||
}
|
||||
})
|
||||
// And then compact them down into one bit flag
|
||||
return sliceutils.Compact(
|
||||
valids,
|
||||
func(acc, next LoginNextState) LoginNextState { return acc | next },
|
||||
)
|
||||
}
|
||||
|
||||
func oneStorageAuthToLoginState(in models.AuthenticationMethodType) LoginNextState {
|
||||
switch in {
|
||||
case models.AuthMethodGAuth:
|
||||
return LoginNext2FaTotp
|
||||
case models.AuthMethodMail:
|
||||
return LoginNext2FaMail
|
||||
case models.AuthMethodPasskey:
|
||||
return LoginStartPasskey
|
||||
case models.AuthMethodPasskey2fa:
|
||||
return LoginNext2FaPasskey
|
||||
case models.AuthMethodPassword:
|
||||
return LoginStartPassword
|
||||
default:
|
||||
return LoginUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func hashPassword(password string) ([]byte, error) {
|
||||
return bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
}
|
||||
|
||||
func comparePassword(password string, hash []byte) bool {
|
||||
return bcrypt.CompareHashAndPassword(hash, []byte(password)) == nil
|
||||
}
|
||||
|
||||
// Start a login process with a username (NOT account ID) and password
|
||||
// Returns the next state, a token corresponding to that state and error
|
||||
// Token will be empty on failure, error describes the reason for the
|
||||
// failure
|
||||
func (a *Authenticator) StartPasswordLogin(
|
||||
username string,
|
||||
password string,
|
||||
) (nextState LoginNextState, token string, err error) {
|
||||
var acc *models.User
|
||||
acc, err = dbgen.User.Where(dbgen.User.Username.Eq(username)).First()
|
||||
switch err {
|
||||
case nil:
|
||||
break
|
||||
case gorm.ErrRecordNotFound:
|
||||
return LoginNextFailure, "", ErrInvalidCombination
|
||||
}
|
||||
|
||||
var method *models.UserAuthMethod
|
||||
for _, authMethod := range acc.AuthMethods {
|
||||
if oneStorageAuthToLoginState(authMethod.AuthMethod) == LoginStartPassword {
|
||||
method = &authMethod
|
||||
break
|
||||
}
|
||||
}
|
||||
if method == nil {
|
||||
return LoginNextFailure, "", ErrUnsupportedAuthMethod
|
||||
}
|
||||
if !comparePassword(password, method.Token) {
|
||||
return LoginNextFailure, "", ErrInvalidCombination
|
||||
}
|
||||
nextStates := ConvertNewStorageAuthMethodsToLoginState(
|
||||
sliceutils.Map(
|
||||
acc.AuthMethods,
|
||||
func(t models.UserAuthMethod) models.AuthenticationMethodType {
|
||||
return t.AuthMethod
|
||||
},
|
||||
),
|
||||
false,
|
||||
)
|
||||
// Catch unknown login methods
|
||||
if nextStates&LoginUnknown == LoginUnknown {
|
||||
return LoginNextFailure, "", ErrUnknownAuthMethod
|
||||
}
|
||||
|
||||
if nextStates == LoginNextFailure {
|
||||
// Login ok, no 2fa needed
|
||||
// Create a new token. Don't generate the token itself, let postgres handle that
|
||||
token := models.AccessToken{
|
||||
User: *acc,
|
||||
UserId: acc.ID,
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24 * 365),
|
||||
}
|
||||
err = dbgen.AccessToken.
|
||||
// technically, the chance of a conflict is so incredibly low that it should never ever occur
|
||||
// since both the username and a randomly generated uuid would need to be created the same. Twice
|
||||
// But, just in case, do this
|
||||
Clauses(clause.OnConflict{DoNothing: true}).
|
||||
Omit(dbgen.AccessToken.Token).
|
||||
Create(&token)
|
||||
if err != nil {
|
||||
return LoginNextFailure, "", other.Error(
|
||||
"auth",
|
||||
"failed to create new access token",
|
||||
err,
|
||||
)
|
||||
}
|
||||
return LoginNextSucess, token.Token, nil
|
||||
}
|
||||
// TODO: Generate login process token
|
||||
loginToken := models.LoginProcessToken{
|
||||
User: *acc,
|
||||
UserId: acc.ID,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 5),
|
||||
}
|
||||
err = dbgen.LoginProcessToken.Clauses(clause.OnConflict{DoNothing: true}).
|
||||
Omit(dbgen.LoginProcessToken.Token).
|
||||
Create(&loginToken)
|
||||
|
||||
if err != nil {
|
||||
return LoginNextFailure, "", other.Error(
|
||||
"auth",
|
||||
"failed to create login process token",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return nextStates, loginToken.Token, nil
|
||||
}
|
16
auth-new/passkey.go
Normal file
16
auth-new/passkey.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package auth
|
||||
|
||||
import "git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||
|
||||
func (a *Authenticator) StartPasskeyLogin(username string) error {
|
||||
acc, err := dbgen.User.Where(dbgen.User.Username.Eq(username)).First()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = acc
|
||||
panic("Not implemented") // TODO: Implement me
|
||||
}
|
||||
|
||||
func (a *Authenticator) CompletePasskeyLogin(username string) error {
|
||||
panic("Not implemented") // TODO: Implement me
|
||||
}
|
Loading…
Reference in a new issue