# frozen_string_literal: true
module Doorkeeper
module AccessTokenMixin
extend ActiveSupport::Concern
include OAuth::Helpers
include Models::Expirable
include Models::Reusable
include Models::Revocable
include Models::Accessible
include Models::Orderable
include Models::SecretStorable
include Models::Scopes
include Models::ResourceOwnerable
include Models::ExpirationTimeSqlMath
module ClassMethods
# Returns an instance of the Doorkeeper::AccessToken with
# specific plain text token value.
#
# @param token [#to_s]
# Plain text token value (any object that responds to `#to_s`)
#
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
# if there is no record with such token
#
def by_token(token)
find_by_plaintext_token(:token, token)
end
# Returns an instance of the Doorkeeper::AccessToken
# with specific token value.
#
# @param refresh_token [#to_s]
# refresh token value (any object that responds to `#to_s`)
#
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
# if there is no record with such refresh token
#
def by_refresh_token(refresh_token)
find_by_plaintext_token(:refresh_token, refresh_token)
end
# Returns an instance of the Doorkeeper::AccessToken
# found by previous refresh token. Keep in mind that value
# of the previous_refresh_token isn't encrypted using
# secrets strategy.
#
# @param previous_refresh_token [#to_s]
# previous refresh token value (any object that responds to `#to_s`)
#
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
# if there is no record with such refresh token
#
def by_previous_refresh_token(previous_refresh_token)
find_by(refresh_token: previous_refresh_token)
end
# Revokes AccessToken records that have not been revoked and associated
# with the specific Application and Resource Owner.
#
# @param application_id [Integer]
# ID of the Application
# @param resource_owner [ActiveRecord::Base, Integer]
# instance of the Resource Owner model or it's ID
#
def revoke_all_for(application_id, resource_owner, clock = Time)
by_resource_owner(resource_owner)
.where(
application_id: application_id,
revoked_at: nil,
)
.update_all(revoked_at: clock.now.utc)
end
# Looking for not revoked Access Token with a matching set of scopes
# that belongs to specific Application and Resource Owner.
#
# @param application [Doorkeeper::Application]
# Application instance
# @param resource_owner [ActiveRecord::Base, Integer]
# Resource Owner model instance or it's ID
# @param scopes [String, Doorkeeper::OAuth::Scopes]
# set of scopes
# @param custom_attributes [Nilable Hash]
# A nil value, or hash with keys corresponding to the custom attributes
# configured with the `custom_access_token_attributes` config option.
# A nil value will ignore custom attributes.
#
# @return [Doorkeeper::AccessToken, nil] Access Token instance or
# nil if matching record was not found
#
def matching_token_for(application, resource_owner, scopes, custom_attributes: nil, include_expired: true)
tokens = authorized_tokens_for(application&.id, resource_owner)
tokens = tokens.not_expired unless include_expired
find_matching_token(tokens, application, custom_attributes, scopes)
end
# Interface to enumerate access token records in batches in order not
# to bloat the memory. Could be overloaded in any ORM extension.
#
def find_access_token_in_batches(relation, **args, &block)
relation.find_in_batches(**args, &block)
end
# Enumerates AccessToken records in batches to find a matching token.
# Batching is required in order not to pollute the memory if Application
# has huge amount of associated records.
#
# ActiveRecord 5.x - 6.x ignores custom ordering so we can't perform a
# database sort by created_at, so we need to load all the matching records,
# sort them and find latest one.
#
# @param relation [ActiveRecord::Relation]
# Access tokens relation
# @param application [Doorkeeper::Application]
# Application instance
# @param scopes [String, Doorkeeper::OAuth::Scopes]
# set of scopes
# @param custom_attributes [Nilable Hash]
# A nil value, or hash with keys corresponding to the custom attributes
# configured with the `custom_access_token_attributes` config option.
# A nil value will ignore custom attributes.
#
# @return [Doorkeeper::AccessToken, nil] Access Token instance or
# nil if matching record was not found
#
def find_matching_token(relation, application, custom_attributes, scopes)
return nil unless relation
matching_tokens = []
batch_size = Doorkeeper.configuration.token_lookup_batch_size
find_access_token_in_batches(relation, batch_size: batch_size) do |batch|
tokens = batch.select do |token|
scopes_match?(token.scopes, scopes, application&.scopes) &&
custom_attributes_match?(token, custom_attributes)
end
matching_tokens.concat(tokens)
end
matching_tokens.max_by(&:created_at)
end
# Checks whether the token scopes match the scopes from the parameters
#
# @param token_scopes [#to_s]
# set of scopes (any object that responds to `#to_s`)
# @param param_scopes [Doorkeeper::OAuth::Scopes]
# scopes from params
# @param app_scopes [Doorkeeper::OAuth::Scopes]
# Application scopes
#
# @return [Boolean] true if the param scopes match the token scopes,
# and all the param scopes are defined in the application (or in the
# server configuration if the application doesn't define any scopes),
# and false in other cases
#
def scopes_match?(token_scopes, param_scopes, app_scopes)
return true if token_scopes.empty? && param_scopes.empty?
(token_scopes.sort == param_scopes.sort) &&
Doorkeeper::OAuth::Helpers::ScopeChecker.valid?(
scope_str: param_scopes.to_s,
server_scopes: Doorkeeper.config.scopes,
app_scopes: app_scopes,
)
end
# Checks whether the token custom attribute values match the custom
# attributes from the parameters.
#
# @param token [Doorkeeper::AccessToken]
# The access token whose custom attributes are being compared
# to the custom_attributes.
#
# @param custom_attributes [Hash]
# A hash of the attributes for which we want to determine whether
# the token's custom attributes match.
#
# @return [Boolean] true if the token's custom attribute values
# match those in the custom_attributes, or if both are empty/blank.
# False otherwise.
def custom_attributes_match?(token, custom_attributes)
return true if custom_attributes.nil?
token_attribs = token.custom_attributes
return true if token_attribs.blank? && custom_attributes.blank?
Doorkeeper.config.custom_access_token_attributes.all? do |attribute|
token_attribs[attribute] == custom_attributes[attribute]
end
end
# Looking for not expired AccessToken record with a matching set of
# scopes that belongs to specific Application and Resource Owner.
# If it doesn't exists - then creates it.
#
# @param application [Doorkeeper::Application]
# Application instance
# @param resource_owner [ActiveRecord::Base, Integer]
# Resource Owner model instance or it's ID
# @param scopes [#to_s]
# set of scopes (any object that responds to `#to_s`)
# @param token_attributes [Hash]
# Additional attributes to use when creating a token
# @option token_attributes [Integer] :expires_in
# token lifetime in seconds
# @option token_attributes [Boolean] :use_refresh_token
# whether to use the refresh token
#
# @return [Doorkeeper::AccessToken] existing record or a new one
#
def find_or_create_for(application:, resource_owner:, scopes:, **token_attributes)
scopes = Doorkeeper::OAuth::Scopes.from_string(scopes) if scopes.is_a?(String)
if Doorkeeper.config.reuse_access_token
custom_attributes = extract_custom_attributes(token_attributes).presence
access_token = matching_token_for(
application, resource_owner, scopes, custom_attributes: custom_attributes, include_expired: false)
return access_token if access_token&.reusable?
end
create_for(
application: application,
resource_owner: resource_owner,
scopes: scopes,
**token_attributes,
)
end
# Creates a not expired AccessToken record with a matching set of
# scopes that belongs to specific Application and Resource Owner.
#
# @param application [Doorkeeper::Application]
# Application instance
# @param resource_owner [ActiveRecord::Base, Integer]
# Resource Owner model instance or it's ID
# @param scopes [#to_s]
# set of scopes (any object that responds to `#to_s`)
# @param token_attributes [Hash]
# Additional attributes to use when creating a token
# @option token_attributes [Integer] :expires_in
# token lifetime in seconds
# @option token_attributes [Boolean] :use_refresh_token
# whether to use the refresh token
#
# @return [Doorkeeper::AccessToken] new access token
#
def create_for(application:, resource_owner:, scopes:, **token_attributes)
token_attributes[:application] = application
token_attributes[:scopes] = scopes.to_s
if Doorkeeper.config.polymorphic_resource_owner?
token_attributes[:resource_owner] = resource_owner
else
token_attributes[:resource_owner_id] = resource_owner_id_for(resource_owner)
end
create!(token_attributes)
end
# Looking for not revoked Access Token records that belongs to specific
# Application and Resource Owner.
#
# @param application_id [Integer]
# ID of the Application model instance
# @param resource_owner [ActiveRecord::Base, Integer]
# Resource Owner model instance or it's ID
#
# @return [ActiveRecord::Relation]
# collection of matching AccessToken objects
#
def authorized_tokens_for(application_id, resource_owner)
by_resource_owner(resource_owner).where(
application_id: application_id,
revoked_at: nil,
)
end
# Convenience method for backwards-compatibility, return the last
# matching token for the given Application and Resource Owner.
#
# @param application_id [Integer]
# ID of the Application model instance
# @param resource_owner [ActiveRecord::Base, Integer]
# ID of the Resource Owner model instance
#
# @return [Doorkeeper::AccessToken, nil] matching AccessToken object or
# nil if nothing was found
#
def last_authorized_token_for(application_id, resource_owner)
authorized_tokens_for(application_id, resource_owner)
.ordered_by(:created_at, :desc)
.first
end
##
# Determines the secret storing transformer
# Unless configured otherwise, uses the plain secret strategy
#
# @return [Doorkeeper::SecretStoring::Base]
#
def secret_strategy
::Doorkeeper.config.token_secret_strategy
end
##
# Determine the fallback storing strategy
# Unless configured, there will be no fallback
def fallback_secret_strategy
::Doorkeeper.config.token_secret_fallback_strategy
end
# Extracts the token's custom attributes (defined by the
# custom_access_token_attributes config option) from the token's attributes.
#
# @param attributes [Hash]
# A hash of the access token's attributes.
# @return [Hash]
# A hash containing only the custom access token attributes.
def extract_custom_attributes(attributes)
attributes.with_indifferent_access.slice(
*Doorkeeper.configuration.custom_access_token_attributes)
end
end
# Access Token type: Bearer.
# @see https://datatracker.ietf.org/doc/html/rfc6750
# The OAuth 2.0 Authorization Framework: Bearer Token Usage
#
def token_type
"Bearer"
end
def use_refresh_token?
@use_refresh_token ||= false
!!@use_refresh_token
end
# JSON representation of the Access Token instance.
#
# @return [Hash] hash with token data
def as_json(_options = {})
{
resource_owner_id: resource_owner_id,
scope: scopes,
expires_in: expires_in_seconds,
application: { uid: application.try(:uid) },
created_at: created_at.to_i,
}.tap do |json|
if Doorkeeper.configuration.polymorphic_resource_owner?
json[:resource_owner_type] = resource_owner_type
end
end
end
# The token's custom attributes, as defined by
# the custom_access_token_attributes config option.
#
# @return [Hash] hash of custom access token attributes.
def custom_attributes
self.class.extract_custom_attributes(attributes)
end
# Indicates whether the token instance have the same credential
# as the other Access Token.
#
# @param access_token [Doorkeeper::AccessToken] other token
#
# @return [Boolean] true if credentials are same of false in other cases
#
def same_credential?(access_token)
application_id == access_token.application_id &&
same_resource_owner?(access_token)
end
# Indicates whether the token instance have the same credential
# as the other Access Token.
#
# @param access_token [Doorkeeper::AccessToken] other token
#
# @return [Boolean] true if credentials are same of false in other cases
#
def same_resource_owner?(access_token)
if Doorkeeper.configuration.polymorphic_resource_owner?
resource_owner == access_token.resource_owner
else
resource_owner_id == access_token.resource_owner_id
end
end
# Indicates if token is acceptable for specific scopes.
#
# @param scopes [Array<String>] scopes
#
# @return [Boolean] true if record is accessible and includes scopes or
# false in other cases
#
def acceptable?(scopes)
accessible? && includes_scope?(*scopes)
end
# We keep a volatile copy of the raw refresh token for initial communication
# The stored refresh_token may be mapped and not available in cleartext.
def plaintext_refresh_token
if secret_strategy.allows_restoring_secrets?
secret_strategy.restore_secret(self, :refresh_token)
else
@raw_refresh_token
end
end
# We keep a volatile copy of the raw token for initial communication
# The stored refresh_token may be mapped and not available in cleartext.
#
# Some strategies allow restoring stored secrets (e.g. symmetric encryption)
# while hashing strategies do not, so you cannot rely on this value
# returning a present value for persisted tokens.
def plaintext_token
if secret_strategy.allows_restoring_secrets?
secret_strategy.restore_secret(self, :token)
else
@raw_token
end
end
# Revokes token with `:refresh_token` equal to `:previous_refresh_token`
# and clears `:previous_refresh_token` attribute.
#
def revoke_previous_refresh_token!
return if !self.class.refresh_token_revoked_on_use? || previous_refresh_token.blank?
old_refresh_token&.revoke
update_attribute(:previous_refresh_token, "")
end
private
# Searches for Access Token record with `:refresh_token` equal to
# `:previous_refresh_token` value.
#
# @return [Doorkeeper::AccessToken, nil]
# Access Token record or nil if nothing found
#
def old_refresh_token
@old_refresh_token ||= self.class.by_previous_refresh_token(previous_refresh_token)
end
# Generates refresh token with UniqueToken generator.
#
# @return [String] refresh token value
#
def generate_refresh_token
@raw_refresh_token = UniqueToken.generate
secret_strategy.store_secret(self, :refresh_token, @raw_refresh_token)
end
# Generates and sets the token value with the
# configured Generator class (see Doorkeeper.config).
#
# @return [String] generated token value
#
# @raise [Doorkeeper::Errors::UnableToGenerateToken]
# custom class doesn't implement .generate method
# @raise [Doorkeeper::Errors::TokenGeneratorNotFound]
# custom class doesn't exist
#
def generate_token
self.created_at ||= Time.now.utc
@raw_token = token_generator.generate(attributes_for_token_generator)
secret_strategy.store_secret(self, :token, @raw_token)
@raw_token
end
# Set of attributes that would be passed to token generator to
# generate unique token based on them.
#
# @return [Hash] set of attributes
#
def attributes_for_token_generator
{
resource_owner_id: resource_owner_id,
scopes: scopes,
application: application,
expires_in: expires_in,
created_at: created_at,
}.tap do |attributes|
if Doorkeeper.config.polymorphic_resource_owner?
attributes[:resource_owner] = resource_owner
end
Doorkeeper.config.custom_access_token_attributes.each do |attribute_name|
attributes[attribute_name] = public_send(attribute_name)
end
end
end
def token_generator
generator_name = Doorkeeper.config.access_token_generator
generator = generator_name.constantize
return generator if generator.respond_to?(:generate)
raise Errors::UnableToGenerateToken, "#{generator} does not respond to `.generate`."
rescue NameError
raise Errors::TokenGeneratorNotFound, "#{generator_name} not found"
end
end
end