diff --git a/cmd/desktop/main.go b/cmd/desktop/main.go index 0e40b3a..7a00c14 100644 --- a/cmd/desktop/main.go +++ b/cmd/desktop/main.go @@ -142,80 +142,65 @@ func startOIDCServer() *http.Server { }) } - h("/oidc/open", func(w http.ResponseWriter, r *http.Request) { + h("/oidc/start", func(w http.ResponseWriter, r *http.Request) { apiUrl := strings.TrimRight(r.URL.Query().Get("apiUrl"), "/") + returnURL := r.URL.Query().Get("ret") if apiUrl == "" { http.Error(w, "apiUrl required", http.StatusBadRequest) return } - - sessionID := randomString(16) - - go func() { - oidcCfg, err := fetchOIDCConfig(apiUrl) - if err != nil { - return - } - - verifier, challenge, _ := pkceParams() - state := randomString(32) - redirectURI := fmt.Sprintf("http://127.0.0.1:%d/oidc/callback", oidcCallbackPort) - - ch := make(chan string, 1) - oidcSessionsMu.Lock() - oidcSessions[sessionID] = &oidcSession{apiUrl: apiUrl, verifier: verifier, state: state, ch: ch} - oidcSessionsMu.Unlock() - - authURL := fmt.Sprintf("%s?%s", - oidcCfg.AuthorizationEndpoint, - url.Values{ - "response_type": {"code"}, - "client_id": {oidcCfg.ClientID}, - "redirect_uri": {redirectURI}, - "code_challenge": {challenge}, - "code_challenge_method": {"S256"}, - "state": {state}, - "scope": {"openid profile email"}, - }.Encode(), - ) - - exec.Command("open", authURL).Start() - - select { - case token := <-ch: - if token != "" { - c := &ConfigService{} - c.SaveConfig(apiUrl, token) - } - case <-time.After(5 * time.Minute): - } - }() - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"id": sessionID}) - }) - - h("/oidc/result", func(w http.ResponseWriter, r *http.Request) { - sessionID := r.URL.Query().Get("id") - var token string - - oidcSessionsMu.Lock() - session, ok := oidcSessions[sessionID] - if ok { - select { - case t := <-session.ch: - token = t - session.ch <- t // put it back for other pollers - default: - } + if returnURL == "" { + returnURL = "/?desktop=1" } + + oidcCfg, err := fetchOIDCConfig(apiUrl) + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + + verifier, challenge, _ := pkceParams() + state := randomString(32) + redirectURI := fmt.Sprintf("http://127.0.0.1:%d/oidc/callback", oidcCallbackPort) + + ch := make(chan string, 1) + oidcSessionsMu.Lock() + sessionID := randomString(16) + oidcSessions[sessionID] = &oidcSession{apiUrl: apiUrl, verifier: verifier, state: state, ch: ch} oidcSessionsMu.Unlock() - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{ - "token": token, - "pending": fmt.Sprintf("%t", !ok || (ok && token == "")), - }) + authURL := fmt.Sprintf("%s?%s", + oidcCfg.AuthorizationEndpoint, + url.Values{ + "response_type": {"code"}, + "client_id": {oidcCfg.ClientID}, + "redirect_uri": {redirectURI}, + "code_challenge": {challenge}, + "code_challenge_method": {"S256"}, + "state": {state}, + "scope": {"openid profile email"}, + }.Encode(), + ) + + exec.Command("open", authURL).Start() + + select { + case token := <-ch: + if token != "" { + c := &ConfigService{} + c.SaveConfig(apiUrl, token) + returnURL += "&token=" + url.QueryEscape(token) + } + case <-time.After(5 * time.Minute): + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, `Oikos + + +

Connected

Redirecting back to Oikos…

`, returnURL) }) h("/oidc/callback", func(w http.ResponseWriter, r *http.Request) { diff --git a/desktop b/desktop new file mode 100755 index 0000000..a62ccf1 Binary files /dev/null and b/desktop differ diff --git a/web/src/lib/oidc.ts b/web/src/lib/oidc.ts index 3e59d17..0916c47 100644 --- a/web/src/lib/oidc.ts +++ b/web/src/lib/oidc.ts @@ -103,7 +103,9 @@ export async function startLogin(): Promise { if (isDesktop) { const apiUrl = getConfig().apiUrl || '' - throw new Error('DESKTOP_OIDC:' + apiUrl) + const ret = encodeURIComponent(location.origin + location.pathname.replace(/\/$/, '') + '?desktop=1') + location.href = `http://127.0.0.1:18901/oidc/start?apiUrl=${encodeURIComponent(apiUrl)}&ret=${ret}` + return } location.href = `${cfg.authorization_endpoint.replace(/\/$/, '')}/?${params}` diff --git a/web/src/main.ts b/web/src/main.ts index 22ef279..7f5dd59 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,10 +1,26 @@ import { mount } from 'svelte' import App from './App.svelte' import './app.css' -import { initConfig } from '$lib/config' +import { initConfig, setConfig, getConfig } from '$lib/config' + +function handleDesktopToken() { + const params = new URLSearchParams(location.search) + const token = params.get('token') + if (token && new URLSearchParams(location.search).has('desktop')) { + const apiUrl = getConfig().apiUrl || '' + setConfig({ apiUrl, token, isDesktop: true }) + initConfig({ apiUrl, token, isDesktop: true }) + params.delete('token') + const q = params.toString() + history.replaceState(null, '', location.pathname + (q ? '?' + q : '')) + return true + } + return false +} function start() { initConfig() + handleDesktopToken() requestAnimationFrame(() => import('./lib/renderers')) diff --git a/web/src/pages/Config.svelte b/web/src/pages/Config.svelte index 27bf25a..1cbfb25 100644 --- a/web/src/pages/Config.svelte +++ b/web/src/pages/Config.svelte @@ -72,43 +72,11 @@ try { await startLogin() } catch (e: any) { - const msg = e?.message || e || '' - if (msg.startsWith('DESKTOP_OIDC:')) { - const url = msg.substring('DESKTOP_OIDC:'.length) - await desktopOIDC(url) - return - } - error = msg || 'OIDC login failed' + error = e.message || 'OIDC login failed' oidcLoggingIn = false } } - async function desktopOIDC(apiUrl: string) { - try { - const resp = await fetch(`http://127.0.0.1:18901/oidc/open?apiUrl=${encodeURIComponent(apiUrl)}`) - const { id } = await resp.json() - if (!id) throw new Error('No session ID') - - for (let i = 0; i < 600; i++) { - await new Promise(r => setTimeout(r, 500)) - const r = await fetch(`http://127.0.0.1:18901/oidc/result?id=${id}`) - const data = await r.json() - if (data.token) { - setConfig({ apiUrl, token: data.token, isDesktop: true }) - initConfig({ apiUrl, token: data.token, isDesktop: true }) - oidcLoggingIn = false - onConnected() - return - } - if (!data.pending || data.pending === 'false') break - } - error = 'Login timed out' - } catch (e: any) { - error = e?.message || 'Could not reach login service' - } - oidcLoggingIn = false - } - function logoutOIDC() { oidcLogout() oidcUser = null