Skip to content

feat: new login workflow with password authentication - #2061

Open
Ubisoft-potato wants to merge 57 commits into
mainfrom
feat-password-authentication
Open

feat: new login workflow with password authentication #2061
Ubisoft-potato wants to merge 57 commits into
mainfrom
feat-password-authentication

Conversation

@Ubisoft-potato

@Ubisoft-potato Ubisoft-potato commented Aug 11, 2025

Copy link
Copy Markdown
Collaborator

Fix #2214

Summary

This PR implements a comprehensive password authentication system for Bucketeer with support for multiple authentication methods (password, Google OIDC, Company OIDC) and domain-based authentication policies. It introduces a new two-stage login workflow that supports multi-organization users.

Key Changes

Authentication & Authorization

  • Password Authentication: SignIn, password setup/reset flows with email notifications
  • Google OIDC: OAuth integration for Google Sign-In
  • Company OIDC: Custom OIDC provider support with PKCE
  • Two-Stage Login: Temporary token (5 min) → Organization selection → Full token (24h)
  • Domain Auth Policies: Configure authentication methods per email domain

Infrastructure

  • Database: New tables for account_credentials and domain_auth_policy
  • Email Service: Multi-provider support (SendGrid, SES, SMTP, MailerSend) with customizable templates
  • Security: Password hashing (bcrypt), token-based reset/setup flows, PKCE for OIDC

API Endpoints

POST   /v1/auth/signin_password          # Password login → temporary token
POST   /v1/auth/google_oidc/url          # Get Google OAuth URL
POST   /v1/auth/google_oidc/token        # Exchange Google code → temporary token
POST   /v1/auth/company_oidc/url         # Get Company OIDC URL
POST   /v1/auth/company_oidc/token       # Exchange Company OIDC code → temporary token
POST   /v1/auth/switch_organization      # Temporary token → full org-scoped token
POST   /v1/auth/options                  # Get auth options for email domain
POST   /v1/auth/password/setup/initiate  # Send password setup email
POST   /v1/auth/password/setup           # Complete password setup
POST   /v1/auth/password/reset/initiate  # Send password reset email
POST   /v1/auth/password/reset           # Complete password reset
PUT    /v1/auth/password                 # Update password (authenticated)
POST   /v1/auth/domain_policy            # Create domain auth policy (admin)
GET    /v1/auth/domain_policy/{domain}   # Get domain auth policy (admin)
PUT    /v1/auth/domain_policy/{domain}   # Update domain auth policy (admin)
DELETE /v1/auth/domain_policy/{domain}   # Delete domain auth policy (admin)
GET    /v1/auth/domain_policies          # List domain auth policies (admin)

New Login Workflow

The new login workflow uses a two-stage authentication process to support multi-organization users:

Stage 1: Initial Authentication → Temporary Token (5 minutes)

Users authenticate via password, Google OIDC, or Company OIDC and receive a temporary token without organization scope.

Stage 2: Organization Selection → Full Token (24 hours)

Users select their organization and exchange the temporary token for a full organization-scoped token.

API Call Flow

sequenceDiagram
    participant User
    participant Frontend
    participant Backend

    Note over User,Backend: Stage 1: Initial Authentication

    User->>Frontend: 1. Enter email
    Frontend->>Backend: POST /v1/auth/options
    Note right of Frontend: {"email": "user@gmail.com"}
    Backend-->>Frontend: Auth options for domain
    Note left of Backend: {"passwordEnabled": true,<br/>"googleOidcEnabled": true}

    alt Password Login
        User->>Frontend: 2a. Enter password
        Frontend->>Backend: POST /v1/auth/signin_password
        Note right of Frontend: {"email": "...", "password": "..."}
        Backend-->>Frontend: Temporary token (5 min)
        Note left of Backend: {"token": {"accessToken": "...",<br/>"organizationId": ""}}
    else Google OIDC Login
        User->>Frontend: 2b. Click "Sign in with Google"
        Frontend->>Backend: POST /v1/auth/google_oidc/url
        Backend-->>Frontend: Google auth URL
        User->>Frontend: Complete Google OAuth
        Frontend->>Backend: POST /v1/auth/google_oidc/token
        Note right of Frontend: {"code": "...", "state": "..."}
        Backend-->>Frontend: Temporary token (5 min)
    else Company OIDC Login
        User->>Frontend: 2c. Click "Sign in with Company SSO"
        Frontend->>Backend: POST /v1/auth/company_oidc/url
        Backend-->>Frontend: Company OIDC auth URL
        User->>Frontend: Complete Company OAuth
        Frontend->>Backend: POST /v1/auth/company_oidc/token
        Backend-->>Frontend: Temporary token (5 min)
    end

    Note over User,Backend: Stage 2: Organization Selection

    Frontend->>Backend: 3. GET /v1/account/my_organizations
    Note right of Frontend: Authorization: Bearer {tempToken}
    Backend-->>Frontend: List of organizations
    Note left of Backend: [{"id": "org1", "systemAdmin": true},<br/>{"id": "org2"}]

    User->>Frontend: 4. Select organization
    Frontend->>Backend: POST /v1/auth/switch_organization
    Note right of Frontend: {"access_token": "{tempToken}",<br/>"organization_id": "org1"}
    Backend-->>Frontend: Full token (24 hours)
    Note left of Backend: {"token": {"accessToken": "...",<br/>"organizationId": "org1",<br/>"refreshToken": "..."}}

    Frontend->>Frontend: 5. Store full token
    Frontend->>Backend: Use token for all API calls
    Note right of Frontend: Authorization: Bearer {fullToken}
Loading

Token Characteristics

Token Type Duration Organization ID Use Case
Temporary 5 minutes Empty ("") Get organizations, switch org
Full 24 hours Populated All API operations

Domain Authentication Policies

Administrators can configure authentication methods per email domain:

# Example: Configure gmail.com to allow password + Google OIDC
POST /v1/auth/domain_policy
{
  "domain": "gmail.com",
  "auth_policy": {
    "password": {"enabled": true, "required": false},
    "google_oidc": {"enabled": true, "display_name": "Sign in with Google"},
    "company_oidc": {"enabled": false}
  }
}

Password Setup Workflow

New User Account Creation Workflow

flowchart TD
    A[Admin Creates New User Account] --> B[System Creates Account in Database]
    B --> C[System Generates Password Setup Token]
    C --> D[System Sends Setup Email to User]
    D --> E[User Clicks Email Link]
    E --> F[User Sets New Password]
    F --> G[Password Setup Complete]

    style C fill:#e8f5e8
    style D fill:#fff3e0
Loading

Process:

  1. Admin creates a new user account through the admin interface
  2. System automatically creates the account and generates a secure setup token
  3. Setup email with token link is sent to the new user
  4. User clicks the link and sets their password
  5. User can now login with email/password

Existing OAuth User Password Setup Workflow

flowchart TD
    A[User Logs in via OAuth] --> B[System Checks if User Has Password]
    B --> C{Has Password?}
    C -->|Yes| D[Login Complete - No Action Needed]
    C -->|No| E[System Generates Password Setup Token]
    E --> F[System Sends Setup Email to User]
    F --> G[User Clicks Email Link]
    G --> H[User Sets New Password]
    H --> I[User Now Has Both OAuth + Password Login]

    style E fill:#e8f5e8
    style F fill:#fff3e0
Loading

Process:

  1. Existing OAuth user (Google/GitHub) logs in successfully
  2. System checks if user already has password credentials
  3. If no password exists, system generates setup token and sends email
  4. User optionally sets up password for alternative login method
  5. User can now login via OAuth OR email/password

Password Setup Page Workflow

sequenceDiagram
    participant User
    participant Frontend
    participant Backend

    Note over User, Backend: User receives setup email with setupToken
    User->>Frontend: Clicks setup link with setupToken
    Frontend->>Backend: POST /v1/auth/password/setup/validate
    Note right of Frontend: Body: {"setupToken": "xyz"}
    Backend-->>Frontend: 200 OK with {"isValid": true, "email": "user@example.com"}

    alt Token Valid
        Frontend->>User: Show password setup form with email
        User->>Frontend: Enters new password
        Frontend->>Backend: POST /v1/auth/password/setup
        Note right of Frontend: Body: {"setupToken": "xyz", "newPassword": "newpass"}
        Backend-->>Frontend: 200 OK or 400 Bad Request

        alt Setup Success
            Frontend->>User: Show success message
            Frontend->>Frontend: Redirect to login page
        else Setup Failed
            Frontend->>User: Show error message and keep form open
        end
    else Token Invalid
        Frontend->>User: Show "Invalid Token" error
    end
Loading

@Ubisoft-potato
Ubisoft-potato force-pushed the feat-password-authentication branch from 3081014 to 784d790 Compare August 19, 2025 00:39
@cre8ivejp

Copy link
Copy Markdown
Member

We don't need to do this in this PR, but we will need to implement the password and google authentication as a setting in the organization settings.
So, the user can select the authentication types that are allowed in their organization.

When inviting a new user, we will need a flow for typing the new password when accessing the console for the first time, too.

@Ubisoft-potato
Ubisoft-potato force-pushed the feat-password-authentication branch 5 times, most recently from d536062 to d95c698 Compare August 29, 2025 03:30
@Ubisoft-potato
Ubisoft-potato force-pushed the feat-password-authentication branch from d95c698 to 2dc5295 Compare September 3, 2025 02:48
Comment on lines +123 to +129
templates:
passwordChanged:
subject: "✅ Password Changed Successfully"
body: "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#333}.container{max-width:600px;margin:0 auto;padding:20px}.alert{background:#fff3cd;padding:15px;border-radius:5px;margin:20px 0}</style></head><body><div class=\"container\"><h1>✅ Password Changed Successfully</h1><p>Hello,</p><p>This email confirms that your Bucketeer password has been successfully changed.</p><div class=\"alert\"><strong>Security Notice:</strong> If you did not make this change, please contact your system administrator immediately.</div><p>Thank you for keeping your account secure.</p></div></body></html>"
passwordSetup:
subject: "🔐 Set Up Your Bucketeer Password"
body: "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>body{font-family:Arial,sans-serif;color:#333}.container{max-width:600px;margin:0 auto;padding:20px}.button{display:inline-block;padding:12px 24px;background:#007bff;color:white;text-decoration:none;border-radius:5px}.warning{background:#fff3cd;padding:15px;border-radius:5px;margin:20px 0}</style></head><body><div class=\"container\"><h1>Set Up Your Bucketeer Password</h1><p>Hello,</p><p>Your Bucketeer account is ready! To get started, please set up your password by clicking the button below:</p><p style=\"text-align:center;margin:30px 0\"><a href=\"{{setupURL}}\" class=\"button\">Set Up Password</a></p><p>Or copy and paste this link: {{setupURL}}</p><div class=\"warning\"><strong>Security Note:</strong> This link will expire in {{expirationTime}}. Never share this link with anyone. Choose a strong, unique password.</div></div></body></html>"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we support multiple languages, we need it in the templates.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, let me implement it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added ja and en language email template for now.

Comment on lines 118 to +153
demoSignIn:
enabled: true
email: demo@bucketeer.io
password: demo
organizationId: demo
organizationOwnerEmail: demo@bucketeer.io
projectId: demo
environmentId: demo
email: "demo@bucketeer.io"
password: "demo"
organizationId: "demo"
organizationOwnerEmail: "demo@bucketeer.io"
projectId: "demo"
environmentId: "demo"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are implementing password authentication to replace the old implementation.
We will also need to update the initialization scripts for the dev container and docker-compose so we can access the console when deploying.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I already implemented it, when web service started, it will create demo user's demo password to database.

@cre8ivejp

Copy link
Copy Markdown
Member

@Ubisoft-potato, can you update the PR's description to show the whole flow using Mermaid?
That will make it much easier to visualize how it works.

@Ubisoft-potato

Copy link
Copy Markdown
Collaborator Author

@Ubisoft-potato, can you update the PR's description to show the whole flow using Mermaid? That will make it much easier to visualize how it works.

Sure, I will show the whole workflow using Mermaid!

@Ubisoft-potato

Copy link
Copy Markdown
Collaborator Author

@Ubisoft-potato, can you update the PR's description to show the whole flow using Mermaid? That will make it much easier to visualize how it works.

@cre8ivejp I had updated the description with the detailed worflow, please take a look.

Comment thread manifests/bucketeer/values.dev.yaml Outdated
Comment thread pkg/account/api/account.go
Comment thread pkg/auth/api/api.go Outdated
Comment thread pkg/auth/api/api.go Outdated
Comment thread pkg/auth/api/password.go
Comment thread pkg/auth/api/password.go Outdated
@hvn2k1

hvn2k1 commented Sep 4, 2025

Copy link
Copy Markdown
Contributor

@Ubisoft-potato thank you for your great work 💯
I haven't fully reviewed your pr yet but left some comments

@Ubisoft-potato
Ubisoft-potato force-pushed the feat-password-authentication branch 4 times, most recently from 1f74eff to 8c8b275 Compare September 9, 2025 03:28
Comment thread pkg/auth/storage/sql/credentials/insert_credentials.sql
@Ubisoft-potato
Ubisoft-potato marked this pull request as ready for review September 10, 2025 03:50
Comment thread pkg/auth/api/api.go Outdated
@Ubisoft-potato
Ubisoft-potato force-pushed the feat-password-authentication branch from 8c8b275 to d295fa4 Compare September 12, 2025 02:41
Comment thread pkg/auth/api/password.go Outdated
Ubisoft-potato and others added 29 commits August 26, 2026 09:20
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
…ication

� Conflicts:
�	pkg/web/cmd/server/server.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 88 out of 95 changed files in this pull request and generated 15 comments.

Suppressed comments (4)

pkg/auth/api/signin.go:112

  • Password authentication never checks the domain policy before accepting credentials. A user who already has a password can call this endpoint even when password auth is disabled or company OIDC is required, making the policy only a UI hint. Normalize the email, load the active policy, and reject password authentication unless it is enabled before validating credentials.
    pkg/auth/api/company_oidc.go:138
  • The token exchange repeats the company-option check but again ignores the policy's top-level Enabled flag. Even if URL generation is fixed, an authorization code obtained earlier can still be exchanged after the policy is disabled. Reject disabled policies here too.
	// Check if company OIDC is enabled
	if policy.AuthPolicy == nil || policy.AuthPolicy.CompanyOidc == nil || !policy.AuthPolicy.CompanyOidc.Enabled {

pkg/account/api/admin_account.go:272

  • This GET path creates empty credentials and reports password setup as required without consulting the domain policy. Google-only or company-OIDC-required users will be prompted to set a password, while InitiatePasswordSetup later refuses to send them a link. Make this a read-only policy-aware check and only report setup required when password auth is enabled.
	// At this point: credentials either don't exist OR exist with empty password hash
	// If credentials don't exist, create empty credentials record for frontend password setup flow
	if err != nil && errors.Is(err, authstorage.ErrCredentialsNotFound) {
		err = s.credentialsStorage.CreateCredentials(ctx, email, "")

pkg/email/service.go:88

  • resetURL embeds a bearer reset token, so writing it to application logs exposes an account-takeover credential. Remove the URL from the log and retain only non-sensitive recipient/language metadata.

Comment thread pkg/auth/api/signin.go
Comment on lines +37 to +38

return s.handlePasswordSignIn(ctx, request)
Comment on lines +62 to +64
// Exchange code for user info using existing Google authenticator
userInfo, err := s.googleAuthenticator.Exchange(ctx, request.Code, request.RedirectUrl)
if err != nil {
}

// Check if company OIDC is enabled
if policy.AuthPolicy == nil || policy.AuthPolicy.CompanyOidc == nil || !policy.AuthPolicy.CompanyOidc.Enabled {
Comment on lines +153 to +154
// Exchange code for token and get user info
userInfo, err := provider.ExchangeToken(ctx, request.Code, request.CodeVerifier, request.Nonce)
Comment on lines +34 to +36
string issuer = 3;
string client_id = 4;
string client_secret = 5; // Never returned to client
resetPath:
setupPath:
tokenParam:
demoSignIn:
Comment thread pkg/email/service.go
Comment on lines +74 to +78
s.logger.Info("No-op email service: password setup email not sent",
zap.String("to", to),
zap.String("setupURL", setupURL),
zap.String("language", language),
)
Comment on lines +65 to +68
s.logger.Error("Failed to exchange Google OIDC code",
zap.Error(err),
zap.String("code", request.Code),
)
Comment on lines +95 to +98
now := time.Now().Unix()
policy := &authdomain.DomainAuthPolicy{
Domain: request.Domain,
AuthPolicy: request.AuthPolicy,
Comment thread pkg/auth/api/password.go
Comment on lines +433 to +437
func (s *authService) ResetPassword(
ctx context.Context,
request *authproto.ResetPasswordRequest,
) (*authproto.ResetPasswordResponse, error) {
err := validateResetPasswordRequest(request)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: SSO integration support (SAML/OIDC) for admin console login

4 participants