lib/rodauth/features/oauth_base.rb in rodauth-oauth-0.10.4 vs lib/rodauth/features/oauth_base.rb in rodauth-oauth-1.0.0.pre.beta1
- old
+ new
@@ -1,21 +1,24 @@
# frozen_string_literal: true
require "time"
require "base64"
require "securerandom"
-require "net/http"
require "rodauth/version"
-require "rodauth/oauth/version"
-require "rodauth/oauth/ttl_store"
+require "rodauth/oauth"
require "rodauth/oauth/database_extensions"
-require "rodauth/oauth/refinements"
+require "rodauth/oauth/http_extensions"
module Rodauth
Feature.define(:oauth_base, :OauthBase) do
- using RegexpExtensions
+ include OAuth::HTTPExtensions
+ EMPTY_HASH = {}.freeze
+
+ auth_value_methods(:http_request)
+ auth_value_methods(:http_request_cache)
+
SCOPES = %w[profile.read].freeze
before "token"
error_flash "Please authorize to continue", "require_authorization"
@@ -24,45 +27,37 @@
button "Cancel", "oauth_cancel"
auth_value_method :json_response_content_type, "application/json"
auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
- auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
+ auth_value_method :oauth_access_token_expires_in, 60 * 60 # 60 minutes
auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
auth_value_method :oauth_unique_id_generation_retries, 3
- auth_value_method :oauth_response_mode, "query"
- auth_value_method :oauth_auth_methods_supported, %w[client_secret_basic client_secret_post]
+ auth_value_method :oauth_token_endpoint_auth_methods_supported, %w[client_secret_basic client_secret_post]
+ auth_value_method :oauth_grant_types_supported, %w[refresh_token]
+ auth_value_method :oauth_response_types_supported, []
+ auth_value_method :oauth_response_modes_supported, []
auth_value_method :oauth_valid_uri_schemes, %w[https]
auth_value_method :oauth_scope_separator, " "
- auth_value_method :oauth_tokens_table, :oauth_tokens
- auth_value_method :oauth_tokens_id_column, :id
-
- %i[
- oauth_application_id oauth_token_id oauth_grant_id account_id
- token refresh_token scopes
- expires_in revoked_at
- ].each do |column|
- auth_value_method :"oauth_tokens_#{column}_column", column
- end
-
# OAuth Grants
auth_value_method :oauth_grants_table, :oauth_grants
auth_value_method :oauth_grants_id_column, :id
%i[
- account_id oauth_application_id
- redirect_uri code scopes access_type
+ account_id oauth_application_id type
+ redirect_uri code scopes
expires_in revoked_at
+ token refresh_token
].each do |column|
auth_value_method :"oauth_grants_#{column}_column", column
end
- # Oauth Token Hash
- auth_value_method :oauth_tokens_token_hash_column, nil
- auth_value_method :oauth_tokens_refresh_token_hash_column, nil
+ # Enables Token Hash
+ auth_value_method :oauth_grants_token_hash_column, :token
+ auth_value_method :oauth_grants_refresh_token_hash_column, :refresh_token
# Access Token reuse
auth_value_method :oauth_reuse_access_token, false
auth_value_method :oauth_applications_table, :oauth_applications
@@ -71,40 +66,38 @@
%i[
account_id
name description scopes
client_id client_secret
homepage_url redirect_uri
- token_endpoint_auth_method grant_types response_types
+ token_endpoint_auth_method grant_types response_types response_modes
logo_uri tos_uri policy_uri jwks jwks_uri
contacts software_id software_version
].each do |column|
auth_value_method :"oauth_applications_#{column}_column", column
end
+ # Enables client secret Hash
+ auth_value_method :oauth_applications_client_secret_hash_column, :client_secret
- auth_value_method :authorization_required_error_status, 401
- auth_value_method :invalid_oauth_response_status, 400
- auth_value_method :already_in_use_response_status, 409
+ auth_value_method :oauth_authorization_required_error_status, 401
+ auth_value_method :oauth_invalid_response_status, 400
+ auth_value_method :oauth_already_in_use_response_status, 409
# Feature options
- auth_value_method :oauth_application_default_scope, SCOPES.first
auth_value_method :oauth_application_scopes, SCOPES
auth_value_method :oauth_token_type, "bearer"
- auth_value_method :oauth_refresh_token_protection_policy, "none" # can be: none, sender_constrained, rotation
+ auth_value_method :oauth_refresh_token_protection_policy, "rotation" # can be: none, sender_constrained, rotation
- translatable_method :invalid_client_message, "Invalid client"
- translatable_method :invalid_grant_type_message, "Invalid grant type"
- translatable_method :invalid_grant_message, "Invalid grant"
- translatable_method :invalid_scope_message, "Invalid scope"
- translatable_method :unsupported_token_type_message, "Invalid token type hint"
+ translatable_method :oauth_invalid_client_message, "Invalid client"
+ translatable_method :oauth_invalid_grant_type_message, "Invalid grant type"
+ translatable_method :oauth_invalid_grant_message, "Invalid grant"
+ translatable_method :oauth_invalid_scope_message, "Invalid scope"
+ translatable_method :oauth_unsupported_token_type_message, "Invalid token type hint"
- translatable_method :unique_error_message, "is already in use"
- translatable_method :already_in_use_message, "error generating unique token"
- auth_value_method :already_in_use_error_code, "invalid_request"
- auth_value_method :invalid_grant_type_error_code, "unsupported_grant_type"
+ translatable_method :oauth_already_in_use_message, "error generating unique token"
+ auth_value_method :oauth_already_in_use_error_code, "invalid_request"
+ auth_value_method :oauth_invalid_grant_type_error_code, "unsupported_grant_type"
- # Resource Server params
- # Only required to use if the plugin is to be used in a resource server
auth_value_method :is_authorization_server?, true
auth_value_methods(:only_json?)
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
@@ -120,45 +113,43 @@
:secret_hash,
:generate_token_hash,
:secret_matches?,
:authorization_server_url,
:oauth_unique_id_generator,
- :oauth_tokens_unique_columns,
- :require_authorizable_account
+ :oauth_grants_unique_columns,
+ :require_authorizable_account,
+ :oauth_account_ds,
+ :oauth_application_ds
)
# /token
- route(:token) do |r|
- next unless is_authorization_server?
-
- before_token_route
+ auth_server_route(:token) do |r|
require_oauth_application
+ before_token_route
r.post do
catch_error do
- validate_oauth_token_params
+ validate_token_params
- oauth_token = nil
+ oauth_grant = nil
transaction do
before_token
- oauth_token = create_oauth_token(param("grant_type"))
+ oauth_grant = create_token(param("grant_type"))
end
- json_response_success(json_access_token_payload(oauth_token))
+ json_response_success(json_access_token_payload(oauth_grant))
end
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
+ throw_json_response_error(oauth_invalid_response_status, "invalid_request")
end
end
- def oauth_server_metadata(issuer = nil)
+ def load_oauth_server_metadata_route(issuer = nil)
request.on(".well-known") do
- request.on("oauth-authorization-server") do
- request.get do
- json_response_success(oauth_server_metadata_body(issuer), true)
- end
+ request.get("oauth-authorization-server") do
+ json_response_success(oauth_server_metadata_body(issuer), true)
end
end
end
def check_csrf?
@@ -168,48 +159,52 @@
else
super
end
end
- # Overrides session_value, so that a valid authorization token also authenticates a request
- # TODO: deprecate
- def session_value
- super || oauth_token_subject
- end
-
def oauth_token_subject
return unless authorization_token
- # TODO: fix this once tokens know which type they were generated with
- authorization_token[oauth_tokens_account_id_column] ||
- authorization_token[oauth_tokens_oauth_application_id_column]
+ authorization_token[oauth_grants_account_id_column] ||
+ db[oauth_applications_table].where(
+ oauth_applications_id_column => authorization_token[oauth_grants_oauth_application_id_column]
+ ).select_map(oauth_applications_client_id_column).first
end
+ def current_oauth_account
+ account_id = authorization_token[oauth_grants_account_id_column]
+
+ return unless account_id
+
+ oauth_account_ds(account_id).first
+ end
+
+ def current_oauth_application
+ oauth_application_ds(authorization_token[oauth_grants_oauth_application_id_column]).first
+ end
+
def accepts_json?
return true if only_json?
(accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
end
- unless method_defined?(:json_request?)
- # copied from the jwt feature
- def json_request?
- return @json_request if defined?(@json_request)
+ # copied from the jwt feature
+ def json_request?
+ return super if features.include?(:jsonn)
+ return @json_request if defined?(@json_request)
- @json_request = request.content_type =~ json_request_regexp
- end
+ @json_request = request.content_type =~ json_request_regexp
end
def scopes
scope = request.params["scope"]
case scope
when Array
scope
when String
scope.split(" ")
- when nil
- Array(oauth_application_default_scope)
end
end
def redirect_uri
param_or_nil("redirect_uri") || begin
@@ -252,69 +247,64 @@
# check if there is a token
bearer_token = fetch_access_token
return unless bearer_token
- @authorization_token = if is_authorization_server?
- # check if token has not expired
- # check if token has been revoked
- oauth_token_by_token(bearer_token)
- else
- # where in resource server, NOT the authorization server.
- payload = introspection_request("access_token", bearer_token)
-
- return unless payload["active"]
-
- payload
- end
+ @authorization_token = oauth_grant_by_token(bearer_token)
end
def require_oauth_authorization(*scopes)
authorization_required unless authorization_token
- scopes << oauth_application_default_scope if scopes.empty?
+ token_scopes = authorization_token[oauth_grants_scopes_column].split(oauth_scope_separator)
- token_scopes = if is_authorization_server?
- authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
- else
- aux_scopes = authorization_token["scope"]
- if aux_scopes
- aux_scopes.split(oauth_scope_separator)
- else
- []
- end
- end
-
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
end
def use_date_arithmetic?
true
end
+ # override
+ def translate(key, default, args = EMPTY_HASH)
+ return i18n_translate(key, default, **args) if features.include?(:i18n)
+ # do not attempt to translate by default
+ return default if args.nil?
+
+ default % args
+ end
+
def post_configure
super
+ i18n_register(File.expand_path(File.join(__dir__, "..", "..", "..", "locales"))) if features.include?(:i18n)
+
# all of the extensions below involve DB changes. Resource server mode doesn't use
# database functions for OAuth though.
return unless is_authorization_server?
self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
# Check whether we can reutilize db entries for the same account / application pair
- one_oauth_token_per_account = db.indexes(oauth_tokens_table).values.any? do |definition|
+ one_oauth_token_per_account = db.indexes(oauth_grants_table).values.any? do |definition|
definition[:unique] &&
- definition[:columns] == oauth_tokens_unique_columns
+ definition[:columns] == oauth_grants_unique_columns
end
self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
-
- i18n_register(File.expand_path(File.join(__dir__, "..", "..", "..", "locales"))) if features.include?(:i18n)
end
private
+ def oauth_account_ds(account_id)
+ account_ds(account_id)
+ end
+
+ def oauth_application_ds(oauth_application_id)
+ db[oauth_applications_table].where(oauth_applications_id_column => oauth_application_id)
+ end
+
def require_authorizable_account
require_account
end
def rescue_from_uniqueness_error(&block)
@@ -327,15 +317,15 @@
retry
end
end
# OAuth Token Unique/Reuse
- def oauth_tokens_unique_columns
+ def oauth_grants_unique_columns
[
- oauth_tokens_oauth_application_id_column,
- oauth_tokens_account_id_column,
- oauth_tokens_scopes_column
+ oauth_grants_oauth_application_id_column,
+ oauth_grants_account_id_column,
+ oauth_grants_scopes_column
]
end
def authorization_server_url
base_url
@@ -351,70 +341,88 @@
# to be used internally. Same semantics as require account, must:
# fetch an authorization basic header
# parse client id and secret
#
def require_oauth_application
- # get client credentials
- auth_method = nil
- client_id = client_secret = nil
+ @oauth_application = if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
+ # client_secret_basic
+ require_oauth_application_from_client_secret_basic(token)
+ elsif (client_id = param_or_nil("client_id"))
+ if (client_secret = param_or_nil("client_secret"))
+ # client_secret_post
+ require_oauth_application_from_client_secret_post(client_id, client_secret)
+ else
+ # none
+ require_oauth_application_from_none(client_id)
+ end
+ else
+ authorization_required
+ end
+ end
- if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
- # client_secret_basic
- client_id, client_secret = Base64.decode64(token).split(/:/, 2)
- auth_method = "client_secret_basic"
- else
- # client_secret_post
- client_id = param_or_nil("client_id")
- client_secret = param_or_nil("client_secret")
- auth_method = "client_secret_post" if client_secret
- end
-
+ def require_oauth_application_from_client_secret_basic(token)
+ client_id, client_secret = Base64.decode64(token).split(/:/, 2)
authorization_required unless client_id
+ oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
+ authorization_required unless supports_auth_method?(oauth_application,
+ "client_secret_basic") && secret_matches?(oauth_application, client_secret)
+ oauth_application
+ end
- @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
+ def require_oauth_application_from_client_secret_post(client_id, client_secret)
+ oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
+ authorization_required unless supports_auth_method?(oauth_application,
+ "client_secret_post") && secret_matches?(oauth_application, client_secret)
+ oauth_application
+ end
- authorization_required unless @oauth_application
-
- authorization_required unless authorized_oauth_application?(@oauth_application, client_secret, auth_method)
+ def require_oauth_application_from_none(client_id)
+ oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
+ authorization_required unless supports_auth_method?(oauth_application, "none")
+ oauth_application
end
- def authorized_oauth_application?(oauth_application, client_secret, auth_method)
+ def supports_auth_method?(oauth_application, auth_method)
supported_auth_methods = if oauth_application[oauth_applications_token_endpoint_auth_method_column]
oauth_application[oauth_applications_token_endpoint_auth_method_column].split(/ +/)
else
- oauth_auth_methods_supported
+ oauth_token_endpoint_auth_methods_supported
end
- if auth_method
- supported_auth_methods.include?(auth_method) && secret_matches?(oauth_application, client_secret)
- else
- supported_auth_methods.include?("none")
- end
+ supported_auth_methods.include?(auth_method)
end
- def no_auth_oauth_application?(_oauth_application)
- supported_auth_methods.include?("none")
- end
-
def require_oauth_application_from_account
ds = db[oauth_applications_table]
- .join(oauth_tokens_table, Sequel[oauth_tokens_table][oauth_tokens_oauth_application_id_column] =>
+ .join(oauth_grants_table, Sequel[oauth_grants_table][oauth_grants_oauth_application_id_column] =>
Sequel[oauth_applications_table][oauth_applications_id_column])
- .where(oauth_token_by_token_ds(param("token")).opts.fetch(:where, true))
+ .where(oauth_grant_by_token_ds(param("token")).opts.fetch(:where, true))
.where(Sequel[oauth_applications_table][oauth_applications_account_id_column] => account_id)
@oauth_application = ds.qualify.first
return if @oauth_application
set_redirect_error_flash revoke_unauthorized_account_error_flash
redirect request.referer || "/"
end
def secret_matches?(oauth_application, secret)
- BCrypt::Password.new(oauth_application[oauth_applications_client_secret_column]) == secret
+ if oauth_applications_client_secret_hash_column
+ BCrypt::Password.new(oauth_application[oauth_applications_client_secret_hash_column]) == secret
+ else
+ oauth_application[oauth_applications_client_secret_column] == secret
+ end
end
+ def set_client_secret(params, secret)
+ if oauth_applications_client_secret_hash_column
+ params[oauth_applications_client_secret_hash_column] = secret_hash(secret)
+ else
+ params[oauth_applications_client_secret_column] = secret
+ end
+ end
+
def secret_hash(secret)
password_hash(secret)
end
def oauth_unique_id_generator
@@ -423,264 +431,342 @@
def generate_token_hash(token)
Base64.urlsafe_encode64(Digest::SHA256.digest(token))
end
- def token_from_application?(oauth_token, oauth_application)
- oauth_token[oauth_tokens_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
+ def grant_from_application?(oauth_grant, oauth_application)
+ oauth_grant[oauth_grants_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
end
- unless method_defined?(:password_hash)
- # From login_requirements_base feature
+ def password_hash(password)
+ return super if features.include?(:login_password_requirements_base)
- def password_hash(password)
- BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
- end
+ BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
end
- def generate_oauth_token(params = {}, should_generate_refresh_token = true)
- create_params = {
- oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
- }.merge(params)
-
- if create_params[oauth_tokens_scopes_column].is_a?(Array)
- create_params[oauth_tokens_scopes_column] =
- create_params[oauth_tokens_scopes_column].join(" ")
+ def generate_token(grant_params = {}, should_generate_refresh_token = true)
+ if grant_params[oauth_grants_id_column] && (oauth_reuse_access_token &&
+ (
+ if oauth_grants_token_hash_column
+ grant_params[oauth_grants_token_hash_column]
+ else
+ grant_params[oauth_grants_token_column]
+ end
+ ))
+ return grant_params
end
+ update_params = {
+ oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in),
+ oauth_grants_code_column => nil
+ }
+
rescue_from_uniqueness_error do
- access_token = _generate_access_token(create_params)
- refresh_token = _generate_refresh_token(create_params) if should_generate_refresh_token
- oauth_token = _store_oauth_token(create_params)
- oauth_token[oauth_tokens_token_column] = access_token
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
- oauth_token
+ access_token = _generate_access_token(update_params)
+ refresh_token = _generate_refresh_token(update_params) if should_generate_refresh_token
+ oauth_grant = store_token(grant_params, update_params)
+
+ return unless oauth_grant
+
+ oauth_grant[oauth_grants_token_column] = access_token
+ oauth_grant[oauth_grants_refresh_token_column] = refresh_token if refresh_token
+ oauth_grant
end
end
def _generate_access_token(params = {})
token = oauth_unique_id_generator
- if oauth_tokens_token_hash_column
- params[oauth_tokens_token_hash_column] = generate_token_hash(token)
+ if oauth_grants_token_hash_column
+ params[oauth_grants_token_hash_column] = generate_token_hash(token)
else
- params[oauth_tokens_token_column] = token
+ params[oauth_grants_token_column] = token
end
token
end
def _generate_refresh_token(params)
token = oauth_unique_id_generator
- if oauth_tokens_refresh_token_hash_column
- params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(token)
+ if oauth_grants_refresh_token_hash_column
+ params[oauth_grants_refresh_token_hash_column] = generate_token_hash(token)
else
- params[oauth_tokens_refresh_token_column] = token
+ params[oauth_grants_refresh_token_column] = token
end
token
end
- def _store_oauth_token(params = {})
- ds = db[oauth_tokens_table]
+ def _grant_with_access_token?(oauth_grant)
+ if oauth_grants_token_hash_column
+ oauth_grant[oauth_grants_token_hash_column]
+ else
+ oauth_grant[oauth_grants_token_column]
+ end
+ end
+ def store_token(grant_params, update_params = {})
+ ds = db[oauth_grants_table]
+
if __one_oauth_token_per_account
+ to_update_if_null = [
+ oauth_grants_token_column,
+ oauth_grants_token_hash_column,
+ oauth_grants_refresh_token_column,
+ oauth_grants_refresh_token_hash_column
+ ].compact.map do |attribute|
+ [
+ attribute,
+ (
+ if ds.respond_to?(:supports_insert_conflict?) && ds.supports_insert_conflict?
+ Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], Sequel[:excluded][attribute])
+ else
+ Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], update_params[attribute])
+ end
+ )
+ ]
+ end
+
token = __insert_or_update_and_return__(
ds,
- oauth_tokens_id_column,
- oauth_tokens_unique_columns,
- params,
- Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
- ([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
+ oauth_grants_id_column,
+ oauth_grants_unique_columns,
+ grant_params.merge(update_params),
+ Sequel.expr(Sequel[oauth_grants_table][oauth_grants_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
+ Hash[to_update_if_null]
)
# if the previous operation didn't return a row, it means that the conditions
# invalidated the update, and the existing token is still valid.
token || ds.where(
- oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
- oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
+ oauth_grants_account_id_column => update_params[oauth_grants_account_id_column],
+ oauth_grants_oauth_application_id_column => update_params[oauth_grants_oauth_application_id_column]
).first
else
+
if oauth_reuse_access_token
- unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
- valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
- .where(unique_conds).first
+ unique_conds = Hash[oauth_grants_unique_columns.map { |column| [column, update_params[column]] }]
+ valid_token_ds = valid_oauth_grant_ds(unique_conds)
+ if oauth_grants_token_hash_column
+ valid_token_ds.exclude(oauth_grants_token_hash_column => nil)
+ else
+ valid_token_ds.exclude(oauth_grants_token_column => nil)
+ end
+
+ valid_token = valid_token_ds.first
+
return valid_token if valid_token
end
- __insert_and_return__(ds, oauth_tokens_id_column, params)
+
+ if grant_params[oauth_grants_id_column]
+ __update_and_return__(ds.where(oauth_grants_id_column => grant_params[oauth_grants_id_column]), update_params)
+ else
+ __insert_and_return__(ds, oauth_grants_id_column, grant_params.merge(update_params))
+ end
end
end
- def oauth_token_by_token_ds(token)
- ds = db[oauth_tokens_table]
+ def valid_locked_oauth_grant(grant_params = nil)
+ oauth_grant = valid_oauth_grant_ds(grant_params).for_update.first
- ds = if oauth_tokens_token_hash_column
- ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_hash_column] => generate_token_hash(token))
- else
- ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_column] => token)
- end
+ redirect_response_error("invalid_grant") unless oauth_grant
- ds.where(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
- .where(Sequel[oauth_tokens_table][oauth_tokens_revoked_at_column] => nil)
+ oauth_grant
end
- def oauth_token_by_token(token)
- oauth_token_by_token_ds(token).first
+ def valid_oauth_grant_ds(grant_params = nil)
+ ds = db[oauth_grants_table]
+ .where(Sequel[oauth_grants_table][oauth_grants_revoked_at_column] => nil)
+ .where(Sequel.expr(Sequel[oauth_grants_table][oauth_grants_expires_in_column]) >= Sequel::CURRENT_TIMESTAMP)
+ ds = ds.where(grant_params) if grant_params
+
+ ds
end
- def oauth_token_by_refresh_token(token, revoked: false)
- ds = db[oauth_tokens_table].where(oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column])
+ def oauth_grant_by_token_ds(token)
+ ds = valid_oauth_grant_ds
+
+ if oauth_grants_token_hash_column
+ ds.where(Sequel[oauth_grants_table][oauth_grants_token_hash_column] => generate_token_hash(token))
+ else
+ ds.where(Sequel[oauth_grants_table][oauth_grants_token_column] => token)
+ end
+ end
+
+ def oauth_grant_by_token(token)
+ oauth_grant_by_token_ds(token).first
+ end
+
+ def oauth_grant_by_refresh_token_ds(token, revoked: false)
+ ds = db[oauth_grants_table].where(oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column])
#
# filter expired refresh tokens out.
# an expired refresh token is a token whose access token expired for a period longer than the
# refresh token expiration period.
#
- ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP)
+ ds = ds.where(Sequel.date_add(oauth_grants_expires_in_column,
+ seconds: (oauth_refresh_token_expires_in - oauth_access_token_expires_in)) >= Sequel::CURRENT_TIMESTAMP)
- ds = if oauth_tokens_refresh_token_hash_column
- ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
+ ds = if oauth_grants_refresh_token_hash_column
+ ds.where(oauth_grants_refresh_token_hash_column => generate_token_hash(token))
else
- ds.where(oauth_tokens_refresh_token_column => token)
+ ds.where(oauth_grants_refresh_token_column => token)
end
- ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked
+ ds = ds.where(oauth_grants_revoked_at_column => nil) unless revoked
- ds.first
+ ds
end
- def json_access_token_payload(oauth_token)
+ def oauth_grant_by_refresh_token(token, **kwargs)
+ oauth_grant_by_refresh_token_ds(token, **kwargs).first
+ end
+
+ def json_access_token_payload(oauth_grant)
payload = {
- "access_token" => oauth_token[oauth_tokens_token_column],
+ "access_token" => oauth_grant[oauth_grants_token_column],
"token_type" => oauth_token_type,
- "expires_in" => oauth_token_expires_in
+ "expires_in" => oauth_access_token_expires_in
}
- payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[oauth_tokens_refresh_token_column]
+ payload["refresh_token"] = oauth_grant[oauth_grants_refresh_token_column] if oauth_grant[oauth_grants_refresh_token_column]
payload
end
# Access Tokens
- def validate_oauth_token_params
+ def validate_token_params
unless (grant_type = param_or_nil("grant_type"))
redirect_response_error("invalid_request")
end
redirect_response_error("invalid_request") if grant_type == "refresh_token" && !param_or_nil("refresh_token")
end
- def create_oauth_token(grant_type)
- if supported_grant_type?(grant_type, "refresh_token")
- # fetch potentially revoked oauth token
- oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
+ def create_token(grant_type)
+ redirect_response_error("invalid_request") unless supported_grant_type?(grant_type, "refresh_token")
- if !oauth_token
- redirect_response_error("invalid_grant")
- elsif oauth_token[oauth_tokens_revoked_at_column]
- if oauth_refresh_token_protection_policy == "rotation"
- # https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
- #
- # If a refresh token is compromised and subsequently used by both the attacker and the legitimate
- # client, one of them will present an invalidated refresh token, which will inform the authorization
- # server of the breach. The authorization server cannot determine which party submitted the invalid
- # refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
- # forcing the legitimate client to obtain a fresh authorization grant.
+ refresh_token = param("refresh_token")
+ # fetch potentially revoked oauth token
+ oauth_grant = oauth_grant_by_refresh_token_ds(refresh_token, revoked: true).for_update.first
- db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
- .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
- end
- redirect_response_error("invalid_grant")
- end
+ update_params = { oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
+ seconds: oauth_access_token_expires_in) }
- update_params = {
- oauth_tokens_oauth_application_id_column => oauth_token[oauth_tokens_oauth_application_id_column],
- oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
- }
- create_oauth_token_from_token(oauth_token, update_params)
- else
- redirect_response_error("invalid_request")
+ if !oauth_grant || oauth_grant[oauth_grants_revoked_at_column]
+ redirect_response_error("invalid_grant")
+ elsif oauth_refresh_token_protection_policy == "rotation"
+ # https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
+ #
+ # If a refresh token is compromised and subsequently used by both the attacker and the legitimate
+ # client, one of them will present an invalidated refresh token, which will inform the authorization
+ # server of the breach. The authorization server cannot determine which party submitted the invalid
+ # refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
+ # forcing the legitimate client to obtain a fresh authorization grant.
+
+ refresh_token = _generate_refresh_token(update_params)
end
+
+ update_params[oauth_grants_oauth_application_id_column] = oauth_grant[oauth_grants_oauth_application_id_column]
+
+ oauth_grant = create_token_from_token(oauth_grant, update_params)
+ oauth_grant[oauth_grants_refresh_token_column] = refresh_token
+ oauth_grant
end
- def create_oauth_token_from_token(oauth_token, update_params)
- redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
+ def create_token_from_token(oauth_grant, update_params)
+ redirect_response_error("invalid_grant") unless grant_from_application?(oauth_grant, oauth_application)
rescue_from_uniqueness_error do
- oauth_tokens_ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
+ oauth_grants_ds = db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
access_token = _generate_access_token(update_params)
+ oauth_grant = __update_and_return__(oauth_grants_ds, update_params)
- if oauth_refresh_token_protection_policy == "rotation"
- update_params = {
- **update_params,
- oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
- oauth_tokens_account_id_column => oauth_token[oauth_tokens_account_id_column],
- oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
- }
-
- refresh_token = _generate_refresh_token(update_params)
- else
- refresh_token = param("refresh_token")
- end
- oauth_token = __update_and_return__(oauth_tokens_ds, update_params)
-
- oauth_token[oauth_tokens_token_column] = access_token
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token
- oauth_token
+ oauth_grant[oauth_grants_token_column] = access_token
+ oauth_grant
end
end
def supported_grant_type?(grant_type, expected_grant_type = grant_type)
return false unless grant_type == expected_grant_type
- return true unless (grant_types_supported = oauth_application[oauth_applications_grant_types_column])
+ grant_types_supported = if oauth_application[oauth_applications_grant_types_column]
+ oauth_application[oauth_applications_grant_types_column].split(/ +/)
+ else
+ oauth_grant_types_supported
+ end
- grant_types_supported = grant_types_supported.split(/ +/)
-
grant_types_supported.include?(grant_type)
end
+ def supported_response_type?(response_type, expected_response_type = response_type)
+ return false unless response_type == expected_response_type
+
+ response_types_supported = if oauth_application[oauth_applications_grant_types_column]
+ oauth_application[oauth_applications_response_types_column].split(/ +/)
+ else
+ oauth_response_types_supported
+ end
+
+ response_types = response_type.split(/ +/)
+
+ (response_types - response_types_supported).empty?
+ end
+
+ def supported_response_mode?(response_mode, expected_response_mode = response_mode)
+ return false unless response_mode == expected_response_mode
+
+ response_modes_supported = if oauth_application[oauth_applications_response_modes_column]
+ oauth_application[oauth_applications_response_modes_column].split(/ +/)
+ else
+ oauth_response_modes_supported
+ end
+
+ response_modes_supported.include?(response_mode)
+ end
+
def oauth_server_metadata_body(path = nil)
issuer = base_url
issuer += "/#{path}" if path
{
issuer: issuer,
token_endpoint: token_url,
scopes_supported: oauth_application_scopes,
- response_types_supported: [],
- response_modes_supported: [],
- grant_types_supported: %w[refresh_token],
- token_endpoint_auth_methods_supported: oauth_auth_methods_supported,
+ response_types_supported: oauth_response_types_supported,
+ response_modes_supported: oauth_response_modes_supported,
+ grant_types_supported: oauth_grant_types_supported,
+ token_endpoint_auth_methods_supported: oauth_token_endpoint_auth_methods_supported,
service_documentation: oauth_metadata_service_documentation,
ui_locales_supported: oauth_metadata_ui_locales_supported,
op_policy_uri: oauth_metadata_op_policy_uri,
op_tos_uri: oauth_metadata_op_tos_uri
}
end
def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
if accepts_json?
- status_code = if respond_to?(:"#{error_code}_response_status")
- send(:"#{error_code}_response_status")
+ status_code = if respond_to?(:"oauth_#{error_code}_response_status")
+ send(:"oauth_#{error_code}_response_status")
else
- invalid_oauth_response_status
+ oauth_invalid_response_status
end
throw_json_response_error(status_code, error_code)
else
redirect_url = URI.parse(redirect_url)
query_params = []
- query_params << if respond_to?(:"#{error_code}_error_code")
- "error=#{send(:"#{error_code}_error_code")}"
+ query_params << if respond_to?(:"oauth_#{error_code}_error_code")
+ "error=#{send(:"oauth_#{error_code}_error_code")}"
else
"error=#{error_code}"
end
- if respond_to?(:"#{error_code}_message")
- message = send(:"#{error_code}_message")
+ if respond_to?(:"oauth_#{error_code}_message")
+ message = send(:"oauth_#{error_code}_message")
query_params << ["error_description=#{CGI.escape(message)}"]
end
query_params << redirect_url.query if redirect_url.query
redirect_url.query = query_params.join("&")
@@ -703,30 +789,30 @@
return_response(json_payload)
end
def throw_json_response_error(status, error_code, message = nil)
set_response_error_status(status)
- code = if respond_to?(:"#{error_code}_error_code")
- send(:"#{error_code}_error_code")
+ code = if respond_to?(:"oauth_#{error_code}_error_code")
+ send(:"oauth_#{error_code}_error_code")
else
error_code
end
payload = { "error" => code }
- payload["error_description"] = message || (send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message"))
+ payload["error_description"] = message || (send(:"oauth_#{error_code}_message") if respond_to?(:"oauth_#{error_code}_message"))
json_payload = _json_response_body(payload)
response["Content-Type"] ||= json_response_content_type
response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
return_response(json_payload)
end
- unless method_defined?(:_json_response_body)
- def _json_response_body(hash)
- if request.respond_to?(:convert_to_json)
- request.send(:convert_to_json, hash)
- else
- JSON.dump(hash)
- end
+ def _json_response_body(hash)
+ return super if features.include?(:json)
+
+ if request.respond_to?(:convert_to_json)
+ request.send(:convert_to_json, hash)
+ else
+ JSON.dump(hash)
end
end
if Gem::Version.new(Rodauth.version) < Gem::Version.new("2.23")
def return_response(body = nil)
@@ -734,11 +820,11 @@
request.halt
end
end
def authorization_required
- throw_json_response_error(authorization_required_error_status, "invalid_client")
+ throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
end
def check_valid_scopes?
return false unless scopes
@@ -749,36 +835,13 @@
URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
end
# Resource server mode
- SERVER_METADATA = OAuth::TtlStore.new
-
def authorization_server_metadata
- auth_url = URI(authorization_server_url)
+ auth_url = URI(authorization_server_url).dup
+ auth_url.path = "/.well-known/oauth-authorization-server"
- server_metadata = SERVER_METADATA[auth_url]
-
- return server_metadata if server_metadata
-
- SERVER_METADATA.set(auth_url) do
- http = Net::HTTP.new(auth_url.host, auth_url.port)
- http.use_ssl = auth_url.scheme == "https"
-
- request = Net::HTTP::Get.new("/.well-known/oauth-authorization-server")
- request["accept"] = json_response_content_type
- response = http.request(request)
- authorization_required unless response.code.to_i == 200
-
- # time-to-live
- ttl = if response.key?("cache-control")
- cache_control = response["cache-control"]
- cache_control[/max-age=(\d+)/, 1].to_i
- elsif response.key?("expires")
- Time.parse(response["expires"]).to_i - Time.now.to_i
- end
-
- [JSON.parse(response.body, symbolize_names: true), ttl]
- end
+ http_request_with_cache(auth_url)
end
end
end