Module: StandardId::Oauth::OauthSessionPersistence

Defined in:
lib/standard_id/oauth/oauth_session_persistence.rb

Overview

Persists a Session record when ‘config.session.session_type_resolver` elects to materialise one for an OAuth token grant.

Only supports BrowserSession / DeviceSession; ServiceSession requires fields (service_name / service_version / owner) that the OAuth token grant flow doesn’t have context for.

Uses a stable, deterministic device_id derived from account + user-agent + audience so repeated token requests from the same device reuse the session row instead of accumulating rows (mirrors the sidekick workaround this config hook replaces).

Class Method Summary collapse

Class Method Details

.persist!(session_class:, account:, request:, audience:, grant_type:) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/standard_id/oauth/oauth_session_persistence.rb', line 17

def persist!(session_class:, account:, request:, audience:, grant_type:)
  case session_class.name
  when "StandardId::DeviceSession"
    upsert_device_session!(
      account: ,
      request: request,
      audience: audience,
      grant_type: grant_type
    )
  when "StandardId::BrowserSession"
    StandardId::BrowserSession.create!(
      account: ,
      ip_address: StandardId::Utils::IpNormalizer.normalize(request.remote_ip),
      user_agent: request.user_agent.presence || "OAuth:#{grant_type}",
      expires_at: StandardId::BrowserSession.expiry
    )
  else
    raise StandardId::ConfigurationError,
      "session_type_resolver returned #{session_class.name} for flow :oauth_token_issued; " \
      "only :browser and :device are supported for OAuth-token-issued session creation."
  end
end

.stable_device_id(account:, user_agent:, audience:) ⇒ Object



75
76
77
78
# File 'lib/standard_id/oauth/oauth_session_persistence.rb', line 75

def stable_device_id(account:, user_agent:, audience:)
  audience_key = Array(audience).join(",")
  Digest::SHA256.hexdigest("oauth:#{audience_key}:#{.id}:#{user_agent}")[0, 36]
end

.upsert_device_session!(account:, request:, audience:, grant_type:) ⇒ Object



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
# File 'lib/standard_id/oauth/oauth_session_persistence.rb', line 40

def upsert_device_session!(account:, request:, audience:, grant_type:)
  user_agent = request.user_agent
  device_id = stable_device_id(account: , user_agent: user_agent, audience: audience)
  ip_address = StandardId::Utils::IpNormalizer.normalize(request.remote_ip)

  # Serialize concurrent upserts for the same account. There's no
  # DB-level unique constraint on (account_id, device_id), so a raw
  # find_by + create! would TOCTOU-race two concurrent token requests
  # for the same device into two duplicate rows. We acquire a SELECT
  # ... FOR UPDATE on the account row to serialize — account.with_lock
  # is unavailable because StandardId::AccountLocking overrides lock!
  # with a business-level method that takes a :reason kwarg.
  # The outer transaction (opened by TokenGrantFlow#generate_token_response)
  # releases the lock on commit/rollback.
  .class.where(id: .id).lock.first

  existing = StandardId::DeviceSession.find_by(account: , device_id: device_id)
  if existing
    existing.update!(
      expires_at: StandardId::DeviceSession.expiry,
      ip_address: ip_address || existing.ip_address,
      device_agent: user_agent || existing.device_agent
    )
    existing
  else
    StandardId::DeviceSession.create!(
      account: ,
      device_id: device_id,
      device_agent: user_agent.presence || "OAuth:#{grant_type}",
      ip_address: ip_address || "0.0.0.0",
      expires_at: StandardId::DeviceSession.expiry
    )
  end
end