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(` Sign out of NetKingdom

Sign out of NetKingdom?

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.

Back to my account

`)) 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) } }