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