Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ gem "rubocop-rails", require: false
gem "rubocop-rspec", require: false

gem "bcrypt", "~> 3.1", require: false
gem "jwt", require: false

gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw]
Expand Down
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,16 @@ en:
invalid_token:
revoked: "The access token was revoked"
expired: "The access token expired"
invalid_dpop_key_binding: "Invalid DPoP key binding"
unknown: "The access token is invalid"
revoke:
unauthorized: "You are not authorized to revoke this token"

forbidden_token:
missing_scope: 'Access to this resource requires scope "%{oauth_scopes}".'

invalid_dpop_proof: "Invalid DPoP proof"

flash:
applications:
create:
Expand Down
1 change: 1 addition & 0 deletions gemfiles/rails_7_0.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ gem "base64"
gem "drb"
gem "mutex_m"
gem "concurrent-ruby", "1.3.4"
gem "jwt", require: false

gemspec path: "../"
1 change: 1 addition & 0 deletions gemfiles/rails_7_1.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ gem "sprockets-rails"
gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw, :x64_mingw]
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw]
gem "timecop"
gem "jwt", require: false

gemspec path: "../"
1 change: 1 addition & 0 deletions gemfiles/rails_7_2.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ gem "sprockets-rails"
gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw, :x64_mingw]
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw]
gem "timecop"
gem "jwt", require: false

gemspec path: "../"
1 change: 1 addition & 0 deletions gemfiles/rails_8_0.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ gem "sprockets-rails"
gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw]
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw]
gem "timecop"
gem "jwt", require: false

gemspec path: "../"
1 change: 1 addition & 0 deletions gemfiles/rails_edge.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ gem "sprockets-rails"
gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw]
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw]
gem "timecop"
gem "jwt", require: false

gemspec path: "../"
2 changes: 2 additions & 0 deletions lib/doorkeeper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ module OAuth
autoload :Client, "doorkeeper/oauth/client"
autoload :ClientCredentialsRequest, "doorkeeper/oauth/client_credentials_request"
autoload :CodeRequest, "doorkeeper/oauth/code_request"
autoload :DPoPProof, "doorkeeper/oauth/dpop_proof"
autoload :ErrorResponse, "doorkeeper/oauth/error_response"
autoload :Error, "doorkeeper/oauth/error"
autoload :InvalidDPoPProofResponse, "doorkeeper/oauth/invalid_dpop_proof_response"
autoload :InvalidTokenResponse, "doorkeeper/oauth/invalid_token_response"
autoload :InvalidRequestResponse, "doorkeeper/oauth/invalid_request_response"
autoload :ForbiddenTokenResponse, "doorkeeper/oauth/forbidden_token_response"
Expand Down
19 changes: 15 additions & 4 deletions lib/doorkeeper/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def reuse_access_token
@config.instance_variable_set(:@reuse_access_token, true)
end

# Choose to use the url path for native autorization codes
# Choose to use the url path for native autorization codes
# Enabling this flag sets the authorization code response route for
# native redirect uris to oauth/authorize/<code>. The default is
# oauth/authorize/native?code=<code>.
Expand Down Expand Up @@ -129,6 +129,11 @@ def force_pkce
@config.instance_variable_set(:@force_pkce, true)
end

# Require all access token token requests to include a DPoP proof (disabled by default)
def force_dpop
@config.instance_variable_set(:@force_dpop, true)
end

# Use an API mode for applications generated with --api argument
# It will skip applications controller, disable forgery protection
def api_only
Expand Down Expand Up @@ -256,6 +261,8 @@ def configure_secrets_for(type, using:, fallback:)
option :pkce_code_challenge_methods, default: %w[plain S256]
option :handle_auth_errors, default: :render
option :token_lookup_batch_size, default: 10_000
option :dpop_iat_leeway, default: 300
option :dpop_signature_algorithms, default: %w[ES256 PS256]
# Sets the token_reuse_limit
# It will be used only when reuse_access_token option in enabled
# By default it will be 100
Expand Down Expand Up @@ -513,6 +520,10 @@ def force_pkce?
option_set? :force_pkce
end

def force_dpop?
option_set? :force_dpop
end

def enforce_configured_scopes?
option_set? :enforce_configured_scopes
end
Expand Down Expand Up @@ -575,7 +586,7 @@ def scopes_by_grant_type

def pkce_code_challenge_methods_supported
return [] unless access_grant_model.pkce_supported?

pkce_code_challenge_methods
end

Expand All @@ -588,7 +599,7 @@ def access_token_methods
from_bearer_authorization
from_access_token_param
from_bearer_param
]
].tap { |it| it.prepend(:from_dpop_authorization) if access_token_model.dpop_supported? }
end

def enabled_grant_flows
Expand Down Expand Up @@ -616,7 +627,7 @@ def token_grant_types
def deprecated_token_grant_types_resolver
@deprecated_token_grant_types ||= calculate_token_grant_types
end

def native_authorization_code_route
@use_url_path_for_native_authorization = false unless defined?(@use_url_path_for_native_authorization)
@use_url_path_for_native_authorization ? '/:code' : '/native'
Expand Down
7 changes: 7 additions & 0 deletions lib/doorkeeper/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ def self.translate_options
end
end

class InvalidDPoPProof < BaseResponseError
def self.name_for_response
:invalid_dpop_proof
end
end

UnableToGenerateToken = Class.new(DoorkeeperError)
TokenGeneratorNotFound = Class.new(DoorkeeperError)
NoOrmCleaner = Class.new(DoorkeeperError)
Expand All @@ -81,5 +87,6 @@ def self.translate_options
TokenRevoked = Class.new(InvalidToken)
TokenUnknown = Class.new(InvalidToken)
TokenForbidden = Class.new(InvalidToken)
TokenInvalidDPoPKeyBinding = Class.new(InvalidToken)
end
end
16 changes: 6 additions & 10 deletions lib/doorkeeper/grape/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module Helpers
include Doorkeeper::Rails::Helpers

# endpoint specific scopes > parameter scopes > default scopes
def doorkeeper_authorize!(*scopes)
def doorkeeper_authorize!(*scopes, dpop: nil)
endpoint_scopes = endpoint.route_setting(:scopes) ||
endpoint.options[:route_options][:scopes]

Expand All @@ -22,7 +22,10 @@ def doorkeeper_authorize!(*scopes)
Doorkeeper::OAuth::Scopes.from_array(scopes)
end

super(*scopes)
endpoint_dpop = endpoint.route_setting(:dpop) ||
endpoint.options[:route_options][:dpop]

super(*scopes, dpop: endpoint_dpop || dpop)
end

def doorkeeper_render_error_with(error)
Expand All @@ -36,14 +39,7 @@ def endpoint
env["api.endpoint"]
end

def doorkeeper_token
@doorkeeper_token ||= OAuth::Token.authenticate(
decorated_request,
*Doorkeeper.config.access_token_methods,
)
end

def decorated_request
def __doorkeeper_request__
AuthorizationDecorator.new(request)
end

Expand Down
39 changes: 36 additions & 3 deletions lib/doorkeeper/models/access_token_mixin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -326,14 +326,24 @@ def extract_custom_attributes(attributes)
attributes.with_indifferent_access.slice(
*Doorkeeper.configuration.custom_access_token_attributes)
end

# Checks whether the token can be sender-constrained using DPoP.
#
# @see https://datatracker.ietf.org/doc/html/rfc9449
# OAuth 2.0 Demonstrating Proof of Possession (DPoP)
def dpop_supported?
column_names.include?("dpop_jkt")
end
end

# Access Token type: Bearer.
# Access Token type: Bearer or DPoP
#
# @see https://datatracker.ietf.org/doc/html/rfc6750
# The OAuth 2.0 Authorization Framework: Bearer Token Usage
#
# @see https://datatracker.ietf.org/doc/html/rfc9449
# OAuth 2.0 Demonstrating Proof of Possession (DPoP)
def token_type
"Bearer"
uses_dpop? ? "DPoP" : "Bearer"
end

def use_refresh_token?
Expand Down Expand Up @@ -438,6 +448,29 @@ def revoke_previous_refresh_token!
update_attribute(:previous_refresh_token, "")
end

# Checks whether the token has been sender-constrained using DPoP. The token
# is never considered sender-constrained if the DPoP migration was not run.
#
# @see https://datatracker.ietf.org/doc/html/rfc9449
# OAuth 2.0 Demonstrating Proof of Possession (DPoP)
def uses_dpop?
self.class.dpop_supported? && dpop_jkt.present?
end

# Checks whether the token is bound to the given DPoP key thumbprint (`jkt`).
#
# DPoP-bound (sender-constrained) access tokens are bound to a public key by its JWK
# SHA-256 thumbprint (`dpop_jkt`). This method returns `false` if the token is not DPoP-bound,
# otherwise it returns whether the token's stored thumbprint matches the provided `jkt`.
#
# @param jkt [String] The JWK SHA-256 thumbprint of the DPoP public key jkt
# @return [Boolean] True if the token is DPoP-bound and bound to the given `jkt`
def dpop_binding_matches?(jkt)
return false unless uses_dpop?

ActiveSupport::SecurityUtils.secure_compare(dpop_jkt, jkt)
end

private

# Searches for Access Token record with `:refresh_token` equal to
Expand Down
4 changes: 3 additions & 1 deletion lib/doorkeeper/oauth/authorization_code_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class AuthorizationCodeRequest < BaseRequest
attr_reader :grant, :client, :redirect_uri, :access_token, :code_verifier,
:invalid_request_reason, :missing_param

def initialize(server, grant, client, parameters = {})
def initialize(server, grant, client, **base_options)
super(**base_options)

@server = server
@client = client
@grant = grant
Expand Down
32 changes: 31 additions & 1 deletion lib/doorkeeper/oauth/base_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@ module OAuth
class BaseRequest
include Validations

attr_reader :grant_type, :server
attr_reader :grant_type, :parameters, :server

delegate :default_scopes, to: :server

def self.inherited(subclass)
super
subclass.validate :dpop_proof, error: Errors::InvalidDPoPProof
end

def initialize(dpop_proof: nil, parameters: {})
@dpop_proof = dpop_proof
@parameters = parameters
end

def authorize
if valid?
before_successful_response
Expand Down Expand Up @@ -36,6 +46,7 @@ def find_or_create_access_token(client, resource_owner, scopes, custom_attribute
scopes: scopes,
expires_in: Authorization::Token.access_token_expires_in(server, context),
use_refresh_token: Authorization::Token.refresh_token_enabled?(server, context),
**dpop_token_attributes,
}

@access_token =
Expand All @@ -52,6 +63,8 @@ def after_successful_response

private

attr_reader :dpop_proof

def build_scopes
if @original_scopes.present?
OAuth::Scopes.from_string(@original_scopes)
Expand All @@ -63,6 +76,23 @@ def build_scopes
client_scopes.allowed(default_scopes)
end
end

def dpop_supported?
Doorkeeper.config.access_token_model.dpop_supported?
end

def dpop_token_attributes
return {} unless dpop_supported?

{ dpop_jkt: dpop_proof&.jkt }.compact
end

def validate_dpop_proof
return true unless dpop_supported?
return true unless Doorkeeper.config.force_dpop? || dpop_proof.present?

dpop_proof.valid?
end
end
end
end
17 changes: 10 additions & 7 deletions lib/doorkeeper/oauth/client_credentials_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@
module Doorkeeper
module OAuth
class ClientCredentialsRequest < BaseRequest
attr_reader :client, :original_scopes, :parameters, :response
attr_reader :client, :original_scopes, :response

alias error_response response

delegate :error, to: :issuer
def initialize(server, client, parameters: {}, **base_options)
super(parameters: parameters.except(:scope), **base_options)

def initialize(server, client, parameters = {})
@client = client
@server = server
@response = nil
@original_scopes = parameters[:scope]
@parameters = parameters.except(:scope)
end

def access_token
Expand All @@ -28,12 +27,16 @@ def issuer
)
end

private
def error
@error || issuer.error
end

def valid?
issuer.create(client, scopes, custom_token_attributes_with_data)
def validate
super && issuer.create(client, scopes, dpop_token_attributes.merge(custom_token_attributes_with_data))
end

private

def custom_token_attributes_with_data
parameters
.with_indifferent_access
Expand Down
Loading