This commit is contained in:
+35
-17
@@ -31,8 +31,14 @@ type User struct {
|
||||
ID, Username, Email, DisplayName, Status string
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
PasswordChangeRequired bool
|
||||
// RegistrationPending keeps a partially completed public registration
|
||||
// ineligible for authentication until its credentials, personal scope, and
|
||||
// recovery material have been committed atomically.
|
||||
RegistrationPending bool
|
||||
}
|
||||
|
||||
func (user User) Active() bool { return user.Status == "active" && !user.RegistrationPending }
|
||||
|
||||
type Principal struct {
|
||||
User User
|
||||
Roles []string
|
||||
@@ -167,7 +173,7 @@ func (service *Service) ChangePassword(ctx context.Context, userID, currentPassw
|
||||
if !VerifyPassword(currentHash, currentPassword) {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
if user.Status != "active" {
|
||||
if !user.Active() {
|
||||
return ErrInactiveUser
|
||||
}
|
||||
if currentPassword == newPassword {
|
||||
@@ -199,7 +205,7 @@ func (service *Service) ResetPassword(ctx context.Context, input AdministrativeP
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("auth: load credentials for administrative reset: %w", err)
|
||||
}
|
||||
if user.Status != "active" {
|
||||
if !user.Active() {
|
||||
return User{}, ErrInactiveUser
|
||||
}
|
||||
if VerifyPassword(currentHash, input.TemporaryPassword) {
|
||||
@@ -233,24 +239,36 @@ func (service *Service) ResetPassword(ctx context.Context, input AdministrativeP
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// VerifyPassword verifies the password credential for an active account
|
||||
// without creating a session. Applications use it as the first step of a
|
||||
// bounded multi-factor ceremony and must not treat success as an authenticated
|
||||
// browser session on its own.
|
||||
func (service *Service) VerifyPassword(ctx context.Context, identifier, password string) (User, error) {
|
||||
user, hash, err := service.repository.CredentialByIdentifier(ctx, strings.TrimSpace(identifier))
|
||||
if errors.Is(err, ErrUserNotFound) {
|
||||
_ = VerifyPassword(dummyPasswordHash, password)
|
||||
return User{}, ErrInvalidCredentials
|
||||
}
|
||||
if err != nil {
|
||||
_ = VerifyPassword(dummyPasswordHash, password)
|
||||
return User{}, fmt.Errorf("auth: load credentials: %w", err)
|
||||
}
|
||||
if !VerifyPassword(hash, password) {
|
||||
return User{}, ErrInvalidCredentials
|
||||
}
|
||||
if !user.Active() {
|
||||
return User{}, ErrInactiveUser
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (service *Service) Authenticate(ctx context.Context, identifier, password string, lifetime time.Duration) (string, Principal, error) {
|
||||
if lifetime < 5*time.Minute || lifetime > 30*24*time.Hour {
|
||||
return "", Principal{}, errors.New("auth: invalid session lifetime")
|
||||
}
|
||||
user, hash, err := service.repository.CredentialByIdentifier(ctx, strings.TrimSpace(identifier))
|
||||
if errors.Is(err, ErrUserNotFound) {
|
||||
_ = VerifyPassword(dummyPasswordHash, password)
|
||||
return "", Principal{}, ErrInvalidCredentials
|
||||
}
|
||||
user, err := service.VerifyPassword(ctx, identifier, password)
|
||||
if err != nil {
|
||||
_ = VerifyPassword(dummyPasswordHash, password)
|
||||
return "", Principal{}, fmt.Errorf("auth: load credentials: %w", err)
|
||||
}
|
||||
if !VerifyPassword(hash, password) {
|
||||
return "", Principal{}, ErrInvalidCredentials
|
||||
}
|
||||
if user.Status != "active" {
|
||||
return "", Principal{}, ErrInactiveUser
|
||||
return "", Principal{}, err
|
||||
}
|
||||
return service.IssueSession(ctx, user.ID, lifetime)
|
||||
}
|
||||
@@ -282,7 +300,7 @@ func (service *Service) IssueSession(ctx context.Context, userID string, lifetim
|
||||
_ = service.repository.DeleteSession(ctx, digest)
|
||||
return "", Principal{}, err
|
||||
}
|
||||
if principal.User.Status != "active" {
|
||||
if !principal.User.Active() {
|
||||
_ = service.repository.DeleteSession(ctx, digest)
|
||||
return "", Principal{}, ErrInactiveUser
|
||||
}
|
||||
@@ -302,7 +320,7 @@ func (service *Service) Session(ctx context.Context, token string) (Principal, e
|
||||
if err != nil {
|
||||
return Principal{}, fmt.Errorf("auth: load session: %w", err)
|
||||
}
|
||||
if principal.User.Status != "active" {
|
||||
if !principal.User.Active() {
|
||||
_ = service.repository.DeleteSession(ctx, digest)
|
||||
return Principal{}, ErrInactiveUser
|
||||
}
|
||||
|
||||
@@ -57,6 +57,31 @@ func TestIssueSessionRejectsInactiveRepositoryPrincipal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPasswordDoesNotIssueSession(t *testing.T) {
|
||||
hash, err := HashPassword("correct horse battery staple")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repository := &credentialRepository{
|
||||
user: User{ID: "valid-user-id", Username: "person", Email: "person@example.test", Status: "active"},
|
||||
hash: hash,
|
||||
}
|
||||
service, err := New(repository, Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user, err := service.VerifyPassword(t.Context(), "person@example.test", "correct horse battery staple")
|
||||
if err != nil || user.ID != repository.user.ID {
|
||||
t.Fatalf("user=%+v err=%v", user, err)
|
||||
}
|
||||
if repository.sessionCreated {
|
||||
t.Fatal("password verification issued a session")
|
||||
}
|
||||
if _, err = service.VerifyPassword(t.Context(), "person@example.test", "wrong password"); !errors.Is(err, ErrInvalidCredentials) {
|
||||
t.Fatalf("wrong password err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type recordingRepository struct {
|
||||
repositoryStub
|
||||
deleted bool
|
||||
@@ -68,6 +93,22 @@ type activeSessionRepository struct {
|
||||
deleted bool
|
||||
}
|
||||
|
||||
type credentialRepository struct {
|
||||
repositoryStub
|
||||
user User
|
||||
hash string
|
||||
sessionCreated bool
|
||||
}
|
||||
|
||||
func (repository *credentialRepository) CredentialByIdentifier(context.Context, string) (User, string, error) {
|
||||
return repository.user, repository.hash, nil
|
||||
}
|
||||
|
||||
func (repository *credentialRepository) CreateSession(context.Context, Session) error {
|
||||
repository.sessionCreated = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repository *activeSessionRepository) PrincipalBySession(context.Context, [32]byte, time.Time) (Principal, Session, error) {
|
||||
return repository.principal, Session{}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user