Class: Admin::DeviceAuthorization

Inherits:
ApplicationRecord show all
Defined in:
app/models/admin/device_authorization.rb

Defined Under Namespace

Classes: TokenError

Constant Summary collapse

REQUEST_EXPIRES_IN =
10.minutes
TOKEN_EXPIRES_IN =
1.hour

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.consume_request!(device_code:) ⇒ Object

Consume an approved request, returns the issued token payload.

Raises:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'app/models/admin/device_authorization.rb', line 72

def self.consume_request!(device_code:)
  device_authorization = find_by(device_code_digest: digest(device_code.to_s))
  raise TokenError.new("invalid_grant") unless device_authorization

  device_authorization.with_lock do
    device_authorization.reload

    if (error = device_authorization.token_error)
      raise TokenError.new(error)
    end

    device_authorization.consume!
  end
end

.create_request!(requested_ip:, user_agent:) ⇒ Object

Creates a new un-approved request.



49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'app/models/admin/device_authorization.rb', line 49

def self.create_request!(requested_ip:, user_agent:)
  device_code = SecureRandom.urlsafe_base64(32)

  device_authorization = create!(
    device_code_digest: digest(device_code),
    user_code:          generate_user_code,
    request_expires_at: REQUEST_EXPIRES_IN.from_now,
    requested_ip:,
    user_agent:,
  )

  [device_authorization, device_code]
end

.digest(device_code) ⇒ Object



63
64
65
# File 'app/models/admin/device_authorization.rb', line 63

def self.digest(device_code)
  Digest::SHA256.hexdigest(device_code)
end

.generate_user_codeObject



67
68
69
# File 'app/models/admin/device_authorization.rb', line 67

def self.generate_user_code
  "#{SecureRandom.alphanumeric(4).upcase}-#{SecureRandom.alphanumeric(4).upcase}"
end

.issue_token!(principal:) ⇒ Object

Issue a new token directly from a validated assertion, returns the issued token payload.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'app/models/admin/device_authorization.rb', line 88

def self.issue_token!(principal:, **)
  case principal.scope
  when "admin/user"
    admin_user = Admin::User.find_by(principal.attributes_for_find)

    unless admin_user
      Rails.logger.warn("Koi::Identity rejected #{principal}: no matching admin user")
      raise JWT::VerificationError, "unknown user for #{principal}"
    end

    new(admin_user:, principal:, **).consume!
  when %r{\Aadmin/role/(?<slug>[a-z0-9_]+)\z}
    admin_role = Admin::Role.materialize(Regexp.last_match(:slug))

    new(admin_role:, principal:, **).consume!
  else
    raise ArgumentError, "unknown scope #{principal.scope.inspect}"
  end
end

Instance Method Details

#actionable?Boolean

Returns:

  • (Boolean)


153
154
155
# File 'app/models/admin/device_authorization.rb', line 153

def actionable?
  pending? && !expired?
end

#actorAdmin::User, ...

Returns:



109
110
111
# File 'app/models/admin/device_authorization.rb', line 109

def actor
  admin_user || admin_role
end

#approve!(admin_user:) ⇒ Object



138
139
140
141
142
143
144
# File 'app/models/admin/device_authorization.rb', line 138

def approve!(admin_user:)
  update!(
    status:      "approved",
    approved_at: Time.current,
    admin_user:,
  )
end

#consume!Object



128
129
130
131
132
133
134
135
136
# File 'app/models/admin/device_authorization.rb', line 128

def consume!
  update!(
    status:           "consumed",
    consumed_at:      Time.current,
    token_expires_at: TOKEN_EXPIRES_IN.from_now,
  )

  token_payload
end

#deny!(admin_user:) ⇒ Object



146
147
148
149
150
151
# File 'app/models/admin/device_authorization.rb', line 146

def deny!(admin_user:)
  update!(
    status:     "denied",
    admin_user:,
  )
end

#expired?Boolean

Returns:

  • (Boolean)


113
114
115
# File 'app/models/admin/device_authorization.rb', line 113

def expired?
  request_expires_at.present? && request_expires_at <= Time.current
end

#issuable?Boolean

Returns:

  • (Boolean)


117
118
119
# File 'app/models/admin/device_authorization.rb', line 117

def issuable?
  approved? && !expired?
end

#token_errorObject



121
122
123
124
125
126
# File 'app/models/admin/device_authorization.rb', line 121

def token_error
  return "authorization_pending" if pending?
  return "access_denied" if denied?

  "invalid_grant" if consumed? || expired?
end