-
Notifications
You must be signed in to change notification settings - Fork 130
RP Initiated Logout
This gem supports the server side of OpenID Connect RP-Initiated Logout 1.0 as follows: it advertises your logout endpoint in the discovery document, and it registers and validates per-client post_logout_redirect_uris (client metadata, doorkeeper-openid_connect >= 2.0). The end-session endpoint itself is implemented by the host application — ending a session is inseparable from however your app manages sessions (Devise, custom cookies, …), so the gem stays out of that part.
Configure end_session_endpoint with a block returning the URL of your logout endpoint. The block is executed in the controller's scope, so route helpers are available:
# config/initializers/doorkeeper_openid_connect.rb
Doorkeeper::OpenidConnect.configure do
# ...
end_session_endpoint -> { oauth_logout_url }
endThe URL is published as the end_session_endpoint member of /.well-known/openid-configuration, which is where client libraries such as oidc-client-js pick it up. When the setting is not configured, the member is omitted from the discovery document.
Per RP-Initiated Logout 1.0 §3, the OP must only redirect after logout to a post_logout_redirect_uri that the client registered beforehand. 2.0 stores these per client in a nullable post_logout_redirect_uris column on oauth_applications (newline-separated, like redirect_uri). New installations get the column from the regular install migration; existing installations add it with:
rails generate doorkeeper:openid_connect:add_post_logout_redirect_uris
rails db:migrateOn Doorkeeper::Application this provides:
-
post_logout_redirect_uris— returns the registered URIs as an array (empty array when none are registered, or when the column hasn't been added yet). -
post_logout_redirect_uris=— accepts an array or a newline-separated string. RaisesActiveModel::MissingAttributeErrorwith a pointer to the migration when the column is missing, so an assigned value can't be dropped silently. -
valid_post_logout_redirect_uri?(uri)— whether the given URI has been registered; this is the check your logout endpoint calls before redirecting.
Registered values are validated with exactly the same rules Doorkeeper applies to redirect_uri (forbidden schemes, fragments, relative URIs, force_ssl_in_redirect_uri, …), by delegating to Doorkeeper's own RedirectUriValidator. Registration is optional per the spec, so a blank value is allowed. Dynamic Client Registration accepts and echoes post_logout_redirect_uris, rejecting invalid values with invalid_client_metadata.
The logout request arrives as a GET (or POST) with the optional parameters id_token_hint, client_id, post_logout_redirect_uri and state (RP-Initiated Logout 1.0 §2). The rules your endpoint needs to follow: identify the client from id_token_hint (recommended) or client_id, end the local session, redirect only to a registered post_logout_redirect_uri — appending state when the client sent one — and fall back to a local page otherwise. Expired ID Tokens are still acceptable as hints; they were issued by this server, so they can be verified with the configured signing key.
# config/routes.rb
get "/oauth/logout", to: "logouts#show", as: :oauth_logout# app/controllers/logouts_controller.rb
class LogoutsController < ApplicationController
def show
application = application_from_id_token_hint(params[:id_token_hint]) ||
application_from_client_id(params[:client_id])
sign_out(current_user) if current_user # your session teardown here
uri = params[:post_logout_redirect_uri]
if application&.valid_post_logout_redirect_uri?(uri)
redirect_to with_state_param(uri, params[:state]), allow_other_host: true
else
# Never redirect to an unregistered URI (RP-Initiated Logout 1.0 §3).
redirect_to root_url
end
end
private
def application_from_client_id(client_id)
Doorkeeper.config.application_model.by_uid(client_id) if client_id.present?
end
def application_from_id_token_hint(hint)
return if hint.blank?
payload, = JWT.decode(
hint,
Doorkeeper::OpenidConnect.signing_key.keypair,
true,
algorithms: [Doorkeeper::OpenidConnect.signing_algorithm.to_s],
verify_expiration: false, # expired ID Tokens remain valid as hints
)
Doorkeeper.config.application_model.by_uid(payload["aud"])
rescue JWT::DecodeError
nil
end
def with_state_param(uri, state)
return uri if state.blank?
parsed = URI.parse(uri)
parsed.query = [parsed.query, { state: state }.to_query].compact.join("&")
parsed.to_s
end
endNote
The verification example assumes a single asymmetric signing key. If you use key rotation (signing_key as an array), try each entry of Doorkeeper::OpenidConnect.signing_keys in turn, so ID Tokens signed with a retired key are still accepted as hints during the rotation window.
-
Configuration —
end_session_endpointand the other initializer settings -
Dynamic Client Registration — registering
post_logout_redirect_urisdynamically -
Migration from Old Versions — adding the
post_logout_redirect_uriscolumn to an existing installation