feat: new login workflow with password authentication - #2061
feat: new login workflow with password authentication #2061Ubisoft-potato wants to merge 57 commits into
Conversation
3081014 to
784d790
Compare
|
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. When inviting a new user, we will need a flow for typing the new password when accessing the console for the first time, too. |
d536062 to
d95c698
Compare
d95c698 to
2dc5295
Compare
| 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>" |
There was a problem hiding this comment.
Since we support multiple languages, we need it in the templates.
There was a problem hiding this comment.
Oh, let me implement it.
There was a problem hiding this comment.
I added ja and en language email template for now.
| 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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Yes, I already implemented it, when web service started, it will create demo user's demo password to database.
|
@Ubisoft-potato, can you update the PR's description to show the whole flow using Mermaid? |
Sure, I will show the whole workflow using Mermaid! |
@cre8ivejp I had updated the description with the detailed worflow, please take a look. |
|
@Ubisoft-potato thank you for your great work 💯 |
1f74eff to
8c8b275
Compare
8c8b275 to
d295fa4
Compare
…iple authentication types
…dChangedNotification
…ticationSettings structure
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
…s and password setup requirement
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
There was a problem hiding this comment.
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
Enabledflag. 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
InitiatePasswordSetuplater 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
resetURLembeds 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.
|
|
||
| return s.handlePasswordSignIn(ctx, request) |
| // 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 { |
| // Exchange code for token and get user info | ||
| userInfo, err := provider.ExchangeToken(ctx, request.Code, request.CodeVerifier, request.Nonce) |
| string issuer = 3; | ||
| string client_id = 4; | ||
| string client_secret = 5; // Never returned to client |
| resetPath: | ||
| setupPath: | ||
| tokenParam: | ||
| demoSignIn: |
| s.logger.Info("No-op email service: password setup email not sent", | ||
| zap.String("to", to), | ||
| zap.String("setupURL", setupURL), | ||
| zap.String("language", language), | ||
| ) |
| s.logger.Error("Failed to exchange Google OIDC code", | ||
| zap.Error(err), | ||
| zap.String("code", request.Code), | ||
| ) |
| now := time.Now().Unix() | ||
| policy := &authdomain.DomainAuthPolicy{ | ||
| Domain: request.Domain, | ||
| AuthPolicy: request.AuthPolicy, |
| func (s *authService) ResetPassword( | ||
| ctx context.Context, | ||
| request *authproto.ResetPasswordRequest, | ||
| ) (*authproto.ResetPasswordResponse, error) { | ||
| err := validateResetPasswordRequest(request) |
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
Infrastructure
account_credentialsanddomain_auth_policyAPI Endpoints
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}Token Characteristics
"")Domain Authentication Policies
Administrators can configure authentication methods per email domain:
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:#fff3e0Process:
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:#fff3e0Process:
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