Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
bin/
logs/app.log
120 changes: 76 additions & 44 deletions cli/cmd/auth_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,57 @@ package cmd

import (
"bufio"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)

// TODO: Update with real auth URL
const authUrl = "https://example.com/login"
const (
serverBaseUrl = "http://localhost:3000"
authUrl = serverBaseUrl + "/oauth/authorize"
tokenUrl = serverBaseUrl + "/oauth/token"
clientID = "cli"
redirectURI = serverBaseUrl + "/oauth/callback"
)

type LoginCmd struct {
}

func openUrl() error {
func generateRandomString(n int) (string, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}

func generateCodeChallenge(verifier string) string {
s := sha256.New()
s.Write([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(s.Sum(nil))
}

func openUrl(url string) error {
var cmd *exec.Cmd
url := authUrl
switch runtime.GOOS {
case "darwin":
cmd = exec.Command("open", url)
case "windows":
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
default: // Linux, BSD, etc.
default: // Linux
cmd = exec.Command("xdg-open", url)
}

return cmd.Start()
}

Expand All @@ -51,59 +74,68 @@ func saveTokenToConfig(ctx *AppContext, token string) error {
if err := os.WriteFile(configPath, data, 0o600); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
fmt.Println("Token saved:", token)
fmt.Println("Token saved")
return nil
}

func getLongLivedToken(shortLivedToken string) (string, error) {
// Placeholder for actual implementation to exchange short-lived token for long-lived token
// In a real scenario, this would involve making an HTTP request to the auth server
return shortLivedToken + "_long_lived", nil
}

func (l *LoginCmd) Run(ctx *AppContext) error {
// mist auth login
if ctx.Config != nil && ctx.Config.AccessToken != "" {

// Already logged in, ask if they want to re-login
fmt.Println("Already logged in with token:", ctx.Config.AccessToken)
fmt.Print("Re-enter token? (y/N): ")
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
fmt.Println("Aborting login.")
return nil
}
verifier, err := generateRandomString(32)
if err != nil {
return fmt.Errorf("failed to generate verifier: %w", err)
}
challenge := generateCodeChallenge(verifier)

fmt.Println("Opening browser for authentication...")
fmt.Printf("If your browser didn't open, click here: \033]8;;%s\033\\%s\033]8;;\033\\\n", authUrl, authUrl)
u, _ := url.Parse(authUrl)
q := u.Query()
q.Set("client_id", clientID)
q.Set("response_type", "code")
q.Set("redirect_uri", redirectURI)
q.Set("code_challenge", challenge)
q.Set("code_challenge_method", "S256")
u.RawQuery = q.Encode()

err := openUrl()
if err != nil {
fmt.Println("Error opening browser:", err)
return err
fmt.Printf("If your browser doesn't open, visit: %s\n", u.String())

if err := openUrl(u.String()); err != nil {
fmt.Printf("Failed to open browser: %v\n", err)
}
fmt.Print("token: ")

fmt.Print("Enter the authorization code: ")
reader := bufio.NewReader(os.Stdin)
token, _ := reader.ReadString('\n')
token = strings.TrimSpace(token)
code, _ := reader.ReadString('\n')
code = strings.TrimSpace(code)

token, err = getLongLivedToken(token)
if err != nil {
fmt.Println("Error obtaining long-lived token:", err)
return err
if code == "" {
return fmt.Errorf("authorization code is required")
}

err = saveTokenToConfig(ctx, token)
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("client_id", clientID)
data.Set("code", code)
data.Set("redirect_uri", redirectURI)
data.Set("code_verifier", verifier)

resp, err := http.PostForm(tokenUrl, data)
if err != nil {
fmt.Println("Error during token saving")
return err
return fmt.Errorf("failed to exchange token: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("token exchange failed: %s", body)
}

fmt.Println("Saved token to config")
var tokenResp map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return fmt.Errorf("failed to decode token response: %w", err)
}

return nil
accessToken, ok := tokenResp["access_token"].(string)
if !ok {
return fmt.Errorf("invalid token response: missing access_token")
}

return saveTokenToConfig(ctx, accessToken)
}
Empty file added go.sum
Empty file.
Loading