mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat(auth): add no-browser option for OAuth login
This commit is contained in:
+17
-2
@@ -30,6 +30,15 @@ type OAuthProviderConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
type LoginBrowserOptions struct {
|
||||
NoBrowser bool
|
||||
}
|
||||
|
||||
var (
|
||||
openBrowserFunc = OpenBrowser
|
||||
browserLoginInput io.Reader = os.Stdin
|
||||
)
|
||||
|
||||
func OpenAIOAuthConfig() OAuthProviderConfig {
|
||||
return OAuthProviderConfig{
|
||||
Issuer: "https://auth.openai.com",
|
||||
@@ -76,6 +85,10 @@ func GenerateState() (string, error) {
|
||||
}
|
||||
|
||||
func LoginBrowser(cfg OAuthProviderConfig) (*AuthCredential, error) {
|
||||
return LoginBrowserWithOptions(cfg, LoginBrowserOptions{})
|
||||
}
|
||||
|
||||
func LoginBrowserWithOptions(cfg OAuthProviderConfig, opts LoginBrowserOptions) (*AuthCredential, error) {
|
||||
pkce, err := GeneratePKCE()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generating PKCE: %w", err)
|
||||
@@ -128,7 +141,9 @@ func LoginBrowser(cfg OAuthProviderConfig) (*AuthCredential, error) {
|
||||
|
||||
fmt.Printf("Open this URL to authenticate:\n\n%s\n\n", authURL)
|
||||
|
||||
if err := OpenBrowser(authURL); err != nil {
|
||||
if opts.NoBrowser {
|
||||
fmt.Println("Browser auto-open disabled. Open the URL manually to continue.")
|
||||
} else if err := openBrowserFunc(authURL); err != nil {
|
||||
fmt.Printf("Could not open browser automatically.\nPlease open this URL manually:\n\n%s\n\n", authURL)
|
||||
}
|
||||
|
||||
@@ -144,7 +159,7 @@ func LoginBrowser(cfg OAuthProviderConfig) (*AuthCredential, error) {
|
||||
// Start manual input in a goroutine
|
||||
manualCh := make(chan string)
|
||||
go func() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
reader := bufio.NewReader(browserLoginInput)
|
||||
input, _ := reader.ReadString('\n')
|
||||
manualCh <- strings.TrimSpace(input)
|
||||
}()
|
||||
|
||||
@@ -3,6 +3,7 @@ package auth
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -373,3 +374,117 @@ func TestParseDeviceCodeResponseInvalidInterval(t *testing.T) {
|
||||
t.Fatal("expected error for invalid interval")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginBrowserWithOptionsSkipsAutoOpenWhenDisabled(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/oauth/token" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"access_token": "mock-access-token",
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"expires_in": 3600,
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
origOpenBrowserFunc := openBrowserFunc
|
||||
origBrowserLoginInput := browserLoginInput
|
||||
t.Cleanup(func() {
|
||||
openBrowserFunc = origOpenBrowserFunc
|
||||
browserLoginInput = origBrowserLoginInput
|
||||
})
|
||||
|
||||
var openCalls int
|
||||
openBrowserFunc = func(string) error {
|
||||
openCalls++
|
||||
return nil
|
||||
}
|
||||
browserLoginInput = strings.NewReader("manual-code\n")
|
||||
|
||||
cfg := OAuthProviderConfig{
|
||||
Issuer: server.URL,
|
||||
ClientID: "test-client",
|
||||
Scopes: "openid",
|
||||
Port: freeLocalPort(t),
|
||||
}
|
||||
|
||||
cred, err := LoginBrowserWithOptions(cfg, LoginBrowserOptions{NoBrowser: true})
|
||||
if err != nil {
|
||||
t.Fatalf("LoginBrowserWithOptions() error: %v", err)
|
||||
}
|
||||
|
||||
if openCalls != 0 {
|
||||
t.Fatalf("openBrowserFunc call count = %d, want 0", openCalls)
|
||||
}
|
||||
if cred.AccessToken != "mock-access-token" {
|
||||
t.Fatalf("AccessToken = %q, want %q", cred.AccessToken, "mock-access-token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginBrowserWithOptionsAutoOpensByDefault(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/oauth/token" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"access_token": "mock-access-token",
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"expires_in": 3600,
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
origOpenBrowserFunc := openBrowserFunc
|
||||
origBrowserLoginInput := browserLoginInput
|
||||
t.Cleanup(func() {
|
||||
openBrowserFunc = origOpenBrowserFunc
|
||||
browserLoginInput = origBrowserLoginInput
|
||||
})
|
||||
|
||||
var openCalls int
|
||||
openBrowserFunc = func(string) error {
|
||||
openCalls++
|
||||
return nil
|
||||
}
|
||||
browserLoginInput = strings.NewReader("manual-code\n")
|
||||
|
||||
cfg := OAuthProviderConfig{
|
||||
Issuer: server.URL,
|
||||
ClientID: "test-client",
|
||||
Scopes: "openid",
|
||||
Port: freeLocalPort(t),
|
||||
}
|
||||
|
||||
_, err := LoginBrowserWithOptions(cfg, LoginBrowserOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("LoginBrowserWithOptions() error: %v", err)
|
||||
}
|
||||
|
||||
if openCalls != 1 {
|
||||
t.Fatalf("openBrowserFunc call count = %d, want 1", openCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func freeLocalPort(t *testing.T) int {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen() error: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
addr, ok := listener.Addr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
t.Fatalf("listener addr type = %T, want *net.TCPAddr", listener.Addr())
|
||||
}
|
||||
|
||||
return addr.Port
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user