Add central login recovery and confirmed shared sign-out
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 44s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 44s
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
89694ad6df
commit
074c2ce498
9 changed files with 334 additions and 49 deletions
99
src/internal/server/oidc/account.go
Normal file
99
src/internal/server/oidc/account.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (h *AuthorizeHandler) authenticationFailure(w http.ResponseWriter, r *http.Request) {
|
||||
h.browserFailure(w, r, http.StatusUnauthorized, "authentication failed")
|
||||
}
|
||||
|
||||
func (h *AuthorizeHandler) browserFailure(w http.ResponseWriter, r *http.Request, status int, message string) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
if h.AccountPortalURL != "" {
|
||||
// Never carry upstream code, state, error details or an unverified identity.
|
||||
http.Redirect(w, r, strings.TrimRight(h.AccountPortalURL, "/")+"/access-recovery", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Error(w, message, status)
|
||||
}
|
||||
|
||||
// AccountLogoutHandler confirms browser-wide sign-out without accepting a return URL
|
||||
// from the browser. Authelia owns the upstream cookie and destroys it on its origin.
|
||||
type AccountLogoutHandler struct {
|
||||
PortalURL string
|
||||
UpstreamLogoutURL string
|
||||
Issuer string
|
||||
Logins *LoginSessionStore
|
||||
}
|
||||
|
||||
const logoutCSRF = "__Host-keycape-logout"
|
||||
|
||||
var accountLogoutPage = template.Must(template.New("logout").Parse(`<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Sign out of NetKingdom</title><main><h1>Sign out of NetKingdom?</h1>
|
||||
<p>This ends your shared sign-in session in this browser so you can use another account.
|
||||
Applications that already have their own sessions may remain signed in.</p>
|
||||
<form method="post" action="/account/logout"><input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<button type="submit">Sign out of NetKingdom</button></form>
|
||||
<p><a href="{{.Portal}}">Back to my account</a></p></main></html>`))
|
||||
|
||||
func (h *AccountLogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
var nonce [32]byte
|
||||
if _, err := rand.Read(nonce[:]); err != nil {
|
||||
http.Error(w, "sign-out unavailable", 503)
|
||||
return
|
||||
}
|
||||
csrf := base64.RawURLEncoding.EncodeToString(nonce[:])
|
||||
http.SetCookie(w, &http.Cookie{Name: logoutCSRF, Value: csrf, Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, MaxAge: 600})
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = accountLogoutPage.Execute(w, struct{ CSRF, Portal string }{csrf, h.PortalURL})
|
||||
case http.MethodPost:
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 4096)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "invalid form", 400)
|
||||
return
|
||||
}
|
||||
cookie, err := r.Cookie(logoutCSRF)
|
||||
csrf := r.PostForm.Get("csrf")
|
||||
origin, parseErr := url.Parse(h.Issuer)
|
||||
expected := ""
|
||||
if parseErr == nil {
|
||||
expected = origin.Scheme + "://" + origin.Host
|
||||
}
|
||||
if err != nil || len(csrf) != 43 || subtle.ConstantTimeCompare([]byte(cookie.Value), []byte(csrf)) != 1 || r.Header.Get("Origin") != expected {
|
||||
http.Error(w, "sign-out confirmation expired; reload this page", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if session := h.Logins.fromRequest(r); session != nil {
|
||||
h.Logins.Delete(session.ID)
|
||||
}
|
||||
clearLoginCookie(w, issuerIsHTTPS(h.Issuer))
|
||||
http.SetCookie(w, &http.Cookie{Name: logoutCSRF, Value: "", Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, MaxAge: -1})
|
||||
target, err := url.Parse(h.UpstreamLogoutURL)
|
||||
if err != nil {
|
||||
http.Error(w, "sign-out unavailable", 503)
|
||||
return
|
||||
}
|
||||
q := target.Query()
|
||||
q.Set("rd", strings.TrimRight(h.PortalURL, "/")+"/logged-out")
|
||||
target.RawQuery = q.Encode()
|
||||
http.Redirect(w, r, target.String(), http.StatusSeeOther)
|
||||
default:
|
||||
w.Header().Set("Allow", "GET, POST")
|
||||
http.Error(w, "method not allowed", 405)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue