Class: StandardId::Api::Oauth::RevocationsController

Inherits:
BaseController show all
Defined in:
app/controllers/standard_id/api/oauth/revocations_controller.rb

Constant Summary

Constants included from RateLimitHandling

RateLimitHandling::RATE_LIMIT_STORE

Instance Method Summary collapse

Methods included from ControllerPolicy

all_controllers, authenticated_controllers, public_controllers, register, registry_snapshot, reset_registry!

Instance Method Details

#createObject

POST /oauth/revoke RFC 7009 - OAuth 2.0 Token Revocation

Accepts a token and optional token_type_hint parameter. Always responds with 200 OK regardless of whether the token was valid or revocation was successful (per RFC 7009 Section 2.1).



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'app/controllers/standard_id/api/oauth/revocations_controller.rb', line 15

def create
  token = params[:token]
  head :ok and return if token.blank?

  payload = StandardId::JwtService.decode(token)
  head :ok and return unless payload&.dig(:sub)

   = payload[:sub]

  sessions = StandardId::DeviceSession
    .where(account_id: )
    .active

  # token_type_hint is accepted but ignored — we always attempt
  # revocation via sub claim regardless of token type (RFC 7009 §2.1)
  revoked_sessions = sessions.to_a
  if revoked_sessions.any?
    now = Time.current
    session_ids = revoked_sessions.map(&:id)

    # Bulk-revoke in two queries (one UPDATE per table) instead of
    # issuing session.revoke! per row, which would be O(N) UPDATEs plus
    # another O(N) cascades to refresh_tokens.
    #
    # Tradeoff: update_all skips ActiveRecord callbacks, so the per-row
    # SESSION_REVOKED event emitted by Session#revoke! is not fired
    # automatically. We re-emit it explicitly below so audit-trail
    # subscribers (account status/locking, etc.) still see one event
    # per revoked session — the semantics are preserved, only the SQL
    # shape has changed.
    ActiveRecord::Base.transaction do
      StandardId::Session.where(id: session_ids).update_all(revoked_at: now)
      StandardId::RefreshToken
        .where(session_id: session_ids, revoked_at: nil)
        .update_all(revoked_at: now)
    end

    # DB state is already committed above; event publishing is best-effort
    # audit emission. A failing subscriber must not short-circuit the loop
    # and leave later sessions without their SESSION_REVOKED event, which
    # would permanently desync audit-trail consumers from the DB.
    #
    # All revoked_sessions share the same account_id (we filtered by it
    # at line 25), so we load the account once rather than calling
    # session.account per row, which would issue N extra SELECTs.
     = revoked_sessions.first.
    revoked_sessions.each do |session|
      session.revoked_at = now
      begin
        StandardId::Events.publish(
          StandardId::Events::SESSION_REVOKED,
          session: session,
          account: ,
          reason: "token_revocation"
        )
      rescue StandardError => e
        StandardId.logger.error(
          "[StandardId::Revocations] Failed to publish SESSION_REVOKED " \
          "for session #{session.id}: #{e.class}: #{e.message}"
        )
      end
    end

    begin
      StandardId::Events.publish(
        StandardId::Events::OAUTH_TOKEN_REVOKED,
        account_id: ,
        sessions_revoked: revoked_sessions.size
      )
    rescue StandardError => e
      StandardId.logger.error(
        "[StandardId::Revocations] Failed to publish OAUTH_TOKEN_REVOKED " \
        "for account #{}: #{e.class}: #{e.message}"
      )
    end
  end

  head :ok
end