authenticateEmailCodeLogin handles the email + 6-digit code flow. Existing users: verify code → return user (downstream pipeline handles workspace-level gates). Unknown emails: verify code → gate checks on pre-invited workspace → create user + provision workspace.
(ctx context.Context, request *v1pb.LoginRequest)
| 1811 | // Existing users: verify code → return user (downstream pipeline handles workspace-level gates). |
| 1812 | // Unknown emails: verify code → gate checks on pre-invited workspace → create user + provision workspace. |
| 1813 | func (s *AuthService) authenticateEmailCodeLogin(ctx context.Context, request *v1pb.LoginRequest) (*store.UserMessage, error) { |
| 1814 | if request.Password != "" || request.GetIdpName() != "" { |
| 1815 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf("email_code is mutually exclusive with password and idp_name")) |
| 1816 | } |
| 1817 | email := strings.ToLower(strings.TrimSpace(request.Email)) |
| 1818 | if email == "" { |
| 1819 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf("email is required")) |
| 1820 | } |
| 1821 | |
| 1822 | codeRow, err := s.verifyEmailCode(ctx, email, storepb.EmailVerificationCodePurpose_LOGIN, *request.EmailCode) |
| 1823 | if err != nil { |
| 1824 | return nil, err |
| 1825 | } |
| 1826 | |
| 1827 | // Existing user → return. allow_email_code_signin is checked later in validateLoginPermissions |
| 1828 | // against the actually-resolved login workspace (which may not match the send-time workspace |
| 1829 | // for multi-workspace users — resolveWorkspaceForLogin prefers LastLoginWorkspace). |
| 1830 | user, err := s.store.GetUserByEmail(ctx, email) |
| 1831 | if err != nil { |
| 1832 | return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, "failed to find user")) |
| 1833 | } |
| 1834 | if user != nil { |
| 1835 | return user, nil |
| 1836 | } |
| 1837 | |
| 1838 | // Unknown email → signup path. Validate email format to prevent reserved-namespace collisions. |
| 1839 | if err := validateEndUserEmail(email); err != nil { |
| 1840 | return nil, err |
| 1841 | } |
| 1842 | |
| 1843 | // Gate checks run BEFORE user creation to prevent orphan accounts. |
| 1844 | // We only consult AllowEmailCodeSignin here: DisallowSignup governs password self-service |
| 1845 | // signup (the Signup RPC), not email-code onboarding — the two paths are independent. |
| 1846 | // Admins who want to block new users via email-code set AllowEmailCodeSignin=false. |
| 1847 | restriction, err := getAccountRestriction(ctx, s.store, s.licenseService, s.profile.SaaS, codeRow.Workspace) |
| 1848 | if err != nil { |
| 1849 | return nil, err |
| 1850 | } |
| 1851 | if !restriction.AllowEmailCodeSignin { |
| 1852 | return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Errorf("email code login is not enabled for this workspace")) |
| 1853 | } |
| 1854 | // Signup is always allowed for SaaS |
| 1855 | if !s.profile.SaaS { |
| 1856 | if restriction.DisallowSignup { |
| 1857 | return nil, connect.NewError(connect.CodePermissionDenied, errors.Errorf("sign up is disallowed for this workspace")) |
| 1858 | } |
| 1859 | } |
| 1860 | |
| 1861 | // Provision workspace BEFORE creating the user so retries are self-healing: if user |
| 1862 | // creation fails, the next attempt's FindWorkspace(email) finds the already-provisioned |
| 1863 | // workspace via its IAM binding and returns it. The reverse order would leave a user |
| 1864 | // without a workspace, and subsequent retries would early-return via GetUserByEmail and |
| 1865 | // never re-run provisioning — permanently stuck. Matches the Signup RPC's ordering. |
| 1866 | if _, err := s.provisionWorkspaceForNewUser(ctx, email); err != nil { |
| 1867 | return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, "failed to provision workspace")) |
| 1868 | } |
| 1869 | |
| 1870 | // Create principal with random bcrypt password. |
no test coverage detected