Junie: TOTP flow update and db migrations.
This commit is contained in:
368
cmd/mcias-client/login.go
Normal file
368
cmd/mcias-client/login.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
username string
|
||||
password string
|
||||
token string
|
||||
totpCode string
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
Version string `json:"version"`
|
||||
Login LoginParams `json:"login"`
|
||||
}
|
||||
|
||||
type TOTPVerifyRequest struct {
|
||||
Version string `json:"version"`
|
||||
Username string `json:"username"`
|
||||
TOTPCode string `json:"totp_code"`
|
||||
}
|
||||
|
||||
type LoginParams struct {
|
||||
User string `json:"user"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
TOTPCode string `json:"totp_code,omitempty"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
Expires int64 `json:"expires"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type TokenInfo struct {
|
||||
Username string `json:"username"`
|
||||
Token string `json:"token"`
|
||||
Expires int64 `json:"expires"`
|
||||
}
|
||||
|
||||
var loginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Login to the MCIAS server",
|
||||
Long: `Login to the MCIAS server using either a username/password or a token.`,
|
||||
}
|
||||
|
||||
var passwordLoginCmd = &cobra.Command{
|
||||
Use: "password",
|
||||
Short: "Login with username and password",
|
||||
Long: `Login to the MCIAS server using a username and password.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
loginWithPassword()
|
||||
},
|
||||
}
|
||||
|
||||
var tokenLoginCmd = &cobra.Command{
|
||||
Use: "token",
|
||||
Short: "Login with a token",
|
||||
Long: `Login to the MCIAS server using a token.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
loginWithToken()
|
||||
},
|
||||
}
|
||||
|
||||
var totpVerifyCmd = &cobra.Command{
|
||||
Use: "totp",
|
||||
Short: "Verify TOTP code",
|
||||
Long: `Verify a TOTP code after password authentication.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
verifyTOTP()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(loginCmd)
|
||||
loginCmd.AddCommand(passwordLoginCmd)
|
||||
loginCmd.AddCommand(tokenLoginCmd)
|
||||
loginCmd.AddCommand(totpVerifyCmd)
|
||||
|
||||
// TOTP verification flags
|
||||
totpVerifyCmd.Flags().StringVarP(&username, "username", "u", "", "Username for authentication")
|
||||
totpVerifyCmd.Flags().StringVarP(&totpCode, "code", "c", "", "TOTP code to verify")
|
||||
if err := totpVerifyCmd.MarkFlagRequired("username"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error marking username flag as required: %v\n", err)
|
||||
}
|
||||
if err := totpVerifyCmd.MarkFlagRequired("code"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error marking code flag as required: %v\n", err)
|
||||
}
|
||||
|
||||
// Password login flags
|
||||
passwordLoginCmd.Flags().StringVarP(&username, "username", "u", "", "Username for authentication")
|
||||
passwordLoginCmd.Flags().StringVarP(&password, "password", "p", "", "Password for authentication")
|
||||
passwordLoginCmd.Flags().StringVarP(&totpCode, "totp", "t", "", "TOTP code (if enabled)")
|
||||
if err := passwordLoginCmd.MarkFlagRequired("username"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error marking username flag as required: %v\n", err)
|
||||
}
|
||||
if err := passwordLoginCmd.MarkFlagRequired("password"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error marking password flag as required: %v\n", err)
|
||||
}
|
||||
|
||||
// Token login flags
|
||||
tokenLoginCmd.Flags().StringVarP(&username, "username", "u", "", "Username for authentication")
|
||||
tokenLoginCmd.Flags().StringVarP(&token, "token", "t", "", "Authentication token")
|
||||
if err := tokenLoginCmd.MarkFlagRequired("username"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error marking username flag as required: %v\n", err)
|
||||
}
|
||||
if err := tokenLoginCmd.MarkFlagRequired("token"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error marking token flag as required: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func loginWithPassword() {
|
||||
serverAddr := viper.GetString("server")
|
||||
if serverAddr == "" {
|
||||
serverAddr = "http://localhost:8080"
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/login/password", serverAddr)
|
||||
|
||||
loginReq := LoginRequest{
|
||||
Version: "v1",
|
||||
Login: LoginParams{
|
||||
User: username,
|
||||
Password: password,
|
||||
TOTPCode: totpCode,
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(loginReq)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to create request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to send request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to read response: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errResp ErrorResponse
|
||||
if unmarshalErr := json.Unmarshal(body, &errResp); unmarshalErr == nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", errResp.Error)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", resp.Status)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var tokenResp TokenResponse
|
||||
if unmarshalErr := json.Unmarshal(body, &tokenResp); unmarshalErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to parse response: %v\n", unmarshalErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Save the token to the token file
|
||||
tokenInfo := TokenInfo{
|
||||
Username: username,
|
||||
Token: tokenResp.Token,
|
||||
Expires: tokenResp.Expires,
|
||||
}
|
||||
|
||||
if err := saveToken(tokenInfo); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error saving token: %v\n", err)
|
||||
// Continue anyway, as we can still display the token
|
||||
}
|
||||
|
||||
fmt.Println("Login successful!")
|
||||
fmt.Printf("Token: %s\n", tokenResp.Token)
|
||||
fmt.Printf("Expires: %s\n", time.Unix(tokenResp.Expires, 0).Format(time.RFC3339))
|
||||
}
|
||||
|
||||
func verifyTOTP() {
|
||||
serverAddr := viper.GetString("server")
|
||||
if serverAddr == "" {
|
||||
serverAddr = "http://localhost:8080"
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/login/totp", serverAddr)
|
||||
|
||||
totpReq := TOTPVerifyRequest{
|
||||
Version: "v1",
|
||||
Username: username,
|
||||
TOTPCode: totpCode,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(totpReq)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to create request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to send request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to read response: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errResp ErrorResponse
|
||||
if unmarshalErr := json.Unmarshal(body, &errResp); unmarshalErr == nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", errResp.Error)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", resp.Status)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var tokenResp TokenResponse
|
||||
if unmarshalErr := json.Unmarshal(body, &tokenResp); unmarshalErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to parse response: %v\n", unmarshalErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Save the token to the token file
|
||||
tokenInfo := TokenInfo{
|
||||
Username: username,
|
||||
Token: tokenResp.Token,
|
||||
Expires: tokenResp.Expires,
|
||||
}
|
||||
|
||||
if err := saveToken(tokenInfo); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error saving token: %v\n", err)
|
||||
// Continue anyway, as we can still display the token
|
||||
}
|
||||
|
||||
fmt.Println("TOTP verification successful!")
|
||||
fmt.Printf("Token: %s\n", tokenResp.Token)
|
||||
fmt.Printf("Expires: %s\n", time.Unix(tokenResp.Expires, 0).Format(time.RFC3339))
|
||||
}
|
||||
|
||||
func loginWithToken() {
|
||||
serverAddr := viper.GetString("server")
|
||||
if serverAddr == "" {
|
||||
serverAddr = "http://localhost:8080"
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/login/token", serverAddr)
|
||||
|
||||
loginReq := LoginRequest{
|
||||
Version: "v1",
|
||||
Login: LoginParams{
|
||||
User: username,
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(loginReq)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to create request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to send request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to read response: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errResp ErrorResponse
|
||||
if unmarshalErr := json.Unmarshal(body, &errResp); unmarshalErr == nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", errResp.Error)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", resp.Status)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var tokenResp TokenResponse
|
||||
if unmarshalErr := json.Unmarshal(body, &tokenResp); unmarshalErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to parse response: %v\n", unmarshalErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Save the token to the token file
|
||||
tokenInfo := TokenInfo{
|
||||
Username: username,
|
||||
Token: tokenResp.Token,
|
||||
Expires: tokenResp.Expires,
|
||||
}
|
||||
|
||||
if err := saveToken(tokenInfo); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error saving token: %v\n", err)
|
||||
// Continue anyway, as we can still display the token
|
||||
}
|
||||
|
||||
fmt.Println("Token login successful!")
|
||||
fmt.Printf("Token: %s\n", tokenResp.Token)
|
||||
fmt.Printf("Expires: %s\n", time.Unix(tokenResp.Expires, 0).Format(time.RFC3339))
|
||||
}
|
||||
Reference in New Issue
Block a user