Files
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

376 lines
11 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package logout
import (
"net/http"
"net/url"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/kratos/x/redir"
"github.com/ory/x/httprouterx"
"github.com/ory/x/httpx"
"go.opentelemetry.io/otel/trace"
"github.com/pkg/errors"
"github.com/ory/kratos/x/events"
"github.com/ory/herodot"
"github.com/ory/x/decoderx"
"github.com/ory/x/sqlcon"
"github.com/ory/x/urlx"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/selfservice/errorx"
"github.com/ory/kratos/session"
"github.com/ory/kratos/x"
)
const (
RouteInitBrowserFlow = "/self-service/logout/browser"
RouteAPIFlow = "/self-service/logout/api"
RouteSubmitFlow = "/self-service/logout"
)
type (
dependencies interface {
httpx.WriterProvider
nosurfx.CSRFProvider
session.ManagementProvider
session.PersistenceProvider
errorx.ManagementProvider
config.Provider
}
HandlerProvider interface {
LogoutHandler() *Handler
}
Handler struct{ d dependencies }
)
func NewHandler(d dependencies) *Handler { return &Handler{d: d} }
func (h *Handler) RegisterPublicRoutes(router *httprouterx.RouterPublic) {
h.d.CSRFHandler().IgnorePath(RouteAPIFlow)
router.GET(RouteInitBrowserFlow, h.createBrowserLogoutFlow)
router.DELETE(RouteAPIFlow, h.performNativeLogout)
router.GET(RouteSubmitFlow, h.updateLogoutFlow)
}
func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) {
admin.GET(RouteInitBrowserFlow, redir.RedirectToPublicRoute(h.d))
admin.DELETE(RouteAPIFlow, redir.RedirectToPublicRoute(h.d))
admin.GET(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d))
}
// Logout Flow
//
// swagger:model logoutFlow
type logoutFlow struct {
// LogoutURL can be opened in a browser to sign the user out.
//
// format: uri
// required: true
LogoutURL string `json:"logout_url"`
// LogoutToken can be used to perform logout using AJAX.
//
// required: true
LogoutToken string `json:"logout_token"`
}
// Create Browser Logout Flow Parameters
//
// swagger:parameters createBrowserLogoutFlow
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type createBrowserLogoutFlow struct {
// HTTP Cookies
//
// If you call this endpoint from a backend, please include the
// original Cookie header in the request.
//
// in: header
// name: cookie
Cookie string `json:"cookie"`
// Return to URL
//
// The URL to which the browser should be redirected to after the logout
// has been performed.
//
// in: query
ReturnTo string `json:"return_to"`
}
// swagger:route GET /self-service/logout/browser frontend createBrowserLogoutFlow
//
// # Create a Logout URL for Browsers
//
// This endpoint initializes a browser-based user logout flow and a URL which can be used to log out the user.
//
// This endpoint is NOT INTENDED for API clients and only works
// with browsers (Chrome, Firefox, ...). For API clients you can
// call the `/self-service/logout/api` URL directly with the Ory Session Token.
//
// The URL is only valid for the currently signed in user. If no user is signed in, this endpoint returns
// a 401 error.
//
// When calling this endpoint from a backend, please ensure to properly forward the HTTP cookies.
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: logoutFlow
// 400: errorGeneric
// 401: errorGeneric
// 500: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-public-medium
func (h *Handler) createBrowserLogoutFlow(w http.ResponseWriter, r *http.Request) {
sess, err := h.d.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandNothing, identity.ExpandNothing)
if err != nil {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
return
}
conf := h.d.Config()
requestURL := x.RequestURL(r)
var returnTo *url.URL
if requestURL.Query().Get("return_to") != "" {
// Pre-validate the return to URL which is contained in the HTTP request.
returnTo, err = redir.SecureRedirectTo(r,
h.d.Config().SelfServiceFlowLogoutRedirectURL(r.Context()),
redir.SecureRedirectUseSourceURL(requestURL.String()),
redir.SecureRedirectAllowURLs(conf.SelfServiceBrowserAllowedReturnToDomains(r.Context())),
redir.SecureRedirectAllowSelfServiceURLs(conf.SelfPublicURL(r.Context())),
)
if err != nil {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
return
}
}
params := url.Values{"token": {sess.LogoutToken}}
if returnTo != nil {
params.Set("return_to", returnTo.String())
}
h.d.Writer().Write(w, r, &logoutFlow{
LogoutToken: sess.LogoutToken,
LogoutURL: urlx.CopyWithQuery(urlx.AppendPaths(h.d.Config().SelfPublicURL(r.Context()), RouteSubmitFlow), params).String(),
})
}
// Perform Native Logout Parameters
//
// swagger:parameters performNativeLogout
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type performNativeLogout struct {
// in: body
// required: true
Body performNativeLogoutBody
}
// Perform Native Logout Request Body
//
// swagger:model performNativeLogoutBody
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type performNativeLogoutBody struct {
// The Session Token
//
// Invalidate this session token.
//
// required: true
SessionToken string `json:"session_token"`
}
// swagger:route DELETE /self-service/logout/api frontend performNativeLogout
//
// # Perform Logout for Native Apps
//
// Use this endpoint to log out an identity using an Ory Session Token. If the Ory Session Token was successfully
// revoked, the server returns a 204 No Content response. A 204 No Content response is also sent when
// the Ory Session Token has been revoked already before.
//
// If the Ory Session Token is malformed or does not exist a 403 Forbidden response will be returned.
//
// This endpoint does not remove any HTTP
// Cookies - use the Browser-Based Self-Service Logout Flow instead.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 204: emptyResponse
// 400: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-public-medium
func (h *Handler) performNativeLogout(w http.ResponseWriter, r *http.Request) {
var p performNativeLogoutBody
if err := decoderx.Decode(r, &p,
decoderx.HTTPJSONDecoder(),
decoderx.HTTPDecoderAllowedMethods("DELETE")); err != nil {
h.d.Writer().WriteError(w, r, err)
return
}
revoked, err := h.d.SessionPersister().RevokeSessionByToken(r.Context(), p.SessionToken)
if err != nil {
if errors.Is(err, sqlcon.ErrNoRows()) {
h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrForbidden().WithReason("The provided Ory Session Token could not be found, is invalid, or otherwise malformed.")))
return
}
h.d.Writer().WriteError(w, r, err)
return
}
trace.SpanFromContext(r.Context()).AddEvent(events.NewSessionRevoked(r.Context(), revoked.ID, revoked.IdentityID))
w.WriteHeader(http.StatusNoContent)
}
// Update Logout Flow Parameters
//
// swagger:parameters updateLogoutFlow
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type updateLogoutFlow struct {
// A Valid Logout Token
//
// If you do not have a logout token because you only have a session cookie,
// call `/self-service/logout/browser` to generate a URL for this endpoint.
//
// in: query
Token string `json:"token"`
// The URL to return to after the logout was completed.
//
// in: query
ReturnTo string `json:"return_to"`
// HTTP Cookies
//
// When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header
// sent by the client to your server here. This ensures that CSRF and session cookies are respected.
//
// in: header
// name: Cookie
Cookies string `json:"Cookie"`
}
// swagger:route GET /self-service/logout frontend updateLogoutFlow
//
// # Update Logout Flow
//
// This endpoint logs out an identity in a self-service manner.
//
// If the `Accept` HTTP header is not set to `application/json`, the browser will be redirected (HTTP 303 See Other)
// to the `return_to` parameter of the initial request or fall back to `urls.default_return_to`.
//
// If the `Accept` HTTP header is set to `application/json`, a 204 No Content response
// will be sent on successful logout instead.
//
// This endpoint is NOT INTENDED for API clients and only works
// with browsers (Chrome, Firefox, ...). For API clients you can
// call the `/self-service/logout/api` URL directly with the Ory Session Token.
//
// More information can be found at [Ory Kratos User Logout Documentation](https://www.ory.com/docs/next/kratos/self-service/flows/user-logout).
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 303: emptyResponse
// 204: emptyResponse
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-public-high
func (h *Handler) updateLogoutFlow(w http.ResponseWriter, r *http.Request) {
expected := r.URL.Query().Get("token")
if len(expected) == 0 {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest().WithReason("Please include a token in the URL query.")))
return
}
sess, err := h.d.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandNothing, identity.ExpandNothing)
if err != nil {
// We could handle `session.ErrNoActiveSessionFound` gracefully with `h.completeLogout()` here but that would
// actually be an issue as it incorrectly indicates to clients that the session has been removed even if
// `RevokeSessionByToken` has not actually been called.
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
return
}
if sess.LogoutToken != expected {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrForbidden().WithReason("Unable to log out because the logout token in the URL query does not match the session cookie.")))
return
}
if err := h.d.SessionManager().PurgeFromRequest(r.Context(), w, r); err != nil {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
return
}
trace.SpanFromContext(r.Context()).AddEvent(events.NewSessionRevoked(r.Context(), sess.ID, sess.IdentityID))
h.completeLogout(w, r)
}
func (h *Handler) completeLogout(w http.ResponseWriter, r *http.Request) {
_ = h.d.CSRFHandler().RegenerateToken(w, r)
ret, err := redir.SecureRedirectTo(r, h.d.Config().SelfServiceFlowLogoutRedirectURL(r.Context()),
redir.SecureRedirectUseSourceURL(r.RequestURI),
redir.SecureRedirectAllowURLs(h.d.Config().SelfServiceBrowserAllowedReturnToDomains(r.Context())),
redir.SecureRedirectAllowSelfServiceURLs(h.d.Config().SelfPublicURL(r.Context())),
)
if err != nil {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
return
}
// Instruct the browser to clear cookies, storage, and caches for this origin.
// We only send the header over HTTPS because browsers ignore Clear-Site-Data
// on insecure origins, and gate it behind a config flag (off by default) so we
// do not clear data that other applications on the same origin may rely on.
if h.d.Config().SelfServiceFlowLogoutClearBrowserData(r.Context()) && x.RequestURL(r).Scheme == "https" {
w.Header().Set("Clear-Site-Data", `"cookies", "storage", "cache"`)
}
if x.IsJSONRequest(r) {
w.WriteHeader(http.StatusNoContent)
return
}
http.Redirect(w, r, ret.String(), http.StatusSeeOther)
}