369 lines
9.6 KiB
Go
369 lines
9.6 KiB
Go
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))
|
|
}
|