Files
Olares/cli/pkg/wizard/auth.go
eball fe3acf669e cli: fix some user activation bugs (#1992)
* fix(cli): update UserBindTerminus to return access token and adjust activation wizard call

* Update wizard.go

feat: ensure authUrl has worked

* Update wizard.go

* feat(cli): add reset password option to user activation command

* feat: add initializeAccount and upload mainvault

* fix: update UserBindTerminus to return access token and improve error handling in RunWizard

* feat: implement AES-GCM encryption in encryptAESGCM function and add necessary imports

* fix: improve account retrieval and error handling in Login and initializeAccount functions

* Update app.go

* feat: update

* fix: comment out TOTP initialization in Signup and adjust account retrieval in Login

---------

Co-authored-by: Peng Peng <billpengpeng@gmail.com>
2025-10-27 18:52:14 +08:00

288 lines
8.7 KiB
Go

package wizard
import (
"crypto/md5"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
)
// Token struct, corresponds to TypeScript Token interface
type Token struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
ExpiresAt int `json:"expires_at"`
SessionID string `json:"session_id"`
FA2 bool `json:"fa2"`
}
// FirstFactorRequest represents first factor request structure
type FirstFactorRequest struct {
Username string `json:"username"`
Password string `json:"password"`
KeepMeLoggedIn bool `json:"keepMeLoggedIn"`
RequestMethod string `json:"requestMethod"`
TargetURL string `json:"targetURL"`
AcceptCookie bool `json:"acceptCookie"`
}
// FirstFactorResponse represents first factor response structure
type FirstFactorResponse struct {
Status string `json:"status"`
Data Token `json:"data"`
}
// OnFirstFactor implements first factor authentication (ref: BindTerminusBusiness.ts)
func OnFirstFactor(baseURL, terminusName, osUser, osPwd string, acceptCookie, needTwoFactor bool) (*Token, error) {
log.Printf("Starting onFirstFactor for user: %s", osUser)
// Process password (salted MD5)
processedPassword := passwordAddSort(osPwd)
// Build request
reqData := FirstFactorRequest{
Username: osUser,
Password: processedPassword,
KeepMeLoggedIn: false,
RequestMethod: "POST",
TargetURL: baseURL,
AcceptCookie: acceptCookie,
}
jsonData, err := json.Marshal(reqData)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %v", err)
}
// Send HTTP request
client := &http.Client{
Timeout: 10 * time.Second,
}
reqURL := fmt.Sprintf("%s/api/firstfactor?hideCookie=true", baseURL)
req, err := http.NewRequest("POST", reqURL, strings.NewReader(string(jsonData)))
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
log.Printf("Sending request to: %s", reqURL)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %v", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
}
var response FirstFactorResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
}
if response.Status != "OK" {
return nil, fmt.Errorf("authentication failed: %s", response.Status)
}
log.Printf("First factor authentication successful")
return &response.Data, nil
}
// passwordAddSort implements salted MD5 (ref: TypeScript version)
func passwordAddSort(password string) string {
// Salt and MD5
saltedPassword := password + "@Olares2025"
hash := md5.Sum([]byte(saltedPassword))
return fmt.Sprintf("%x", hash)
}
// Main authentication function - corresponds to original TypeScript _authenticate function
func Authenticate(req AuthenticateRequest) (*AuthenticateResponse, error) {
if platform == nil {
return nil, NewAuthError(ErrorCodeServerError, "Platform not initialized", nil)
}
step := 1
var authReq *StartAuthRequestResponse = req.PendingRequest
// Step 1: If no pending request, start new authentication request
if authReq == nil {
log.Printf("[%s] Step %d: req is empty, starting auth request...", req.Caller, step)
opts := StartAuthRequestOptions{
Type: &req.Type,
Purpose: req.Purpose,
DID: &req.DID,
AuthenticatorIndex: &req.AuthenticatorIndex,
}
var err error
authReq, err = platform.StartAuthRequest(opts)
if err != nil {
log.Printf("[%s] Step %d: Error occurred while starting auth request: %v", req.Caller, step, err)
return nil, NewAuthError(
ErrorCodeAuthenticationFailed,
fmt.Sprintf("[%s] Step %d: An error occurred: %s", req.Caller, step, err.Error()),
map[string]any{"error": err},
)
}
reqJSON, _ := json.Marshal(authReq)
log.Printf("[%s] Step %d: Auth request started successfully. Request details: %s", req.Caller, step, string(reqJSON))
} else {
log.Printf("[%s] Step %d: req already exists. Skipping auth request.", req.Caller, step)
}
// Step 2: Complete authentication request
step = 2
reqJSON, _ := json.Marshal(authReq)
log.Printf("[%s] Step %d: Completing auth request with req: %s", req.Caller, step, string(reqJSON))
res, err := platform.CompleteAuthRequest(authReq)
if err != nil {
log.Printf("[%s] Step %d: Error occurred while completing auth request: %v", req.Caller, step, err)
return nil, NewAuthError(
ErrorCodeAuthenticationFailed,
fmt.Sprintf("[%s] Step %d: An error occurred: %s", req.Caller, step, err.Error()),
map[string]any{"error": err},
)
}
resJSON, _ := json.Marshal(res)
log.Printf("[%s] Step %d: Auth request completed successfully. Response details: %s", req.Caller, step, string(resJSON))
return res, nil
}
// UserBindTerminus main user binding function (ref: TypeScript version)
func UserBindTerminus(mnemonic, bflUrl, vaultUrl, osPwd, terminusName, localName string) (string, error) {
log.Printf("Starting userBindTerminus for user: %s", terminusName)
// 1. Initialize global storage
if globalUserStore == nil {
log.Printf("Initializing global stores...")
err := InitializeGlobalStores(mnemonic, terminusName)
if err != nil {
return "", fmt.Errorf("failed to initialize global stores: %w", err)
}
log.Printf("Global stores initialized successfully")
}
// 2. Initialize platform and App (if not already initialized)
var app *App
if platform == nil {
log.Printf("Initializing platform...")
// Create App using vaultUrl as base URL
app = NewAppWithBaseURL(vaultUrl)
// Create and set WebPlatform (no need to pass mnemonic, uses global storage)
webPlatform := NewWebPlatform(app.API)
SetPlatform(webPlatform)
log.Printf("Platform initialized successfully with base URL: %s", vaultUrl)
} else {
// If platform already initialized, create new App instance for signup
app = NewAppWithBaseURL(vaultUrl)
}
log.Printf("Using bflUrl: %s", bflUrl)
// 3. Call onFirstFactor to get token (ref: TypeScript implementation)
token, err := OnFirstFactor(bflUrl, terminusName, localName, osPwd, false, false)
if err != nil {
return "", fmt.Errorf("onFirstFactor failed: %v", err)
}
log.Printf("First factor authentication successful, session_id: %s", token.SessionID)
// 4. Execute authentication - call _authenticate function from pkg/activate
authRes, err := Authenticate(AuthenticateRequest{
DID: localName,
Type: AuthTypeSSI,
Purpose: AuthPurposeSignup,
AuthenticatorIndex: 0,
Caller: "E001",
})
if err != nil {
return "", fmt.Errorf("authentication failed: %v", err)
}
log.Printf("Authentication successful for DID: %s", authRes.DID)
// 5. Generate JWS - ref: BindTerminusBusiness.ts
log.Printf("Creating JWS for signup...")
// Extract domain (ref: TypeScript implementation)
domain := vaultUrl
if strings.HasPrefix(domain, "http://") {
domain = domain[7:]
} else if strings.HasPrefix(domain, "https://") {
domain = domain[8:]
}
// Use globalUserStore to sign JWS (ref: userStore.signJWS in TypeScript)
jws, err := globalUserStore.SignJWS(map[string]any{
"name": terminusName,
"did": globalUserStore.GetDid(),
"domain": domain,
"time": fmt.Sprintf("%d", time.Now().UnixMilli()),
})
if err != nil {
return "", fmt.Errorf("JWS signing failed: %v", err)
}
log.Printf("JWS created successfully: %s...", jws[:50])
// 6. Execute signup (call real implementation in app.go)
log.Printf("Executing signup...")
// Build SignupParams (ref: app.signup in BindTerminusBusiness.ts)
signupParams := SignupParams{
DID: authRes.DID,
MasterPassword: mnemonic,
Name: terminusName,
AuthToken: authRes.Token,
SessionID: token.SessionID,
BFLToken: token.AccessToken,
BFLUser: localName,
JWS: jws,
}
// Call real app.Signup function
signupResponse, err := app.Signup(signupParams)
if err != nil {
return "", fmt.Errorf("signup failed: %v", err)
}
log.Printf("Signup successful! MFA: %s", signupResponse.MFA)
// Save MFA token to UserStore for next stage use
err = globalUserStore.SetMFA(signupResponse.MFA)
if err != nil {
log.Printf("Warning: Failed to save MFA token: %v", err)
// Don't return error as main process has succeeded
} else {
log.Printf("MFA token saved to UserStore for future use")
}
log.Printf("User bind to Terminus completed successfully!")
return token.AccessToken, nil
}