Class: Iron::Oauth::Grant

Inherits:
ApplicationRecord show all
Defined in:
app/models/iron/oauth/grant.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.issue!(client:, user:, redirect_uri:, scopes:, code_challenge:, resource:, code_challenge_method: "S256", ttl: 10.minutes) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'app/models/iron/oauth/grant.rb', line 9

def self.issue!(client:, user:, redirect_uri:, scopes:, code_challenge:, resource:, code_challenge_method: "S256", ttl: 10.minutes)
  code = Secret.generate
  grant = create!(
    client: client,
    user: user,
    redirect_uri: redirect_uri,
    scopes: scopes,
    code_challenge: code_challenge,
    code_challenge_method: code_challenge_method,
    resource: resource,
    code_digest: Secret.digest(code),
    expires_at: ttl.from_now
  )
  [ grant, code ]
end

.redeem(code, client:, redirect_uri:, verifier:, resource:) ⇒ Object

Consumes the code only once the exchange is fully bound to the client, redirect URI, and PKCE verifier — all inside the lock. Validating before marking it redeemed stops an interceptor without the verifier from burning a legitimate client's code. When a block is given, the token is issued in the same transaction, so a failure there rolls the redemption back and leaves the code usable for a retry.



31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'app/models/iron/oauth/grant.rb', line 31

def self.redeem(code, client:, redirect_uri:, verifier:, resource:)
  grant = find_by(code_digest: Secret.digest(code))
  return unless grant

  result = nil
  grant.with_lock do
    if grant.redeemable_by?(client:, redirect_uri:, verifier:, resource:)
      grant.update!(redeemed_at: Time.current)
      result = block_given? ? yield(grant) : grant
    end
  end
  result
end

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


62
63
64
# File 'app/models/iron/oauth/grant.rb', line 62

def active?
  redeemed_at.nil? && !expired?
end

#expired?Boolean

Returns:

  • (Boolean)


58
59
60
# File 'app/models/iron/oauth/grant.rb', line 58

def expired?
  expires_at.past?
end

#pkce_verified?(verifier) ⇒ Boolean

Returns:

  • (Boolean)


53
54
55
56
# File 'app/models/iron/oauth/grant.rb', line 53

def pkce_verified?(verifier)
  expected = Base64.urlsafe_encode64(OpenSSL::Digest::SHA256.digest(verifier.to_s), padding: false)
  ActiveSupport::SecurityUtils.secure_compare(expected, code_challenge)
end

#redeemable_by?(client:, redirect_uri:, verifier:, resource:) ⇒ Boolean

Returns:

  • (Boolean)


45
46
47
48
49
50
51
# File 'app/models/iron/oauth/grant.rb', line 45

def redeemable_by?(client:, redirect_uri:, verifier:, resource:)
  active? &&
    client_id == client.id &&
    self.redirect_uri == redirect_uri &&
    self.resource == resource &&
    pkce_verified?(verifier)
end