All Files ( 96.17% covered at 381.06 hits/line )
41 files in total.
3500 relevant lines,
3366 lines covered and
134 lines missed.
(
96.17%
)
-
# frozen_string_literal: true
-
-
9
require "rails/generators"
-
9
require "rails/generators/migration"
-
9
require "rails/generators/active_record"
-
-
9
module Rodauth::OAuth::Rails
-
9
module Generators
-
9
class InstallGenerator < ::Rails::Generators::Base
-
9
include ::Rails::Generators::Migration
-
-
9
source_root "#{__dir__}/templates"
-
9
namespace "rodauth:oauth:install"
-
-
9
def create_rodauth_migration
-
18
return unless defined?(ActiveRecord::Base)
-
-
18
migration_template "db/migrate/create_rodauth_oauth.rb", "db/migrate/create_rodauth_oauth.rb"
-
end
-
-
9
def create_oauth_models
-
18
return unless defined?(ActiveRecord::Base)
-
-
18
template "app/models/oauth_application.rb"
-
18
template "app/models/oauth_grant.rb"
-
end
-
-
9
private
-
-
# required by #migration_template action
-
9
def self.next_migration_number(dirname)
-
18
ActiveRecord::Generators::Base.next_migration_number(dirname)
-
end
-
-
9
def migration_version
-
18
if ActiveRecord.version >= Gem::Version.new("5.0.0")
-
18
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
-
end
-
end
-
-
9
def adapter
-
ActiveRecord::Base.connection_config.fetch(:adapter)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
9
require "rails/generators"
-
-
9
module Rodauth::OAuth
-
9
module Rails
-
9
module Generators
-
9
class ViewsGenerator < ::Rails::Generators::Base
-
9
source_root "#{__dir__}/templates"
-
9
namespace "rodauth:oauth:views"
-
9
desc "Generate db migrations for rodauth-oauth in your application."
-
-
9
DEFAULT = %w[authorize].freeze
-
1
VIEWS = {
-
8
oauth_authorize: DEFAULT,
-
oauth_applications: %w[oauth_applications oauth_application new_oauth_application]
-
}.freeze
-
-
9
DEPENDENCIES = {
-
}.freeze
-
-
9
class_option :features, type: :array,
-
desc: "Rodauth OAuth features to generate views for (oauth_applications etc.)",
-
default: DEFAULT
-
-
9
class_option :all, aliases: "-a", type: :boolean,
-
desc: "Generates views for all Rodauth OAuth features",
-
default: false
-
-
9
class_option :directory, aliases: "-d", type: :string,
-
desc: "The directory under app/views/* into which to create views",
-
default: "rodauth"
-
-
9
def create_views
-
36
features = options[:all] ? VIEWS.keys : (%i[oauth_authorize] + options[:features]).map(&:to_sym).uniq
-
-
36
views = features.inject([]) do |list, feature|
-
72
list |= VIEWS[feature] || []
-
72
list |= VIEWS[DEPENDENCIES[feature]] || []
-
end
-
-
36
directory = options[:directory].underscore
-
36
views.each do |view|
-
90
copy_file "app/views/rodauth/#{view}.html.erb",
-
"app/views/#{directory}/#{view}.html.erb" do |content|
-
90
content = content.gsub("rodauth/", "#{directory}/")
-
90
content
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_application_management, :OauthApplicationManagement) do
-
13
depends :oauth_management_base, :oauth_token_revocation
-
-
13
before "create_oauth_application"
-
13
after "create_oauth_application"
-
-
13
error_flash "There was an error registering your oauth application", "create_oauth_application"
-
13
notice_flash "Your oauth application has been registered", "create_oauth_application"
-
-
13
view "oauth_applications", "Oauth Applications", "oauth_applications"
-
13
view "oauth_application", "Oauth Application", "oauth_application"
-
13
view "new_oauth_application", "New Oauth Application", "new_oauth_application"
-
13
view "oauth_application_oauth_grants", "Oauth Application Grants", "oauth_application_oauth_grants"
-
-
# Application
-
13
APPLICATION_REQUIRED_PARAMS = %w[name scopes homepage_url redirect_uri client_secret].freeze
-
13
auth_value_method :oauth_application_required_params, APPLICATION_REQUIRED_PARAMS
-
-
13
(APPLICATION_REQUIRED_PARAMS + %w[description client_id]).each do |param|
-
91
auth_value_method :"oauth_application_#{param}_param", param
-
end
-
-
13
translatable_method :oauth_applications_name_label, "Name"
-
13
translatable_method :oauth_applications_description_label, "Description"
-
13
translatable_method :oauth_applications_scopes_label, "Default scopes"
-
13
translatable_method :oauth_applications_contacts_label, "Contacts"
-
13
translatable_method :oauth_applications_tos_uri_label, "Terms of service"
-
13
translatable_method :oauth_applications_policy_uri_label, "Policy"
-
13
translatable_method :oauth_applications_jwks_label, "JSON Web Keys"
-
13
translatable_method :oauth_applications_jwks_uri_label, "JSON Web Keys URI"
-
13
translatable_method :oauth_applications_homepage_url_label, "Homepage URL"
-
13
translatable_method :oauth_applications_redirect_uri_label, "Redirect URI"
-
13
translatable_method :oauth_applications_client_secret_label, "Client Secret"
-
13
translatable_method :oauth_applications_client_id_label, "Client ID"
-
-
13
%w[type token refresh_token expires_in revoked_at].each do |param|
-
65
translatable_method :"oauth_grants_#{param}_label", param.gsub("_", " ").capitalize
-
end
-
-
13
button "Register", "oauth_application"
-
13
button "Revoke", "oauth_grant_revoke"
-
-
13
auth_value_method :oauth_applications_oauth_grants_path, "oauth-grants"
-
13
auth_value_method :oauth_applications_route, "oauth-applications"
-
13
auth_value_method :oauth_applications_per_page, 20
-
13
auth_value_method :oauth_applications_id_pattern, Integer
-
13
auth_value_method :oauth_grants_per_page, 20
-
-
13
translatable_method :invalid_url_message, "Invalid URL"
-
13
translatable_method :null_error_message, "is not filled"
-
-
13
translatable_method :oauth_no_applications_text, "No oauth applications yet!"
-
13
translatable_method :oauth_no_grants_text, "No oauth grants yet!"
-
-
13
auth_methods(
-
:oauth_application_path
-
)
-
-
13
def oauth_applications_path(opts = {})
-
690
route_path(oauth_applications_route, opts)
-
end
-
-
13
def oauth_application_path(id)
-
153
"#{oauth_applications_path}/#{id}"
-
end
-
-
# /oauth-applications routes
-
13
def load_oauth_application_management_routes
-
325
request.on(oauth_applications_route) do
-
325
check_csrf if check_csrf?
-
325
require_account
-
-
325
request.get "new" do
-
52
new_oauth_application_view
-
end
-
-
273
request.on(oauth_applications_id_pattern) do |id|
-
104
oauth_application = db[oauth_applications_table]
-
.where(oauth_applications_id_column => id)
-
.where(oauth_applications_account_id_column => account_id)
-
.first
-
104
next unless oauth_application
-
-
91
scope.instance_variable_set(:@oauth_application, oauth_application)
-
-
91
request.is do
-
39
request.get do
-
39
oauth_application_view
-
end
-
end
-
-
52
request.on(oauth_applications_oauth_grants_path) do
-
52
page = Integer(param_or_nil("page") || 1)
-
52
per_page = per_page_param(oauth_grants_per_page)
-
52
oauth_grants = db[oauth_grants_table]
-
.where(oauth_grants_oauth_application_id_column => id)
-
.order(Sequel.desc(oauth_grants_id_column))
-
52
scope.instance_variable_set(:@oauth_grants, oauth_grants.paginate(page, per_page))
-
52
request.is do
-
52
request.get do
-
52
oauth_application_oauth_grants_view
-
end
-
end
-
end
-
end
-
-
169
request.is do
-
169
request.get do
-
104
page = Integer(param_or_nil("page") || 1)
-
104
per_page = per_page_param(oauth_applications_per_page)
-
104
scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table]
-
.where(oauth_applications_account_id_column => account_id)
-
.order(Sequel.desc(oauth_applications_id_column))
-
.paginate(page, per_page))
-
-
104
oauth_applications_view
-
end
-
-
65
request.post do
-
65
catch_error do
-
65
validate_oauth_application_params
-
-
39
transaction do
-
39
before_create_oauth_application
-
39
id = create_oauth_application
-
39
after_create_oauth_application
-
39
set_notice_flash create_oauth_application_notice_flash
-
39
redirect "#{request.path}/#{id}"
-
end
-
end
-
26
set_error_flash create_oauth_application_error_flash
-
26
new_oauth_application_view
-
end
-
end
-
end
-
end
-
-
13
private
-
-
13
def oauth_application_params
-
299
@oauth_application_params ||= oauth_application_required_params.each_with_object({}) do |param, params|
-
325
value = request.params[__send__(:"oauth_application_#{param}_param")]
-
325
if value && !value.empty?
-
162
params[param] = value
-
else
-
91
set_field_error(param, null_error_message)
-
end
-
end
-
end
-
-
13
def validate_oauth_application_params
-
65
oauth_application_params.each do |key, value|
-
234
if key == oauth_application_homepage_url_param
-
-
52
set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
-
-
182
elsif key == oauth_application_redirect_uri_param
-
-
52
if value.respond_to?(:each)
-
13
value.each do |uri|
-
26
next if uri.empty?
-
-
26
set_field_error(key, invalid_url_message) unless check_valid_no_fragment_uri?(uri)
-
end
-
else
-
39
set_field_error(key, invalid_url_message) unless check_valid_no_fragment_uri?(value)
-
end
-
130
elsif key == oauth_application_scopes_param
-
-
39
value.each do |scope|
-
78
set_field_error(key, oauth_invalid_scope_message) unless oauth_application_scopes.include?(scope)
-
end
-
end
-
end
-
-
65
throw :rodauth_error if @field_errors && !@field_errors.empty?
-
end
-
-
13
def create_oauth_application
-
15
create_params = {
-
24
oauth_applications_account_id_column => account_id,
-
oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
-
oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
-
oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
-
oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
-
}
-
-
39
redirect_uris = oauth_application_params[oauth_application_redirect_uri_param]
-
39
redirect_uris = redirect_uris.to_a.reject(&:empty?).join(" ") if redirect_uris.respond_to?(:each)
-
39
create_params[oauth_applications_redirect_uri_column] = redirect_uris unless redirect_uris.empty?
-
-
# set client ID/secret pairs
-
39
set_client_secret(create_params, oauth_application_params[oauth_application_client_secret_param])
-
-
39
if create_params[oauth_applications_scopes_column]
-
27
create_params[oauth_applications_scopes_column] = create_params[oauth_applications_scopes_column].join(oauth_scope_separator)
-
end
-
-
39
rescue_from_uniqueness_error do
-
27
create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
-
39
db[oauth_applications_table].insert(create_params)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_assertion_base, :OauthAssertionBase) do
-
13
depends :oauth_base
-
-
13
auth_methods(
-
:assertion_grant_type?,
-
:client_assertion_type?,
-
:assertion_grant_type,
-
:client_assertion_type
-
)
-
-
13
private
-
-
13
def validate_token_params
-
104
return super unless assertion_grant_type?
-
-
52
redirect_response_error("invalid_grant") unless param_or_nil("assertion")
-
end
-
-
13
def require_oauth_application
-
208
if assertion_grant_type?
-
52
@oauth_application = __send__(:"require_oauth_application_from_#{assertion_grant_type}_assertion_issuer", param("assertion"))
-
156
elsif client_assertion_type?
-
117
@oauth_application = __send__(:"require_oauth_application_from_#{client_assertion_type}_assertion_subject",
-
param("client_assertion"))
-
-
78
if (client_id = param_or_nil("client_id")) &&
-
client_id != @oauth_application[oauth_applications_client_id_column]
-
# If present, the value of the
-
# "client_id" parameter MUST identify the same client as is
-
# identified by the client assertion.
-
26
redirect_response_error("invalid_grant")
-
end
-
else
-
39
super
-
end
-
end
-
-
13
def account_from_bearer_assertion_subject(subject)
-
52
__insert_or_do_nothing_and_return__(
-
db[accounts_table],
-
account_id_column,
-
[login_column],
-
login_column => subject
-
)
-
end
-
-
13
def create_token(grant_type)
-
65
return super unless assertion_grant_type?(grant_type) && supported_grant_type?(grant_type)
-
-
52
account = __send__(:"account_from_#{assertion_grant_type}_assertion", param("assertion"))
-
-
52
redirect_response_error("invalid_grant") unless account
-
-
52
grant_scopes = if param_or_nil("scope")
-
26
redirect_response_error("invalid_scope") unless check_valid_scopes?
-
13
scopes
-
else
-
26
@oauth_application[oauth_applications_scopes_column]
-
end
-
-
15
grant_params = {
-
24
oauth_grants_type_column => grant_type,
-
oauth_grants_account_id_column => account[account_id_column],
-
oauth_grants_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
-
oauth_grants_scopes_column => grant_scopes
-
}
-
-
39
generate_token(grant_params, false)
-
end
-
-
13
def assertion_grant_type?(grant_type = param("grant_type"))
-
377
grant_type.start_with?("urn:ietf:params:oauth:grant-type:")
-
end
-
-
13
def client_assertion_type?(client_assertion_type = param("client_assertion_type"))
-
156
client_assertion_type.start_with?("urn:ietf:params:oauth:client-assertion-type:")
-
end
-
-
13
def assertion_grant_type(grant_type = param("grant_type"))
-
104
grant_type.delete_prefix("urn:ietf:params:oauth:grant-type:").tr("-", "_")
-
end
-
-
13
def client_assertion_type(assertion_type = param("client_assertion_type"))
-
117
assertion_type.delete_prefix("urn:ietf:params:oauth:client-assertion-type:").tr("-", "_")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_authorization_code_grant, :OauthAuthorizationCodeGrant) do
-
13
depends :oauth_authorize_base
-
-
13
auth_value_method :oauth_response_mode, "form_post"
-
-
13
def oauth_grant_types_supported
-
4732
super | %w[authorization_code]
-
end
-
-
13
def oauth_response_types_supported
-
2171
super | %w[code]
-
end
-
-
13
def oauth_response_modes_supported
-
3614
super | %w[query form_post]
-
end
-
-
13
private
-
-
13
def validate_authorize_params
-
2973
super
-
-
2765
response_mode = param_or_nil("response_mode")
-
-
2765
return unless response_mode
-
-
1079
redirect_response_error("invalid_request") unless oauth_response_modes_supported.include?(response_mode)
-
-
1079
response_type = param_or_nil("response_type")
-
-
1079
return unless response_type.nil? || response_type == "code"
-
-
897
redirect_response_error("invalid_request") unless oauth_response_modes_for_code_supported.include?(response_mode)
-
end
-
-
13
def oauth_response_modes_for_code_supported
-
897
%w[query form_post]
-
end
-
-
13
def validate_token_params
-
1963
redirect_response_error("invalid_request") if param_or_nil("grant_type") == "authorization_code" && !param_or_nil("code")
-
1963
super
-
end
-
-
13
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
-
1066
response_mode ||= oauth_response_mode
-
-
1066
redirect_response_error("invalid_request") unless response_mode.nil? || supported_response_mode?(response_mode)
-
-
1066
response_type = param_or_nil("response_type")
-
-
1066
redirect_response_error("invalid_request") unless response_type.nil? || supported_response_type?(response_type)
-
-
738
case response_type
-
when "code", nil
-
715
response_params.replace(_do_authorize_code)
-
end
-
-
1053
response_params["state"] = param("state") if param_or_nil("state")
-
-
1053
[response_params, response_mode]
-
end
-
-
13
def _do_authorize_code
-
325
create_params = {
-
520
oauth_grants_type_column => "authorization_code",
-
**resource_owner_params
-
}
-
-
845
{ "code" => create_oauth_grant(create_params) }
-
end
-
-
13
def authorize_response(params, mode)
-
637
redirect_url = URI.parse(redirect_uri)
-
441
case mode
-
when "query"
-
611
params = [URI.encode_www_form(params)]
-
611
params << redirect_url.query if redirect_url.query
-
611
redirect_url.query = params.join("&")
-
611
redirect(redirect_url.to_s)
-
when "form_post"
-
26
inline_html = form_post_response_html(redirect_uri) do
-
24
params.map do |name, value|
-
26
"<input type=\"hidden\" name=\"#{scope.h(name)}\" value=\"#{scope.h(value)}\" />"
-
2
end.join
-
end
-
26
scope.view layout: false, inline: inline_html
-
end
-
end
-
-
13
def _redirect_response_error(redirect_url, params)
-
416
response_mode = param_or_nil("response_mode") || oauth_response_mode
-
-
288
case response_mode
-
when "form_post"
-
9
response["Content-Type"] = "text/html"
-
13
error_body = form_post_error_response_html(redirect_url) do
-
12
params.map do |name, value|
-
26
"<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
-
1
end.join
-
end
-
13
response.write(error_body)
-
13
request.halt
-
else
-
403
super
-
end
-
end
-
-
13
def form_post_response_html(url)
-
27
<<-FORM
-
12
<html>
-
<head><title>Authorized</title></head>
-
<body onload="javascript:document.forms[0].submit()">
-
12
<form method="post" action="#{url}">
-
12
#{yield}
-
12
<input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
-
</form>
-
</body>
-
</html>
-
FORM
-
end
-
-
13
def form_post_error_response_html(url)
-
9
<<-FORM
-
4
<html>
-
<head><title></title></head>
-
<body onload="javascript:document.forms[0].submit()">
-
4
<form method="post" action="#{url}">
-
4
#{yield}
-
</form>
-
</body>
-
</html>
-
FORM
-
end
-
-
13
def create_token(grant_type)
-
1742
return super unless supported_grant_type?(grant_type, "authorization_code")
-
-
540
grant_params = {
-
864
oauth_grants_code_column => param("code"),
-
oauth_grants_redirect_uri_column => param("redirect_uri"),
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
-
}
-
-
1404
create_token_from_authorization_code(grant_params)
-
end
-
-
13
def check_valid_response_type?
-
1894
response_type = param_or_nil("response_type")
-
-
1894
response_type == "code" || response_type == "none" || super
-
end
-
-
13
def oauth_server_metadata_body(*)
-
351
super.tap do |data|
-
243
data[:authorization_endpoint] = authorize_url
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "ipaddr"
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_authorize_base, :OauthAuthorizeBase) do
-
13
depends :oauth_base
-
-
13
before "authorize"
-
13
after "authorize"
-
-
13
view "authorize", "Authorize", "authorize"
-
13
view "authorize_error", "Authorize Error", "authorize_error"
-
-
13
button "Authorize", "oauth_authorize"
-
13
button "Back to Client Application", "oauth_authorize_post"
-
-
13
auth_value_method :use_oauth_access_type?, false
-
-
13
auth_value_method :oauth_grants_access_type_column, :access_type
-
-
13
translatable_method :authorize_page_lead, "The application %<name>s would like to access your data"
-
13
translatable_method :oauth_grants_scopes_label, "Scopes"
-
13
translatable_method :oauth_applications_contacts_label, "Contacts"
-
13
translatable_method :oauth_applications_tos_uri_label, "Terms of service URL"
-
13
translatable_method :oauth_applications_policy_uri_label, "Policy URL"
-
13
translatable_method :oauth_unsupported_response_type_message, "Unsupported response type"
-
13
translatable_method :oauth_authorize_parameter_required, "Invalid or missing '%<parameter>s'"
-
-
13
auth_methods(
-
:resource_owner_params,
-
:oauth_grants_resource_owner_columns
-
)
-
-
13
OAUTH_ACCESS_TYPES = %w[offline online].freeze
-
-
13
OAUTH_APPROVAL_PROMPTS = %w[force auto].freeze
-
-
# /authorize
-
13
auth_server_route(:authorize) do |r|
-
3299
require_authorizable_account
-
3160
before_authorize_route
-
-
3160
validate_authorize_params
-
-
2505
r.get do
-
1387
authorize_view
-
end
-
-
1118
r.post do
-
1118
params, mode = transaction do
-
1118
before_authorize
-
1118
do_authorize
-
end
-
-
1105
authorize_response(params, mode)
-
end
-
end
-
-
13
def check_csrf?
-
8760
case request.path
-
when authorize_path
-
3299
only_json? ? false : super
-
else
-
9337
super
-
end
-
end
-
-
13
def authorize_scopes
-
1387
scopes || begin
-
195
oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)
-
end
-
end
-
-
13
private
-
-
13
def validate_authorize_params
-
2943
redirect_authorize_error("client_id") unless oauth_application
-
-
2882
redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
-
-
2882
if (redirect_uri = param_or_nil("redirect_uri"))
-
611
normalized_redirect_uri = normalize_redirect_uri_for_comparison(redirect_uri)
-
611
unless redirect_uris.include?(normalized_redirect_uri) || redirect_uris.include?(redirect_uri)
-
13
redirect_authorize_error("redirect_uri")
-
end
-
2271
elsif redirect_uris.size > 1
-
13
redirect_authorize_error("redirect_uri")
-
end
-
-
2856
redirect_response_error("unsupported_response_type") unless check_valid_response_type?
-
-
2830
redirect_response_error("invalid_request") unless check_valid_access_type? && check_valid_approval_prompt?
-
-
2830
try_approval_prompt if use_oauth_access_type? && request.get?
-
-
2830
redirect_response_error("invalid_scope") if (request.post? || param_or_nil("scope")) && !check_valid_scopes?
-
-
2804
response_mode = param_or_nil("response_mode")
-
-
2804
redirect_response_error("invalid_request") unless response_mode.nil? || oauth_response_modes_supported.include?(response_mode)
-
end
-
-
13
def check_valid_scopes?(scp = scopes)
-
2557
super(scp - %w[offline_access])
-
end
-
-
13
def check_valid_response_type?
-
26
false
-
end
-
-
13
def check_valid_access_type?
-
2830
return true unless use_oauth_access_type?
-
-
39
access_type = param_or_nil("access_type")
-
39
!access_type || OAUTH_ACCESS_TYPES.include?(access_type)
-
end
-
-
13
def check_valid_approval_prompt?
-
2830
return true unless use_oauth_access_type?
-
-
39
approval_prompt = param_or_nil("approval_prompt")
-
39
!approval_prompt || OAUTH_APPROVAL_PROMPTS.include?(approval_prompt)
-
end
-
-
13
def resource_owner_params
-
1586
{ oauth_grants_account_id_column => account_id }
-
end
-
-
13
def oauth_grants_resource_owner_columns
-
[oauth_grants_account_id_column]
-
end
-
-
13
def try_approval_prompt
-
26
approval_prompt = param_or_nil("approval_prompt")
-
-
26
return unless approval_prompt && approval_prompt == "auto"
-
-
12
return if db[oauth_grants_table].where(resource_owner_params).where(
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
-
oauth_grants_redirect_uri_column => redirect_uri,
-
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
-
oauth_grants_access_type_column => "online"
-
1
).none?
-
-
# if there's a previous oauth grant for the params combo, it means that this user has approved before.
-
9
request.env["REQUEST_METHOD"] = "POST"
-
end
-
-
13
def redirect_authorize_error(parameter, referer = request.referer || default_redirect)
-
113
error_message = oauth_authorize_parameter_required(parameter: parameter)
-
-
113
if accepts_json?
-
status_code = oauth_invalid_response_status
-
-
throw_json_response_error(status_code, "invalid_request", error_message)
-
else
-
113
scope.instance_variable_set(:@error, error_message)
-
113
scope.instance_variable_set(:@back_url, referer)
-
-
113
return_response(authorize_error_view)
-
end
-
end
-
-
13
def authorization_required
-
451
if accepts_json?
-
429
throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
-
else
-
22
set_redirect_error_flash(require_authorization_error_flash)
-
22
redirect(authorize_path)
-
end
-
end
-
-
13
def do_authorize(*args); end
-
-
13
def authorize_response(params, mode); end
-
-
13
def create_token_from_authorization_code(grant_params, should_generate_refresh_token = !use_oauth_access_type?, oauth_grant: nil)
-
# fetch oauth grant
-
1352
oauth_grant ||= valid_locked_oauth_grant(grant_params)
-
-
1131
should_generate_refresh_token ||= oauth_grant[oauth_grants_access_type_column] == "offline"
-
-
1131
generate_token(oauth_grant, should_generate_refresh_token)
-
end
-
-
13
def create_oauth_grant(create_params = {})
-
897
create_params[oauth_grants_oauth_application_id_column] ||= oauth_application[oauth_applications_id_column]
-
897
create_params[oauth_grants_redirect_uri_column] ||= redirect_uri
-
897
create_params[oauth_grants_expires_in_column] ||= Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in)
-
897
create_params[oauth_grants_scopes_column] ||= scopes.join(oauth_scope_separator)
-
-
897
if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
-
18
create_params[oauth_grants_access_type_column] = access_type
-
end
-
-
897
ds = db[oauth_grants_table]
-
-
621
create_params[oauth_grants_code_column] = oauth_unique_id_generator
-
-
897
if oauth_reuse_access_token
-
416
unique_conds = Hash[oauth_grants_unique_columns.map { |column| [column, create_params[column]] }]
-
104
valid_grant = valid_oauth_grant_ds(unique_conds).select(oauth_grants_id_column).first
-
104
if valid_grant
-
72
create_params[oauth_grants_id_column] = valid_grant[oauth_grants_id_column]
-
104
rescue_from_uniqueness_error do
-
104
__insert_or_update_and_return__(
-
ds,
-
oauth_grants_id_column,
-
[oauth_grants_id_column],
-
create_params
-
)
-
end
-
104
return create_params[oauth_grants_code_column]
-
end
-
end
-
-
793
rescue_from_uniqueness_error do
-
832
if __one_oauth_token_per_account
-
256
__insert_or_update_and_return__(
-
ds,
-
oauth_grants_id_column,
-
oauth_grants_unique_columns,
-
create_params,
-
nil,
-
{
-
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
-
oauth_grants_revoked_at_column => nil
-
}
-
)
-
else
-
576
__insert_and_return__(ds, oauth_grants_id_column, create_params)
-
end
-
end
-
780
create_params[oauth_grants_code_column]
-
end
-
-
13
def normalize_redirect_uri_for_comparison(redirect_uri)
-
611
uri = URI(redirect_uri)
-
-
611
return redirect_uri unless uri.scheme == "http" && uri.port
-
-
52
hostname = uri.hostname
-
-
# https://www.rfc-editor.org/rfc/rfc8252#section-7.3
-
# ignore (potentially ephemeral) port number for native clients per RFC8252
-
4
begin
-
52
ip = IPAddr.new(hostname)
-
26
uri.port = nil if ip.loopback?
-
rescue IPAddr::InvalidAddressError
-
# https://www.rfc-editor.org/rfc/rfc8252#section-8.3
-
# Although the use of localhost is NOT RECOMMENDED, it is still allowed.
-
26
uri.port = nil if hostname == "localhost"
-
end
-
-
52
uri.to_s
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "time"
-
13
require "base64"
-
13
require "securerandom"
-
13
require "cgi"
-
13
require "digest/sha2"
-
13
require "rodauth/version"
-
13
require "rodauth/oauth"
-
13
require "rodauth/oauth/database_extensions"
-
13
require "rodauth/oauth/http_extensions"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_base, :OauthBase) do
-
13
include OAuth::HTTPExtensions
-
-
13
EMPTY_HASH = {}.freeze
-
-
13
auth_value_methods(:http_request)
-
13
auth_value_methods(:http_request_cache)
-
-
13
before "token"
-
-
13
error_flash "Please authorize to continue", "require_authorization"
-
13
error_flash "You are not authorized to revoke this token", "revoke_unauthorized_account"
-
-
13
button "Cancel", "oauth_cancel"
-
-
13
auth_value_method :json_response_content_type, "application/json"
-
-
13
auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
-
13
auth_value_method :oauth_access_token_expires_in, 60 * 60 # 60 minutes
-
13
auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
-
13
auth_value_method :oauth_unique_id_generation_retries, 3
-
-
13
auth_value_method :oauth_token_endpoint_auth_methods_supported, %w[client_secret_basic client_secret_post]
-
13
auth_value_method :oauth_grant_types_supported, %w[refresh_token]
-
13
auth_value_method :oauth_response_types_supported, []
-
13
auth_value_method :oauth_response_modes_supported, []
-
-
13
auth_value_method :oauth_valid_uri_schemes, %w[https]
-
13
auth_value_method :oauth_scope_separator, " "
-
-
# OAuth Grants
-
13
auth_value_method :oauth_grants_table, :oauth_grants
-
13
auth_value_method :oauth_grants_id_column, :id
-
12
%i[
-
account_id oauth_application_id type
-
redirect_uri code scopes
-
expires_in revoked_at
-
token refresh_token
-
1
].each do |column|
-
130
auth_value_method :"oauth_grants_#{column}_column", column
-
end
-
-
# Enables Token Hash
-
13
auth_value_method :oauth_grants_token_hash_column, :token
-
13
auth_value_method :oauth_grants_refresh_token_hash_column, :refresh_token
-
-
# Access Token reuse
-
13
auth_value_method :oauth_reuse_access_token, false
-
-
13
auth_value_method :oauth_applications_table, :oauth_applications
-
13
auth_value_method :oauth_applications_id_column, :id
-
-
12
%i[
-
account_id
-
name description scopes
-
client_id client_secret
-
homepage_url redirect_uri
-
token_endpoint_auth_method grant_types response_types response_modes
-
logo_uri tos_uri policy_uri jwks jwks_uri
-
contacts software_id software_version
-
1
].each do |column|
-
260
auth_value_method :"oauth_applications_#{column}_column", column
-
end
-
# Enables client secret Hash
-
13
auth_value_method :oauth_applications_client_secret_hash_column, :client_secret
-
-
13
auth_value_method :oauth_authorization_required_error_status, 401
-
13
auth_value_method :oauth_invalid_response_status, 400
-
13
auth_value_method :oauth_already_in_use_response_status, 409
-
-
# Feature options
-
13
auth_value_method :oauth_application_scopes, []
-
13
auth_value_method :oauth_token_type, "bearer"
-
13
auth_value_method :oauth_refresh_token_protection_policy, "rotation" # can be: none, sender_constrained, rotation
-
-
13
translatable_method :oauth_invalid_client_message, "Invalid client"
-
13
translatable_method :oauth_invalid_grant_type_message, "Invalid grant type"
-
13
translatable_method :oauth_invalid_grant_message, "Invalid grant"
-
13
translatable_method :oauth_invalid_scope_message, "Invalid scope"
-
13
translatable_method :oauth_unsupported_token_type_message, "Invalid token type hint"
-
-
13
translatable_method :oauth_already_in_use_message, "error generating unique token"
-
13
auth_value_method :oauth_already_in_use_error_code, "invalid_request"
-
13
auth_value_method :oauth_invalid_grant_type_error_code, "unsupported_grant_type"
-
-
13
auth_value_method :is_authorization_server?, true
-
-
13
auth_value_methods(:only_json?)
-
-
13
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
-
-
# METADATA
-
13
auth_value_method :oauth_metadata_service_documentation, nil
-
13
auth_value_method :oauth_metadata_ui_locales_supported, nil
-
13
auth_value_method :oauth_metadata_op_policy_uri, nil
-
13
auth_value_method :oauth_metadata_op_tos_uri, nil
-
-
13
auth_value_methods(
-
:authorization_server_url,
-
:oauth_grants_unique_columns
-
)
-
-
13
auth_methods(
-
:fetch_access_token,
-
:secret_hash,
-
:generate_token_hash,
-
:secret_matches?,
-
:oauth_unique_id_generator,
-
:require_authorizable_account,
-
:oauth_account_ds,
-
:oauth_application_ds
-
)
-
-
# /token
-
13
auth_server_route(:token) do |r|
-
2626
require_oauth_application
-
2314
before_token_route
-
-
2314
r.post do
-
2314
catch_error do
-
2314
validate_token_params
-
-
2119
oauth_grant = nil
-
-
2119
transaction do
-
2119
before_token
-
2119
oauth_grant = create_token(param("grant_type"))
-
end
-
-
1391
json_response_success(json_access_token_payload(oauth_grant))
-
end
-
-
throw_json_response_error(oauth_invalid_response_status, "invalid_request")
-
end
-
end
-
-
13
def load_oauth_server_metadata_route(issuer = nil)
-
260
request.on(".well-known") do
-
260
request.get("oauth-authorization-server") do
-
260
json_response_success(oauth_server_metadata_body(issuer), true)
-
end
-
end
-
end
-
-
13
def check_csrf?
-
8292
case request.path
-
when token_path
-
2626
false
-
else
-
9334
super
-
end
-
end
-
-
13
def oauth_token_subject
-
143
return unless authorization_token
-
-
143
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
-
-
13
def current_oauth_account
-
143
account_id = authorization_token[oauth_grants_account_id_column]
-
-
143
return unless account_id
-
-
117
oauth_account_ds(account_id).first
-
end
-
-
13
def current_oauth_application
-
169
oauth_application_ds(authorization_token[oauth_grants_oauth_application_id_column]).first
-
end
-
-
13
def accepts_json?
-
2202
return true if only_json?
-
-
2189
(accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
-
end
-
-
# copied from the jwt feature
-
13
def json_request?
-
260
return super if features.include?(:jsonn)
-
260
return @json_request if defined?(@json_request)
-
-
260
@json_request = request.content_type =~ json_request_regexp
-
end
-
-
13
def scopes
-
6735
scope = request.params["scope"]
-
4671
case scope
-
when Array
-
2665
scope
-
when String
-
3667
scope.split(" ")
-
end
-
end
-
-
13
def redirect_uri
-
4698
param_or_nil("redirect_uri") || begin
-
3909
return unless oauth_application
-
-
3909
redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
-
3909
redirect_uris.size == 1 ? redirect_uris.first : nil
-
end
-
end
-
-
13
def oauth_application
-
45562
return @oauth_application if defined?(@oauth_application)
-
-
1392
@oauth_application = begin
-
3632
client_id = param_or_nil("client_id")
-
-
3632
return unless client_id
-
-
3493
db[oauth_applications_table].filter(oauth_applications_client_id_column => client_id).first
-
end
-
end
-
-
13
def fetch_access_token
-
880
if (token = request.params["access_token"])
-
26
if request.post? && !(request.content_type.start_with?("application/x-www-form-urlencoded") &&
-
request.params.size == 1)
-
return
-
end
-
else
-
854
token = fetch_access_token_from_authorization_header
-
end
-
-
880
return if token.nil? || token.empty?
-
-
689
token
-
end
-
-
13
def fetch_access_token_from_authorization_header(token_type = oauth_token_type)
-
906
value = request.env["HTTP_AUTHORIZATION"]
-
-
906
return unless value && !value.empty?
-
-
793
scheme, token = value.split(" ", 2)
-
-
793
return unless scheme.downcase == token_type
-
-
767
token
-
end
-
-
13
def authorization_token
-
1192
return @authorization_token if defined?(@authorization_token)
-
-
# check if there is a token
-
386
access_token = fetch_access_token
-
-
386
return unless access_token
-
-
234
@authorization_token = oauth_grant_by_token(access_token)
-
end
-
-
13
def require_oauth_authorization(*scopes)
-
360
authorization_required unless authorization_token
-
-
182
token_scopes = authorization_token[oauth_grants_scopes_column].split(oauth_scope_separator)
-
-
390
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
-
end
-
-
13
def use_date_arithmetic?
-
5699
true
-
end
-
-
# override
-
13
def translate(key, default, args = EMPTY_HASH)
-
31177
return i18n_translate(key, default, **args) if features.include?(:i18n)
-
# do not attempt to translate by default
-
104
return default if args.nil?
-
-
104
default % args
-
end
-
-
13
def post_configure
-
5985
super
-
-
5985
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.
-
5985
return unless is_authorization_server?
-
-
5803
self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
-
-
# Check whether we can reutilize db entries for the same account / application pair
-
5803
one_oauth_token_per_account = db.indexes(oauth_grants_table).values.any? do |definition|
-
31696
definition[:unique] &&
-
definition[:columns] == oauth_grants_unique_columns
-
end
-
-
7831
self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
-
end
-
-
13
private
-
-
13
def oauth_account_ds(account_id)
-
286
account_ds(account_id)
-
end
-
-
13
def oauth_application_ds(oauth_application_id)
-
169
db[oauth_applications_table].where(oauth_applications_id_column => oauth_application_id)
-
end
-
-
13
def require_authorizable_account
-
3572
require_account
-
end
-
-
13
def rescue_from_uniqueness_error(&block)
-
3068
retries = oauth_unique_id_generation_retries
-
236
begin
-
3146
transaction(savepoint: :only, &block)
-
40
rescue Sequel::UniqueConstraintViolation
-
104
redirect_response_error("already_in_use") if retries.zero?
-
54
retries -= 1
-
78
retry
-
end
-
end
-
-
# OAuth Token Unique/Reuse
-
13
def oauth_grants_unique_columns
-
13466
[
-
18584
oauth_grants_oauth_application_id_column,
-
oauth_grants_account_id_column,
-
oauth_grants_scopes_column
-
]
-
end
-
-
13
def authorization_server_url
-
2094
base_url
-
end
-
-
13
def template_path(page)
-
30469
path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str")
-
30469
return super unless File.exist?(path)
-
-
1173
path
-
end
-
-
# to be used internally. Same semantics as require account, must:
-
# fetch an authorization basic header
-
# parse client id and secret
-
#
-
13
def require_oauth_application
-
2600
@oauth_application = if (token = (v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1])
-
# client_secret_basic
-
715
require_oauth_application_from_client_secret_basic(token)
-
1885
elsif (client_id = param_or_nil("client_id"))
-
1794
if (client_secret = param_or_nil("client_secret"))
-
# client_secret_post
-
1261
require_oauth_application_from_client_secret_post(client_id, client_secret)
-
else
-
# none
-
533
require_oauth_application_from_none(client_id)
-
end
-
else
-
91
authorization_required
-
end
-
end
-
-
13
def require_oauth_application_from_client_secret_basic(token)
-
715
client_id, client_secret = Base64.decode64(token).split(":", 2)
-
715
authorization_required unless client_id
-
715
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
-
660
authorization_required unless supports_auth_method?(oauth_application,
-
55
"client_secret_basic") && secret_matches?(oauth_application, client_secret)
-
689
oauth_application
-
end
-
-
13
def require_oauth_application_from_client_secret_post(client_id, client_secret)
-
1261
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
-
1164
authorization_required unless supports_auth_method?(oauth_application,
-
97
"client_secret_post") && secret_matches?(oauth_application, client_secret)
-
1235
oauth_application
-
end
-
-
13
def require_oauth_application_from_none(client_id)
-
533
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
-
533
authorization_required unless supports_auth_method?(oauth_application, "none")
-
416
oauth_application
-
end
-
-
13
def supports_auth_method?(oauth_application, auth_method)
-
2769
return false unless oauth_application
-
-
2730
supported_auth_methods = if oauth_application[oauth_applications_token_endpoint_auth_method_column]
-
715
oauth_application[oauth_applications_token_endpoint_auth_method_column].split(/ +/)
-
else
-
2015
oauth_token_endpoint_auth_methods_supported
-
end
-
-
2730
supported_auth_methods.include?(auth_method)
-
end
-
-
13
def require_oauth_application_from_account
-
13
ds = db[oauth_applications_table]
-
.join(oauth_grants_table, Sequel[oauth_grants_table][oauth_grants_oauth_application_id_column] =>
-
Sequel[oauth_applications_table][oauth_applications_id_column])
-
.where(oauth_grant_by_token_ds(param("token")).opts.fetch(:where, true))
-
.where(Sequel[oauth_applications_table][oauth_applications_account_id_column] => account_id)
-
-
13
@oauth_application = ds.qualify.first
-
13
return if @oauth_application
-
-
set_redirect_error_flash revoke_unauthorized_account_error_flash
-
redirect request.referer || "/"
-
end
-
-
13
def secret_matches?(oauth_application, secret)
-
1924
if oauth_applications_client_secret_hash_column
-
1924
BCrypt::Password.new(oauth_application[oauth_applications_client_secret_hash_column]) == secret
-
else
-
oauth_application[oauth_applications_client_secret_column] == secret
-
end
-
end
-
-
13
def set_client_secret(params, secret)
-
741
if oauth_applications_client_secret_hash_column
-
513
params[oauth_applications_client_secret_hash_column] = secret_hash(secret)
-
else
-
params[oauth_applications_client_secret_column] = secret
-
end
-
end
-
-
13
def secret_hash(secret)
-
1742
password_hash(secret)
-
end
-
-
13
def oauth_unique_id_generator
-
4849
SecureRandom.urlsafe_base64(32)
-
end
-
-
13
def generate_token_hash(token)
-
325
Base64.urlsafe_encode64(Digest::SHA256.digest(token))
-
end
-
-
13
def grant_from_application?(oauth_grant, oauth_application)
-
221
oauth_grant[oauth_grants_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
-
end
-
-
13
def password_hash(password)
-
1742
return super if features.include?(:login_password_requirements_base)
-
-
BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
-
end
-
-
13
def generate_token(grant_params = {}, should_generate_refresh_token = true)
-
1300
if grant_params[oauth_grants_id_column] && oauth_reuse_access_token &&
-
(
-
208
if oauth_grants_token_hash_column
-
104
grant_params[oauth_grants_token_hash_column]
-
else
-
104
grant_params[oauth_grants_token_column]
-
end
-
)
-
72
return grant_params
-
end
-
-
460
update_params = {
-
736
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in),
-
oauth_grants_code_column => nil
-
}
-
-
1196
rescue_from_uniqueness_error do
-
1196
access_token = _generate_access_token(update_params)
-
1196
refresh_token = _generate_refresh_token(update_params) if should_generate_refresh_token
-
1196
oauth_grant = store_token(grant_params, update_params)
-
-
1196
return unless oauth_grant
-
-
828
oauth_grant[oauth_grants_token_column] = access_token
-
1196
oauth_grant[oauth_grants_refresh_token_column] = refresh_token if refresh_token
-
1196
oauth_grant
-
end
-
end
-
-
13
def _generate_access_token(params = {})
-
689
token = oauth_unique_id_generator
-
-
689
if oauth_grants_token_hash_column
-
81
params[oauth_grants_token_hash_column] = generate_token_hash(token)
-
else
-
396
params[oauth_grants_token_column] = token
-
end
-
-
689
token
-
end
-
-
13
def _generate_refresh_token(params)
-
806
token = oauth_unique_id_generator
-
-
806
if oauth_grants_refresh_token_hash_column
-
81
params[oauth_grants_refresh_token_hash_column] = generate_token_hash(token)
-
else
-
477
params[oauth_grants_refresh_token_column] = token
-
end
-
-
806
token
-
end
-
-
13
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
-
-
13
def store_token(grant_params, update_params = {})
-
1196
ds = db[oauth_grants_table]
-
-
1196
if __one_oauth_token_per_account
-
-
to_update_if_null = [
-
368
oauth_grants_token_column,
-
oauth_grants_token_hash_column,
-
oauth_grants_refresh_token_column,
-
oauth_grants_refresh_token_hash_column
-
].compact.map do |attribute|
-
[
-
808
attribute,
-
(
-
808
if ds.respond_to?(:supports_insert_conflict?) && ds.supports_insert_conflict?
-
404
Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], Sequel[:excluded][attribute])
-
else
-
404
Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], update_params[attribute])
-
end
-
)
-
]
-
end
-
-
368
token = __insert_or_update_and_return__(
-
ds,
-
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.
-
368
token || ds.where(
-
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
-
-
828
if oauth_reuse_access_token
-
288
unique_conds = Hash[oauth_grants_unique_columns.map { |column| [column, update_params[column]] }]
-
72
valid_token_ds = valid_oauth_grant_ds(unique_conds)
-
72
if oauth_grants_token_hash_column
-
36
valid_token_ds.exclude(oauth_grants_token_hash_column => nil)
-
else
-
36
valid_token_ds.exclude(oauth_grants_token_column => nil)
-
end
-
-
72
valid_token = valid_token_ds.first
-
-
72
return valid_token if valid_token
-
end
-
-
828
if grant_params[oauth_grants_id_column]
-
711
__update_and_return__(ds.where(oauth_grants_id_column => grant_params[oauth_grants_id_column]), update_params)
-
else
-
117
__insert_and_return__(ds, oauth_grants_id_column, grant_params.merge(update_params))
-
end
-
end
-
end
-
-
13
def valid_locked_oauth_grant(grant_params = nil)
-
1404
oauth_grant = valid_oauth_grant_ds(grant_params).for_update.first
-
-
1404
redirect_response_error("invalid_grant") unless oauth_grant
-
-
1170
oauth_grant
-
end
-
-
13
def valid_oauth_grant_ds(grant_params = nil)
-
2425
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)
-
2425
ds = ds.where(grant_params) if grant_params
-
-
2425
ds
-
end
-
-
13
def oauth_grant_by_token_ds(token)
-
481
ds = valid_oauth_grant_ds
-
-
481
if oauth_grants_token_hash_column
-
52
ds.where(Sequel[oauth_grants_table][oauth_grants_token_hash_column] => generate_token_hash(token))
-
else
-
429
ds.where(Sequel[oauth_grants_table][oauth_grants_token_column] => token)
-
end
-
end
-
-
13
def oauth_grant_by_token(token)
-
403
oauth_grant_by_token_ds(token).first
-
end
-
-
13
def oauth_grant_by_refresh_token_ds(token, revoked: false)
-
429
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.
-
#
-
429
ds = ds.where(Sequel.date_add(oauth_grants_expires_in_column,
-
429
seconds: (oauth_refresh_token_expires_in - oauth_access_token_expires_in)) >= Sequel::CURRENT_TIMESTAMP)
-
-
429
ds = if oauth_grants_refresh_token_hash_column
-
39
ds.where(oauth_grants_refresh_token_hash_column => generate_token_hash(token))
-
else
-
390
ds.where(oauth_grants_refresh_token_column => token)
-
end
-
-
429
ds = ds.where(oauth_grants_revoked_at_column => nil) unless revoked
-
-
429
ds
-
end
-
-
13
def oauth_grant_by_refresh_token(token, **kwargs)
-
104
oauth_grant_by_refresh_token_ds(token, **kwargs).first
-
end
-
-
13
def json_access_token_payload(oauth_grant)
-
570
payload = {
-
912
"access_token" => oauth_grant[oauth_grants_token_column],
-
"token_type" => oauth_token_type,
-
"expires_in" => oauth_access_token_expires_in
-
}
-
1482
payload["refresh_token"] = oauth_grant[oauth_grants_refresh_token_column] if oauth_grant[oauth_grants_refresh_token_column]
-
1482
payload
-
end
-
-
# Access Tokens
-
-
13
def validate_token_params
-
2132
unless (grant_type = param_or_nil("grant_type"))
-
65
redirect_response_error("invalid_request")
-
end
-
-
2067
redirect_response_error("invalid_request") if grant_type == "refresh_token" && !param_or_nil("refresh_token")
-
end
-
-
13
def create_token(grant_type)
-
468
redirect_response_error("invalid_request") unless supported_grant_type?(grant_type, "refresh_token")
-
-
325
refresh_token = param("refresh_token")
-
# fetch potentially revoked oauth token
-
325
oauth_grant = oauth_grant_by_refresh_token_ds(refresh_token, revoked: true).for_update.first
-
-
325
update_params = { oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
-
seconds: oauth_access_token_expires_in) }
-
-
325
if !oauth_grant || oauth_grant[oauth_grants_revoked_at_column]
-
156
redirect_response_error("invalid_grant")
-
169
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.
-
-
78
refresh_token = _generate_refresh_token(update_params)
-
end
-
-
117
update_params[oauth_grants_oauth_application_id_column] = oauth_grant[oauth_grants_oauth_application_id_column]
-
-
169
oauth_grant = create_token_from_token(oauth_grant, update_params)
-
108
oauth_grant[oauth_grants_refresh_token_column] = refresh_token
-
156
oauth_grant
-
end
-
-
13
def create_token_from_token(oauth_grant, update_params)
-
169
redirect_response_error("invalid_grant") unless grant_from_application?(oauth_grant, oauth_application)
-
-
169
rescue_from_uniqueness_error do
-
208
oauth_grants_ds = db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
-
208
access_token = _generate_access_token(update_params)
-
208
oauth_grant = __update_and_return__(oauth_grants_ds, update_params)
-
-
108
oauth_grant[oauth_grants_token_column] = access_token
-
156
oauth_grant
-
end
-
end
-
-
13
def supported_grant_type?(grant_type, expected_grant_type = grant_type)
-
2496
return false unless grant_type == expected_grant_type
-
-
2028
grant_types_supported = if oauth_application[oauth_applications_grant_types_column]
-
52
oauth_application[oauth_applications_grant_types_column].split(/ +/)
-
else
-
1976
oauth_grant_types_supported
-
end
-
-
2028
grant_types_supported.include?(grant_type)
-
end
-
-
13
def supported_response_type?(response_type, expected_response_type = response_type)
-
1118
return false unless response_type == expected_response_type
-
-
1118
response_types_supported = if oauth_application[oauth_applications_response_types_column]
-
13
oauth_application[oauth_applications_response_types_column].split(/ +/)
-
else
-
1105
oauth_response_types_supported
-
end
-
-
1118
response_types = response_type.split(/ +/)
-
-
1118
(response_types - response_types_supported).empty?
-
end
-
-
13
def supported_response_mode?(response_mode, expected_response_mode = response_mode)
-
1105
return false unless response_mode == expected_response_mode
-
-
1105
response_modes_supported = if oauth_application[oauth_applications_response_modes_column]
-
oauth_application[oauth_applications_response_modes_column].split(/ +/)
-
else
-
1105
oauth_response_modes_supported
-
end
-
-
1105
response_modes_supported.include?(response_mode)
-
end
-
-
13
def oauth_server_metadata_body(path = nil)
-
351
issuer = base_url
-
351
issuer += "/#{path}" if path
-
-
135
{
-
216
issuer: issuer,
-
token_endpoint: token_url,
-
scopes_supported: oauth_application_scopes,
-
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
-
-
13
def redirect_response_error(error_code, message = nil)
-
1586
if accepts_json?
-
1001
status_code = if respond_to?(:"oauth_#{error_code}_response_status")
-
26
send(:"oauth_#{error_code}_response_status")
-
else
-
975
oauth_invalid_response_status
-
end
-
-
1001
throw_json_response_error(status_code, error_code, message)
-
else
-
585
redirect_url = redirect_uri || request.referer || default_redirect
-
585
redirect_url = URI.parse(redirect_url)
-
585
params = response_error_params(error_code, message)
-
585
state = param_or_nil("state")
-
585
params["state"] = state if state
-
585
_redirect_response_error(redirect_url, params)
-
end
-
end
-
-
13
def _redirect_response_error(redirect_url, params)
-
390
params = URI.encode_www_form(params)
-
390
if redirect_url.query
-
params << "&" unless params.empty?
-
params << redirect_url.query
-
end
-
390
redirect_url.query = params
-
390
redirect(redirect_url.to_s)
-
end
-
-
13
def response_error_params(error_code, message = nil)
-
3107
code = if respond_to?(:"oauth_#{error_code}_error_code")
-
91
send(:"oauth_#{error_code}_error_code")
-
else
-
3016
error_code
-
end
-
3107
payload = { "error" => code }
-
3107
error_description = message
-
3107
error_description ||= send(:"oauth_#{error_code}_message") if respond_to?(:"oauth_#{error_code}_message")
-
3107
payload["error_description"] = error_description if error_description
-
-
3107
payload
-
end
-
-
13
def json_response_success(body, cache = false)
-
2249
response.status = 200
-
2249
response["Content-Type"] ||= json_response_content_type
-
2249
if cache
-
# defaulting to 1-day for everyone, for now at least
-
390
max_age = 60 * 60 * 24
-
270
response["Cache-Control"] = "private, max-age=#{max_age}"
-
else
-
1287
response["Cache-Control"] = "no-store"
-
1287
response["Pragma"] = "no-cache"
-
end
-
2249
json_payload = _json_response_body(body)
-
2249
return_response(json_payload)
-
end
-
-
13
def throw_json_response_error(status, error_code, message = nil)
-
2522
set_response_error_status(status)
-
2522
payload = response_error_params(error_code, message)
-
2522
json_payload = _json_response_body(payload)
-
2522
response["Content-Type"] ||= json_response_content_type
-
2522
response["WWW-Authenticate"] = www_authenticate_header(payload) if status == 401
-
2522
return_response(json_payload)
-
end
-
-
13
def www_authenticate_header(*)
-
624
oauth_token_type.capitalize
-
end
-
-
13
def _json_response_body(hash)
-
5499
return super if features.include?(:json)
-
-
5499
if request.respond_to?(:convert_to_json)
-
request.send(:convert_to_json, hash)
-
else
-
5499
JSON.dump(hash)
-
end
-
end
-
-
13
if Gem::Version.new(Rodauth.version) < Gem::Version.new("2.23")
-
def return_response(body = nil)
-
response.write(body) if body
-
request.halt
-
end
-
end
-
-
13
def authorization_required
-
208
throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
-
end
-
-
13
def check_valid_scopes?(scp = scopes)
-
2596
return false unless scp
-
-
2596
(scp - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
-
end
-
-
13
def check_valid_uri?(uri)
-
9698
URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
-
end
-
-
13
def check_valid_no_fragment_uri?(uri)
-
2990
check_valid_uri?(uri) && URI.parse(uri).fragment.nil?
-
end
-
-
# Resource server mode
-
-
13
def authorization_server_metadata
-
52
auth_url = URI(authorization_server_url).dup
-
52
auth_url.path = "/.well-known/oauth-authorization-server"
-
-
52
http_request_with_cache(auth_url)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_client_credentials_grant, :OauthClientCredentialsGrant) do
-
13
depends :oauth_base
-
-
13
def oauth_grant_types_supported
-
104
super | %w[client_credentials]
-
end
-
-
13
private
-
-
13
def create_token(grant_type)
-
78
return super unless supported_grant_type?(grant_type, "client_credentials")
-
-
65
grant_scopes = scopes
-
-
65
grant_scopes = if grant_scopes
-
13
redirect_response_error("invalid_scope") unless check_valid_scopes?
-
13
grant_scopes.join(oauth_scope_separator)
-
else
-
52
oauth_application[oauth_applications_scopes_column]
-
end
-
-
25
grant_params = {
-
40
oauth_grants_type_column => "client_credentials",
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
-
oauth_grants_scopes_column => grant_scopes
-
}
-
65
generate_token(grant_params, false)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_device_code_grant, :OauthDeviceCodeGrant) do
-
13
depends :oauth_authorize_base
-
-
13
before "device_authorization"
-
13
before "device_verification"
-
-
13
notice_flash "The device is verified", "device_verification"
-
13
error_flash "No device to authorize with the given user code", "user_code_not_found"
-
-
13
view "device_verification", "Device Verification", "device_verification"
-
13
view "device_search", "Device Search", "device_search"
-
-
13
button "Verify", "oauth_device_verification"
-
13
button "Search", "oauth_device_search"
-
-
13
auth_value_method :oauth_grants_user_code_column, :user_code
-
13
auth_value_method :oauth_grants_last_polled_at_column, :last_polled_at
-
-
13
translatable_method :oauth_device_search_page_lead, "Insert the user code from the device you'd like to authorize."
-
13
translatable_method :oauth_device_verification_page_lead, "The device with user code %<user_code>s would like to access your data."
-
13
translatable_method :oauth_expired_token_message, "the device code has expired"
-
13
translatable_method :oauth_access_denied_message, "the authorization request has been denied"
-
13
translatable_method :oauth_authorization_pending_message, "the authorization request is still pending"
-
13
translatable_method :oauth_slow_down_message, "authorization request is still pending but poll interval should be increased"
-
-
13
auth_value_method :oauth_device_code_grant_polling_interval, 5 # seconds
-
13
auth_value_method :oauth_device_code_grant_user_code_size, 8 # characters
-
13
%w[user_code].each do |param|
-
13
auth_value_method :"oauth_grant_#{param}_param", param
-
end
-
13
translatable_method :oauth_grant_user_code_label, "User code"
-
-
13
auth_methods(
-
:generate_user_code
-
)
-
-
# /device-authorization
-
13
auth_server_route(:device_authorization) do |r|
-
26
require_oauth_application
-
26
before_device_authorization_route
-
-
26
r.post do
-
26
user_code = generate_user_code
-
26
device_code = transaction do
-
26
before_device_authorization
-
26
create_oauth_grant(
-
oauth_grants_type_column => "device_code",
-
oauth_grants_user_code_column => user_code
-
)
-
end
-
-
26
json_response_success \
-
"device_code" => device_code,
-
"user_code" => user_code,
-
"verification_uri" => device_url,
-
"verification_uri_complete" => device_url(user_code: user_code),
-
"expires_in" => oauth_grant_expires_in,
-
"interval" => oauth_device_code_grant_polling_interval
-
end
-
end
-
-
# /device
-
13
auth_server_route(:device) do |r|
-
273
require_authorizable_account
-
260
before_device_route
-
-
260
r.get do
-
221
if (user_code = param_or_nil("user_code"))
-
78
oauth_grant = valid_oauth_grant_ds(oauth_grants_user_code_column => user_code).first
-
-
78
unless oauth_grant
-
39
set_redirect_error_flash user_code_not_found_error_flash
-
39
redirect device_path
-
end
-
-
39
scope.instance_variable_set(:@oauth_grant, oauth_grant)
-
39
device_verification_view
-
else
-
143
device_search_view
-
end
-
end
-
-
39
r.post do
-
39
catch_error do
-
39
unless (user_code = param_or_nil("user_code")) && !user_code.empty?
-
13
set_redirect_error_flash oauth_invalid_grant_message
-
13
redirect device_path
-
end
-
-
26
transaction do
-
26
before_device_verification
-
26
create_token("device_code")
-
end
-
end
-
26
set_notice_flash device_verification_notice_flash
-
26
redirect device_path
-
end
-
end
-
-
13
def check_csrf?
-
423
case request.path
-
when device_authorization_path
-
26
false
-
else
-
585
super
-
end
-
end
-
-
13
def oauth_grant_types_supported
-
143
super | %w[urn:ietf:params:oauth:grant-type:device_code]
-
end
-
-
13
private
-
-
13
def generate_user_code
-
26
user_code_size = oauth_device_code_grant_user_code_size
-
24
SecureRandom.random_number(36**user_code_size)
-
.to_s(36) # 0 to 9, a to z
-
.upcase
-
2
.rjust(user_code_size, "0")
-
end
-
-
# TODO: think about removing this and recommend PKCE
-
13
def supports_auth_method?(oauth_application, auth_method)
-
169
return super unless auth_method == "none"
-
-
143
request.path == device_authorization_path || request.params.key?("device_code") || super
-
end
-
-
13
def create_token(grant_type)
-
156
if supported_grant_type?(grant_type, "urn:ietf:params:oauth:grant-type:device_code")
-
-
130
oauth_grant = db[oauth_grants_table].where(
-
oauth_grants_type_column => "device_code",
-
oauth_grants_code_column => param("device_code"),
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
-
).for_update.first
-
-
130
throw_json_response_error(oauth_invalid_response_status, "invalid_grant") unless oauth_grant
-
-
117
now = Time.now
-
-
117
if oauth_grant[oauth_grants_user_code_column].nil?
-
16
return create_token_from_authorization_code(
-
{ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] },
-
oauth_grant: oauth_grant
-
2
)
-
end
-
-
91
if oauth_grant[oauth_grants_revoked_at_column]
-
26
throw_json_response_error(oauth_invalid_response_status, "access_denied")
-
65
elsif oauth_grant[oauth_grants_expires_in_column] < now
-
13
throw_json_response_error(oauth_invalid_response_status, "expired_token")
-
else
-
52
last_polled_at = oauth_grant[oauth_grants_last_polled_at_column]
-
52
if last_polled_at && convert_timestamp(last_polled_at) + oauth_device_code_grant_polling_interval > now
-
13
throw_json_response_error(oauth_invalid_response_status, "slow_down")
-
else
-
39
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
-
3
.update(oauth_grants_last_polled_at_column => Sequel::CURRENT_TIMESTAMP)
-
39
throw_json_response_error(oauth_invalid_response_status, "authorization_pending")
-
end
-
end
-
26
elsif grant_type == "device_code"
-
-
# fetch oauth grant
-
26
rs = valid_oauth_grant_ds(
-
oauth_grants_user_code_column => param("user_code")
-
).update(oauth_grants_user_code_column => nil, oauth_grants_type_column => "device_code")
-
-
26
rs if rs.positive?
-
else
-
super
-
end
-
end
-
-
13
def validate_token_params
-
143
grant_type = param_or_nil("grant_type")
-
-
143
if grant_type == "urn:ietf:params:oauth:grant-type:device_code" && !param_or_nil("device_code")
-
13
redirect_response_error("invalid_request")
-
end
-
130
super
-
end
-
-
13
def store_token(grant_params, update_params = {})
-
26
return super unless grant_params[oauth_grants_user_code_column]
-
-
# do not clean up device code just yet
-
update_params.delete(oauth_grants_code_column)
-
update_params[oauth_grants_user_code_column] = nil
-
update_params.merge!(resource_params)
-
-
super(grant_params, update_params)
-
end
-
-
13
def oauth_server_metadata_body(*)
-
13
super.tap do |data|
-
9
data[:device_authorization_endpoint] = device_authorization_url
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_dpop, :OauthDpop) do
-
13
depends :oauth_jwt, :oauth_authorize_base
-
-
13
auth_value_method :oauth_invalid_token_error_response_status, 401
-
13
auth_value_method :oauth_multiple_auth_methods_response_status, 401
-
13
auth_value_method :oauth_access_token_dpop_bound_response_status, 401
-
-
13
translatable_method :oauth_invalid_dpop_proof_message, "Invalid DPoP proof"
-
13
translatable_method :oauth_multiple_auth_methods_message, "Multiple methods used to include access token"
-
13
auth_value_method :oauth_multiple_dpop_proofs_error_code, "invalid_request"
-
13
translatable_method :oauth_multiple_dpop_proofs_message, "Multiple DPoP proofs used"
-
13
auth_value_method :oauth_invalid_dpop_jkt_error_code, "invalid_dpop_proof"
-
13
translatable_method :oauth_invalid_dpop_jkt_message, "Invalid DPoP JKT"
-
13
auth_value_method :oauth_invalid_dpop_jti_error_code, "invalid_dpop_proof"
-
13
translatable_method :oauth_invalid_dpop_jti_message, "Invalid DPoP jti"
-
13
auth_value_method :oauth_invalid_dpop_htm_error_code, "invalid_dpop_proof"
-
13
translatable_method :oauth_invalid_dpop_htm_message, "Invalid DPoP htm"
-
13
auth_value_method :oauth_invalid_dpop_htu_error_code, "invalid_dpop_proof"
-
13
translatable_method :oauth_invalid_dpop_htu_message, "Invalid DPoP htu"
-
13
translatable_method :oauth_access_token_dpop_bound_message, "DPoP bound access token requires DPoP proof"
-
-
13
translatable_method :oauth_use_dpop_nonce_message, "DPoP nonce is required"
-
-
13
auth_value_method :oauth_dpop_proof_expires_in, 60 * 5 # 5 minutes
-
13
auth_value_method :oauth_dpop_bound_access_tokens, false
-
13
auth_value_method :oauth_dpop_use_nonce, false
-
13
auth_value_method :oauth_dpop_nonce_expires_in, 5 # 5 seconds
-
13
auth_value_method :oauth_dpop_signing_alg_values_supported,
-
%w[
-
RS256
-
RS384
-
RS512
-
PS256
-
PS384
-
PS512
-
ES256
-
ES384
-
ES512
-
ES256K
-
]
-
-
13
auth_value_method :oauth_applications_dpop_bound_access_tokens_column, :dpop_bound_access_tokens
-
13
auth_value_method :oauth_grants_dpop_jkt_column, :dpop_jkt
-
13
auth_value_method :oauth_pushed_authorization_requests_dpop_jkt_column, :dpop_jkt
-
-
13
auth_value_method :oauth_dpop_proofs_table, :oauth_dpop_proofs
-
13
auth_value_method :oauth_dpop_proofs_jti_column, :jti
-
13
auth_value_method :oauth_dpop_proofs_first_use_column, :first_use
-
-
13
auth_methods(:validate_dpop_proof_usage)
-
-
13
def require_oauth_authorization(*scopes)
-
52
@dpop_access_token = fetch_access_token_from_authorization_header("dpop")
-
-
52
unless @dpop_access_token
-
39
authorization_required if oauth_dpop_bound_access_tokens
-
-
# Specifically, such a protected resource MUST reject a DPoP-bound access token received as a bearer token
-
26
redirect_response_error("access_token_dpop_bound") if authorization_token && authorization_token.dig("cnf", "jkt")
-
-
9
return super
-
end
-
-
13
dpop = fetch_dpop_token
-
-
13
dpop_claims = validate_dpop_token(dpop)
-
-
# 4.3.12
-
13
validate_ath(dpop_claims, @dpop_access_token)
-
-
13
@authorization_token = decode_access_token(@dpop_access_token)
-
-
# 4.3.12 - confirm that the public key to which the access token is bound matches the public key from the DPoP proof.
-
13
jkt = authorization_token.dig("cnf", "jkt")
-
-
13
redirect_response_error("invalid_dpop_jkt") if oauth_dpop_bound_access_tokens && !jkt
-
-
13
redirect_response_error("invalid_dpop_jkt") unless jkt == @dpop_thumbprint
-
-
13
super
-
end
-
-
13
private
-
-
13
def validate_token_params
-
260
dpop = fetch_dpop_token
-
-
260
unless dpop
-
13
authorization_required if dpop_bound_access_tokens_required?
-
-
return super
-
end
-
-
247
validate_dpop_token(dpop)
-
-
143
super
-
end
-
-
13
def validate_par_params
-
52
super
-
-
52
return unless (dpop = fetch_dpop_token)
-
-
39
validate_dpop_token(dpop)
-
-
39
if (dpop_jkt = param_or_nil("dpop_jkt"))
-
26
redirect_response_error("invalid_request") if dpop_jkt != @dpop_thumbprint
-
else
-
9
request.params["dpop_jkt"] = @dpop_thumbprint
-
end
-
end
-
-
13
def validate_dpop_token(dpop)
-
# 4.3.2
-
299
@dpop_claims = dpop_decode(dpop)
-
260
redirect_response_error("invalid_dpop_proof") unless @dpop_claims
-
-
247
validate_dpop_jwt_claims(@dpop_claims)
-
-
# 4.3.10
-
221
validate_nonce(@dpop_claims)
-
-
# 11.1
-
# To prevent multiple uses of the same DPoP proof, servers can store, in the
-
# context of the target URI, the jti value of each DPoP proof for the time window
-
# in which the respective DPoP proof JWT would be accepted.
-
208
validate_dpop_proof_usage(@dpop_claims)
-
-
195
@dpop_claims
-
end
-
-
13
def validate_dpop_proof_usage(claims)
-
208
jti = claims["jti"]
-
-
208
dpop_proof = __insert_or_do_nothing_and_return__(
-
db[oauth_dpop_proofs_table],
-
oauth_dpop_proofs_jti_column,
-
[oauth_dpop_proofs_jti_column],
-
oauth_dpop_proofs_jti_column => Digest::SHA256.hexdigest(jti),
-
oauth_dpop_proofs_first_use_column => Sequel::CURRENT_TIMESTAMP
-
)
-
-
208
return unless (Time.now - dpop_proof[oauth_dpop_proofs_first_use_column]) > oauth_dpop_proof_expires_in
-
-
13
redirect_response_error("invalid_dpop_proof")
-
end
-
-
13
def dpop_decode(dpop)
-
# decode first without verifying!
-
299
_, headers = jwt_decode_no_key(dpop)
-
-
299
redirect_response_error("invalid_dpop_proof") unless verify_dpop_jwt_headers(headers)
-
-
260
dpop_jwk = headers["jwk"]
-
-
260
jwt_decode(
-
dpop,
-
jws_key: jwk_key(dpop_jwk),
-
jws_algorithm: headers["alg"],
-
verify_iss: false,
-
verify_aud: false,
-
verify_jti: false
-
)
-
end
-
-
13
def verify_dpop_jwt_headers(headers)
-
# 4.3.4 - A field with the value dpop+jwt
-
299
return false unless headers["typ"] == "dpop+jwt"
-
-
# 4.3.5 - It MUST NOT be none or an identifier for a symmetric algorithm
-
286
alg = headers["alg"]
-
286
return false unless alg && oauth_dpop_signing_alg_values_supported.include?(alg)
-
-
273
dpop_jwk = headers["jwk"]
-
-
273
return false unless dpop_jwk
-
-
# 4.3.7 - It MUST NOT contain a private key.
-
273
return false if private_jwk?(dpop_jwk)
-
-
# store thumbprint for future assertions
-
260
@dpop_thumbprint = jwk_thumbprint(dpop_jwk)
-
-
260
true
-
end
-
-
13
def validate_dpop_jwt_claims(claims)
-
247
jti = claims["jti"]
-
-
247
unless jti && jti == Digest::SHA256.hexdigest("#{request.request_method}:#{request.url}:#{claims['iat']}")
-
redirect_response_error("invalid_dpop_jti")
-
end
-
-
247
htm = claims["htm"]
-
-
# 4.3.8 - Check if htm matches the request method
-
247
redirect_response_error("invalid_dpop_htm") unless htm && htm == request.request_method
-
-
234
htu = claims["htu"]
-
-
# 4.3.9 - Check if htu matches the request URL
-
234
redirect_response_error("invalid_dpop_htu") unless htu && htu == request.url
-
end
-
-
13
def validate_ath(claims, access_token)
-
# When the DPoP proof is used in conjunction with the presentation of an access token in protected resource access
-
# the DPoP proof MUST also contain the following claim
-
13
ath = claims["ath"]
-
-
13
redirect_response_error("invalid_token") unless ath
-
-
# The value MUST be the result of a base64url encoding of the SHA-256 hash of the ASCII encoding of
-
# the associated access token's value.
-
13
redirect_response_error("invalid_token") unless ath == Base64.urlsafe_encode64(Digest::SHA256.digest(access_token), padding: false)
-
end
-
-
13
def validate_nonce(claims)
-
221
nonce = claims["nonce"]
-
-
221
unless nonce
-
208
dpop_nonce_required(claims) if dpop_use_nonce?
-
-
135
return
-
end
-
-
13
dpop_nonce_required(claims) unless valid_dpop_nonce?(nonce)
-
end
-
-
13
def jwt_claims(oauth_grant)
-
117
claims = super
-
117
if @dpop_thumbprint
-
# the authorization server associates the issued access token with the
-
# public key from the DPoP proof
-
81
claims[:cnf] = { jkt: @dpop_thumbprint }
-
end
-
117
claims
-
end
-
-
13
def generate_token(grant_params = {}, should_generate_refresh_token = true)
-
# When an authorization server supporting DPoP issues a refresh token to a public client
-
# that presents a valid DPoP proof at the token endpoint, the refresh token MUST be bound to the respective public key.
-
117
grant_params[oauth_grants_dpop_jkt_column] = @dpop_thumbprint if @dpop_thumbprint
-
117
super
-
end
-
-
13
def valid_oauth_grant_ds(grant_params = nil)
-
143
ds = super
-
-
143
ds = ds.where(oauth_grants_dpop_jkt_column => nil)
-
143
ds = ds.or(oauth_grants_dpop_jkt_column => @dpop_thumbprint) if @dpop_thumbprint
-
143
ds
-
end
-
-
13
def oauth_grant_by_refresh_token_ds(_token, revoked: false)
-
ds = super
-
# The binding MUST be validated when the refresh token is later presented to get new access tokens.
-
ds = ds.where(oauth_grants_dpop_jkt_column => nil)
-
ds = ds.or(oauth_grants_dpop_jkt_column => @dpop_thumbprint) if @dpop_thumbprint
-
ds
-
end
-
-
13
def oauth_grant_by_token_ds(_token)
-
ds = super
-
# The binding MUST be validated when the refresh token is later presented to get new access tokens.
-
ds = ds.where(oauth_grants_dpop_jkt_column => nil)
-
ds = ds.or(oauth_grants_dpop_jkt_column => @dpop_thumbprint) if @dpop_thumbprint
-
ds
-
end
-
-
13
def create_oauth_grant(create_params = {})
-
# 10. Authorization Code Binding to DPoP Key
-
# Binding the authorization code issued to the client's proof-of-possession key can enable end-to-end
-
# binding of the entire authorization flow.
-
65
if (dpop_jkt = param_or_nil("dpop_jkt"))
-
45
create_params[oauth_grants_dpop_jkt_column] = dpop_jkt
-
end
-
-
65
super
-
end
-
-
13
def json_access_token_payload(oauth_grant)
-
117
payload = super
-
# 5. A token_type of DPoP MUST be included in the access token response to
-
# signal to the client that the access token was bound to its DPoP key
-
117
payload["token_type"] = "DPoP" if @dpop_claims
-
117
payload
-
end
-
-
13
def fetch_dpop_token
-
325
dpop = request.env["HTTP_DPOP"]
-
-
325
return if dpop.nil? || dpop.empty?
-
-
# 4.3.1 - There is not more than one DPoP HTTP request header field.
-
299
redirect_response_error("multiple_dpop_proofs") if dpop.split(";").size > 1
-
-
299
dpop
-
end
-
-
13
def dpop_bound_access_tokens_required?
-
65
oauth_dpop_bound_access_tokens || (oauth_application && oauth_application[oauth_applications_dpop_bound_access_tokens_column])
-
end
-
-
13
def dpop_use_nonce?
-
208
oauth_dpop_use_nonce || (oauth_application && oauth_application[oauth_applications_dpop_bound_access_tokens_column])
-
end
-
-
13
def valid_dpop_proof_required(error_code = "invalid_dpop_proof")
-
if @dpop_access_token
-
# protected resource access
-
throw_json_response_error(401, error_code)
-
else
-
redirect_response_error(error_code)
-
end
-
end
-
-
13
def dpop_nonce_required(dpop_claims)
-
9
response["DPoP-Nonce"] = generate_dpop_nonce(dpop_claims)
-
-
13
if @dpop_access_token
-
# protected resource access
-
throw_json_response_error(401, "use_dpop_nonce")
-
else
-
13
redirect_response_error("use_dpop_nonce")
-
end
-
end
-
-
13
def www_authenticate_header(payload)
-
52
header = if dpop_bound_access_tokens_required?
-
26
"DPoP"
-
else
-
18
"#{super}, DPoP"
-
end
-
-
52
error_code = payload["error"]
-
-
52
unless error_code == "invalid_client"
-
13
header = "#{header} error=\"#{error_code}\""
-
-
13
if (desc = payload["error_description"])
-
13
header = "#{header} error_description=\"#{desc}\""
-
end
-
end
-
-
52
algs = oauth_dpop_signing_alg_values_supported.join(" ")
-
-
36
"#{header} algs=\"#{algs}\""
-
end
-
-
# Nonce
-
-
13
def generate_dpop_nonce(dpop_claims)
-
13
issued_at = Time.now.to_i
-
-
13
aud = "#{dpop_claims['htm']}:#{dpop_claims['htu']}"
-
-
5
nonce_claims = {
-
8
iss: oauth_jwt_issuer,
-
iat: issued_at,
-
exp: issued_at + oauth_dpop_nonce_expires_in,
-
aud: aud
-
}
-
-
13
jwt_encode(nonce_claims)
-
end
-
-
13
def valid_dpop_nonce?(nonce)
-
13
nonce_claims = jwt_decode(nonce, verify_aud: false, verify_jti: false)
-
-
13
return false unless nonce_claims
-
-
13
jti = nonce_claims["jti"]
-
-
13
return false unless jti
-
-
13
return false unless jti == Digest::SHA256.hexdigest("#{request.request_method}:#{request.url}:#{nonce_claims['iat']}")
-
-
13
return false unless nonce_claims.key?("aud")
-
-
13
htm, htu = nonce_claims["aud"].split(":", 2)
-
-
13
htm == request.request_method && htu == request.url
-
end
-
-
13
def json_token_introspect_payload(grant_or_claims)
-
13
claims = super
-
-
13
return claims unless grant_or_claims
-
-
13
if (jkt = grant_or_claims.dig("cnf", "jkt"))
-
9
(claims[:cnf] ||= {})[:jkt] = jkt
-
9
claims[:token_type] = "DPoP"
-
end
-
-
13
claims
-
end
-
-
13
def oauth_server_metadata_body(*)
-
13
super.tap do |data|
-
9
data[:dpop_signing_alg_values_supported] = oauth_dpop_signing_alg_values_supported
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_dynamic_client_registration, :OauthDynamicClientRegistration) do
-
13
depends :oauth_base
-
-
13
before "register"
-
-
13
auth_value_method :oauth_client_registration_required_params, %w[redirect_uris client_name]
-
13
auth_value_method :oauth_applications_registration_access_token_column, :registration_access_token
-
13
auth_value_method :registration_client_uri_route, "register"
-
-
13
PROTECTED_APPLICATION_ATTRIBUTES = %w[account_id client_id].freeze
-
-
13
def load_registration_client_uri_routes
-
52
request.on(registration_client_uri_route) do
-
# CLIENT REGISTRATION URI
-
52
request.on(String) do |client_id|
-
52
token = (v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Bearer (.*)\Z/, 1]
-
-
52
next unless token
-
-
52
oauth_application = db[oauth_applications_table]
-
.where(oauth_applications_client_id_column => client_id)
-
.first
-
52
next unless oauth_application
-
-
52
authorization_required unless password_hash_match?(oauth_application[oauth_applications_registration_access_token_column], token)
-
-
52
request.is do
-
52
request.get do
-
13
json_response_oauth_application(oauth_application)
-
end
-
39
request.on method: :put do
-
24
%w[client_id registration_access_token registration_client_uri client_secret_expires_at
-
2
client_id_issued_at].each do |prohibited_param|
-
78
if request.params.key?(prohibited_param)
-
13
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(prohibited_param))
-
end
-
end
-
13
validate_client_registration_params
-
-
# if the client includes the "client_secret" field in the request, the value of this field MUST match the currently
-
# issued client secret for that client. The client MUST NOT be allowed to overwrite its existing client secret with
-
# its own chosen value.
-
13
authorization_required if request.params.key?("client_secret") && secret_matches?(oauth_application,
-
request.params["client_secret"])
-
-
13
oauth_application = transaction do
-
13
applications_ds = db[oauth_applications_table]
-
13
__update_and_return__(applications_ds, @oauth_application_params)
-
end
-
13
json_response_oauth_application(oauth_application)
-
end
-
-
13
request.on method: :delete do
-
13
applications_ds = db[oauth_applications_table]
-
13
applications_ds.where(oauth_applications_client_id_column => client_id).delete
-
13
response.status = 204
-
9
response["Cache-Control"] = "no-store"
-
9
response["Pragma"] = "no-cache"
-
13
response.finish
-
end
-
end
-
end
-
end
-
end
-
-
# /register
-
13
auth_server_route(:register) do |r|
-
1456
before_register_route
-
-
1456
r.post do
-
1456
oauth_client_registration_required_params.each do |required_param|
-
2860
unless request.params.key?(required_param)
-
52
register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
-
end
-
end
-
-
1404
validate_client_registration_params
-
-
702
response_params = transaction do
-
702
before_register
-
702
do_register
-
end
-
-
702
response.status = 201
-
486
response["Content-Type"] = json_response_content_type
-
486
response["Cache-Control"] = "no-store"
-
486
response["Pragma"] = "no-cache"
-
702
response.write(_json_response_body(response_params))
-
end
-
end
-
-
13
def check_csrf?
-
1044
case request.path
-
when register_path
-
1456
false
-
else
-
52
super
-
end
-
end
-
-
13
private
-
-
13
def _before_register
-
raise %{dynamic client registration requires authentication.
-
Override ´before_register` to perform it.
-
example:
-
-
before_register do
-
account = _account_from_login(request.env["HTTP_X_USER_EMAIL"])
-
authorization_required unless account
-
@oauth_application_params[:account_id] = account[:id]
-
end
-
}
-
end
-
-
13
def validate_client_registration_params(request_params = request.params)
-
1443
@oauth_application_params = request_params.each_with_object({}) do |(key, value), params|
-
12177
case key
-
when "redirect_uris"
-
1404
if value.is_a?(Array)
-
1391
value = value.each do |uri|
-
2652
unless check_valid_no_fragment_uri?(uri)
-
26
register_throw_json_response_error("invalid_redirect_uri",
-
register_invalid_uri_message(uri))
-
end
-
end.join(" ")
-
else
-
13
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
-
end
-
1365
key = oauth_applications_redirect_uri_column
-
when "token_endpoint_auth_method"
-
663
unless oauth_token_endpoint_auth_methods_supported.include?(value)
-
13
register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
-
end
-
# verify if in range
-
650
key = oauth_applications_token_endpoint_auth_method_column
-
when "grant_types"
-
728
if value.is_a?(Array)
-
715
value = value.each do |grant_type|
-
1313
unless oauth_grant_types_supported.include?(grant_type)
-
26
register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(grant_type, value))
-
end
-
end.join(" ")
-
else
-
13
register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
-
end
-
689
key = oauth_applications_grant_types_column
-
when "response_types"
-
754
if value.is_a?(Array)
-
741
grant_types = request_params["grant_types"] || %w[authorization_code]
-
741
value = value.each do |response_type|
-
754
unless oauth_response_types_supported.include?(response_type)
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_response_type_message(response_type))
-
end
-
-
741
validate_client_registration_response_type(response_type, grant_types)
-
end.join(" ")
-
else
-
13
register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
-
end
-
676
key = oauth_applications_response_types_column
-
# verify if in range and match grant type
-
when "client_uri", "logo_uri", "tos_uri", "policy_uri", "jwks_uri"
-
6487
register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value)) unless check_valid_uri?(value)
-
4446
case key
-
when "client_uri"
-
1339
key = oauth_applications_homepage_url_column
-
when "jwks_uri"
-
1222
if request_params.key?("jwks")
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_jwks_param_message(key, "jwks"))
-
end
-
end
-
6409
key = __send__(:"oauth_applications_#{key}_column")
-
when "jwks"
-
26
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash)
-
13
if request_params.key?("jwks_uri")
-
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_jwks_param_message(key, "jwks_uri"))
-
end
-
-
13
key = oauth_applications_jwks_column
-
13
value = JSON.dump(value)
-
when "scope"
-
1352
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(String)
-
1352
scopes = value.split(" ") - oauth_application_scopes
-
1352
register_throw_json_response_error("invalid_client_metadata", register_invalid_scopes_message(value)) unless scopes.empty?
-
1326
key = oauth_applications_scopes_column
-
# verify if in range
-
when "contacts"
-
1300
register_throw_json_response_error("invalid_client_metadata", register_invalid_contacts_message(value)) unless value.is_a?(Array)
-
1287
value = value.join(" ")
-
1287
key = oauth_applications_contacts_column
-
when "client_name"
-
1352
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(String)
-
1352
key = oauth_applications_name_column
-
when "dpop_bound_access_tokens"
-
39
unless respond_to?(:oauth_applications_dpop_bound_access_tokens_column)
-
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_param_message(key))
-
end
-
27
request_params[key] = value = convert_to_boolean(key, value)
-
-
26
key = oauth_applications_dpop_bound_access_tokens_column
-
when "require_signed_request_object"
-
39
unless respond_to?(:oauth_applications_require_signed_request_object_column)
-
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_param_message(key))
-
end
-
27
request_params[key] = value = convert_to_boolean(key, value)
-
-
26
key = oauth_applications_require_signed_request_object_column
-
when "require_pushed_authorization_requests"
-
39
unless respond_to?(:oauth_applications_require_pushed_authorization_requests_column)
-
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_param_message(key))
-
end
-
27
request_params[key] = value = convert_to_boolean(key, value)
-
-
26
key = oauth_applications_require_pushed_authorization_requests_column
-
when "tls_client_certificate_bound_access_tokens"
-
13
property = :oauth_applications_tls_client_certificate_bound_access_tokens_column
-
13
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key)) unless respond_to?(property)
-
-
9
request_params[key] = value = convert_to_boolean(key, value)
-
-
13
key = oauth_applications_tls_client_certificate_bound_access_tokens_column
-
when /\Atls_client_auth_/
-
91
unless respond_to?(:"oauth_applications_#{key}_column")
-
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_param_message(key))
-
end
-
-
# client using the tls_client_auth authentication method MUST use exactly one of the below metadata
-
# parameters to indicate the certificate subject value that the authorization server is to expect when
-
# authenticating the respective client.
-
1105
if params.any? { |k, _| k.to_s.start_with?("tls_client_auth_") }
-
13
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
-
end
-
-
78
key = __send__(:"oauth_applications_#{key}_column")
-
else
-
3302
if respond_to?(:"oauth_applications_#{key}_column")
-
3237
if PROTECTED_APPLICATION_ATTRIBUTES.include?(key)
-
13
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
-
end
-
3224
property = :"oauth_applications_#{key}_column"
-
3224
key = __send__(property)
-
65
elsif !db[oauth_applications_table].columns.include?(key.to_sym)
-
39
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
-
end
-
end
-
11898
params[key] = value
-
end
-
end
-
-
13
def validate_client_registration_response_type(response_type, grant_types)
-
477
case response_type
-
when "code"
-
611
unless grant_types.include?("authorization_code")
-
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_response_type_for_grant_type_message(response_type,
-
"authorization_code"))
-
end
-
when "token"
-
65
unless grant_types.include?("implicit")
-
26
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_response_type_for_grant_type_message(response_type, "implicit"))
-
end
-
when "none"
-
13
if grant_types.include?("implicit") || grant_types.include?("authorization_code")
-
13
register_throw_json_response_error("invalid_client_metadata", register_invalid_response_type_message(response_type))
-
end
-
end
-
end
-
-
13
def do_register(return_params = request.params.dup)
-
702
applications_ds = db[oauth_applications_table]
-
702
application_columns = applications_ds.columns
-
-
# set defaults
-
702
create_params = @oauth_application_params
-
-
# If omitted, an authorization server MAY register a client with a default set of scopes
-
702
create_params[oauth_applications_scopes_column] ||= return_params["scopes"] = oauth_application_scopes.join(" ")
-
-
# https://datatracker.ietf.org/doc/html/rfc7591#section-2
-
702
if create_params[oauth_applications_grant_types_column] ||= begin
-
# If omitted, the default behavior is that the client will use only the "authorization_code" Grant Type.
-
261
return_params["grant_types"] = %w[authorization_code] # rubocop:disable Lint/AssignmentInCondition
-
377
"authorization_code"
-
end
-
702
create_params[oauth_applications_token_endpoint_auth_method_column] ||= begin
-
# If unspecified or omitted, the default is "client_secret_basic", denoting the HTTP Basic
-
# authentication scheme as specified in Section 2.3.1 of OAuth 2.0.
-
270
return_params["token_endpoint_auth_method"] =
-
"client_secret_basic"
-
390
"client_secret_basic"
-
end
-
end
-
702
create_params[oauth_applications_response_types_column] ||= begin
-
# If omitted, the default is that the client will use only the "code" response type.
-
261
return_params["response_types"] = %w[code]
-
377
"code"
-
end
-
702
rescue_from_uniqueness_error do
-
702
initialize_register_params(create_params, return_params)
-
13715
create_params.delete_if { |k, _| !application_columns.include?(k) }
-
702
applications_ds.insert(create_params)
-
end
-
-
702
return_params
-
end
-
-
13
def initialize_register_params(create_params, return_params)
-
702
client_id = oauth_unique_id_generator
-
486
create_params[oauth_applications_client_id_column] = client_id
-
486
return_params["client_id"] = client_id
-
486
return_params["client_id_issued_at"] = Time.now.utc.iso8601
-
-
702
registration_access_token = oauth_unique_id_generator
-
486
create_params[oauth_applications_registration_access_token_column] = secret_hash(registration_access_token)
-
486
return_params["registration_access_token"] = registration_access_token
-
486
return_params["registration_client_uri"] = "#{base_url}/#{registration_client_uri_route}/#{return_params['client_id']}"
-
-
702
if create_params.key?(oauth_applications_client_secret_column)
-
13
set_client_secret(create_params, create_params[oauth_applications_client_secret_column])
-
13
return_params.delete("client_secret")
-
else
-
689
client_secret = oauth_unique_id_generator
-
689
set_client_secret(create_params, client_secret)
-
477
return_params["client_secret"] = client_secret
-
477
return_params["client_secret_expires_at"] = 0
-
-
end
-
end
-
-
13
def register_throw_json_response_error(code, message)
-
767
throw_json_response_error(oauth_invalid_response_status, code, message)
-
end
-
-
13
def register_required_param_message(key)
-
65
"The param '#{key}' is required by this server."
-
end
-
-
13
def register_invalid_param_message(key)
-
143
"The param '#{key}' is not supported by this server."
-
end
-
-
13
def register_invalid_client_metadata_message(key, value)
-
208
"The value '#{value}' is not supported by this server for param '#{key}'."
-
end
-
-
13
def register_invalid_contacts_message(contacts)
-
13
"The contacts '#{contacts}' are not allowed by this server."
-
end
-
-
13
def register_invalid_uri_message(uri)
-
234
"The '#{uri}' URL is not allowed by this server."
-
end
-
-
13
def register_invalid_jwks_param_message(key1, key2)
-
13
"The param '#{key1}' cannot be accepted together with param '#{key2}'."
-
end
-
-
13
def register_invalid_scopes_message(scopes)
-
26
"The given scopes (#{scopes}) are not allowed by this server."
-
end
-
-
13
def register_oauth_invalid_grant_type_message(grant_type)
-
"The grant type #{grant_type} is not allowed by this server."
-
end
-
-
13
def register_invalid_response_type_message(response_type)
-
26
"The response type #{response_type} is not allowed by this server."
-
end
-
-
13
def register_invalid_response_type_for_grant_type_message(response_type, grant_type)
-
39
"The grant type '#{grant_type}' must be registered for the response " \
-
12
"type '#{response_type}' to be allowed."
-
end
-
-
13
def convert_to_boolean(key, value)
-
108
case value
-
when true, false then value
-
78
when "true" then true
-
39
when "false" then false
-
else
-
39
register_throw_json_response_error(
-
"invalid_client_metadata",
-
register_invalid_param_message(key)
-
)
-
end
-
end
-
-
13
def json_response_oauth_application(oauth_application)
-
11712
params = methods.map { |k| k.to_s[/\Aoauth_applications_(\w+)_column\z/, 1] }.compact
-
-
26
body = params.each_with_object({}) do |k, hash|
-
598
next if %w[id account_id client_id client_secret cliennt_secret_hash].include?(k)
-
-
494
value = oauth_application[__send__(:"oauth_applications_#{k}_column")]
-
-
494
next unless value
-
-
126
case k
-
when "redirect_uri"
-
18
hash["redirect_uris"] = value.split(" ")
-
when "token_endpoint_auth_method", "grant_types", "response_types", "request_uris", "post_logout_redirect_uris"
-
hash[k] = value.split(" ")
-
when "scopes"
-
18
hash["scope"] = value
-
when "jwks"
-
hash[k] = value.is_a?(String) ? JSON.parse(value) : value
-
when "homepage_url"
-
18
hash["client_uri"] = value
-
when "name"
-
18
hash["client_name"] = value
-
else
-
54
hash[k] = value
-
end
-
end
-
-
26
response.status = 200
-
26
response["Content-Type"] ||= json_response_content_type
-
18
response["Cache-Control"] = "no-store"
-
18
response["Pragma"] = "no-cache"
-
26
json_payload = _json_response_body(body)
-
26
return_response(json_payload)
-
end
-
-
13
def oauth_server_metadata_body(*)
-
26
super.tap do |data|
-
18
data[:registration_endpoint] = register_url
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_grant_management, :OauthTokenManagement) do
-
13
depends :oauth_management_base, :oauth_token_revocation
-
-
13
view "oauth_grants", "My Oauth Grants", "oauth_grants"
-
-
13
button "Revoke", "oauth_grant_revoke"
-
-
13
auth_value_method :oauth_grants_path, "oauth-grants"
-
-
13
%w[type token refresh_token expires_in revoked_at].each do |param|
-
65
translatable_method :"oauth_grants_#{param}_label", param.gsub("_", " ").capitalize
-
end
-
13
translatable_method :oauth_no_grants_text, "No oauth grants yet!"
-
-
13
auth_value_method :oauth_grants_route, "oauth-grants"
-
13
auth_value_method :oauth_grants_id_pattern, Integer
-
13
auth_value_method :oauth_grants_per_page, 20
-
-
13
auth_methods(
-
:oauth_grant_path
-
)
-
-
13
def oauth_grants_path(opts = {})
-
515
route_path(oauth_grants_route, opts)
-
end
-
-
13
def oauth_grant_path(id)
-
206
"#{oauth_grants_path}/#{id}"
-
end
-
-
13
def load_oauth_grant_management_routes
-
114
request.on(oauth_grants_route) do
-
114
check_csrf if check_csrf?
-
114
require_account
-
-
114
request.post(oauth_grants_id_pattern) do |id|
-
12
db[oauth_grants_table]
-
.where(oauth_grants_id_column => id)
-
.where(oauth_grants_account_id_column => account_id)
-
1
.update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
-
-
13
set_notice_flash revoke_oauth_grant_notice_flash
-
13
redirect oauth_grants_path || "/"
-
end
-
-
101
request.is do
-
101
request.get do
-
101
page = Integer(param_or_nil("page") || 1)
-
101
per_page = per_page_param(oauth_grants_per_page)
-
-
101
scope.instance_variable_set(:@oauth_grants, db[oauth_grants_table]
-
.select(Sequel[oauth_grants_table].*, Sequel[oauth_applications_table][oauth_applications_name_column])
-
.join(oauth_applications_table, Sequel[oauth_grants_table][oauth_grants_oauth_application_id_column] =>
-
Sequel[oauth_applications_table][oauth_applications_id_column])
-
.where(Sequel[oauth_grants_table][oauth_grants_account_id_column] => account_id)
-
.where(oauth_grants_revoked_at_column => nil)
-
.order(Sequel.desc(oauth_grants_id_column))
-
.paginate(page, per_page))
-
101
oauth_grants_view
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_implicit_grant, :OauthImplicitGrant) do
-
13
depends :oauth_authorize_base
-
-
13
def oauth_grant_types_supported
-
2990
super | %w[implicit]
-
end
-
-
13
def oauth_response_types_supported
-
1521
super | %w[token]
-
end
-
-
13
def oauth_response_modes_supported
-
1690
super | %w[fragment]
-
end
-
-
13
private
-
-
13
def validate_authorize_params
-
1751
super
-
-
1647
response_mode = param_or_nil("response_mode")
-
-
1647
return unless response_mode
-
-
429
response_type = param_or_nil("response_type")
-
-
429
return unless response_type == "token"
-
-
78
redirect_response_error("invalid_request") unless oauth_response_modes_for_token_supported.include?(response_mode)
-
end
-
-
13
def oauth_response_modes_for_token_supported
-
78
%w[fragment]
-
end
-
-
13
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
-
689
response_type = param("response_type")
-
689
return super unless response_type == "token" && supported_response_type?(response_type)
-
-
52
response_mode ||= "fragment"
-
-
52
redirect_response_error("invalid_request") unless supported_response_mode?(response_mode)
-
-
52
oauth_grant = _do_authorize_token
-
-
52
response_params.replace(json_access_token_payload(oauth_grant))
-
-
52
response_params["state"] = param("state") if param_or_nil("state")
-
-
52
[response_params, response_mode]
-
end
-
-
13
def _do_authorize_token(grant_params = {})
-
25
grant_params = {
-
40
oauth_grants_type_column => "implicit",
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
-
oauth_grants_scopes_column => scopes,
-
**resource_owner_params
-
}.merge(grant_params)
-
-
65
generate_token(grant_params, false)
-
end
-
-
13
def _redirect_response_error(redirect_url, params)
-
260
response_types = param("response_type").split(/ +/)
-
-
260
return super if response_types.empty? || response_types == %w[code]
-
-
296
params = params.map { |k, v| "#{k}=#{v}" }
-
143
redirect_url.fragment = params.join("&")
-
143
redirect(redirect_url.to_s)
-
end
-
-
13
def authorize_response(params, mode)
-
585
return super unless mode == "fragment"
-
-
364
redirect_url = URI.parse(redirect_uri)
-
364
params = [URI.encode_www_form(params)]
-
364
params << redirect_url.query if redirect_url.query
-
364
redirect_url.fragment = params.join("&")
-
364
redirect(redirect_url.to_s)
-
end
-
-
13
def check_valid_response_type?
-
854
return true if param_or_nil("response_type") == "token"
-
-
711
super
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
13
require "rodauth/oauth/http_extensions"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_jwt, :OauthJwt) do
-
13
depends :oauth_jwt_base, :oauth_jwt_jwks
-
-
13
auth_value_method :oauth_jwt_access_tokens, true
-
-
13
auth_methods(
-
:jwt_claims,
-
:verify_access_token_headers
-
)
-
-
13
def require_oauth_authorization(*scopes)
-
273
return super unless oauth_jwt_access_tokens
-
-
273
authorization_required unless authorization_token
-
-
247
token_scopes = authorization_token["scope"].split(" ")
-
-
494
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
-
end
-
-
13
def oauth_token_subject
-
377
return super unless oauth_jwt_access_tokens
-
-
377
return unless authorization_token
-
-
377
authorization_token["sub"]
-
end
-
-
13
def current_oauth_account
-
182
subject = oauth_token_subject
-
-
182
return if subject == authorization_token["client_id"]
-
-
169
oauth_account_ds(subject).first
-
end
-
-
13
def current_oauth_application
-
204
db[oauth_applications_table].where(
-
oauth_applications_client_id_column => authorization_token["client_id"]
-
17
).first
-
end
-
-
13
private
-
-
13
def authorization_token
-
1872
return super unless oauth_jwt_access_tokens
-
-
1872
return @authorization_token if defined?(@authorization_token)
-
-
403
@authorization_token = decode_access_token
-
end
-
-
13
def verify_access_token_headers(headers)
-
390
headers["typ"] == "at+jwt"
-
end
-
-
13
def decode_access_token(access_token = fetch_access_token)
-
416
return unless access_token
-
-
403
jwt_claims = jwt_decode(access_token, verify_headers: method(:verify_access_token_headers))
-
-
403
return unless jwt_claims
-
-
390
return unless jwt_claims["sub"]
-
-
390
return unless jwt_claims["aud"]
-
-
390
jwt_claims
-
end
-
# /token
-
-
13
def create_token_from_token(_grant, update_params)
-
104
oauth_grant = super
-
-
104
if oauth_jwt_access_tokens
-
104
access_token = _generate_jwt_access_token(oauth_grant)
-
72
oauth_grant[oauth_grants_token_column] = access_token
-
end
-
104
oauth_grant
-
end
-
-
13
def generate_token(_grant_params = {}, should_generate_refresh_token = true)
-
624
oauth_grant = super
-
624
if oauth_jwt_access_tokens
-
611
access_token = _generate_jwt_access_token(oauth_grant)
-
423
oauth_grant[oauth_grants_token_column] = access_token
-
end
-
624
oauth_grant
-
end
-
-
13
def _generate_jwt_access_token(oauth_grant)
-
741
claims = jwt_claims(oauth_grant)
-
-
# one of the points of using jwt is avoiding database lookups, so we put here all relevant
-
# token data.
-
513
claims[:scope] = oauth_grant[oauth_grants_scopes_column]
-
-
# RFC8725 section 3.11: Use Explicit Typing
-
# RFC9068 section 2.1 : The "typ" value used SHOULD be "at+jwt".
-
741
jwt_encode(claims, headers: { typ: "at+jwt" })
-
end
-
-
13
def _generate_access_token(*)
-
728
super unless oauth_jwt_access_tokens
-
end
-
-
13
def jwt_claims(oauth_grant)
-
1352
issued_at = Time.now.to_i
-
-
520
{
-
832
iss: oauth_jwt_issuer, # issuer
-
iat: issued_at, # issued at
-
#
-
# sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of
-
# access tokens obtained through grants where a resource owner is
-
# involved, such as the authorization code grant, the value of "sub"
-
# SHOULD correspond to the subject identifier of the resource owner.
-
# In case of access tokens obtained through grants where no resource
-
# owner is involved, such as the client credentials grant, the value
-
# of "sub" SHOULD correspond to an identifier the authorization
-
# server uses to indicate the client application.
-
sub: jwt_subject(oauth_grant[oauth_grants_account_id_column]),
-
client_id: oauth_application[oauth_applications_client_id_column],
-
-
exp: issued_at + oauth_access_token_expires_in,
-
aud: oauth_jwt_audience
-
}
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
13
require "rodauth/oauth/http_extensions"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_jwt_base, :OauthJwtBase) do
-
13
depends :oauth_base
-
-
13
auth_value_method :oauth_application_jwt_public_key_param, "jwt_public_key"
-
13
auth_value_method :oauth_application_jwks_param, "jwks"
-
-
13
auth_value_method :oauth_jwt_keys, {}
-
13
auth_value_method :oauth_jwt_public_keys, {}
-
-
13
auth_value_method :oauth_jwt_jwe_keys, {}
-
13
auth_value_method :oauth_jwt_jwe_public_keys, {}
-
-
13
auth_value_method :oauth_jwt_jwe_copyright, nil
-
-
13
auth_methods(
-
:jwt_encode,
-
:jwt_decode,
-
:jwt_decode_no_key,
-
:generate_jti,
-
:oauth_jwt_issuer,
-
:oauth_jwt_audience,
-
:resource_owner_params_from_jwt_claims
-
)
-
-
13
private
-
-
13
def oauth_jwt_issuer
-
# The JWT MUST contain an "iss" (issuer) claim that contains a
-
# unique identifier for the entity that issued the JWT.
-
3064
@oauth_jwt_issuer ||= authorization_server_url
-
end
-
-
13
def oauth_jwt_audience
-
# The JWT MUST contain an "aud" (audience) claim containing a
-
# value that identifies the authorization server as an intended
-
# audience. The token endpoint URL of the authorization server
-
# MAY be used as a value for an "aud" element to identify the
-
# authorization server as an intended audience of the JWT.
-
1352
@oauth_jwt_audience ||= if is_authorization_server?
-
1040
oauth_application[oauth_applications_client_id_column]
-
else
-
metadata = authorization_server_metadata
-
-
return unless metadata
-
-
metadata[:token_endpoint]
-
end
-
end
-
-
13
def grant_from_application?(grant_or_claims, oauth_application)
-
117
return super if grant_or_claims[oauth_grants_id_column]
-
-
if grant_or_claims["client_id"]
-
grant_or_claims["client_id"] == oauth_application[oauth_applications_client_id_column]
-
else
-
Array(grant_or_claims["aud"]).include?(oauth_application[oauth_applications_client_id_column])
-
end
-
end
-
-
13
def jwt_subject(account_unique_id, client_application = oauth_application)
-
1391
(account_unique_id || client_application[oauth_applications_client_id_column]).to_s
-
end
-
-
13
def resource_owner_params_from_jwt_claims(claims)
-
117
{ oauth_grants_account_id_column => claims["sub"] }
-
end
-
-
13
def oauth_server_metadata_body(path = nil)
-
208
metadata = super
-
208
metadata.merge! \
-
token_endpoint_auth_signing_alg_values_supported: oauth_jwt_keys.keys.uniq
-
208
metadata
-
end
-
-
13
def _jwt_key
-
289
@_jwt_key ||= (oauth_application_jwks(oauth_application) if oauth_application)
-
end
-
-
# Resource Server only!
-
#
-
# returns the jwks set from the authorization server.
-
13
def auth_server_jwks_set
-
52
metadata = authorization_server_metadata
-
-
52
return unless metadata && (jwks_uri = metadata[:jwks_uri])
-
-
52
jwks_uri = URI(jwks_uri)
-
-
52
http_request_with_cache(jwks_uri)
-
end
-
-
13
def generate_jti(payload)
-
# Use the key and iat to create a unique key per request to prevent replay attacks
-
751
jti_raw = [
-
1188
payload[:aud] || payload["aud"],
-
payload[:iat] || payload["iat"]
-
].join(":").to_s
-
1939
Digest::SHA256.hexdigest(jti_raw)
-
end
-
-
13
def verify_jti(jti, claims)
-
340
generate_jti(claims) == jti
-
end
-
-
13
def verify_aud(expected_aud, aud)
-
707
expected_aud == aud
-
end
-
-
13
def oauth_application_jwks(oauth_application)
-
1472
jwks = oauth_application[oauth_applications_jwks_column]
-
-
1472
if jwks
-
679
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
-
470
return jwks
-
end
-
-
793
jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
-
-
793
return unless jwks_uri
-
-
26
jwks_uri = URI(jwks_uri)
-
-
26
http_request_with_cache(jwks_uri)
-
end
-
-
13
if defined?(JSON::JWT)
-
# json-jwt
-
-
3
auth_value_method :oauth_jwt_jws_algorithms_supported, %w[
-
HS256 HS384 HS512
-
RS256 RS384 RS512
-
PS256 PS384 PS512
-
ES256 ES384 ES512 ES256K
-
]
-
3
auth_value_method :oauth_jwt_jwe_algorithms_supported, %w[
-
RSA1_5 RSA-OAEP dir A128KW A256KW
-
]
-
3
auth_value_method :oauth_jwt_jwe_encryption_methods_supported, %w[
-
A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
-
]
-
-
3
def key_to_jwk(key)
-
15
JSON::JWK.new(key)
-
end
-
-
3
def jwk_export(key)
-
12
key_to_jwk(key)
-
end
-
-
3
def jwk_import(jwk)
-
126
JSON::JWK.new(jwk)
-
end
-
-
3
def jwk_key(jwk)
-
60
jwk = jwk_import(jwk) unless jwk.is_a?(JSON::JWK)
-
60
jwk.to_key
-
end
-
-
3
def jwk_thumbprint(jwk)
-
66
jwk = jwk_import(jwk) if jwk.is_a?(Hash)
-
66
jwk.thumbprint
-
end
-
-
3
def private_jwk?(jwk)
-
63
%w[d p q dp dq qi].any?(&jwk.method(:key?))
-
end
-
-
3
def jwt_encode(payload,
-
jwks: nil,
-
headers: {},
-
encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
-
encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
-
jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm,
-
encryption_method]],
-
signing_algorithm: oauth_jwt_keys.keys.first)
-
246
payload[:jti] = generate_jti(payload)
-
369
jwt = JSON::JWT.new(payload)
-
-
369
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
-
369
key = key.first if key.is_a?(Array)
-
-
369
jwk = JSON::JWK.new(key || "")
-
-
# update headers
-
369
headers.each_key do |k|
-
330
if jwt.respond_to?(:"#{k}=")
-
330
jwt.send(:"#{k}=", headers[k])
-
330
headers.delete(k)
-
end
-
end
-
369
jwt.header.merge(headers) unless headers.empty?
-
-
369
jwt = jwt.sign(jwk, signing_algorithm)
-
-
369
return jwt.to_s unless encryption_algorithm && encryption_method
-
-
57
if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
-
18
jwk = JSON::JWK.new(jwk)
-
18
jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
-
18
jwe.to_s
-
3
elsif jwe_key
-
3
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
-
3
algorithm = encryption_algorithm.to_sym
-
3
meth = encryption_method.to_sym
-
3
jwt.encrypt(jwe_key, algorithm, meth)
-
else
-
jwt.to_s
-
end
-
end
-
-
3
def jwt_decode(
-
token,
-
jwks: nil,
-
jws_algorithm: oauth_jwt_public_keys.keys.first || oauth_jwt_keys.keys.first,
-
jws_key: oauth_jwt_keys[jws_algorithm] || _jwt_key,
-
jws_encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
-
jws_encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
-
jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_keys.values.first,
-
verify_claims: true,
-
verify_jti: true,
-
verify_iss: true,
-
verify_aud: true,
-
verify_headers: nil,
-
**
-
)
-
285
jws_key = jws_key.first if jws_key.is_a?(Array)
-
-
285
if jwe_key
-
9
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
-
9
token = JSON::JWT.decode(token, jwe_key).plain_text
-
end
-
-
285
claims = if is_authorization_server?
-
273
if jwks
-
72
jwks = jwks[:keys] if jwks.is_a?(Hash)
-
-
72
enc_algs = [jws_encryption_algorithm].compact
-
72
enc_meths = [jws_encryption_method].compact
-
-
150
sig_algs = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
-
72
sig_algs = sig_algs.compact.map(&:to_sym)
-
-
# JWKs may be set up without a KID, when there's a single one
-
72
if jwks.size == 1 && !jwks[0][:kid]
-
3
key = jwks[0]
-
3
jwk_key = JSON::JWK.new(key)
-
3
jws = JSON::JWT.decode(token, jwk_key)
-
else
-
69
jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
-
63
jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
-
end
-
66
jws
-
201
elsif jws_key
-
198
JSON::JWT.decode(token, jws_key)
-
else
-
3
JSON::JWT.decode(token, nil, jws_algorithm)
-
end
-
12
elsif (jwks = auth_server_jwks_set)
-
12
JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
-
end
-
-
273
now = Time.now
-
273
if verify_claims &&
-
170
(!claims[:exp] || Time.at(claims[:exp]) < now) &&
-
claims[:nbf] && Time.at(claims[:nbf]) < now &&
-
claims[:iat] && Time.at(claims[:iat]) < now &&
-
verify_iss && claims[:iss] != oauth_jwt_issuer &&
-
verify_aud && !verify_aud(claims[:aud], claims[:client_id]) &&
-
verify_jti && !verify_jti(claims[:jti], claims)
-
-
return
-
end
-
-
273
return if verify_headers && !verify_headers.call(claims.header)
-
-
273
claims
-
rescue JSON::JWT::Exception
-
12
nil
-
end
-
-
3
def jwt_decode_no_key(token)
-
93
jws = JSON::JWT.decode(token, :skip_verification)
-
93
[jws.to_h, jws.header]
-
end
-
10
elsif defined?(JWT)
-
# ruby-jwt
-
10
require "rodauth/oauth/jwe_extensions" if defined?(JWE)
-
-
10
auth_value_method :oauth_jwt_jws_algorithms_supported, %w[
-
HS256 HS384 HS512 HS512256
-
RS256 RS384 RS512
-
ED25519
-
ES256 ES384 ES512
-
PS256 PS384 PS512
-
]
-
-
10
if defined?(JWE)
-
10
auth_value_methods(
-
:oauth_jwt_jwe_algorithms_supported,
-
:oauth_jwt_jwe_encryption_methods_supported
-
)
-
-
10
def oauth_jwt_jwe_algorithms_supported
-
300
JWE::VALID_ALG
-
end
-
-
10
def oauth_jwt_jwe_encryption_methods_supported
-
290
JWE::VALID_ENC
-
end
-
else
-
auth_value_method :oauth_jwt_jwe_algorithms_supported, []
-
auth_value_method :oauth_jwt_jwe_encryption_methods_supported, []
-
end
-
-
10
def key_to_jwk(key)
-
50
JWT::JWK.new(key)
-
end
-
-
10
def jwk_export(key)
-
40
key_to_jwk(key).export
-
end
-
-
10
def jwk_import(jwk)
-
620
JWT::JWK.import(jwk)
-
end
-
-
10
def jwk_key(jwk)
-
200
jwk = jwk_import(jwk) unless jwk.is_a?(JWT::JWK)
-
200
jwk.keypair
-
end
-
-
10
def jwk_thumbprint(jwk)
-
220
jwk = jwk_import(jwk) if jwk.is_a?(Hash)
-
220
JWT::JWK::Thumbprint.new(jwk).generate
-
end
-
-
10
def private_jwk?(jwk)
-
210
jwk_import(jwk).private?
-
end
-
-
10
def jwt_encode(payload,
-
signing_algorithm: oauth_jwt_keys.keys.first,
-
headers: {}, **)
-
1230
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
-
1230
key = key.first if key.is_a?(Array)
-
-
861
case key
-
when OpenSSL::PKey::PKey
-
990
jwk = JWT::JWK.new(key)
-
693
headers[:kid] = jwk.kid
-
-
990
key = jwk.keypair
-
end
-
-
# @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
-
861
payload[:jti] = generate_jti(payload)
-
1230
JWT.encode(payload, key, signing_algorithm, headers)
-
end
-
-
10
if defined?(JWE)
-
10
def jwt_encode_with_jwe(
-
payload,
-
jwks: nil,
-
encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
-
encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
-
jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm, encryption_method]],
-
**args
-
)
-
1230
token = jwt_encode_without_jwe(payload, **args)
-
-
1230
return token unless encryption_algorithm && encryption_method
-
-
170
if jwks && jwks.any? { |k| k[:use] == "enc" }
-
60
JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
-
10
elsif jwe_key
-
10
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
-
7
params = {
-
3
zip: "DEF",
-
copyright: oauth_jwt_jwe_copyright
-
}
-
10
params[:enc] = encryption_method if encryption_method
-
10
params[:alg] = encryption_algorithm if encryption_algorithm
-
10
JWE.encrypt(token, jwe_key, **params)
-
else
-
token
-
end
-
end
-
-
10
alias_method :jwt_encode_without_jwe, :jwt_encode
-
10
alias_method :jwt_encode, :jwt_encode_with_jwe
-
end
-
-
10
def jwt_decode(
-
token,
-
jwks: nil,
-
jws_algorithm: oauth_jwt_public_keys.keys.first || oauth_jwt_keys.keys.first,
-
jws_key: oauth_jwt_keys[jws_algorithm] || _jwt_key,
-
verify_claims: true,
-
verify_jti: true,
-
verify_iss: true,
-
verify_aud: true,
-
verify_headers: nil
-
)
-
940
jws_key = jws_key.first if jws_key.is_a?(Array)
-
-
# verifying the JWT implies verifying:
-
#
-
# issuer: check that server generated the token
-
# aud: check the audience field (client is who he says he is)
-
# iat: check that the token didn't expire
-
#
-
# subject can't be verified automatically without having access to the account id,
-
# which we don't because that's the whole point.
-
#
-
940
verify_claims_params = if verify_claims
-
352
{
-
528
verify_iss: verify_iss,
-
iss: oauth_jwt_issuer,
-
# can't use stock aud verification, as it's dependent on the client application id
-
verify_aud: false,
-
880
verify_jti: (verify_jti ? method(:verify_jti) : false),
-
verify_iat: true
-
}
-
else
-
60
{}
-
end
-
-
# decode jwt
-
940
claims, headers = if is_authorization_server?
-
900
if jwks
-
230
jwks = jwks[:keys] if jwks.is_a?(Hash)
-
-
# JWKs may be set up without a KID, when there's a single one
-
230
if jwks.size == 1 && !jwks[0][:kid]
-
10
key = jwks[0]
-
10
algo = key[:alg]
-
10
key = JWT::JWK.import(key).keypair
-
10
JWT.decode(token, key, true, algorithms: [algo], **verify_claims_params)
-
else
-
480
algorithms = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
-
220
JWT.decode(token, nil, true, algorithms: algorithms, jwks: { keys: jwks }, **verify_claims_params)
-
end
-
670
elsif jws_key
-
660
JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params)
-
else
-
10
JWT.decode(token, jws_key, false, **verify_claims_params)
-
end
-
40
elsif (jwks = auth_server_jwks_set)
-
120
algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
-
40
JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params)
-
end
-
-
910
return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
-
-
910
return if verify_headers && !verify_headers.call(headers)
-
-
910
claims
-
rescue JWT::DecodeError, JWT::JWKError
-
30
nil
-
end
-
-
10
if defined?(JWE)
-
10
def jwt_decode_with_jwe(
-
token,
-
jwks: nil,
-
jws_encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
-
jws_encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
-
jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_keys.values.first,
-
**args
-
)
-
1240
token = if jwks && jwks.any? { |k| k[:use] == "enc" }
-
30
JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
-
920
elsif jwe_key
-
30
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
-
30
JWE.decrypt(token, jwe_key)
-
else
-
890
token
-
end
-
-
930
jwt_decode_without_jwe(token, jwks: jwks, **args)
-
rescue JWE::DecodeError => e
-
20
jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
-
end
-
-
10
alias_method :jwt_decode_without_jwe, :jwt_decode
-
10
alias_method :jwt_decode, :jwt_decode_with_jwe
-
end
-
-
10
def jwt_decode_no_key(token)
-
310
JWT.decode(token, nil, false)
-
end
-
else
-
skipped
# :nocov:
-
skipped
def jwk_export(_key)
-
skipped
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
-
skipped
end
-
skipped
-
skipped
def jwk_import(_jwk)
-
skipped
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
-
skipped
end
-
skipped
-
skipped
def jwk_thumbprint(_jwk)
-
skipped
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
-
skipped
end
-
skipped
-
skipped
def jwt_encode(_token)
-
skipped
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
-
skipped
end
-
skipped
-
skipped
def jwt_decode(_token, **)
-
skipped
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
-
skipped
end
-
skipped
-
skipped
def private_jwk?(_jwk)
-
skipped
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
-
skipped
end
-
skipped
# :nocov:
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_jwt_bearer_grant, :OauthJwtBearerGrant) do
-
13
depends :oauth_assertion_base, :oauth_jwt
-
-
13
auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
-
-
13
auth_methods(
-
:require_oauth_application_from_jwt_bearer_assertion_issuer,
-
:require_oauth_application_from_jwt_bearer_assertion_subject,
-
:account_from_jwt_bearer_assertion
-
)
-
-
13
def oauth_token_endpoint_auth_methods_supported
-
39
if oauth_applications_client_secret_hash_column.nil?
-
13
super | %w[client_secret_jwt private_key_jwt urn:ietf:params:oauth:client-assertion-type:jwt-bearer]
-
else
-
26
super | %w[private_key_jwt]
-
end
-
end
-
-
13
def oauth_grant_types_supported
-
104
super | %w[urn:ietf:params:oauth:grant-type:jwt-bearer]
-
end
-
-
13
private
-
-
13
def require_oauth_application_from_jwt_bearer_assertion_issuer(assertion)
-
39
claims = jwt_assertion(assertion)
-
-
39
return unless claims
-
-
36
db[oauth_applications_table].where(
-
oauth_applications_client_id_column => claims["iss"]
-
3
).first
-
end
-
-
13
def require_oauth_application_from_jwt_bearer_assertion_subject(assertion)
-
104
claims, header = jwt_decode_no_key(assertion)
-
-
104
client_id = claims["sub"]
-
-
72
case header["alg"]
-
when "none"
-
# do not accept jwts with no alg set
-
13
authorization_required
-
when /\AHS/
-
39
require_oauth_application_from_client_secret_jwt(client_id, assertion, header["alg"])
-
else
-
52
require_oauth_application_from_private_key_jwt(client_id, assertion)
-
end
-
end
-
-
13
def require_oauth_application_from_client_secret_jwt(client_id, assertion, alg)
-
39
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
-
39
authorization_required unless oauth_application && supports_auth_method?(oauth_application, "client_secret_jwt")
-
26
client_secret = oauth_application[oauth_applications_client_secret_column]
-
26
claims = jwt_assertion(assertion, jws_key: client_secret, jws_algorithm: alg)
-
26
authorization_required unless claims && claims["iss"] == client_id
-
26
oauth_application
-
end
-
-
13
def require_oauth_application_from_private_key_jwt(client_id, assertion)
-
52
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
-
52
authorization_required unless oauth_application && supports_auth_method?(oauth_application, "private_key_jwt")
-
39
jwks = oauth_application_jwks(oauth_application)
-
39
claims = jwt_assertion(assertion, jwks: jwks)
-
39
authorization_required unless claims
-
39
oauth_application
-
end
-
-
13
def account_from_jwt_bearer_assertion(assertion)
-
39
claims = jwt_assertion(assertion)
-
-
39
return unless claims
-
-
39
account_from_bearer_assertion_subject(claims["sub"])
-
end
-
-
13
def jwt_assertion(assertion, **kwargs)
-
143
claims = jwt_decode(assertion, verify_iss: false, verify_aud: false, verify_jti: false, **kwargs)
-
-
143
return unless claims && verify_aud(request.url, claims["aud"])
-
-
143
claims
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
13
require "rodauth/oauth/http_extensions"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_jwt_jwks, :OauthJwtJwks) do
-
13
depends :oauth_jwt_base
-
-
13
auth_methods(:jwks_set)
-
-
13
auth_server_route(:jwks) do |r|
-
39
before_jwks_route
-
-
39
r.get do
-
39
json_response_success({ keys: jwks_set }, true)
-
end
-
end
-
-
13
private
-
-
13
def oauth_server_metadata_body(path = nil)
-
208
metadata = super
-
208
metadata.merge!(jwks_uri: jwks_url)
-
208
metadata
-
end
-
-
13
def jwks_set
-
39
@jwks_set ||= [
-
*(
-
39
unless oauth_jwt_public_keys.empty?
-
78
oauth_jwt_public_keys.flat_map { |algo, pkeys| Array(pkeys).map { |pkey| jwk_export(pkey).merge(use: "sig", alg: algo) } }
-
end
-
),
-
*(
-
39
unless oauth_jwt_jwe_public_keys.empty?
-
13
oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
-
13
Array(pkeys).map do |pkey|
-
13
jwk_export(pkey).merge(use: "enc", alg: algo)
-
end
-
end
-
end
-
)
-
].compact
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_jwt_secured_authorization_request, :OauthJwtSecuredAuthorizationRequest) do
-
13
ALLOWED_REQUEST_URI_CONTENT_TYPES = %w[application/jose application/oauth-authz-req+jwt].freeze
-
-
13
depends :oauth_authorize_base, :oauth_jwt_base
-
-
13
auth_value_method :oauth_require_request_uri_registration, false
-
13
auth_value_method :oauth_require_signed_request_object, false
-
13
auth_value_method :oauth_request_object_signing_alg_allow_none, false
-
-
12
%i[
-
request_uris require_signed_request_object request_object_signing_alg
-
request_object_encryption_alg request_object_encryption_enc
-
1
].each do |column|
-
65
auth_value_method :"oauth_applications_#{column}_column", column
-
end
-
-
13
translatable_method :oauth_invalid_request_object_message, "request object is invalid"
-
-
13
auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
-
-
13
private
-
-
# /authorize
-
-
13
def validate_authorize_params
-
585
request_object = param_or_nil("request")
-
-
585
request_uri = param_or_nil("request_uri")
-
-
585
unless (request_object || request_uri) && oauth_application
-
143
if request.path == authorize_path && request.get? && require_signed_request_object?
-
13
redirect_response_error("invalid_request_object")
-
end
-
-
90
return super
-
end
-
-
442
if request_uri
-
117
request_uri = CGI.unescape(request_uri)
-
-
117
redirect_response_error("invalid_request_uri") unless supported_request_uri?(request_uri, oauth_application)
-
-
65
response = http_request(request_uri)
-
-
65
unless response.code.to_i == 200 && ALLOWED_REQUEST_URI_CONTENT_TYPES.include?(response["content-type"])
-
13
redirect_response_error("invalid_request_uri")
-
end
-
-
52
request_object = response.body
-
end
-
-
377
claims = decode_request_object(request_object)
-
-
247
redirect_response_error("invalid_request_object") unless claims
-
-
247
if (iss = claims["iss"]) && (iss != oauth_application[oauth_applications_client_id_column])
-
13
redirect_response_error("invalid_request_object")
-
end
-
-
234
if (aud = claims["aud"]) && !verify_aud(aud, oauth_jwt_issuer)
-
13
redirect_response_error("invalid_request_object")
-
end
-
-
# If signed, the Authorization Request
-
# Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience)
-
# as members, with their semantics being the same as defined in the JWT
-
# [RFC7519] specification. The value of "aud" should be the value of
-
# the Authorization Server (AS) "issuer" as defined in RFC8414
-
# [RFC8414].
-
221
claims.delete("iss")
-
221
audience = claims.delete("aud")
-
-
221
redirect_response_error("invalid_request_object") if audience && audience != oauth_jwt_issuer
-
-
221
claims.each do |k, v|
-
945
request.params[k.to_s] = v
-
end
-
-
221
super
-
end
-
-
13
def supported_request_uri?(request_uri, oauth_application)
-
117
return false unless check_valid_uri?(request_uri)
-
-
91
request_uris = oauth_application[oauth_applications_request_uris_column]
-
-
156
request_uris.nil? || request_uris.split(oauth_scope_separator).one? { |uri| request_uri.start_with?(uri) }
-
end
-
-
13
def require_signed_request_object?
-
65
return @require_signed_request_object if defined?(@require_signed_request_object)
-
-
52
@require_signed_request_object = (oauth_application[oauth_applications_require_signed_request_object_column] if oauth_application)
-
52
@require_signed_request_object = oauth_require_signed_request_object if @require_signed_request_object.nil?
-
52
@require_signed_request_object
-
end
-
-
13
def decode_request_object(request_object)
-
150
request_sig_enc_opts = {
-
240
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
-
jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
-
jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
-
}.compact
-
-
390
request_sig_enc_opts[:jws_algorithm] ||= "none" if oauth_request_object_signing_alg_allow_none
-
-
390
if request_sig_enc_opts[:jws_algorithm] == "none"
-
39
redirect_response_error("invalid_request_object") if require_signed_request_object?
-
-
13
jwks = nil
-
351
elsif (jwks = oauth_application_jwks(oauth_application))
-
273
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
-
else
-
78
redirect_response_error("invalid_request_object")
-
end
-
-
286
claims = jwt_decode(request_object,
-
jwks: jwks,
-
verify_jti: false,
-
verify_iss: false,
-
verify_aud: false,
-
**request_sig_enc_opts)
-
-
286
redirect_response_error("invalid_request_object") unless claims
-
-
260
claims
-
end
-
-
13
def oauth_server_metadata_body(*)
-
39
super.tap do |data|
-
27
data[:request_parameter_supported] = true
-
27
data[:request_uri_parameter_supported] = true
-
27
data[:require_request_uri_registration] = oauth_require_request_uri_registration
-
27
data[:require_signed_request_object] = oauth_require_signed_request_object
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_jwt_secured_authorization_response_mode, :OauthJwtSecuredAuthorizationResponseMode) do
-
13
depends :oauth_authorize_base, :oauth_jwt_base
-
-
13
auth_value_method :oauth_authorization_response_mode_expires_in, 60 * 5 # 5 minutes
-
-
13
auth_value_method :oauth_applications_authorization_signed_response_alg_column, :authorization_signed_response_alg
-
13
auth_value_method :oauth_applications_authorization_encrypted_response_alg_column, :authorization_encrypted_response_alg
-
13
auth_value_method :oauth_applications_authorization_encrypted_response_enc_column, :authorization_encrypted_response_enc
-
-
13
auth_value_methods(
-
:authorization_signing_alg_values_supported,
-
:authorization_encryption_alg_values_supported,
-
:authorization_encryption_enc_values_supported
-
)
-
-
13
def oauth_response_modes_supported
-
559
jwt_response_modes = %w[jwt]
-
559
jwt_response_modes.push("query.jwt", "form_post.jwt") if features.include?(:oauth_authorization_code_grant)
-
559
jwt_response_modes << "fragment.jwt" if features.include?(:oauth_implicit_grant)
-
-
559
super | jwt_response_modes
-
end
-
-
13
def authorization_signing_alg_values_supported
-
13
oauth_jwt_jws_algorithms_supported
-
end
-
-
13
def authorization_encryption_alg_values_supported
-
26
oauth_jwt_jwe_algorithms_supported
-
end
-
-
13
def authorization_encryption_enc_values_supported
-
26
oauth_jwt_jwe_encryption_methods_supported
-
end
-
-
13
private
-
-
13
def oauth_response_modes_for_code_supported
-
156
return [] unless features.include?(:oauth_authorization_code_grant)
-
-
156
super | %w[query.jwt form_post.jwt jwt]
-
end
-
-
13
def oauth_response_modes_for_token_supported
-
65
return [] unless features.include?(:oauth_implicit_grant)
-
-
65
super | %w[fragment.jwt jwt]
-
end
-
-
13
def authorize_response(params, mode)
-
130
return super unless mode.end_with?("jwt")
-
-
130
response_type = param_or_nil("response_type")
-
-
130
redirect_url = URI.parse(redirect_uri)
-
-
130
jwt = jwt_encode_authorization_response_mode(params)
-
-
130
if mode == "query.jwt" || (mode == "jwt" && response_type == "code")
-
65
return super unless features.include?(:oauth_authorization_code_grant)
-
-
65
params = ["response=#{CGI.escape(jwt)}"]
-
65
params << redirect_url.query if redirect_url.query
-
65
redirect_url.query = params.join("&")
-
65
redirect(redirect_url.to_s)
-
65
elsif mode == "form_post.jwt"
-
13
return super unless features.include?(:oauth_authorization_code_grant)
-
-
9
response["Content-Type"] = "text/html"
-
13
body = form_post_response_html(redirect_url) do
-
13
"<input type=\"hidden\" name=\"response\" value=\"#{scope.h(jwt)}\" />"
-
end
-
13
response.write(body)
-
13
request.halt
-
52
elsif mode == "fragment.jwt" || (mode == "jwt" && response_type == "token")
-
52
return super unless features.include?(:oauth_implicit_grant)
-
-
52
params = ["response=#{CGI.escape(jwt)}"]
-
52
params << redirect_url.query if redirect_url.query
-
52
redirect_url.fragment = params.join("&")
-
52
redirect(redirect_url.to_s)
-
else
-
super
-
end
-
end
-
-
13
def _redirect_response_error(redirect_url, params)
-
39
response_mode = param_or_nil("response_mode")
-
39
return super unless response_mode.end_with?("jwt")
-
-
39
authorize_response(Hash[params], response_mode)
-
end
-
-
13
def jwt_encode_authorization_response_mode(params)
-
130
now = Time.now.to_i
-
50
claims = {
-
80
iss: oauth_jwt_issuer,
-
aud: oauth_application[oauth_applications_client_id_column],
-
exp: now + oauth_authorization_response_mode_expires_in,
-
iat: now
-
}.merge(params)
-
-
50
encode_params = {
-
80
jwks: oauth_application_jwks(oauth_application),
-
signing_algorithm: oauth_application[oauth_applications_authorization_signed_response_alg_column],
-
encryption_algorithm: oauth_application[oauth_applications_authorization_encrypted_response_alg_column],
-
encryption_method: oauth_application[oauth_applications_authorization_encrypted_response_enc_column]
-
}.compact
-
-
130
jwt_encode(claims, **encode_params)
-
end
-
-
13
def oauth_server_metadata_body(*)
-
26
super.tap do |data|
-
18
data[:authorization_signing_alg_values_supported] = authorization_signing_alg_values_supported
-
18
data[:authorization_encryption_alg_values_supported] = authorization_encryption_alg_values_supported
-
18
data[:authorization_encryption_enc_values_supported] = authorization_encryption_enc_values_supported
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_management_base, :OauthManagementBase) do
-
13
depends :oauth_authorize_base
-
-
13
button "Previous", "oauth_management_pagination_previous"
-
13
button "Next", "oauth_management_pagination_next"
-
-
13
def oauth_management_pagination_links(paginated_ds)
-
169
html = +'<nav aria-label="Pagination"><ul class="pagination">'
-
169
html << oauth_management_pagination_link(paginated_ds.prev_page, label: oauth_management_pagination_previous_button)
-
169
html << oauth_management_pagination_link(paginated_ds.current_page - 1) unless paginated_ds.first_page?
-
169
html << oauth_management_pagination_link(paginated_ds.current_page, label: paginated_ds.current_page, current: true)
-
169
html << oauth_management_pagination_link(paginated_ds.current_page + 1) unless paginated_ds.last_page?
-
169
html << oauth_management_pagination_link(paginated_ds.next_page, label: oauth_management_pagination_next_button)
-
169
html << "</ul></nav>"
-
end
-
-
13
def oauth_management_pagination_link(page, label: page, current: false, classes: "")
-
573
classes += " disabled" if current || !page
-
573
classes += " active" if current
-
573
if page
-
301
params = URI.encode_www_form(request.GET.merge("page" => page))
-
-
301
href = "#{request.path}?#{params}"
-
-
185
<<-HTML
-
160
<li class="page-item #{classes}" #{'aria-current="page"' if current}>
-
116
<a class="page-link" href="#{href}" tabindex="-1" aria-disabled="#{current || !page}">
-
116
#{label}
-
</a>
-
</li>
-
HTML
-
else
-
160
<<-HTML
-
112
<li class="page-item #{classes}">
-
<span class="page-link">
-
112
#{label}
-
112
#{'<span class="sr-only">(current)</span>' if current}
-
</span>
-
</li>
-
HTML
-
end
-
end
-
-
13
def post_configure
-
100
super
-
-
# TODO: remove this in v1, when resource-server mode does not load all of the provider features.
-
100
return unless db
-
-
100
db.extension :pagination
-
end
-
-
13
private
-
-
13
def per_page_param(default_per_page)
-
257
per_page = param_or_nil("per_page")
-
-
257
return default_per_page unless per_page
-
-
66
per_page = per_page.to_i
-
-
66
return default_per_page if per_page <= 0
-
-
66
[per_page, default_per_page].min
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_pkce, :OauthPkce) do
-
13
depends :oauth_authorization_code_grant
-
-
13
auth_value_method :oauth_require_pkce, true
-
13
auth_value_method :oauth_pkce_challenge_method, "S256"
-
-
13
auth_value_method :oauth_grants_code_challenge_column, :code_challenge
-
13
auth_value_method :oauth_grants_code_challenge_method_column, :code_challenge_method
-
-
13
auth_value_method :oauth_code_challenge_required_error_code, "invalid_request"
-
13
translatable_method :oauth_code_challenge_required_message, "code challenge required"
-
13
auth_value_method :oauth_unsupported_transform_algorithm_error_code, "invalid_request"
-
13
translatable_method :oauth_unsupported_transform_algorithm_message, "transform algorithm not supported"
-
-
13
private
-
-
13
def supports_auth_method?(oauth_application, auth_method)
-
104
return super unless auth_method == "none"
-
-
78
request.params.key?("code_verifier") || super
-
end
-
-
13
def validate_authorize_params
-
78
validate_pkce_challenge_params
-
-
65
super
-
end
-
-
13
def create_oauth_grant(create_params = {})
-
# PKCE flow
-
26
if (code_challenge = param_or_nil("code_challenge"))
-
26
code_challenge_method = param_or_nil("code_challenge_method") || oauth_pkce_challenge_method
-
-
18
create_params[oauth_grants_code_challenge_column] = code_challenge
-
18
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
-
end
-
-
26
super
-
end
-
-
13
def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
-
104
oauth_grant ||= valid_locked_oauth_grant(grant_params)
-
-
91
if oauth_grant[oauth_grants_code_challenge_column]
-
78
code_verifier = param_or_nil("code_verifier")
-
-
78
redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
-
13
elsif oauth_require_pkce
-
13
redirect_response_error("code_challenge_required")
-
end
-
-
39
super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
-
end
-
-
13
def validate_pkce_challenge_params
-
78
if param_or_nil("code_challenge")
-
-
52
challenge_method = param_or_nil("code_challenge_method")
-
52
redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
-
else
-
26
return unless oauth_require_pkce
-
-
13
redirect_response_error("code_challenge_required")
-
end
-
end
-
-
13
def check_valid_grant_challenge?(grant, verifier)
-
65
challenge = grant[oauth_grants_code_challenge_column]
-
-
45
case grant[oauth_grants_code_challenge_method_column]
-
when "plain"
-
13
challenge == verifier
-
when "S256"
-
39
generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
-
-
39
challenge == generated_challenge
-
else
-
13
redirect_response_error("unsupported_transform_algorithm")
-
end
-
end
-
-
13
def oauth_server_metadata_body(*)
-
13
super.tap do |data|
-
9
data[:code_challenge_methods_supported] = oauth_pkce_challenge_method
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_pushed_authorization_request, :OauthJwtPushedAuthorizationRequest) do
-
13
depends :oauth_authorize_base
-
-
13
auth_value_method :oauth_require_pushed_authorization_requests, false
-
13
auth_value_method :oauth_applications_require_pushed_authorization_requests_column, :require_pushed_authorization_requests
-
13
auth_value_method :oauth_pushed_authorization_request_expires_in, 90 # 90 seconds
-
13
auth_value_method :oauth_require_pushed_authorization_request_iss_request_object, true
-
-
13
auth_value_method :oauth_pushed_authorization_requests_table, :oauth_pushed_requests
-
-
12
%i[
-
oauth_application_id params code expires_in
-
1
].each do |column|
-
52
auth_value_method :"oauth_pushed_authorization_requests_#{column}_column", column
-
end
-
-
# /par
-
13
auth_server_route(:par) do |r|
-
104
require_oauth_application
-
91
before_par_route
-
-
91
r.post do
-
91
validate_par_params
-
-
65
ds = db[oauth_pushed_authorization_requests_table]
-
-
65
code = oauth_unique_id_generator
-
25
push_request_params = {
-
40
oauth_pushed_authorization_requests_oauth_application_id_column => oauth_application[oauth_applications_id_column],
-
oauth_pushed_authorization_requests_code_column => code,
-
oauth_pushed_authorization_requests_params_column => URI.encode_www_form(request.params),
-
oauth_pushed_authorization_requests_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
-
seconds: oauth_pushed_authorization_request_expires_in)
-
}
-
-
65
rescue_from_uniqueness_error do
-
65
ds.insert(push_request_params)
-
end
-
-
65
json_response_success(
-
20
"request_uri" => "urn:ietf:params:oauth:request_uri:#{code}",
-
"expires_in" => oauth_pushed_authorization_request_expires_in
-
)
-
end
-
end
-
-
13
def check_csrf?
-
414
case request.path
-
when par_path
-
104
false
-
else
-
494
super
-
end
-
end
-
-
13
private
-
-
13
def validate_par_params
-
# https://datatracker.ietf.org/doc/html/rfc9126#section-2.1
-
# The request_uri authorization request parameter is one exception, and it MUST NOT be provided.
-
91
redirect_response_error("invalid_request") if param_or_nil("request_uri")
-
-
78
if features.include?(:oauth_jwt_secured_authorization_request)
-
-
13
if (request_object = param_or_nil("request"))
-
13
claims = decode_request_object(request_object)
-
-
# https://datatracker.ietf.org/doc/html/rfc9126#section-3-5.3
-
# reject the request if the authenticated client_id does not match the client_id claim in the Request Object
-
13
if (client_id = claims["client_id"]) && (client_id != oauth_application[oauth_applications_client_id_column])
-
redirect_response_error("invalid_request_object")
-
end
-
-
# requiring the iss claim to match the client_id is at the discretion of the authorization server
-
13
if oauth_require_pushed_authorization_request_iss_request_object &&
-
13
(iss = claims.delete("iss")) &&
-
iss != oauth_application[oauth_applications_client_id_column]
-
redirect_response_error("invalid_request_object")
-
end
-
-
13
if (aud = claims.delete("aud")) && !verify_aud(aud, oauth_jwt_issuer)
-
redirect_response_error("invalid_request_object")
-
end
-
-
13
claims.delete("exp")
-
13
request.params.delete("request")
-
-
13
claims.each do |k, v|
-
54
request.params[k.to_s] = v
-
end
-
elsif require_signed_request_object?
-
redirect_response_error("invalid_request_object")
-
end
-
end
-
-
78
validate_authorize_params
-
end
-
-
13
def validate_authorize_params
-
247
return super unless request.get? && request.path == authorize_path
-
-
117
if (request_uri = param_or_nil("request_uri"))
-
65
code = request_uri.delete_prefix("urn:ietf:params:oauth:request_uri:")
-
-
65
table = oauth_pushed_authorization_requests_table
-
65
ds = db[table]
-
-
65
pushed_request = ds.where(
-
oauth_pushed_authorization_requests_oauth_application_id_column => oauth_application[oauth_applications_id_column],
-
oauth_pushed_authorization_requests_code_column => code
-
).where(
-
Sequel.expr(Sequel[table][oauth_pushed_authorization_requests_expires_in_column]) >= Sequel::CURRENT_TIMESTAMP
-
).first
-
-
65
redirect_response_error("invalid_request") unless pushed_request
-
-
52
URI.decode_www_form(pushed_request[oauth_pushed_authorization_requests_params_column]).each do |k, v|
-
153
request.params[k.to_s] = v
-
end
-
-
52
request.params.delete("request_uri")
-
-
# we're removing the request_uri here, so the checkup for signed reqest has to be invalidated.
-
52
@require_signed_request_object = false
-
52
elsif oauth_require_pushed_authorization_requests ||
-
24
(oauth_application && oauth_application[oauth_applications_require_pushed_authorization_requests_column])
-
26
redirect_authorize_error("request_uri")
-
end
-
78
super
-
end
-
-
13
def oauth_server_metadata_body(*)
-
13
super.tap do |data|
-
9
data[:require_pushed_authorization_requests] = oauth_require_pushed_authorization_requests
-
9
data[:pushed_authorization_request_endpoint] = par_url
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_resource_indicators, :OauthResourceIndicators) do
-
13
depends :oauth_authorize_base
-
-
13
auth_value_method :oauth_grants_resource_column, :resource
-
-
13
def resource_indicators
-
520
return @resource_indicators if defined?(@resource_indicators)
-
-
130
resources = param_or_nil("resource")
-
-
130
return unless resources
-
-
130
if json_request? || param_or_nil("request") # signed request
-
26
resources = Array(resources)
-
else
-
104
query = if request.form_data?
-
65
request.body.rewind
-
65
request.body.read
-
else
-
39
request.query_string
-
end
-
# resource query param does not conform to rack parsing rules
-
104
resources = URI.decode_www_form(query).each_with_object([]) do |(k, v), memo|
-
555
memo << v if k == "resource"
-
end
-
end
-
-
130
@resource_indicators = resources
-
end
-
-
13
def require_oauth_authorization(*)
-
91
super
-
-
# done so to support token-in-grant-db, jwt, and resource-server mode
-
78
token_indicators = authorization_token[oauth_grants_resource_column] || authorization_token["resource"]
-
-
78
return unless token_indicators
-
-
65
token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
-
-
130
authorization_required unless token_indicators.any? { |resource| base_url.start_with?(resource) }
-
end
-
-
13
private
-
-
13
def validate_token_params
-
52
super
-
-
52
return unless resource_indicators
-
-
52
resource_indicators.each do |resource|
-
52
redirect_response_error("invalid_target") unless check_valid_no_fragment_uri?(resource)
-
end
-
end
-
-
13
def create_token_from_token(oauth_grant, update_params)
-
return super unless resource_indicators
-
-
grant_indicators = oauth_grant[oauth_grants_resource_column]
-
-
grant_indicators = grant_indicators.split(" ") if grant_indicators.is_a?(String)
-
-
redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
-
-
super(oauth_grant, update_params.merge(oauth_grants_resource_column => resource_indicators))
-
end
-
-
13
module IndicatorAuthorizationCodeGrant
-
13
private
-
-
13
def validate_authorize_params
-
78
super
-
-
78
return unless resource_indicators
-
-
78
resource_indicators.each do |resource|
-
78
redirect_response_error("invalid_target") unless check_valid_no_fragment_uri?(resource)
-
end
-
end
-
-
13
def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
-
52
return super unless resource_indicators
-
-
52
oauth_grant ||= valid_locked_oauth_grant(grant_params)
-
-
52
redirect_response_error("invalid_target") unless oauth_grant[oauth_grants_resource_column]
-
-
52
grant_indicators = oauth_grant[oauth_grants_resource_column]
-
-
52
grant_indicators = grant_indicators.split(" ") if grant_indicators.is_a?(String)
-
-
52
redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
-
-
# update ownership
-
39
if grant_indicators != resource_indicators
-
13
oauth_grant = __update_and_return__(
-
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column]),
-
oauth_grants_resource_column => resource_indicators
-
)
-
end
-
-
39
super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
-
end
-
-
13
def create_oauth_grant(create_params = {})
-
13
create_params[oauth_grants_resource_column] = resource_indicators.join(" ") if resource_indicators
-
-
13
super
-
end
-
end
-
-
13
module IndicatorIntrospection
-
13
def json_token_introspect_payload(grant)
-
13
return super unless grant && grant[oauth_grants_id_column]
-
-
13
payload = super
-
-
13
token_indicators = grant[oauth_grants_resource_column]
-
-
13
token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
-
-
9
payload[:aud] = token_indicators
-
-
13
payload
-
end
-
-
13
def introspection_request(*)
-
39
payload = super
-
-
39
payload[oauth_grants_resource_column] = payload["aud"] if payload["aud"]
-
-
39
payload
-
end
-
end
-
-
13
module IndicatorJwt
-
13
def jwt_claims(*)
-
13
return super unless resource_indicators
-
-
13
super.merge(aud: resource_indicators)
-
end
-
-
13
def jwt_decode(token, verify_aud: true, **args)
-
39
claims = super(token, verify_aud: false, **args)
-
-
39
return claims unless verify_aud
-
-
26
return unless claims["aud"] && claims["aud"].one? { |aud| request.url.starts_with?(aud) }
-
-
13
claims
-
end
-
end
-
-
13
def self.included(rodauth)
-
195
super
-
195
rodauth.send(:include, IndicatorAuthorizationCodeGrant) if rodauth.features.include?(:oauth_authorization_code_grant)
-
195
rodauth.send(:include, IndicatorIntrospection) if rodauth.features.include?(:oauth_token_introspection)
-
195
rodauth.send(:include, IndicatorJwt) if rodauth.features.include?(:oauth_jwt)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_resource_server, :OauthResourceServer) do
-
13
depends :oauth_token_introspection
-
-
13
auth_value_method :is_authorization_server?, false
-
-
13
auth_methods(
-
:before_introspection_request
-
)
-
-
13
def authorization_token
-
234
return @authorization_token if defined?(@authorization_token)
-
-
# check if there is a token
-
117
access_token = fetch_access_token
-
-
117
return unless access_token
-
-
# where in resource server, NOT the authorization server.
-
91
payload = introspection_request("access_token", access_token)
-
-
91
return unless payload["active"]
-
-
78
@authorization_token = payload
-
end
-
-
13
def require_oauth_authorization(*scopes)
-
117
authorization_required unless authorization_token
-
-
78
aux_scopes = authorization_token["scope"]
-
-
78
token_scopes = if aux_scopes
-
78
aux_scopes.split(oauth_scope_separator)
-
else
-
[]
-
end
-
-
156
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
-
end
-
-
13
private
-
-
13
def introspection_request(token_type_hint, token)
-
91
introspect_url = URI("#{authorization_server_url}#{introspect_path}")
-
-
91
response = http_request(introspect_url, { "token_type_hint" => token_type_hint, "token" => token }) do |request|
-
91
before_introspection_request(request)
-
end
-
-
91
JSON.parse(response.body)
-
end
-
-
13
def before_introspection_request(request); end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "onelogin/ruby-saml"
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_saml_bearer_grant, :OauthSamlBearerGrant) do
-
13
depends :oauth_assertion_base
-
-
13
auth_value_method :oauth_saml_name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
-
13
auth_value_method :oauth_saml_idp_cert_check_expiration, true
-
-
13
auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
-
-
13
auth_value_method :oauth_saml_settings_table, :oauth_saml_settings
-
12
%i[
-
id oauth_application_id
-
idp_cert idp_cert_fingerprint idp_cert_fingerprint_algorithm
-
name_identifier_format
-
issuer
-
audience
-
idp_cert_check_expiration
-
1
].each do |column|
-
117
auth_value_method :"oauth_saml_settings_#{column}_column", column
-
end
-
-
13
translatable_method :oauth_saml_assertion_not_base64_message, "SAML assertion must be in base64 format"
-
13
translatable_method :oauth_saml_assertion_single_issuer_message, "SAML assertion must have a single issuer"
-
13
translatable_method :oauth_saml_settings_not_found_message, "No SAML settings found for issuer"
-
-
13
auth_methods(
-
:require_oauth_application_from_saml2_bearer_assertion_issuer,
-
:require_oauth_application_from_saml2_bearer_assertion_subject,
-
:account_from_saml2_bearer_assertion
-
)
-
-
13
def oauth_grant_types_supported
-
26
super | %w[urn:ietf:params:oauth:grant-type:saml2-bearer]
-
end
-
-
13
private
-
-
13
def require_oauth_application_from_saml2_bearer_assertion_issuer(assertion)
-
13
parse_saml_assertion(assertion)
-
-
13
return unless @saml_settings
-
-
12
db[oauth_applications_table].where(
-
oauth_applications_id_column => @saml_settings[oauth_saml_settings_oauth_application_id_column]
-
1
).first
-
end
-
-
13
def require_oauth_application_from_saml2_bearer_assertion_subject(assertion)
-
13
parse_saml_assertion(assertion)
-
-
13
return unless @assertion
-
-
# 3.3.8 - For client authentication, the Subject MUST be the "client_id" of the OAuth client.
-
12
db[oauth_applications_table].where(
-
oauth_applications_client_id_column => @assertion.nameid
-
1
).first
-
end
-
-
13
def account_from_saml2_bearer_assertion(assertion)
-
13
parse_saml_assertion(assertion)
-
-
13
return unless @assertion
-
-
13
account_from_bearer_assertion_subject(@assertion.nameid)
-
end
-
-
13
def generate_saml_settings(saml_settings)
-
26
settings = OneLogin::RubySaml::Settings.new
-
-
# issuer
-
26
settings.idp_entity_id = saml_settings[oauth_saml_settings_issuer_column]
-
-
# audience
-
26
settings.sp_entity_id = saml_settings[oauth_saml_settings_audience_column] || token_url
-
-
# recipient
-
26
settings.assertion_consumer_service_url = token_url
-
-
26
settings.idp_cert = saml_settings[oauth_saml_settings_idp_cert_column]
-
26
settings.idp_cert_fingerprint = saml_settings[oauth_saml_settings_idp_cert_fingerprint_column]
-
26
settings.idp_cert_fingerprint_algorithm = saml_settings[oauth_saml_settings_idp_cert_fingerprint_algorithm_column]
-
-
26
if settings.idp_cert
-
26
check_idp_cert_expiration = saml_settings[oauth_saml_settings_idp_cert_check_expiration_column]
-
26
check_idp_cert_expiration = oauth_saml_idp_cert_check_expiration if check_idp_cert_expiration.nil?
-
18
settings.security[:check_idp_cert_expiration] = check_idp_cert_expiration
-
end
-
18
settings.security[:strict_audience_validation] = true
-
18
settings.security[:want_name_id] = true
-
-
26
settings.name_identifier_format = saml_settings[oauth_saml_settings_name_identifier_format_column] ||
-
oauth_saml_name_identifier_format
-
26
settings
-
end
-
-
# rubocop:disable Naming/MemoizedInstanceVariableName
-
13
def parse_saml_assertion(assertion)
-
39
return @assertion if defined?(@assertion)
-
-
26
response = OneLogin::RubySaml::Response.new(assertion)
-
-
# The SAML Assertion XML data MUST be encoded using base64url
-
26
redirect_response_error("invalid_grant", oauth_saml_assertion_not_base64_message) unless response.send(:base64_encoded?, assertion)
-
-
# 1. The Assertion's <Issuer> element MUST contain a unique identifier
-
# for the entity that issued the Assertion.
-
26
redirect_response_error("invalid_grant", oauth_saml_assertion_single_issuer_message) unless response.issuers.size == 1
-
-
26
@saml_settings = db[oauth_saml_settings_table].where(
-
oauth_saml_settings_issuer_column => response.issuers.first
-
).first
-
-
26
redirect_response_error("invalid_grant", oauth_saml_settings_not_found_message) unless @saml_settings
-
-
26
response.settings = generate_saml_settings(@saml_settings)
-
-
# 2. The Assertion MUST contain a <Conditions> element ...
-
# 3. he Assertion MUST have an expiry that limits the time window ...
-
# 4. The Assertion MUST have an expiry that limits the time window ...
-
# 5. The <Subject> element MUST contain at least one ...
-
# 6. The authorization server MUST reject the entire Assertion if the ...
-
# 7. If the Assertion issuer directly authenticated the subject, ...
-
26
redirect_response_error("invalid_grant", response.errors.join("; ")) unless response.is_valid?
-
-
26
@assertion = response
-
end
-
# rubocop:enable Naming/MemoizedInstanceVariableName
-
-
13
def oauth_server_metadata_body(*)
-
super.tap do |data|
-
data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:saml2-bearer"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "openssl"
-
13
require "ipaddr"
-
13
require "uri"
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_tls_client_auth, :OauthTlsClientAuth) do
-
13
depends :oauth_base
-
-
13
auth_value_method :oauth_tls_client_certificate_bound_access_tokens, false
-
-
12
%i[
-
tls_client_auth_subject_dn tls_client_auth_san_dns
-
tls_client_auth_san_uri tls_client_auth_san_ip
-
tls_client_auth_san_email tls_client_certificate_bound_access_tokens
-
1
].each do |column|
-
78
auth_value_method :"oauth_applications_#{column}_column", column
-
end
-
-
13
auth_value_method :oauth_grants_certificate_thumbprint_column, :certificate_thumbprint
-
-
13
def oauth_token_endpoint_auth_methods_supported
-
13
super | %w[tls_client_auth self_signed_tls_client_auth]
-
end
-
-
13
private
-
-
13
def validate_token_params
-
# For all requests to the authorization server utilizing mutual-TLS client authentication,
-
# the client MUST include the client_id parameter
-
260
redirect_response_error("invalid_request") if client_certificate && !param_or_nil("client_id")
-
-
260
super
-
end
-
-
13
def require_oauth_application
-
390
return super unless client_certificate
-
-
364
authorization_required unless oauth_application
-
-
364
if supports_auth_method?(oauth_application, "tls_client_auth")
-
# It relies on a validated certificate chain [RFC5280]
-
-
351
ssl_verify = request.env["SSL_CLIENT_VERIFY"] || request.env["HTTP_SSL_CLIENT_VERIFY"] || request.env["HTTP_X_SSL_CLIENT_VERIFY"]
-
-
351
authorization_required unless ssl_verify == "SUCCESS"
-
-
# and a single subject distinguished name (DN) or a single subject alternative name (SAN) to
-
# authenticate the client. Only one subject name value of any type is used for each client.
-
-
351
name_matches = if oauth_application[:tls_client_auth_subject_dn]
-
299
distinguished_name_match?(client_certificate.subject, oauth_application[:tls_client_auth_subject_dn])
-
52
elsif (dns = oauth_application[:tls_client_auth_san_dns])
-
26
client_certificate_sans.any? { |san| san.tag == 2 && OpenSSL::SSL.verify_hostname(dns, san.value) }
-
39
elsif (uri = oauth_application[:tls_client_auth_san_uri])
-
13
uri = URI(uri)
-
52
client_certificate_sans.any? { |san| san.tag == 6 && URI(san.value) == uri }
-
26
elsif (ip = oauth_application[:tls_client_auth_san_ip])
-
13
ip = IPAddr.new(ip).hton
-
39
client_certificate_sans.any? { |san| san.tag == 7 && san.value == ip }
-
13
elsif (email = oauth_application[:tls_client_auth_san_email])
-
65
client_certificate_sans.any? { |san| san.tag == 1 && san.value == email }
-
else
-
false
-
end
-
351
authorization_required unless name_matches
-
-
351
oauth_application
-
13
elsif supports_auth_method?(oauth_application, "self_signed_tls_client_auth")
-
13
jwks = oauth_application_jwks(oauth_application)
-
-
13
thumbprint = jwk_thumbprint(key_to_jwk(client_certificate.public_key))
-
-
# The client is successfully authenticated if the certificate that it presented during the handshake
-
# matches one of the certificates configured or registered for that particular client.
-
26
authorization_required unless jwks.any? { |jwk| Array(jwk[:x5c]).first == thumbprint }
-
-
13
oauth_application
-
else
-
super
-
end
-
rescue URI::InvalidURIError, IPAddr::InvalidAddressError
-
authorization_required
-
end
-
-
13
def store_token(grant_params, update_params = {})
-
187
return super unless client_certificate && (
-
88
oauth_tls_client_certificate_bound_access_tokens ||
-
oauth_application[oauth_applications_tls_client_certificate_bound_access_tokens_column]
-
)
-
-
update_params[oauth_grants_certificate_thumbprint_column] = jwk_thumbprint(key_to_jwk(client_certificate.public_key))
-
super
-
end
-
-
13
def jwt_claims(oauth_grant)
-
claims = super
-
-
return claims unless oauth_grant[oauth_grants_certificate_thumbprint_column]
-
-
claims[:cnf] = {
-
"x5t#S256" => oauth_grant[oauth_grants_certificate_thumbprint_column]
-
}
-
-
claims
-
end
-
-
13
def json_token_introspect_payload(grant_or_claims)
-
91
claims = super
-
-
91
return claims unless grant_or_claims && grant_or_claims[oauth_grants_certificate_thumbprint_column]
-
-
45
(claims[:cnf] ||= {})["x5t#S256"] = grant_or_claims[oauth_grants_certificate_thumbprint_column]
-
-
65
claims
-
end
-
-
13
def oauth_server_metadata_body(*)
-
13
super.tap do |data|
-
9
data[:tls_client_certificate_bound_access_tokens] = oauth_tls_client_certificate_bound_access_tokens
-
end
-
end
-
-
13
def client_certificate
-
1209
return @client_certificate if defined?(@client_certificate)
-
-
1209
unless (pem_cert = request.env["SSL_CLIENT_CERT"] || request.env["HTTP_SSL_CLIENT_CERT"] || request.env["HTTP_X_SSL_CLIENT_CERT"])
-
18
return
-
end
-
-
1183
return if pem_cert.empty?
-
-
1183
@certificate = OpenSSL::X509::Certificate.new(pem_cert)
-
end
-
-
13
def client_certificate_sans
-
52
return @client_certificate_sans if defined?(@client_certificate_sans)
-
-
20
@client_certificate_sans = begin
-
52
return [] unless client_certificate
-
-
260
san = client_certificate.extensions.find { |ext| ext.oid == "subjectAltName" }
-
-
52
return [] unless san
-
-
52
ostr = OpenSSL::ASN1.decode(san.to_der).value.last
-
-
52
sans = OpenSSL::ASN1.decode(ostr.value)
-
-
52
return [] unless sans
-
-
52
sans.value
-
end
-
end
-
-
13
def distinguished_name_match?(sub1, sub2)
-
299
sub1 = OpenSSL::X509::Name.parse(sub1) if sub1.is_a?(String)
-
299
sub2 = OpenSSL::X509::Name.parse(sub2) if sub2.is_a?(String)
-
# OpenSSL::X509::Name#cp calls X509_NAME_cmp via openssl.
-
# https://www.openssl.org/docs/manmaster/man3/X509_NAME_cmp.html
-
# This procedure adheres to the matching rules for Distinguished Names (DN) given in
-
# RFC 4517 section 4.2.15 and RFC 5280 section 7.1.
-
299
sub1.cmp(sub2).zero?
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
13
require "rodauth/oauth/http_extensions"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_token_introspection, :OauthTokenIntrospection) do
-
13
depends :oauth_base
-
-
13
before "introspect"
-
-
13
auth_methods(
-
:resource_owner_identifier
-
)
-
-
# /introspect
-
13
auth_server_route(:introspect) do |r|
-
299
require_oauth_application_for_introspect
-
299
before_introspect_route
-
-
299
r.post do
-
299
catch_error do
-
299
validate_introspect_params
-
-
247
token_type_hint = param_or_nil("token_type_hint")
-
-
247
before_introspect
-
247
oauth_grant = case token_type_hint
-
when "access_token", nil
-
221
if features.include?(:oauth_jwt) && oauth_jwt_access_tokens
-
52
jwt_decode(param("token"))
-
else
-
169
oauth_grant_by_token(param("token"))
-
end
-
when "refresh_token"
-
26
oauth_grant_by_refresh_token(param("token"))
-
end
-
-
247
oauth_grant ||= oauth_grant_by_refresh_token(param("token")) if token_type_hint.nil?
-
-
247
json_response_success(json_token_introspect_payload(oauth_grant))
-
end
-
-
throw_json_response_error(oauth_invalid_response_status, "invalid_request")
-
end
-
end
-
-
# Token introspect
-
-
13
def validate_introspect_params(token_hint_types = %w[access_token refresh_token].freeze)
-
# check if valid token hint type
-
299
if param_or_nil("token_type_hint") && !token_hint_types.include?(param("token_type_hint"))
-
26
redirect_response_error("unsupported_token_type")
-
end
-
-
273
redirect_response_error("invalid_request") unless param_or_nil("token")
-
end
-
-
13
def json_token_introspect_payload(grant_or_claims)
-
247
return { active: false } unless grant_or_claims
-
-
195
if grant_or_claims["sub"]
-
# JWT
-
36
{
-
16
active: true,
-
scope: grant_or_claims["scope"],
-
client_id: grant_or_claims["client_id"],
-
username: resource_owner_identifier(grant_or_claims),
-
token_type: oauth_token_type.capitalize,
-
exp: grant_or_claims["exp"],
-
iat: grant_or_claims["iat"],
-
nbf: grant_or_claims["nbf"],
-
sub: grant_or_claims["sub"],
-
aud: grant_or_claims["aud"],
-
iss: grant_or_claims["iss"],
-
jti: grant_or_claims["jti"]
-
}
-
else
-
99
{
-
44
active: true,
-
scope: grant_or_claims[oauth_grants_scopes_column],
-
client_id: oauth_application[oauth_applications_client_id_column],
-
username: resource_owner_identifier(grant_or_claims),
-
token_type: oauth_token_type,
-
exp: grant_or_claims[oauth_grants_expires_in_column].to_i
-
}
-
end
-
end
-
-
13
def check_csrf?
-
270
case request.path
-
when introspect_path
-
299
false
-
else
-
91
super
-
end
-
end
-
-
13
private
-
-
13
def require_oauth_application_for_introspect
-
299
token = (v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Bearer (.*)\Z/, 1]
-
-
299
return require_oauth_application unless token
-
-
39
oauth_application = current_oauth_application
-
-
39
authorization_required unless oauth_application
-
-
39
@oauth_application = oauth_application
-
end
-
-
13
def oauth_server_metadata_body(*)
-
26
super.tap do |data|
-
18
data[:introspection_endpoint] = introspect_url
-
18
data[:introspection_endpoint_auth_methods_supported] = %w[client_secret_basic]
-
end
-
end
-
-
13
def resource_owner_identifier(grant_or_claims)
-
195
if (account_id = grant_or_claims[oauth_grants_account_id_column])
-
117
account_ds(account_id).select(login_column).first[login_column]
-
78
elsif (app_id = grant_or_claims[oauth_grants_oauth_application_id_column])
-
24
db[oauth_applications_table].where(oauth_applications_id_column => app_id)
-
.select(oauth_applications_name_column)
-
2
.first[oauth_applications_name_column]
-
52
elsif (subject = grant_or_claims["sub"])
-
# JWT
-
52
if subject == grant_or_claims["client_id"]
-
12
db[oauth_applications_table].where(oauth_applications_client_id_column => subject)
-
.select(oauth_applications_name_column)
-
1
.first[oauth_applications_name_column]
-
else
-
39
account_ds(subject).select(login_column).first[login_column]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oauth_token_revocation, :OauthTokenRevocation) do
-
13
depends :oauth_base
-
-
13
before "revoke"
-
13
after "revoke"
-
-
13
notice_flash "The oauth grant has been revoked", "revoke_oauth_grant"
-
-
# /revoke
-
13
auth_server_route(:revoke) do |r|
-
130
if logged_in?
-
13
require_account
-
13
require_oauth_application_from_account
-
else
-
117
require_oauth_application
-
end
-
-
130
before_revoke_route
-
-
130
r.post do
-
130
catch_error do
-
130
validate_revoke_params
-
-
91
oauth_grant = nil
-
91
transaction do
-
91
before_revoke
-
91
oauth_grant = revoke_oauth_grant
-
52
after_revoke
-
end
-
-
52
if accepts_json?
-
15
json_payload = {
-
24
"revoked_at" => convert_timestamp(oauth_grant[oauth_grants_revoked_at_column])
-
}
-
39
if param("token_type_hint") == "refresh_token"
-
18
json_payload["refresh_token"] = oauth_grant[oauth_grants_refresh_token_column]
-
else
-
9
json_payload["token"] = oauth_grant[oauth_grants_token_column]
-
end
-
-
39
json_response_success json_payload
-
else
-
13
set_notice_flash revoke_oauth_grant_notice_flash
-
13
redirect request.referer || "/"
-
end
-
end
-
-
26
redirect_response_error("invalid_request")
-
end
-
end
-
-
13
def validate_revoke_params(token_hint_types = %w[access_token refresh_token].freeze)
-
130
token_hint = param_or_nil("token_type_hint")
-
-
130
if features.include?(:oauth_jwt) && oauth_jwt_access_tokens && (!token_hint || token_hint == "access_token")
-
# JWT access tokens can't be revoked
-
26
throw(:rodauth_error)
-
end
-
-
# check if valid token hint type
-
104
redirect_response_error("unsupported_token_type") if token_hint && !token_hint_types.include?(token_hint)
-
-
91
redirect_response_error("invalid_request") unless param_or_nil("token")
-
end
-
-
13
def check_csrf?
-
777
case request.path
-
when revoke_path
-
130
!json_request?
-
else
-
995
super
-
end
-
end
-
-
13
private
-
-
13
def revoke_oauth_grant
-
91
token = param("token")
-
-
91
if param("token_type_hint") == "refresh_token"
-
26
oauth_grant = oauth_grant_by_refresh_token(token)
-
26
token_column = oauth_grants_refresh_token_column
-
else
-
65
oauth_grant = oauth_grant_by_token_ds(token).where(
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
-
).first
-
65
token_column = oauth_grants_token_column
-
end
-
-
91
redirect_response_error("invalid_request") unless oauth_grant
-
-
52
redirect_response_error("invalid_request") unless grant_from_application?(oauth_grant, oauth_application)
-
-
52
update_params = { oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
-
-
52
ds = db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
-
-
52
oauth_grant = __update_and_return__(ds, update_params)
-
-
36
oauth_grant[token_column] = token
-
52
oauth_grant
-
-
# If the particular
-
# token is a refresh token and the authorization server supports the
-
# revocation of access tokens, then the authorization server SHOULD
-
# also invalidate all access tokens based on the same authorization
-
# grant
-
#
-
# we don't need to do anything here, as we revalidate existing tokens
-
end
-
-
13
def oauth_server_metadata_body(*)
-
26
super.tap do |data|
-
18
data[:revocation_endpoint] = revoke_url
-
18
data[:revocation_endpoint_auth_methods_supported] = nil # because it's client_secret_basic
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oidc, :Oidc) do
-
# https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
-
5
OIDC_SCOPES_MAP = {
-
8
"profile" => %i[name family_name given_name middle_name nickname preferred_username
-
profile picture website gender birthdate zoneinfo locale updated_at].freeze,
-
"email" => %i[email email_verified].freeze,
-
"address" => %i[formatted street_address locality region postal_code country].freeze,
-
"phone" => %i[phone_number phone_number_verified].freeze
-
}.freeze
-
-
13
VALID_METADATA_KEYS = %i[
-
issuer
-
authorization_endpoint
-
end_session_endpoint
-
backchannel_logout_session_supported
-
token_endpoint
-
userinfo_endpoint
-
jwks_uri
-
registration_endpoint
-
scopes_supported
-
response_types_supported
-
response_modes_supported
-
grant_types_supported
-
acr_values_supported
-
subject_types_supported
-
id_token_signing_alg_values_supported
-
id_token_encryption_alg_values_supported
-
id_token_encryption_enc_values_supported
-
userinfo_signing_alg_values_supported
-
userinfo_encryption_alg_values_supported
-
userinfo_encryption_enc_values_supported
-
request_object_signing_alg_values_supported
-
request_object_encryption_alg_values_supported
-
request_object_encryption_enc_values_supported
-
token_endpoint_auth_methods_supported
-
token_endpoint_auth_signing_alg_values_supported
-
display_values_supported
-
claim_types_supported
-
claims_supported
-
service_documentation
-
claims_locales_supported
-
ui_locales_supported
-
claims_parameter_supported
-
request_parameter_supported
-
request_uri_parameter_supported
-
require_request_uri_registration
-
op_policy_uri
-
op_tos_uri
-
check_session_iframe
-
frontchannel_logout_supported
-
frontchannel_logout_session_supported
-
backchannel_logout_supported
-
backchannel_logout_session_supported
-
].freeze
-
-
13
REQUIRED_METADATA_KEYS = %i[
-
issuer
-
authorization_endpoint
-
token_endpoint
-
jwks_uri
-
response_types_supported
-
subject_types_supported
-
id_token_signing_alg_values_supported
-
].freeze
-
-
13
depends :active_sessions, :oauth_jwt, :oauth_jwt_jwks, :oauth_authorization_code_grant, :oauth_implicit_grant
-
-
13
auth_value_method :oauth_application_scopes, %w[openid]
-
-
12
%i[
-
subject_type application_type sector_identifier_uri initiate_login_uri
-
id_token_signed_response_alg id_token_encrypted_response_alg id_token_encrypted_response_enc
-
userinfo_signed_response_alg userinfo_encrypted_response_alg userinfo_encrypted_response_enc
-
1
].each do |column|
-
130
auth_value_method :"oauth_applications_#{column}_column", column
-
end
-
-
13
%i[nonce acr claims_locales claims].each do |column|
-
52
auth_value_method :"oauth_grants_#{column}_column", column
-
end
-
-
13
auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
-
13
auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
-
-
13
translatable_method :oauth_invalid_scope_message, "The Access Token expired"
-
-
13
auth_value_method :oauth_prompt_login_cookie_key, "_rodauth_oauth_prompt_login"
-
13
auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
-
13
auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
-
-
13
auth_value_methods(
-
:userinfo_signing_alg_values_supported,
-
:userinfo_encryption_alg_values_supported,
-
:userinfo_encryption_enc_values_supported,
-
:request_object_signing_alg_values_supported,
-
:request_object_encryption_alg_values_supported,
-
:request_object_encryption_enc_values_supported,
-
:oauth_acr_values_supported
-
)
-
-
13
auth_methods(
-
:get_oidc_account_last_login_at,
-
:oidc_authorize_on_prompt_none?,
-
:fill_with_account_claims,
-
:get_oidc_param,
-
:get_additional_param,
-
:require_acr_value_phr,
-
:require_acr_value_phrh,
-
:require_acr_value,
-
:json_webfinger_payload
-
)
-
-
# /userinfo
-
13
auth_server_route(:userinfo) do |r|
-
117
r.on method: %i[get post] do
-
117
catch_error do
-
117
claims = authorization_token
-
-
117
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless claims
-
-
117
oauth_scopes = claims["scope"].split(" ")
-
-
117
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
-
-
117
account = account_ds(claims["sub"]).first
-
-
117
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless account
-
-
117
oauth_scopes.delete("openid")
-
-
117
oidc_claims = { "sub" => claims["sub"] }
-
-
117
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
-
-
117
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless @oauth_application
-
-
117
oauth_grant = valid_oauth_grant_ds(
-
oauth_grants_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
-
**resource_owner_params_from_jwt_claims(claims)
-
).first
-
-
117
claims_locales = oauth_grant[oauth_grants_claims_locales_column] if oauth_grant
-
-
117
if (claims = oauth_grant[oauth_grants_claims_column])
-
13
claims = JSON.parse(claims)
-
13
if (userinfo_essential_claims = claims["userinfo"])
-
9
oauth_scopes |= userinfo_essential_claims.to_a
-
end
-
end
-
-
# 5.4 - The Claims requested by the profile, email, address, and phone scope values are returned from the UserInfo Endpoint
-
117
fill_with_account_claims(oidc_claims, account, oauth_scopes, claims_locales)
-
-
117
if (algo = @oauth_application[oauth_applications_userinfo_signed_response_alg_column])
-
10
params = {
-
16
jwks: oauth_application_jwks(@oauth_application),
-
encryption_algorithm: @oauth_application[oauth_applications_userinfo_encrypted_response_alg_column],
-
encryption_method: @oauth_application[oauth_applications_userinfo_encrypted_response_enc_column]
-
}.compact
-
-
26
jwt = jwt_encode(
-
oidc_claims.merge(
-
# If signed, the UserInfo Response SHOULD contain the Claims iss (issuer) and aud (audience) as members. The iss value
-
# SHOULD be the OP's Issuer Identifier URL. The aud value SHOULD be or include the RP's Client ID value.
-
iss: oauth_jwt_issuer,
-
aud: @oauth_application[oauth_applications_client_id_column]
-
),
-
signing_algorithm: algo,
-
**params
-
)
-
26
jwt_response_success(jwt)
-
else
-
91
json_response_success(oidc_claims)
-
end
-
end
-
-
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token")
-
end
-
end
-
-
13
def load_openid_configuration_route(alt_issuer = nil)
-
104
request.on(".well-known/openid-configuration") do
-
104
allow_cors(request)
-
-
91
request.is do
-
91
request.get do
-
91
json_response_success(openid_configuration_body(alt_issuer), cache: true)
-
end
-
end
-
end
-
end
-
-
13
def load_webfinger_route
-
26
request.on(".well-known/webfinger") do
-
26
request.get do
-
26
resource = param_or_nil("resource")
-
-
26
throw_json_response_error(400, "invalid_request") unless resource
-
-
13
response.status = 200
-
13
response["Content-Type"] ||= "application/jrd+json"
-
-
13
return_response(json_webfinger_payload)
-
end
-
end
-
end
-
-
13
def check_csrf?
-
3654
case request.path
-
when userinfo_path
-
117
false
-
else
-
5141
super
-
end
-
end
-
-
13
def oauth_response_types_supported
-
1300
grant_types = oauth_grant_types_supported
-
1300
oidc_response_types = %w[id_token none]
-
1300
oidc_response_types |= ["code id_token"] if grant_types.include?("authorization_code")
-
1300
oidc_response_types |= ["code token", "id_token token", "code id_token token"] if grant_types.include?("implicit")
-
1300
super | oidc_response_types
-
end
-
-
13
def current_oauth_account
-
13
subject_type = current_oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
-
-
13
super unless subject_type == "pairwise"
-
end
-
-
13
private
-
-
13
if defined?(::I18n)
-
13
def before_authorize_route
-
1500
if (ui_locales = param_or_nil("ui_locales"))
-
13
ui_locales = ui_locales.split(" ").map(&:to_sym)
-
9
ui_locales &= ::I18n.available_locales
-
-
13
::I18n.locale = ui_locales.first unless ui_locales.empty?
-
end
-
-
1500
super
-
end
-
end
-
-
13
def userinfo_signing_alg_values_supported
-
oauth_jwt_jws_algorithms_supported
-
end
-
-
13
def userinfo_encryption_alg_values_supported
-
oauth_jwt_jwe_algorithms_supported
-
end
-
-
13
def userinfo_encryption_enc_values_supported
-
oauth_jwt_jwe_encryption_methods_supported
-
end
-
-
13
def request_object_signing_alg_values_supported
-
oauth_jwt_jws_algorithms_supported
-
end
-
-
13
def request_object_encryption_alg_values_supported
-
oauth_jwt_jwe_algorithms_supported
-
end
-
-
13
def request_object_encryption_enc_values_supported
-
oauth_jwt_jwe_encryption_methods_supported
-
end
-
-
13
def oauth_acr_values_supported
-
156
acr_values = []
-
156
acr_values << "phrh" if features.include?(:webauthn_login)
-
156
acr_values << "phr" if respond_to?(:require_two_factor_authenticated)
-
156
acr_values
-
end
-
-
13
def oidc_authorize_on_prompt_none?(_account)
-
13
false
-
end
-
-
13
def validate_authorize_params
-
1500
if (max_age = param_or_nil("max_age"))
-
-
18
max_age = Integer(max_age)
-
-
18
redirect_response_error("invalid_request") unless max_age.positive?
-
-
18
if Time.now - get_oidc_account_last_login_at(session_value) > max_age
-
# force user to re-login
-
9
clear_session
-
9
set_session_value(login_redirect_session_key, request.fullpath)
-
9
redirect require_login_redirect
-
end
-
end
-
-
1491
if (claims = param_or_nil("claims"))
-
# The value is a JSON object listing the requested Claims.
-
26
claims = JSON.parse(claims)
-
-
26
claims.each_value do |individual_claims|
-
52
redirect_response_error("invalid_request") unless individual_claims.is_a?(Hash)
-
-
52
individual_claims.each_value do |claim|
-
78
redirect_response_error("invalid_request") unless claim.nil? || individual_claims.is_a?(Hash)
-
end
-
end
-
end
-
-
1491
sc = scopes
-
-
# MUST ensure that the prompt parameter contains consent
-
# MUST ignore the offline_access request unless the Client
-
# is using a response_type value that would result in an
-
# Authorization Code
-
1491
if sc && sc.include?("offline_access") && !(param_or_nil("prompt") == "consent" &&
-
39
(response_type = param_or_nil("response_type")) && response_type.split(" ").include?("code"))
-
26
sc.delete("offline_access")
-
-
18
request.params["scope"] = sc.join(" ")
-
end
-
-
1491
super
-
-
1413
response_type = param_or_nil("response_type")
-
-
1413
is_id_token_response_type = response_type.include?("id_token")
-
-
1413
redirect_response_error("invalid_request") if is_id_token_response_type && !param_or_nil("nonce")
-
-
1400
return unless is_id_token_response_type || response_type == "code token"
-
-
793
response_mode = param_or_nil("response_mode")
-
-
# id_token: The default Response Mode for this Response Type is the fragment encoding and the query encoding MUST NOT be used.
-
-
793
redirect_response_error("invalid_request") unless response_mode.nil? || response_mode == "fragment"
-
end
-
-
13
def require_authorizable_account
-
1682
try_prompt
-
1604
super
-
1578
@acr = try_acr_values
-
end
-
-
13
def get_oidc_account_last_login_at(account_id)
-
629
return get_activity_timestamp(account_id, account_activity_last_activity_column) if features.include?(:account_expiration)
-
-
# active sessions based
-
629
ds = db[active_sessions_table].where(active_sessions_account_id_column => account_id)
-
-
629
ds = ds.order(Sequel.desc(active_sessions_created_at_column))
-
-
629
convert_timestamp(ds.get(active_sessions_created_at_column))
-
end
-
-
13
def jwt_subject(account_unique_id, client_application = oauth_application)
-
1027
subject_type = client_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
-
-
711
case subject_type
-
when "public"
-
988
super
-
when "pairwise"
-
39
identifier_uri = client_application[oauth_applications_sector_identifier_uri_column]
-
-
39
unless identifier_uri
-
39
identifier_uri = client_application[oauth_applications_redirect_uri_column]
-
39
identifier_uri = identifier_uri.split(" ")
-
# If the Client has not provided a value for sector_identifier_uri in Dynamic Client Registration
-
# [OpenID.Registration], the Sector Identifier used for pairwise identifier calculation is the host
-
# component of the registered redirect_uri. If there are multiple hostnames in the registered redirect_uris,
-
# the Client MUST register a sector_identifier_uri.
-
39
if identifier_uri.size > 1
-
# return error message
-
end
-
39
identifier_uri = identifier_uri.first
-
end
-
-
39
identifier_uri = URI(identifier_uri).host
-
-
39
values = [identifier_uri, account_unique_id, oauth_jwt_subject_secret]
-
39
Digest::SHA256.hexdigest(values.join)
-
else
-
raise StandardError, "unexpected subject (#{subject_type})"
-
end
-
end
-
-
# this executes before checking for a logged in account
-
13
def try_prompt
-
1682
return unless (prompt = param_or_nil("prompt"))
-
-
153
case prompt
-
when "none"
-
39
return unless request.get?
-
-
39
redirect_response_error("login_required") unless logged_in?
-
-
26
require_account
-
-
26
redirect_response_error("interaction_required") unless oidc_authorize_on_prompt_none?(account_from_session)
-
-
9
request.env["REQUEST_METHOD"] = "POST"
-
when "login"
-
78
return unless request.get?
-
-
52
if logged_in? && request.cookies[oauth_prompt_login_cookie_key] == "login"
-
26
::Rack::Utils.delete_cookie_header!(response.headers, oauth_prompt_login_cookie_key, oauth_prompt_login_cookie_options)
-
18
return
-
end
-
-
# logging out
-
26
clear_session
-
26
set_session_value(login_redirect_session_key, request.fullpath)
-
-
26
login_cookie_opts = Hash[oauth_prompt_login_cookie_options]
-
18
login_cookie_opts[:value] = "login"
-
26
if oauth_prompt_login_interval
-
18
login_cookie_opts[:expires] = convert_timestamp(Time.now + oauth_prompt_login_interval) # 15 minutes
-
end
-
26
::Rack::Utils.set_cookie_header!(response.headers, oauth_prompt_login_cookie_key, login_cookie_opts)
-
-
26
redirect require_login_redirect
-
when "consent"
-
65
return unless request.post?
-
-
26
require_account
-
-
26
sc = scopes || []
-
-
26
redirect_response_error("consent_required") if sc.empty?
-
-
when "select-account"
-
39
return unless request.get?
-
-
# only works if select_account plugin is available
-
26
require_select_account if respond_to?(:require_select_account)
-
else
-
redirect_response_error("invalid_request")
-
end
-
end
-
-
13
def try_acr_values
-
1578
return unless (acr_values = param_or_nil("acr_values"))
-
-
65
acr_values.split(" ").each do |acr_value|
-
65
next unless oauth_acr_values_supported.include?(acr_value)
-
-
36
case acr_value
-
when "phr"
-
26
return acr_value if require_acr_value_phr
-
when "phrh"
-
26
return acr_value if require_acr_value_phrh
-
else
-
return acr_value if require_acr_value(acr_value)
-
end
-
end
-
-
8
nil
-
end
-
-
13
def require_acr_value_phr
-
52
return false unless respond_to?(:require_two_factor_authenticated)
-
-
52
require_two_factor_authenticated
-
52
true
-
end
-
-
13
def require_acr_value_phrh
-
26
return false unless features.include?(:webauthn_login)
-
-
26
require_acr_value_phr && two_factor_login_type_match?("webauthn")
-
end
-
-
13
def require_acr_value(_acr)
-
true
-
end
-
-
13
def create_oauth_grant(create_params = {})
-
377
create_params.replace(oidc_grant_params.merge(create_params))
-
377
super
-
end
-
-
13
def create_oauth_grant_with_token(create_params = {})
-
26
create_params.merge!(resource_owner_params)
-
18
create_params[oauth_grants_type_column] = "hybrid"
-
18
create_params[oauth_grants_expires_in_column] = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in)
-
26
authorization_code = create_oauth_grant(create_params)
-
26
access_token = if oauth_jwt_access_tokens
-
26
_generate_jwt_access_token(create_params)
-
else
-
oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => authorization_code).first
-
_generate_access_token(oauth_grant)
-
end
-
-
26
json_access_token_payload(oauth_grants_token_column => access_token).merge("code" => authorization_code)
-
end
-
-
13
def create_token(*)
-
325
oauth_grant = super
-
286
generate_id_token(oauth_grant)
-
286
oauth_grant
-
end
-
-
13
def generate_id_token(oauth_grant, include_claims = false)
-
611
oauth_scopes = oauth_grant[oauth_grants_scopes_column].split(oauth_scope_separator)
-
-
611
return unless oauth_scopes.include?("openid")
-
-
611
signing_algorithm = oauth_application[oauth_applications_id_token_signed_response_alg_column] ||
-
oauth_jwt_keys.keys.first
-
-
611
id_claims = id_token_claims(oauth_grant, signing_algorithm)
-
-
611
account = db[accounts_table].where(account_id_column => oauth_grant[oauth_grants_account_id_column]).first
-
-
# this should never happen!
-
# a newly minted oauth token from a grant should have been assigned to an account
-
# who just authorized its generation.
-
611
return unless account
-
-
611
if (claims = oauth_grant[oauth_grants_claims_column])
-
26
claims = JSON.parse(claims)
-
26
if (id_token_essential_claims = claims["id_token"])
-
18
oauth_scopes |= id_token_essential_claims.to_a
-
-
26
include_claims = true
-
end
-
end
-
-
# OpenID Connect Core 1.0's 5.4 Requesting Claims using Scope Values:
-
# If standard claims (profile, email, etc) are requested as scope values in the Authorization Request,
-
# include in the response.
-
611
include_claims ||= (OIDC_SCOPES_MAP.keys & oauth_scopes).any?
-
-
# However, when no Access Token is issued (which is the case for the response_type value id_token),
-
# the resulting Claims are returned in the ID Token.
-
611
fill_with_account_claims(id_claims, account, oauth_scopes, param_or_nil("claims_locales")) if include_claims
-
-
235
params = {
-
376
jwks: oauth_application_jwks(oauth_application),
-
signing_algorithm: signing_algorithm,
-
encryption_algorithm: oauth_application[oauth_applications_id_token_encrypted_response_alg_column],
-
encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column],
-
-
# Not officially part of the spec, but some providers follow this convention.
-
# This is useful for distinguishing between ID Tokens and JWT Access Tokens.
-
headers: { typ: "id_token+jwt" }
-
}.compact
-
-
423
oauth_grant[:id_token] = jwt_encode(id_claims, **params)
-
end
-
-
13
def id_token_claims(oauth_grant, signing_algorithm)
-
611
claims = jwt_claims(oauth_grant)
-
-
611
claims[:nonce] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
-
-
611
claims[:acr] = oauth_grant[oauth_grants_acr_column] if oauth_grant[oauth_grants_acr_column]
-
-
# Time when the End-User authentication occurred.
-
423
claims[:auth_time] = get_oidc_account_last_login_at(oauth_grant[oauth_grants_account_id_column]).to_i
-
-
# Access Token hash value.
-
611
if (access_token = oauth_grant[oauth_grants_token_column])
-
216
claims[:at_hash] = id_token_hash(access_token, signing_algorithm)
-
end
-
-
# code hash value.
-
611
if (code = oauth_grant[oauth_grants_code_column])
-
99
claims[:c_hash] = id_token_hash(code, signing_algorithm)
-
end
-
-
611
claims
-
end
-
-
# aka fill_with_standard_claims
-
13
def fill_with_account_claims(claims, account, scopes, claims_locales)
-
351
additional_claims_info = {}
-
-
351
scopes_by_claim = scopes.each_with_object({}) do |scope, by_oidc|
-
442
next if scope == "openid"
-
-
208
if scope.is_a?(Array)
-
# essential claims
-
65
param, additional_info = scope
-
-
65
param = param.to_sym
-
-
65
oidc, = OIDC_SCOPES_MAP.find do |_, oidc_scopes|
-
143
oidc_scopes.include?(param)
-
end || param.to_s
-
-
65
param = nil if oidc == param.to_s
-
-
45
additional_claims_info[param] = additional_info
-
else
-
-
143
oidc, param = scope.split(".", 2)
-
-
143
param = param.to_sym if param
-
end
-
-
208
by_oidc[oidc] ||= []
-
-
208
by_oidc[oidc] << param.to_sym if param
-
end
-
-
559
oidc_scopes, additional_scopes = scopes_by_claim.keys.partition { |key| OIDC_SCOPES_MAP.key?(key) }
-
-
351
claims_locales = claims_locales.split(" ").map(&:to_sym) if claims_locales
-
-
351
unless oidc_scopes.empty?
-
117
if respond_to?(:get_oidc_param)
-
117
get_oidc_param = proxy_get_param(:get_oidc_param, claims, claims_locales, additional_claims_info)
-
-
117
oidc_scopes.each do |scope|
-
117
scope_claims = claims
-
117
params = scopes_by_claim[scope]
-
117
params = params.empty? ? OIDC_SCOPES_MAP[scope] : (OIDC_SCOPES_MAP[scope] & params)
-
-
117
scope_claims = (claims["address"] = {}) if scope == "address"
-
-
117
params.each do |param|
-
286
get_oidc_param[account, param, scope_claims]
-
end
-
end
-
else
-
warn "`get_oidc_param(account, claim)` must be implemented to use oidc scopes."
-
end
-
end
-
-
351
return if additional_scopes.empty?
-
-
91
if respond_to?(:get_additional_param)
-
91
get_additional_param = proxy_get_param(:get_additional_param, claims, claims_locales, additional_claims_info)
-
-
91
additional_scopes.each do |scope|
-
91
get_additional_param[account, scope.to_sym]
-
end
-
else
-
warn "`get_additional_param(account, claim)` must be implemented to use oidc scopes."
-
end
-
end
-
-
13
def proxy_get_param(get_param_func, claims, claims_locales, additional_claims_info)
-
208
meth = method(get_param_func)
-
208
if meth.arity == 2
-
182
lambda do |account, param, cl = claims|
-
351
additional_info = additional_claims_info[param] || EMPTY_HASH
-
351
value = additional_info["value"] || meth[account, param]
-
351
value = nil if additional_info["values"] && additional_info["values"].include?(value)
-
351
cl[param] = value unless value.nil?
-
end
-
26
elsif claims_locales.nil?
-
lambda do |account, param, cl = claims|
-
additional_info = additional_claims_info[param] || EMPTY_HASH
-
value = additional_info["value"] || meth[account, param, nil]
-
value = nil if additional_info["values"] && additional_info["values"].include?(value)
-
cl[param] = value unless value.nil?
-
end
-
else
-
26
lambda do |account, param, cl = claims|
-
26
claims_values = claims_locales.map do |locale|
-
52
additional_info = additional_claims_info[param] || EMPTY_HASH
-
52
value = additional_info["value"] || meth[account, param, locale]
-
52
value = nil if additional_info["values"] && additional_info["values"].include?(value)
-
52
value
-
end.compact
-
-
26
if claims_values.uniq.size == 1
-
cl[param] = claims_values.first
-
else
-
26
claims_locales.zip(claims_values).each do |locale, value|
-
52
cl["#{param}##{locale}"] = value if value
-
end
-
end
-
end
-
end
-
end
-
-
13
def json_access_token_payload(oauth_grant)
-
338
payload = super
-
338
payload["id_token"] = oauth_grant[:id_token] if oauth_grant[:id_token]
-
338
payload
-
end
-
-
# Authorize
-
-
13
def check_valid_response_type?
-
990
case param_or_nil("response_type")
-
when "none", "id_token", "code id_token", # multiple
-
"code token", "id_token token", "code id_token token"
-
832
true
-
else
-
594
super
-
end
-
end
-
-
13
def supported_response_mode?(response_mode, *)
-
585
return super unless response_mode == "none"
-
-
13
param("response_type") == "none"
-
end
-
-
13
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
-
585
response_type = param("response_type")
-
405
case response_type
-
when "id_token"
-
169
grant_params = oidc_grant_params
-
169
generate_id_token(grant_params, true)
-
169
response_params.replace("id_token" => grant_params[:id_token])
-
when "code token"
-
13
response_params.replace(create_oauth_grant_with_token)
-
when "code id_token"
-
130
params = _do_authorize_code
-
130
oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => params["code"]).first
-
130
generate_id_token(oauth_grant)
-
130
response_params.replace(
-
"id_token" => oauth_grant[:id_token],
-
"code" => params["code"]
-
)
-
when "id_token token"
-
13
grant_params = oidc_grant_params.merge(oauth_grants_type_column => "hybrid")
-
13
oauth_grant = _do_authorize_token(grant_params)
-
13
generate_id_token(oauth_grant)
-
-
13
response_params.replace(json_access_token_payload(oauth_grant))
-
when "code id_token token"
-
13
params = create_oauth_grant_with_token
-
13
oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => params["code"]).first
-
9
oauth_grant[oauth_grants_token_column] = params["access_token"]
-
13
generate_id_token(oauth_grant)
-
-
13
response_params.replace(params.merge("id_token" => oauth_grant[:id_token]))
-
when "none"
-
13
response_mode ||= "none"
-
end
-
585
response_mode ||= "fragment" unless response_params.empty?
-
-
585
super(response_params, response_mode)
-
end
-
-
13
def oidc_grant_params
-
559
grant_params = {
-
**resource_owner_params,
-
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
-
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
-
oauth_grants_redirect_uri_column => param_or_nil("redirect_uri")
-
}
-
559
if (nonce = param_or_nil("nonce"))
-
297
grant_params[oauth_grants_nonce_column] = nonce
-
end
-
559
grant_params[oauth_grants_acr_column] = @acr if @acr
-
559
if (claims_locales = param_or_nil("claims_locales"))
-
18
grant_params[oauth_grants_claims_locales_column] = claims_locales
-
end
-
559
if (claims = param_or_nil("claims"))
-
9
grant_params[oauth_grants_claims_column] = claims
-
end
-
559
grant_params
-
end
-
-
13
def generate_token(grant_params = {}, should_generate_refresh_token = true)
-
260
scopes = grant_params[oauth_grants_scopes_column].split(oauth_scope_separator)
-
-
260
super(grant_params, scopes.include?("offline_access") && should_generate_refresh_token)
-
end
-
-
13
def authorize_response(params, mode)
-
585
redirect_url = URI.parse(redirect_uri)
-
585
redirect(redirect_url.to_s) if mode == "none"
-
572
super
-
end
-
-
# Webfinger
-
-
13
def json_webfinger_payload
-
13
JSON.dump({
-
subject: param("resource"),
-
links: [{
-
rel: "http://openid.net/specs/connect/1.0/issuer",
-
href: authorization_server_url
-
}]
-
})
-
end
-
-
# Metadata
-
-
13
def openid_configuration_body(path = nil)
-
91
metadata = oauth_server_metadata_body(path).slice(*VALID_METADATA_KEYS)
-
-
91
scope_claims = oauth_application_scopes.each_with_object([]) do |scope, claims|
-
143
oidc, param = scope.split(".", 2)
-
143
if param
-
13
claims << param
-
else
-
130
oidc_claims = OIDC_SCOPES_MAP[oidc]
-
130
claims.concat(oidc_claims) if oidc_claims
-
end
-
end
-
-
91
scope_claims.unshift("auth_time")
-
-
84
metadata.merge(
-
userinfo_endpoint: userinfo_url,
-
subject_types_supported: %w[public pairwise],
-
acr_values_supported: oauth_acr_values_supported,
-
claims_parameter_supported: true,
-
-
id_token_signing_alg_values_supported: oauth_jwt_jws_algorithms_supported,
-
id_token_encryption_alg_values_supported: oauth_jwt_jwe_algorithms_supported,
-
id_token_encryption_enc_values_supported: oauth_jwt_jwe_encryption_methods_supported,
-
-
userinfo_signing_alg_values_supported: oauth_jwt_jws_algorithms_supported,
-
userinfo_encryption_alg_values_supported: oauth_jwt_jwe_algorithms_supported,
-
userinfo_encryption_enc_values_supported: oauth_jwt_jwe_encryption_methods_supported,
-
-
request_object_signing_alg_values_supported: oauth_jwt_jws_algorithms_supported,
-
request_object_encryption_alg_values_supported: oauth_jwt_jwe_algorithms_supported,
-
request_object_encryption_enc_values_supported: oauth_jwt_jwe_encryption_methods_supported,
-
-
# These Claim Types are described in Section 5.6 of OpenID Connect Core 1.0 [OpenID.Core].
-
# Values defined by this specification are normal, aggregated, and distributed.
-
# If omitted, the implementation supports only normal Claims.
-
claim_types_supported: %w[normal],
-
claims_supported: %w[sub iss iat exp aud] | scope_claims
-
7
).reject do |key, val|
-
# Filter null values in optional items
-
2717
(!REQUIRED_METADATA_KEYS.include?(key.to_sym) && val.nil?) ||
-
# Claims with zero elements MUST be omitted from the response
-
2353
(val.respond_to?(:empty?) && val.empty?)
-
end
-
end
-
-
13
def allow_cors(request)
-
117
return unless request.request_method == "OPTIONS"
-
-
9
response["Access-Control-Allow-Origin"] = "*"
-
9
response["Access-Control-Allow-Methods"] = "GET, OPTIONS"
-
9
response["Access-Control-Max-Age"] = "3600"
-
13
response.status = 200
-
13
return_response
-
end
-
-
13
def jwt_response_success(jwt, cache = false)
-
26
response.status = 200
-
26
response["Content-Type"] ||= "application/jwt"
-
26
if cache
-
# defaulting to 1-day for everyone, for now at least
-
max_age = 60 * 60 * 24
-
response["Cache-Control"] = "private, max-age=#{max_age}"
-
else
-
18
response["Cache-Control"] = "no-store"
-
18
response["Pragma"] = "no-cache"
-
end
-
26
return_response(jwt)
-
end
-
-
13
def id_token_hash(hash, algo)
-
455
digest = case algo
-
455
when /256/ then Digest::SHA256
-
when /384/ then Digest::SHA384
-
when /512/ then Digest::SHA512
-
end
-
-
455
return unless digest
-
-
455
hash = digest.digest(hash)
-
455
hash = hash[0...(hash.size / 2)]
-
455
Base64.urlsafe_encode64(hash).tr("=", "")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oidc_backchannel_logout, :OidBackchannelLogout) do
-
13
depends :logout, :oidc_logout_base
-
-
13
auth_value_method :oauth_logout_token_expires_in, 60 # 1 minute
-
13
auth_value_method :backchannel_logout_session_supported, true
-
13
auth_value_method :oauth_applications_backchannel_logout_uri_column, :backchannel_logout_uri
-
13
auth_value_method :oauth_applications_backchannel_logout_session_required_column, :backchannel_logout_session_required
-
-
13
auth_methods(
-
:perform_logout_requests
-
)
-
-
13
def logout
-
78
visited_sites = session[visited_sites_key]
-
-
78
return super unless visited_sites
-
-
78
oauth_applications = db[oauth_applications_table].where(oauth_applications_client_id_column => visited_sites.map(&:first))
-
.as_hash(oauth_applications_id_column)
-
-
78
logout_params = oauth_applications.flat_map do |_id, oauth_application|
-
78
logout_url = oauth_application[oauth_applications_backchannel_logout_uri_column]
-
-
78
next unless logout_url
-
-
78
client_id = oauth_application[oauth_applications_client_id_column]
-
-
156
sids = visited_sites.select { |cid, _| cid == client_id }.map(&:last)
-
-
78
sids.map do |sid|
-
78
logout_token = generate_logout_token(oauth_application, sid)
-
-
78
[logout_url, logout_token]
-
end
-
end.compact
-
-
78
perform_logout_requests(logout_params) unless logout_params.empty?
-
-
# now we can clear the session
-
78
super
-
end
-
-
13
private
-
-
13
def generate_logout_token(oauth_application, sid)
-
78
issued_at = Time.now.to_i
-
-
30
logout_claims = {
-
48
iss: oauth_jwt_issuer, # issuer
-
iat: issued_at, # issued at
-
exp: issued_at + oauth_logout_token_expires_in,
-
aud: oauth_application[oauth_applications_client_id_column],
-
events: {
-
"http://schemas.openid.net/event/backchannel-logout": {}
-
}
-
}
-
-
78
logout_claims[:sid] = sid if sid
-
-
78
signing_algorithm = oauth_application[oauth_applications_id_token_signed_response_alg_column] ||
-
oauth_jwt_keys.keys.first
-
-
30
params = {
-
48
jwks: oauth_application_jwks(oauth_application),
-
headers: { typ: "logout+jwt" },
-
signing_algorithm: signing_algorithm,
-
encryption_algorithm: oauth_application[oauth_applications_id_token_encrypted_response_alg_column],
-
encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column]
-
}.compact
-
-
78
jwt_encode(logout_claims, **params)
-
end
-
-
13
def perform_logout_requests(logout_params)
-
# performs logout requests sequentially
-
78
logout_params.each do |logout_url, logout_token|
-
78
http_request(logout_url, { "logout_token" => logout_token })
-
rescue StandardError
-
warn "failed to perform backchannel logout on #{logout_url}"
-
end
-
end
-
-
13
def id_token_claims(oauth_grant, signing_algorithm)
-
78
claims = super
-
-
78
return claims unless oauth_application[oauth_applications_backchannel_logout_uri_column]
-
-
78
session_id_in_claims(oauth_grant, claims)
-
-
78
claims
-
end
-
-
13
def should_set_oauth_application_in_visited_sites?
-
39
true
-
end
-
-
13
def should_set_sid_in_visited_sites?(oauth_application)
-
117
super || requires_backchannel_logout_session?(oauth_application)
-
end
-
-
13
def requires_backchannel_logout_session?(oauth_application)
-
36
(
-
117
oauth_application &&
-
oauth_application[oauth_applications_backchannel_logout_session_required_column]
-
9
) || backchannel_logout_session_supported
-
end
-
-
13
def oauth_server_metadata_body(*)
-
13
super.tap do |data|
-
9
data[:backchannel_logout_supported] = true
-
9
data[:backchannel_logout_session_supported] = backchannel_logout_session_supported
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oidc_dynamic_client_registration, :OidcDynamicClientRegistration) do
-
13
depends :oauth_dynamic_client_registration, :oidc
-
-
13
auth_value_method :oauth_applications_application_type_column, :application_type
-
-
13
private
-
-
13
def validate_client_registration_params(*)
-
663
super
-
-
624
if (value = @oauth_application_params[oauth_applications_application_type_column])
-
63
case value
-
when "native"
-
52
request.params["redirect_uris"].each do |uri|
-
52
uri = URI(uri)
-
# Native Clients MUST only register redirect_uris using custom URI schemes or
-
# URLs using the http: scheme with localhost as the hostname.
-
36
case uri.scheme
-
when "http"
-
26
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(uri)) unless uri.host == "localhost"
-
when "https"
-
13
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(uri))
-
end
-
end
-
when "web"
-
# Web Clients using the OAuth Implicit Grant Type MUST only register URLs using the https scheme as redirect_uris;
-
# they MUST NOT use localhost as the hostname.
-
39
if request.params["grant_types"].include?("implicit")
-
26
request.params["redirect_uris"].each do |uri|
-
26
uri = URI(uri)
-
26
unless uri.scheme == "https" && uri.host != "localhost"
-
13
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(uri))
-
end
-
end
-
end
-
else
-
register_throw_json_response_error("invalid_client_metadata", register_invalid_application_type_message(type))
-
end
-
end
-
-
585
if (value = @oauth_application_params[oauth_applications_sector_identifier_uri_column]) && !check_valid_uri?(value)
-
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
-
end
-
-
585
if (value = @oauth_application_params[oauth_applications_initiate_login_uri_column])
-
26
uri = URI(value)
-
-
26
unless uri.scheme == "https" || uri.host == "localhost"
-
13
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(uri))
-
end
-
end
-
-
572
if features.include?(:oauth_jwt_secured_authorization_request)
-
117
if (value = @oauth_application_params[oauth_applications_request_uris_column])
-
26
if value.is_a?(Array)
-
9
@oauth_application_params[oauth_applications_request_uris_column] = value.each do |req_uri|
-
13
unless check_valid_uri?(req_uri)
-
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(req_uri))
-
end
-
end.join(" ")
-
else
-
13
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
-
end
-
91
elsif oauth_require_request_uri_registration
-
13
register_throw_json_response_error("invalid_client_metadata", register_required_param_message("request_uris"))
-
end
-
end
-
-
546
if (value = @oauth_application_params[oauth_applications_subject_type_column])
-
91
unless %w[pairwise public].include?(value)
-
13
register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message("subject_type", value))
-
end
-
-
78
if value == "pairwise"
-
65
sector_identifier_uri = @oauth_application_params[oauth_applications_sector_identifier_uri_column]
-
-
65
if sector_identifier_uri
-
26
response = http_request(sector_identifier_uri)
-
26
unless response.code.to_i == 200
-
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_param_message("sector_identifier_uri"))
-
end
-
26
uris = JSON.parse(response.body)
-
-
26
if uris != @oauth_application_params[oauth_applications_redirect_uri_column].split(" ")
-
13
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("sector_identifier_uri"))
-
end
-
-
end
-
end
-
end
-
-
520
if (value = @oauth_application_params[oauth_applications_id_token_signed_response_alg_column])
-
52
if value == "none"
-
# The value none MUST NOT be used as the ID Token alg value unless the Client uses only Response Types
-
# that return no ID Token from the Authorization Endpoint
-
response_types = @oauth_application_params[oauth_applications_response_types_column]
-
if response_types && response_types.include?("id_token")
-
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_signed_response_alg"))
-
end
-
52
elsif !oauth_jwt_jws_algorithms_supported.include?(value)
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("id_token_signed_response_alg", value))
-
end
-
end
-
-
507
if features.include?(:oauth_jwt_secured_authorization_request)
-
91
if defined?(oauth_applications_request_object_signing_alg_column) &&
-
91
(value = @oauth_application_params[oauth_applications_request_object_signing_alg_column]) &&
-
13
!oauth_jwt_jws_algorithms_supported.include?(value) && !(value == "none" && oauth_request_object_signing_alg_allow_none)
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("request_object_signing_alg", value))
-
end
-
-
78
if defined?(oauth_applications_request_object_encryption_alg_column) &&
-
78
(value = @oauth_application_params[oauth_applications_request_object_encryption_alg_column]) &&
-
!oauth_jwt_jwe_algorithms_supported.include?(value)
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("request_object_encryption_alg", value))
-
end
-
-
65
if defined?(oauth_applications_request_object_encryption_enc_column) &&
-
65
(value = @oauth_application_params[oauth_applications_request_object_encryption_enc_column]) &&
-
!oauth_jwt_jwe_encryption_methods_supported.include?(value)
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("request_object_encryption_enc", value))
-
end
-
end
-
-
468
if features.include?(:oidc_rp_initiated_logout) && defined?(oauth_applications_post_logout_redirect_uris_column) &&
-
26
(value = @oauth_application_params[oauth_applications_post_logout_redirect_uris_column])
-
26
if value.is_a?(Array)
-
9
@oauth_application_params[oauth_applications_post_logout_redirect_uris_column] = value.each do |redirect_uri|
-
13
unless check_valid_uri?(redirect_uri)
-
register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(redirect_uri))
-
end
-
end.join(" ")
-
else
-
13
register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value))
-
end
-
end
-
-
455
if features.include?(:oidc_frontchannel_logout)
-
39
if (value = @oauth_application_params[oauth_applications_frontchannel_logout_uri_column]) && !check_valid_no_fragment_uri?(value)
-
26
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_uri_message(value))
-
end
-
-
13
if (value = @oauth_application_params[oauth_applications_frontchannel_logout_session_required_column])
-
9
@oauth_application_params[oauth_applications_frontchannel_logout_session_required_column] =
-
convert_to_boolean("frontchannel_logout_session_required", value)
-
end
-
end
-
-
429
if features.include?(:oidc_backchannel_logout)
-
39
if (value = @oauth_application_params[oauth_applications_backchannel_logout_uri_column]) && !check_valid_no_fragment_uri?(value)
-
26
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_uri_message(value))
-
end
-
-
13
if @oauth_application_params.key?(oauth_applications_backchannel_logout_session_required_column)
-
13
value = @oauth_application_params[oauth_applications_backchannel_logout_session_required_column]
-
9
@oauth_application_params[oauth_applications_backchannel_logout_session_required_column] =
-
convert_to_boolean("backchannel_logout_session_required", value)
-
end
-
end
-
-
403
if (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_alg_column]) &&
-
!oauth_jwt_jwe_algorithms_supported.include?(value)
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("id_token_encrypted_response_alg", value))
-
end
-
-
390
if (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_enc_column]) &&
-
!oauth_jwt_jwe_encryption_methods_supported.include?(value)
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("id_token_encrypted_response_enc", value))
-
end
-
-
377
if (value = @oauth_application_params[oauth_applications_userinfo_signed_response_alg_column]) &&
-
!oauth_jwt_jws_algorithms_supported.include?(value)
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("userinfo_signed_response_alg", value))
-
end
-
-
364
if (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_alg_column]) &&
-
!oauth_jwt_jwe_algorithms_supported.include?(value)
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("userinfo_encrypted_response_alg", value))
-
end
-
-
351
if (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_enc_column]) &&
-
!oauth_jwt_jwe_encryption_methods_supported.include?(value)
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("userinfo_encrypted_response_enc", value))
-
end
-
-
338
if features.include?(:oauth_jwt_secured_authorization_response_mode)
-
26
if defined?(oauth_applications_authorization_signed_response_alg_column) &&
-
26
(value = @oauth_application_params[oauth_applications_authorization_signed_response_alg_column]) &&
-
16
(!oauth_jwt_jws_algorithms_supported.include?(value) || value == "none")
-
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("authorization_signed_response_alg", value))
-
end
-
-
26
if defined?(oauth_applications_authorization_encrypted_response_alg_column) &&
-
26
(value = @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]) &&
-
!oauth_jwt_jwe_algorithms_supported.include?(value)
-
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("authorization_encrypted_response_alg", value))
-
end
-
-
26
if defined?(oauth_applications_authorization_encrypted_response_enc_column)
-
26
if (value = @oauth_application_params[oauth_applications_authorization_encrypted_response_enc_column])
-
-
13
unless @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]
-
# When authorization_encrypted_response_enc is included, authorization_encrypted_response_alg MUST also be provided.
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("authorization_encrypted_response_alg", value))
-
-
end
-
-
unless oauth_jwt_jwe_encryption_methods_supported.include?(value)
-
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_client_metadata_message("authorization_encrypted_response_enc", value))
-
end
-
13
elsif @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]
-
# If authorization_encrypted_response_alg is specified, the default for this value is A128CBC-HS256.
-
9
@oauth_application_params[oauth_applications_authorization_encrypted_response_enc_column] = "A128CBC-HS256"
-
end
-
end
-
end
-
-
325
@oauth_application_params
-
end
-
-
13
def validate_client_registration_response_type(response_type, grant_types)
-
459
case response_type
-
when "id_token"
-
52
unless grant_types.include?("implicit")
-
13
register_throw_json_response_error("invalid_client_metadata",
-
register_invalid_response_type_for_grant_type_message(response_type, "implicit"))
-
end
-
else
-
611
super
-
end
-
end
-
-
13
def do_register(return_params = request.params.dup)
-
# set defaults
-
-
299
create_params = @oauth_application_params
-
-
299
create_params[oauth_applications_application_type_column] ||= begin
-
171
return_params["application_type"] = "web"
-
247
"web"
-
end
-
299
create_params[oauth_applications_id_token_signed_response_alg_column] ||= return_params["id_token_signed_response_alg"] =
-
oauth_jwt_keys.keys.first
-
-
299
if create_params.key?(oauth_applications_id_token_encrypted_response_alg_column)
-
13
create_params[oauth_applications_id_token_encrypted_response_enc_column] ||= return_params["id_token_encrypted_response_enc"] =
-
"A128CBC-HS256"
-
-
end
-
299
if create_params.key?(oauth_applications_userinfo_encrypted_response_alg_column)
-
13
create_params[oauth_applications_userinfo_encrypted_response_enc_column] ||= return_params["userinfo_encrypted_response_enc"] =
-
"A128CBC-HS256"
-
-
end
-
299
if defined?(oauth_applications_request_object_encryption_alg_column) &&
-
create_params.key?(oauth_applications_request_object_encryption_alg_column)
-
13
create_params[oauth_applications_request_object_encryption_enc_column] ||= return_params["request_object_encryption_enc"] =
-
"A128CBC-HS256"
-
-
end
-
-
299
super(return_params)
-
end
-
-
13
def register_invalid_application_type_message(application_type)
-
"The application type '#{application_type}' is not allowed."
-
end
-
-
13
def initialize_register_params(create_params, return_params)
-
299
super
-
299
registration_access_token = oauth_unique_id_generator
-
207
create_params[oauth_applications_registration_access_token_column] = secret_hash(registration_access_token)
-
207
return_params["registration_access_token"] = registration_access_token
-
207
return_params["registration_client_uri"] = "#{base_url}/#{registration_client_uri_route}/#{return_params['client_id']}"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
skipped
# :nocov:
-
skipped
raise LoadError, "the `:oidc_frontchannel_logout` requires rodauth 2.32.0 or higher" if Rodauth::VERSION < "2.32.0"
-
skipped
-
skipped
# :nocov:
-
-
13
module Rodauth
-
13
Feature.define(:oidc_frontchannel_logout, :OidFrontchannelLogout) do
-
13
depends :logout, :oidc_logout_base
-
-
13
view "frontchannel_logout", "Logout", "frontchannel_logout"
-
-
13
translatable_method :oauth_frontchannel_logout_redirecting_lead, "You are being redirected..."
-
13
translatable_method :oauth_frontchannel_logout_redirecting_label, "please click %<link>s if your browser does not " \
-
"redirect you in a few seconds."
-
13
translatable_method :oauth_frontchannel_logout_redirecting_link_label, "here"
-
13
auth_value_method :frontchannel_logout_session_supported, true
-
13
auth_value_method :frontchannel_logout_redirect_timeout, 5
-
13
auth_value_method :oauth_applications_frontchannel_logout_uri_column, :frontchannel_logout_uri
-
13
auth_value_method :oauth_applications_frontchannel_logout_session_required_column, :frontchannel_logout_session_required
-
-
13
attr_reader :frontchannel_logout_urls
-
-
13
attr_reader :frontchannel_logout_redirect
-
-
13
def logout
-
91
@visited_sites = session[visited_sites_key]
-
-
91
super
-
end
-
-
13
def _logout_response
-
78
visited_sites = @visited_sites
-
-
78
return super unless visited_sites
-
-
78
logout_urls = db[oauth_applications_table]
-
.where(oauth_applications_client_id_column => visited_sites.map(&:first))
-
.as_hash(oauth_applications_client_id_column, oauth_applications_frontchannel_logout_uri_column)
-
-
78
return super if logout_urls.empty?
-
-
78
generate_frontchannel_logout_urls(visited_sites, logout_urls)
-
-
78
@frontchannel_logout_redirect = logout_redirect
-
-
78
set_notice_flash logout_notice_flash
-
78
return_response frontchannel_logout_view
-
end
-
-
# overrides rp-initiate logout response
-
13
def _oidc_logout_response
-
13
visited_sites = @visited_sites
-
-
13
return super unless visited_sites
-
-
13
logout_urls = db[oauth_applications_table]
-
.where(oauth_applications_client_id_column => visited_sites.map(&:first))
-
.as_hash(oauth_applications_client_id_column, oauth_applications_frontchannel_logout_uri_column)
-
-
13
return super if logout_urls.empty?
-
-
13
generate_frontchannel_logout_urls(visited_sites, logout_urls)
-
-
13
@frontchannel_logout_redirect = oidc_logout_redirect
-
-
13
set_notice_flash logout_notice_flash
-
13
return_response frontchannel_logout_view
-
end
-
-
13
private
-
-
13
def generate_frontchannel_logout_urls(visited_sites, logout_urls)
-
91
@frontchannel_logout_urls = logout_urls.flat_map do |client_id, logout_url|
-
91
next unless logout_url
-
-
182
sids = visited_sites.select { |cid, _| cid == client_id }.map(&:last)
-
-
91
sids.map do |sid|
-
91
logout_url = URI(logout_url)
-
-
91
if sid
-
65
query = logout_url.query
-
65
query = if query
-
26
URI.decode_www_form(query)
-
else
-
39
[]
-
end
-
65
query << ["iss", oauth_jwt_issuer]
-
65
query << ["sid", sid]
-
65
logout_url.query = URI.encode_www_form(query)
-
end
-
-
91
logout_url
-
end
-
end.compact
-
end
-
-
13
def id_token_claims(oauth_grant, signing_algorithm)
-
91
claims = super
-
-
91
return claims unless oauth_application[oauth_applications_frontchannel_logout_uri_column]
-
-
91
session_id_in_claims(oauth_grant, claims)
-
-
91
claims
-
end
-
-
13
def should_set_oauth_application_in_visited_sites?
-
52
true
-
end
-
-
13
def should_set_sid_in_visited_sites?(oauth_application)
-
143
super || requires_frontchannel_logout_session?(oauth_application)
-
end
-
-
13
def requires_frontchannel_logout_session?(oauth_application)
-
44
(
-
143
oauth_application &&
-
oauth_application[oauth_applications_frontchannel_logout_session_required_column]
-
11
) || frontchannel_logout_session_supported
-
end
-
-
13
def oauth_server_metadata_body(*)
-
13
super.tap do |data|
-
9
data[:frontchannel_logout_supported] = true
-
9
data[:frontchannel_logout_session_supported] = frontchannel_logout_session_supported
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oidc_logout_base, :OidcLogoutBase) do
-
13
depends :oidc
-
-
13
session_key :visited_sites_key, :visited_sites
-
-
13
private
-
-
# set application/sid in visited sites when required
-
13
def create_oauth_grant(create_params = {})
-
143
sid_in_visited_sites
-
-
143
super
-
end
-
-
13
def active_sessions?(session_id)
-
13
!active_sessions_ds.where(active_sessions_session_id_column => session_id).empty?
-
end
-
-
13
def session_id_in_claims(oauth_grant, claims)
-
169
oauth_application_in_visited_sites do
-
169
if should_set_sid_in_visited_sites?(oauth_application)
-
# id_token or token response types
-
117
session_id = if (sess = session[session_id_session_key])
-
65
compute_hmac(sess)
-
else
-
# code response type
-
52
ds = db[active_sessions_table]
-
52
ds = ds.where(active_sessions_account_id_column => oauth_grant[oauth_grants_account_id_column])
-
52
ds = ds.order(Sequel.desc(active_sessions_last_use_column))
-
52
ds.get(active_sessions_session_id_column)
-
end
-
-
81
claims[:sid] = session_id
-
end
-
end
-
end
-
-
13
def oauth_application_in_visited_sites
-
260
visited_sites = session[visited_sites_key] || []
-
-
260
session_id = yield
-
-
260
visited_site = [oauth_application[oauth_applications_client_id_column], session_id]
-
-
260
return if visited_sites.include?(visited_site)
-
-
247
visited_sites << visited_site
-
247
set_session_value(visited_sites_key, visited_sites)
-
end
-
-
13
def sid_in_visited_sites
-
143
return unless should_set_oauth_application_in_visited_sites?
-
-
91
oauth_application_in_visited_sites do
-
91
if should_set_sid_in_visited_sites?(oauth_application)
-
65
ds = active_sessions_ds.order(Sequel.desc(active_sessions_last_use_column))
-
-
65
ds.get(active_sessions_session_id_column)
-
end
-
end
-
end
-
-
13
def should_set_oauth_application_in_visited_sites?
-
52
false
-
end
-
-
13
def should_set_sid_in_visited_sites?(*)
-
260
false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oidc_rp_initiated_logout, :OidcRpInitiatedLogout) do
-
13
depends :oidc_logout_base
-
13
response "oidc_logout"
-
-
13
auth_value_method :oauth_applications_post_logout_redirect_uris_column, :post_logout_redirect_uris
-
13
translatable_method :oauth_invalid_id_token_hint_message, "Invalid ID token hint"
-
13
translatable_method :oauth_invalid_post_logout_redirect_uri_message, "Invalid post logout redirect URI"
-
-
13
attr_reader :oidc_logout_redirect
-
-
# /oidc-logout
-
13
auth_server_route(:oidc_logout) do |r|
-
78
require_authorizable_account
-
78
before_oidc_logout_route
-
-
# OpenID Providers MUST support the use of the HTTP GET and POST methods
-
78
r.on method: %i[get post] do
-
78
catch_error do
-
78
validate_oidc_logout_params
-
-
78
claims = nil
-
-
78
if (id_token_hint = param_or_nil("id_token_hint"))
-
#
-
# why this is done:
-
#
-
# we need to decode the id token in order to get the application, because, if the
-
# signing key is application-specific, we don't know how to verify the signature
-
# beforehand. Hence, we have to do it twice: decode-and-do-not-verify, initialize
-
# the @oauth_application, and then decode-and-verify.
-
#
-
78
claims = jwt_decode(id_token_hint, verify_claims: false)
-
-
78
redirect_logout_with_error(oauth_invalid_id_token_hint_message) unless claims
-
-
# If the ID Token's sid claim does not correspond to the RP's current session or a
-
# recent session at the OP, the OP SHOULD treat the logout request as suspect, and
-
# MAY decline to act upon it.
-
78
redirect_logout_with_error(oauth_invalid_client_message) if claims["sid"] && !active_sessions?(claims["sid"])
-
-
78
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["aud"]).first
-
78
oauth_grant = db[oauth_grants_table]
-
.where(resource_owner_params)
-
.where(oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column])
-
.first
-
-
78
unique_account_id = if oauth_grant
-
65
oauth_grant[oauth_grants_account_id_column]
-
else
-
13
account_id
-
end
-
-
# check whether ID token belongs to currently logged-in user
-
78
redirect_logout_with_error(oauth_invalid_client_message) unless claims["sub"] == jwt_subject(unique_account_id,
-
oauth_application)
-
-
# When an id_token_hint parameter is present, the OP MUST validate that it was the issuer of the ID Token.
-
78
redirect_logout_with_error(oauth_invalid_client_message) unless claims && claims["iss"] == oauth_jwt_issuer
-
end
-
-
# now let's logout from IdP
-
78
transaction do
-
78
before_logout
-
78
logout
-
78
after_logout
-
end
-
-
78
error_message = logout_notice_flash
-
-
78
if (post_logout_redirect_uri = param_or_nil("post_logout_redirect_uri"))
-
65
error_message = catch(:default_logout_redirect) do
-
65
throw(:default_logout_redirect, oauth_invalid_id_token_hint_message) unless claims
-
-
65
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
-
-
65
throw(:default_logout_redirect, oauth_invalid_client_message) unless oauth_application
-
-
65
post_logout_redirect_uris = oauth_application[oauth_applications_post_logout_redirect_uris_column].split(" ")
-
-
65
unless post_logout_redirect_uris.include?(post_logout_redirect_uri)
-
throw(:default_logout_redirect,
-
oauth_invalid_post_logout_redirect_uri_message)
-
end
-
-
65
if (state = param_or_nil("state"))
-
13
post_logout_redirect_uri = URI(post_logout_redirect_uri)
-
13
params = ["state=#{CGI.escape(state)}"]
-
13
params << post_logout_redirect_uri.query if post_logout_redirect_uri.query
-
13
post_logout_redirect_uri.query = params.join("&")
-
13
post_logout_redirect_uri = post_logout_redirect_uri.to_s
-
end
-
-
65
@oidc_logout_redirect = post_logout_redirect_uri
-
-
65
require_response(:_oidc_logout_response)
-
end
-
-
end
-
-
13
redirect_logout_with_error(error_message)
-
end
-
-
redirect_response_error("invalid_request")
-
end
-
end
-
-
13
def _oidc_logout_response
-
52
redirect(oidc_logout_redirect)
-
end
-
-
13
private
-
-
# Logout
-
-
13
def validate_oidc_logout_params
-
# check if valid token hint type
-
78
return unless (redirect_uri = param_or_nil("post_logout_redirect_uri"))
-
-
65
return if check_valid_no_fragment_uri?(redirect_uri)
-
-
redirect_logout_with_error(oauth_invalid_client_message)
-
end
-
-
13
def redirect_logout_with_error(error_message = oauth_invalid_client_message)
-
13
set_notice_flash(error_message)
-
13
redirect(logout_redirect)
-
end
-
-
13
def oauth_server_metadata_body(*)
-
13
super.tap do |data|
-
9
data[:end_session_endpoint] = oidc_logout_url
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oidc_self_issued, :OidcSelfIssued) do
-
13
depends :oidc, :oidc_dynamic_client_registration
-
-
13
auth_value_method :oauth_application_scopes, %w[openid profile email address phone]
-
13
auth_value_method :oauth_jwt_jws_algorithms_supported, %w[RS256]
-
-
9
SELF_ISSUED_DEFAULT_APPLICATION_PARAMS = {
-
4
"scope" => "openid profile email address phone",
-
"response_types" => ["id_token"],
-
"subject_type" => "pairwise",
-
"id_token_signed_response_alg" => "RS256",
-
"request_object_signing_alg" => "RS256",
-
"grant_types" => %w[implicit]
-
}.freeze
-
-
13
def oauth_application
-
299
return @oauth_application if defined?(@oauth_application)
-
-
26
return super unless (registration = param_or_nil("registration"))
-
-
# self-issued!
-
26
redirect_uri = param_or_nil("client_id")
-
-
26
registration_params = JSON.parse(registration)
-
-
26
registration_params = SELF_ISSUED_DEFAULT_APPLICATION_PARAMS.merge(registration_params)
-
-
26
client_params = validate_client_registration_params(registration_params)
-
-
18
request.params["redirect_uri"] = client_params[oauth_applications_client_id_column] = redirect_uri
-
26
client_params[oauth_applications_redirect_uri_column] ||= redirect_uri
-
-
26
@oauth_application = client_params
-
end
-
-
13
private
-
-
13
def oauth_response_types_supported
-
26
%w[id_token]
-
end
-
-
13
def request_object_signing_alg_values_supported
-
%w[none RS256]
-
end
-
-
13
def id_token_claims(oauth_grant, signing_algorithm)
-
13
claims = super
-
-
13
return claims unless claims[:client_id] == oauth_grant[oauth_grants_redirect_uri_column]
-
-
# https://openid.net/specs/openid-connect-core-1_0.html#SelfIssued - 7.4
-
-
13
pub_key = oauth_jwt_public_keys[signing_algorithm]
-
13
pub_key = pub_key.first if pub_key.is_a?(Array)
-
9
claims[:sub_jwk] = sub_jwk = jwk_export(pub_key)
-
-
9
claims[:iss] = "https://self-issued.me"
-
-
9
claims[:aud] = oauth_grant[oauth_grants_redirect_uri_column]
-
-
13
jwk_thumbprint = jwk_thumbprint(sub_jwk)
-
-
9
claims[:sub] = Base64.urlsafe_encode64(jwk_thumbprint, padding: false)
-
-
13
claims
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth/oauth"
-
-
13
module Rodauth
-
13
Feature.define(:oidc_session_management, :OidcSessionManagement) do
-
13
depends :oidc
-
-
13
view "check_session", "Check Session", "check_session"
-
-
13
auth_value_method :oauth_oidc_user_agent_state_cookie_key, "_rodauth_oauth_user_agent_state"
-
13
auth_value_method :oauth_oidc_user_agent_state_cookie_options, {}.freeze
-
13
auth_value_method :oauth_oidc_user_agent_state_cookie_expires_in, 365 * 24 * 60 * 60 # 1 year
-
-
13
auth_value_method :oauth_oidc_user_agent_state_js, nil
-
-
13
auth_value_methods(
-
:oauth_oidc_session_management_salt
-
)
-
# /authorize
-
13
auth_server_route(:check_session) do |r|
-
13
allow_cors(r)
-
-
13
r.get do
-
13
set_title(:check_session_page_title)
-
13
scope.view(_view_opts("check_session").merge(layout: false))
-
end
-
end
-
-
13
def clear_session
-
39
super
-
-
# update user agent state in the process
-
# TODO: dangerous if this gets overidden by the user
-
-
39
user_agent_state_cookie_opts = Hash[oauth_oidc_user_agent_state_cookie_options]
-
27
user_agent_state_cookie_opts[:value] = oauth_unique_id_generator
-
27
user_agent_state_cookie_opts[:secure] = true
-
39
if oauth_oidc_user_agent_state_cookie_expires_in
-
27
user_agent_state_cookie_opts[:expires] = convert_timestamp(Time.now + oauth_oidc_user_agent_state_cookie_expires_in)
-
end
-
39
::Rack::Utils.set_cookie_header!(response.headers, oauth_oidc_user_agent_state_cookie_key, user_agent_state_cookie_opts)
-
end
-
-
13
private
-
-
13
def do_authorize(*)
-
13
params, mode = super
-
-
9
params["session_state"] = generate_session_state
-
-
13
[params, mode]
-
end
-
-
13
def response_error_params(*)
-
13
payload = super
-
-
13
return payload unless request.path == authorize_path
-
-
9
payload["session_state"] = generate_session_state
-
13
payload
-
end
-
-
13
def generate_session_state
-
26
salt = oauth_oidc_session_management_salt
-
-
26
uri = URI(redirect_uri)
-
26
origin = if uri.respond_to?(:origin)
-
16
uri.origin
-
else
-
# TODO: remove when not supporting uri < 0.11
-
10
"#{uri.scheme}://#{uri.host}#{":#{uri.port}" if uri.port != uri.default_port}"
-
end
-
26
session_id = "#{oauth_application[oauth_applications_client_id_column]} " \
-
8
"#{origin} " \
-
8
"#{request.cookies[oauth_oidc_user_agent_state_cookie_key]} #{salt}"
-
-
18
"#{Digest::SHA256.hexdigest(session_id)}.#{salt}"
-
end
-
-
13
def oauth_server_metadata_body(*)
-
13
super.tap do |data|
-
9
data[:check_session_iframe] = check_session_url
-
end
-
end
-
-
13
def oauth_oidc_session_management_salt
-
oauth_unique_id_generator
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "rodauth"
-
13
require "rodauth/oauth/version"
-
-
13
module Rodauth
-
13
module OAuth
-
13
module FeatureExtensions
-
13
def auth_server_route(name, *args, &blk)
-
156
routes = route(name, *args, &blk)
-
-
156
handle_meth = routes.last
-
-
156
define_method(:"#{handle_meth}_for_auth_server") do
-
8460
next unless is_authorization_server?
-
-
8460
send(:"#{handle_meth}_not_for_auth_server")
-
end
-
-
156
alias_method :"#{handle_meth}_not_for_auth_server", handle_meth
-
156
alias_method handle_meth, :"#{handle_meth}_for_auth_server"
-
-
# make all requests usable via internal_request feature
-
156
internal_request_method name
-
end
-
-
# override
-
13
def translatable_method(meth, value)
-
33630
define_method(meth) { |*args| translate(meth, value, *args) }
-
2453
auth_value_methods(meth)
-
end
-
end
-
end
-
-
13
Feature.prepend OAuth::FeatureExtensions
-
end
-
-
13
require "rodauth/oauth/railtie" if defined?(Rails)
-
# frozen_string_literal: true
-
-
13
module Rodauth
-
13
module OAuth
-
# rubocop:disable Naming/MethodName
-
13
def self.ExtendDatabase(db)
-
5803
Module.new do
-
5803
dataset = db.dataset
-
-
5803
if dataset.supports_returning?(:insert)
-
3124
def __insert_and_return__(dataset, _pkey, params)
-
845
dataset.returning.insert(params).first
-
end
-
else
-
2679
def __insert_and_return__(dataset, pkey, params)
-
532
id = dataset.insert(params)
-
508
if params.key?(pkey)
-
# mysql returns 0 when the primary key is a varchar.
-
72
id = params[pkey]
-
end
-
508
dataset.where(pkey => id).first
-
end
-
end
-
-
5803
if dataset.supports_returning?(:update)
-
3124
def __update_and_return__(dataset, params)
-
573
dataset.returning.update(params).first
-
end
-
else
-
2679
def __update_and_return__(dataset, params)
-
656
dataset.update(params)
-
632
dataset.first
-
end
-
end
-
-
5803
if dataset.respond_to?(:supports_insert_conflict?) && dataset.supports_insert_conflict?
-
1784
def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, to_update_extra = nil)
-
4888
to_update = Hash[(params.keys - unique_columns).map { |attribute| [attribute, Sequel[:excluded][attribute]] }]
-
-
344
to_update.merge!(to_update_extra) if to_update_extra
-
-
344
dataset = dataset.insert_conflict(
-
target: unique_columns,
-
update: to_update,
-
update_where: conds
-
)
-
-
344
__insert_and_return__(dataset, pkey, params)
-
end
-
-
1784
def __insert_or_do_nothing_and_return__(dataset, pkey, unique_columns, params)
-
80
__insert_and_return__(
-
dataset.insert_conflict(target: unique_columns),
-
pkey,
-
params
-
) || dataset.where(params).first
-
end
-
else
-
4019
def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, to_update_extra = nil)
-
6970
find_params, update_params = params.partition { |key, _| unique_columns.include?(key) }.map { |h| Hash[h] }
-
-
384
dataset_where = dataset.where(find_params)
-
384
record = if conds
-
184
dataset_where_conds = dataset_where.where(conds)
-
-
# this means that there's still a valid entry there, so return early
-
184
return if dataset_where.count != dataset_where_conds.count
-
-
184
dataset_where_conds.first
-
else
-
200
dataset_where.first
-
end
-
-
384
if record
-
232
update_params.merge!(to_update_extra) if to_update_extra
-
-
232
__update_and_return__(dataset_where, update_params)
-
else
-
152
__insert_and_return__(dataset, pkey, params)
-
end
-
end
-
-
4019
def __insert_or_do_nothing_and_return__(dataset, pkey, unique_columns, params)
-
180
find_params = params.slice(*unique_columns)
-
180
dataset.where(find_params).first || __insert_and_return__(dataset, pkey, params)
-
end
-
end
-
end
-
end
-
# rubocop:enable Naming/MethodName
-
end
-
end
-
# frozen_string_literal: true
-
-
13
require "uri"
-
13
require "net/http"
-
13
require "rodauth/oauth/ttl_store"
-
-
13
module Rodauth
-
13
module OAuth
-
13
module HTTPExtensions
-
13
REQUEST_CACHE = OAuth::TtlStore.new
-
-
13
private
-
-
13
def http_request(uri, form_data = nil)
-
377
uri = URI(uri)
-
-
377
http = Net::HTTP.new(uri.host, uri.port)
-
377
http.use_ssl = uri.scheme == "https"
-
377
http.open_timeout = 15
-
377
http.read_timeout = 15
-
377
http.write_timeout = 15 if http.respond_to?(:write_timeout)
-
-
377
if form_data
-
169
request = Net::HTTP::Post.new(uri.request_uri)
-
117
request["content-type"] = "application/x-www-form-urlencoded"
-
169
request.set_form_data(form_data)
-
else
-
208
request = Net::HTTP::Get.new(uri.request_uri)
-
end
-
261
request["accept"] = json_response_content_type
-
-
377
yield request if block_given?
-
-
377
response = http.request(request)
-
377
authorization_required unless (200..299).include?(response.code.to_i)
-
377
response
-
end
-
-
13
def http_request_with_cache(uri, *args)
-
130
uri = URI(uri)
-
-
130
response = http_request_cache[uri]
-
-
130
return response if response
-
-
117
http_request_cache.set(uri) do
-
117
response = http_request(uri, *args)
-
117
ttl = if response.key?("cache-control")
-
78
cache_control = response["cache-control"]
-
78
if cache_control.include?("no-cache")
-
nil
-
else
-
78
max_age = cache_control[/max-age=(\d+)/, 1].to_i
-
78
max_age.zero? ? nil : max_age
-
end
-
39
elsif response.key?("expires")
-
39
expires = response["expires"]
-
3
begin
-
39
Time.parse(expires).to_i - Time.now.to_i
-
rescue ArgumentError
-
nil
-
end
-
end
-
-
117
[JSON.parse(response.body, symbolize_names: true), ttl]
-
end
-
end
-
-
13
def http_request_cache
-
39
REQUEST_CACHE
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
10
module JWE
-
#
-
# this is a monkey-patch!
-
# it's necessary, as the original jwe does not support jwks.
-
# if this works long term, it may be merged upstreamm.
-
#
-
10
def self.__rodauth_oauth_decrypt_from_jwks(payload, jwks, alg: "RSA-OAEP", enc: "A128GCM")
-
30
header, enc_key, iv, ciphertext, tag = Serialization::Compact.decode(payload)
-
20
header = JSON.parse(header)
-
-
20
key = find_key_by_kid(jwks, header["kid"], alg, enc)
-
-
10
check_params(header, key)
-
-
10
cek = Alg.decrypt_cek(header["alg"], key, enc_key)
-
10
cipher = Enc.for(header["enc"], cek, iv, tag)
-
-
10
plaintext = cipher.decrypt(ciphertext, payload.split(".").first)
-
-
10
apply_zip(header, plaintext, :decompress)
-
end
-
-
10
def self.__rodauth_oauth_encrypt_from_jwks(payload, jwks, alg: "RSA-OAEP", enc: "A128GCM", **more_headers)
-
60
header = generate_header(alg, enc, more_headers)
-
-
60
key = find_key_by_alg_enc(jwks, alg, enc)
-
-
60
check_params(header, key)
-
60
payload = apply_zip(header, payload, :compress)
-
-
60
cipher = Enc.for(enc)
-
60
cipher.cek = key if alg == "dir"
-
-
60
json_hdr = header.to_json
-
60
ciphertext = cipher.encrypt(payload, Base64.jwe_encode(json_hdr))
-
-
60
generate_serialization(json_hdr, Alg.encrypt_cek(alg, key, cipher.cek), ciphertext, cipher)
-
end
-
-
10
def self.find_key_by_kid(jwks, kid, alg, enc)
-
20
raise DecodeError, "No key id (kid) found from token headers" unless kid
-
-
70
jwk = jwks.find { |key, _| (key[:kid] || key["kid"]) == kid }
-
-
20
raise DecodeError, "Could not find public key for kid #{kid}" unless jwk
-
20
raise DecodeError, "Expected a different encryption algorithm" unless alg == (jwk[:alg] || jwk["alg"])
-
20
raise DecodeError, "Expected a different encryption method" unless enc == (jwk[:enc] || jwk["enc"])
-
-
10
::JWT::JWK.import(jwk).keypair
-
end
-
-
10
def self.find_key_by_alg_enc(jwks, alg, enc)
-
60
jwk = jwks.find do |key, _|
-
120
(key[:alg] || key["alg"]) == alg &&
-
80
(key[:enc] || key["enc"]) == enc
-
end
-
-
60
raise DecodeError, "No key found" unless jwk
-
-
60
::JWT::JWK.import(jwk).keypair
-
end
-
end
-
# frozen_string_literal: true
-
-
#
-
# The TTL store is a data structure which keeps data by a key, and with a time-to-live.
-
# It is specifically designed for data which is static, i.e. for a certain key in a
-
# sufficiently large span, the value will be the same.
-
#
-
# Because of that, synchronizations around reads do not exist, while write synchronizations
-
# will be short-circuited by a read.
-
#
-
13
class Rodauth::OAuth::TtlStore
-
13
DEFAULT_TTL = 60 * 60 * 24 # default TTL is one day
-
-
13
def initialize
-
13
@store_mutex = Mutex.new
-
13
@store = {}
-
end
-
-
13
def [](key)
-
26
lookup(key, now)
-
end
-
-
13
def set(key, &block)
-
13
@store_mutex.synchronize do
-
# short circuit
-
13
return @store[key][:payload] if @store[key] && @store[key][:ttl] < now
-
end
-
-
13
payload, ttl = block.call
-
-
13
return payload unless ttl
-
-
13
@store_mutex.synchronize do
-
# given that the block call triggers network, and two requests for the same key be processed
-
# at the same time, this ensures the first one wins.
-
13
return @store[key][:payload] if @store[key] && @store[key][:ttl] < now
-
-
9
@store[key] = { payload: payload, ttl: ttl || (now + DEFAULT_TTL) }
-
end
-
13
@store[key][:payload]
-
end
-
-
13
def uncache(key)
-
@store_mutex.synchronize do
-
@store.delete(key)
-
end
-
end
-
-
13
private
-
-
13
def now
-
26
Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
end
-
-
# do not use directly!
-
13
def lookup(key, ttl)
-
26
return unless @store.key?(key)
-
-
13
value = @store[key]
-
-
13
return if value.empty?
-
-
13
return unless value[:ttl] > ttl
-
-
13
value[:payload]
-
end
-
end