Desktop OIDC: non-blocking fetch + poll, don't leave webview
SPA fetches /oidc/open (returns session ID immediately), then polls /oidc/result every 500ms. Go server opens browser in a goroutine. Webview never leaves the Wails origin. Token is saved to keychain and returned through the poll response.
This commit is contained in:
@@ -149,49 +149,73 @@ func startOIDCServer() *http.Server {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
sessionID := randomString(16)
|
||||
ch := make(chan string, 1)
|
||||
|
||||
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()
|
||||
oidcSessions[sessionID] = &oidcSession{apiUrl: apiUrl, verifier: verifier, state: state, ch: ch}
|
||||
session, ok := oidcSessions[sessionID]
|
||||
if ok {
|
||||
select {
|
||||
case t := <-session.ch:
|
||||
token = t
|
||||
session.ch <- t // put it back for other pollers
|
||||
default:
|
||||
}
|
||||
}
|
||||
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)
|
||||
http.Redirect(w, r, "/?desktop=1&token="+url.QueryEscape(token), http.StatusFound)
|
||||
} else {
|
||||
http.Redirect(w, r, "/?desktop=1&error=login_failed", http.StatusFound)
|
||||
}
|
||||
case <-time.After(5 * time.Minute):
|
||||
http.Redirect(w, r, "/?desktop=1&error=timeout", http.StatusFound)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"token": token,
|
||||
"pending": fmt.Sprintf("%t", !ok || (ok && token == "")),
|
||||
})
|
||||
})
|
||||
|
||||
h("/oidc/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -102,9 +102,8 @@ export async function startLogin(): Promise<void> {
|
||||
})
|
||||
|
||||
if (isDesktop) {
|
||||
const apiUrl = getConfig().apiUrl || location.protocol + '//' + location.host
|
||||
location.href = `http://127.0.0.1:18901/oidc/open?apiUrl=${encodeURIComponent(apiUrl)}`
|
||||
throw new Error('Redirecting to login...')
|
||||
const apiUrl = getConfig().apiUrl || ''
|
||||
throw new Error('DESKTOP_OIDC:' + apiUrl)
|
||||
}
|
||||
|
||||
location.href = `${cfg.authorization_endpoint.replace(/\/$/, '')}/?${params}`
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
import { mount } from 'svelte'
|
||||
import App from './App.svelte'
|
||||
import './app.css'
|
||||
import { initConfig, setConfig, getConfig } from '$lib/config'
|
||||
|
||||
function handleDesktopToken() {
|
||||
const params = new URLSearchParams(location.search)
|
||||
const token = params.get('token')
|
||||
if (token) {
|
||||
const apiUrl = getConfig().apiUrl || params.get('apiUrl') || ''
|
||||
setConfig({ apiUrl, token, isDesktop: true })
|
||||
initConfig({ apiUrl, token, isDesktop: true })
|
||||
// clean the URL
|
||||
params.delete('token')
|
||||
params.delete('apiUrl')
|
||||
let q = params.toString()
|
||||
history.replaceState(null, '', location.pathname + (q ? '?' + q : ''))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
import { initConfig } from '$lib/config'
|
||||
|
||||
function start() {
|
||||
initConfig()
|
||||
handleDesktopToken()
|
||||
|
||||
requestAnimationFrame(() => import('./lib/renderers'))
|
||||
|
||||
|
||||
@@ -72,11 +72,43 @@
|
||||
try {
|
||||
await startLogin()
|
||||
} catch (e: any) {
|
||||
error = e.message || 'OIDC login failed'
|
||||
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'
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user