package machines import ( "context" "errors" "strings" "git.1in9.net/raider/wroofauth/internal/entities/user" ) var ( ErrIllegalStateAction = errors.New("illegal action for this state") ErrIllegalState = errors.New("illegal state") ) type SessionState string const ( SessionState_EMPTY SessionState = "EMPTY" SessionState_UNAUTHENTICATED SessionState = "UNAUTHENTICATED" SessionState_AWAITING_FACTOR SessionState = "AWAITING_FACTOR" SessionState_AUTHENTICATED_PENDING SessionState = "AUTHENTICATED_PENDING" SessionState_AUTHENTICATED_FULLY SessionState = "AUTHENTICATED_FULLY" SessionState_AUTHENTICATED_PASSWORD_CHANGE SessionState = "AUTHENTICATED_PASSWORD_CHANGE" SessionState_AUTHENTICATED_2FA_ENROLL SessionState = "AUTHENTICATED_2FA_ENROLL" SessionState_AUTHENTICATED_REVIEW_TOS SessionState = "AUTHENTICATED_REVIEW_TOS" SessionState_AUTHENTICATED_REVIEW_RECOVERY SessionState = "AUTHENTICATED_REVIEW_RECOVERY" ) type AuthenticationMethod string const ( AuthenticationMethod_NONE = "NONE" AuthenticationMethod_PASSWORD = "PASSWORD" ) type SecondFactor string const ( SecondFactor_NONE = "NONE" SecondFactor_TOTP = "TOTP" ) type Session struct { State SessionState AuthenticationMethod AuthenticationMethod SecondFactor SecondFactor User *user.User } func NewSession(ctx context.Context) *Session { return &Session{ State: SessionState_EMPTY, AuthenticationMethod: AuthenticationMethod_NONE, SecondFactor: SecondFactor_NONE, } } // s.Validate checks if the session is in a valid state func (s *Session) Validate(ctx context.Context) error { if s.IsAnyAuthenticated() { if s.User == nil { // We can only be here if a user is set return ErrIllegalState } if s.AuthenticationMethod == AuthenticationMethod_NONE { // We can not be authenticated without any way we got here return ErrIllegalState } if s.SecondFactor == SecondFactor_NONE && s.User.Needs2FA() { // User has either just enabled 2FA, or something went horribly wrong. // Either way, throw away the session! return ErrIllegalState } } return nil } func (s *Session) IsAnyAuthenticated() bool { return strings.HasPrefix(string(s.State), "AUTHENTICATED_") } func (s *Session) performPreflight(ctx context.Context) error { // TODO: Do Preflight Checks. // TODO: Do PASSWORD_EXPIRED check // TODO: Do 2FA-Force-Enrolment check // TODO: Do TOS check // TODO: Do Reveiw Recovery check // TODO: Do Flag check // Preflight ok. s.State = SessionState_AUTHENTICATED_FULLY return nil } func (s *Session) HandleIdentification(ctx context.Context, identification string) error { if s.State != SessionState_EMPTY { return ErrIllegalStateAction // This step may only run on EMPTY sessions } user, err := user.GetByIdentification(ctx, identification) if err != nil { return err } if user == nil { panic("no error, but nil user") } s.User = user //TODO: Check for SAML s.State = SessionState_UNAUTHENTICATED return nil } func (s *Session) HandlePassword(ctx context.Context, password string) error { if s.State != SessionState_UNAUTHENTICATED { return ErrIllegalStateAction // This step may only run on UNAUTHENTICATED sessions } err := s.User.CheckPassword(password) if err != nil { return err } // Password Ok. s.AuthenticationMethod = AuthenticationMethod_PASSWORD if !s.User.Needs2FA() { // No 2fa, jump to AUTHENTICATED_PENDING for preflight s.State = SessionState_AUTHENTICATED_PENDING return s.performPreflight(ctx) } s.State = SessionState_AWAITING_FACTOR return nil } // TODO: Passkey action func (s *Session) HandleTOTP(ctx context.Context, otp string) error { if s.State != SessionState_AWAITING_FACTOR { return ErrIllegalStateAction } err := s.User.ValidateTOTP(otp) if err != nil { return err } // OTP Ok. s.SecondFactor = SecondFactor_TOTP // Good to go for preflight. s.State = SessionState_AUTHENTICATED_PENDING return s.performPreflight(ctx) } func (s *Session) HandleLock(ctx context.Context) error { if !s.IsAnyAuthenticated() { return ErrIllegalStateAction } // TODO: Handle Lock return nil } func (s *Session) HandleLogout(ctx context.Context) error { if !s.IsAnyAuthenticated() { return ErrIllegalStateAction } // TODO: Handle Logout return nil } func (s *Session) Destroy(ctx context.Context) error { // TODO: Destroy Session return nil }