Desktop OIDC: redirect webview to local server, Go opens browser
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

The webview navigates to http://127.0.0.1:18901/oidc/open?apiUrl=...
The Go server opens the system browser to Authentik, waits for callback,
exchanges code for token, saves to keychain, then redirects the webview
back with ?desktop=1&token=TOKEN. main.ts extracts the token from URL.
This commit is contained in:
2026-07-14 00:04:49 +02:00
parent cac5524402
commit d7197c1952
3 changed files with 27 additions and 26 deletions

View File

@@ -142,7 +142,7 @@ func startOIDCServer() *http.Server {
}) })
} }
h("/oidc/login", func(w http.ResponseWriter, r *http.Request) { h("/oidc/open", func(w http.ResponseWriter, r *http.Request) {
apiUrl := strings.TrimRight(r.URL.Query().Get("apiUrl"), "/") apiUrl := strings.TrimRight(r.URL.Query().Get("apiUrl"), "/")
if apiUrl == "" { if apiUrl == "" {
http.Error(w, "apiUrl required", http.StatusBadRequest) http.Error(w, "apiUrl required", http.StatusBadRequest)
@@ -151,7 +151,7 @@ func startOIDCServer() *http.Server {
oidcCfg, err := fetchOIDCConfig(apiUrl) oidcCfg, err := fetchOIDCConfig(apiUrl)
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("OIDC config: %v", err), http.StatusServiceUnavailable) http.Error(w, err.Error(), http.StatusServiceUnavailable)
return return
} }
@@ -185,13 +185,12 @@ func startOIDCServer() *http.Server {
if token != "" { if token != "" {
c := &ConfigService{} c := &ConfigService{}
c.SaveConfig(apiUrl, token) c.SaveConfig(apiUrl, token)
http.Redirect(w, r, "/?desktop=1&token="+url.QueryEscape(token), http.StatusFound)
} else {
http.Redirect(w, r, "/?desktop=1&error=login_failed", http.StatusFound)
} }
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": token})
case <-time.After(5 * time.Minute): case <-time.After(5 * time.Minute):
w.Header().Set("Content-Type", "application/json") http.Redirect(w, r, "/?desktop=1&error=timeout", http.StatusFound)
w.WriteHeader(http.StatusRequestTimeout)
json.NewEncoder(w).Encode(map[string]string{"error": "login timed out"})
} }
}) })

View File

@@ -101,14 +101,13 @@ export async function startLogin(): Promise<void> {
scope: 'openid profile email' scope: 'openid profile email'
}) })
const authURL = `${cfg.authorization_endpoint.replace(/\/$/, '')}/?${params}`
if (isDesktop) { if (isDesktop) {
window.open(authURL, '_blank', 'width=800,height=700') const apiUrl = getConfig().apiUrl || location.protocol + '//' + location.host
throw new Error('Login opened in your browser. After authenticating, copy the token and paste it into the Token tab.') location.href = `http://127.0.0.1:18901/oidc/open?apiUrl=${encodeURIComponent(apiUrl)}`
throw new Error('Redirecting to login...')
} }
location.href = authURL location.href = `${cfg.authorization_endpoint.replace(/\/$/, '')}/?${params}`
} }
export async function handleCallback(code: string, returnedState: string): Promise<boolean> { export async function handleCallback(code: string, returnedState: string): Promise<boolean> {

View File

@@ -1,25 +1,28 @@
import { mount } from 'svelte' import { mount } from 'svelte'
import App from './App.svelte' import App from './App.svelte'
import './app.css' import './app.css'
import { initConfig, setConfig } from '$lib/config' import { initConfig, setConfig, getConfig } from '$lib/config'
async function loadDesktopConfig() { function handleDesktopToken() {
const wails = (window as any).wails const params = new URLSearchParams(location.search)
if (!wails?.Call?.ByName) return const token = params.get('token')
if (token) {
try { const apiUrl = getConfig().apiUrl || params.get('apiUrl') || ''
const cfg = await wails.Call.ByName('GetStoredConfig') setConfig({ apiUrl, token, isDesktop: true })
if (cfg?.apiUrl && cfg?.token) { initConfig({ apiUrl, token, isDesktop: true })
setConfig({ apiUrl: cfg.apiUrl, token: cfg.token, isDesktop: true }) // clean the URL
} params.delete('token')
} catch { params.delete('apiUrl')
// no stored config — Config page will handle it let q = params.toString()
history.replaceState(null, '', location.pathname + (q ? '?' + q : ''))
return true
} }
return false
} }
async function start() { function start() {
initConfig() initConfig()
await loadDesktopConfig() handleDesktopToken()
requestAnimationFrame(() => import('./lib/renderers')) requestAnimationFrame(() => import('./lib/renderers'))