Handler to logout user
()
| 28 | |
| 29 | // Handler to logout user |
| 30 | func (h *httpProvider) LogoutHandler() gin.HandlerFunc { |
| 31 | log := h.Log.With().Str("func", "LogoutHandler").Logger() |
| 32 | return func(gc *gin.Context) { |
| 33 | // OIDC RP-initiated logout uses GET on the end_session_endpoint. |
| 34 | // Without protection, an attacker can place <img src="/logout"> |
| 35 | // on a page they control and the victim's browser will silently |
| 36 | // terminate their session via the cookie. Defence-in-depth: |
| 37 | // |
| 38 | // - GET with id_token_hint → proceed (the ID token proves the |
| 39 | // request originates from a real authenticated session — an |
| 40 | // <img> tag cannot forge an ID token) |
| 41 | // - GET without id_token_hint → render an HTML confirmation |
| 42 | // page; only the POST that follows actually deletes the |
| 43 | // session |
| 44 | // - POST → proceed unconditionally (existing behaviour; CSRF |
| 45 | // middleware enforces Origin/Referer for all POSTs) |
| 46 | if gc.Request.Method == http.MethodGet { |
| 47 | idTokenHint := strings.TrimSpace(gc.Query("id_token_hint")) |
| 48 | if idTokenHint == "" || !h.isValidIDTokenHint(idTokenHint) { |
| 49 | log.Debug().Bool("had_hint", idTokenHint != "").Msg("serving logout confirmation page") |
| 50 | gc.Header("Cache-Control", "no-store") |
| 51 | gc.HTML(http.StatusOK, logoutConfirmTemplate, gin.H{ |
| 52 | "redirect_uri": gc.Query("redirect_uri"), |
| 53 | "post_logout_redirect_uri": gc.Query("post_logout_redirect_uri"), |
| 54 | "state": gc.Query("state"), |
| 55 | }) |
| 56 | return |
| 57 | } |
| 58 | // Valid id_token_hint — fall through to the normal logout flow. |
| 59 | } |
| 60 | |
| 61 | // OIDC RP-Initiated Logout 1.0 §3 uses post_logout_redirect_uri. |
| 62 | // Fall back to the legacy redirect_uri for backward compatibility. |
| 63 | redirectURL := strings.TrimSpace(gc.Query("post_logout_redirect_uri")) |
| 64 | if redirectURL == "" { |
| 65 | redirectURL = strings.TrimSpace(gc.PostForm("post_logout_redirect_uri")) |
| 66 | } |
| 67 | if redirectURL == "" { |
| 68 | redirectURL = strings.TrimSpace(gc.Query("redirect_uri")) |
| 69 | } |
| 70 | if redirectURL == "" { |
| 71 | redirectURL = strings.TrimSpace(gc.PostForm("redirect_uri")) |
| 72 | } |
| 73 | |
| 74 | // state, when present, MUST be echoed on the final redirect per |
| 75 | // OIDC RP-Initiated Logout §3. |
| 76 | state := strings.TrimSpace(gc.Query("state")) |
| 77 | if state == "" { |
| 78 | state = strings.TrimSpace(gc.PostForm("state")) |
| 79 | } |
| 80 | // get fingerprint hash |
| 81 | fingerprintHash, err := cookie.GetSession(gc) |
| 82 | if err != nil { |
| 83 | log.Debug().Err(err).Msg("failed GetSession") |
| 84 | gc.JSON(http.StatusUnauthorized, gin.H{ |
| 85 | "error": "unauthorized", |
| 86 | }) |
| 87 | return |
nothing calls this directly
no test coverage detected