All Files ( 96.15% covered at 408.94 hits/line )
39 files in total.
3453 relevant lines,
3320 lines covered and
133 lines missed.
(
96.15%
)
# 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 = {})
- 1341
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
)
# /authorize
- 13
auth_server_route(:authorize) do |r|
- 3281
require_authorizable_account
- 3151
before_authorize_route
- 3151
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?
- 8742
case request.path
when authorize_path
- 3281
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
- 2934
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
ACCESS_TYPES = %w[offline online].freeze
- 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 || ACCESS_TYPES.include?(access_type)
end
- 13
APPROVAL_PROMPTS = %w[force auto].freeze
- 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 || 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
).count.zero?
# 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)
- 104
error_message = oauth_authorize_parameter_required(parameter: parameter)
- 104
if accepts_json?
status_code = oauth_invalid_response_status
throw_json_response_error(status_code, "invalid_request", error_message)
else
- 104
scope.instance_variable_set(:@error, error_message)
- 104
scope.instance_variable_set(:@back_url, referer)
- 104
return_response(authorize_error_view)
end
end
- 13
def authorization_required
- 442
if accepts_json?
- 429
throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
else
- 13
set_redirect_error_flash(require_authorization_error_flash)
- 13
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?
- 8274
case request.path
when token_path
- 2626
false
else
- 9316
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?
- 2184
return true if only_json?
- 2171
(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
- 4923
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
- 45553
return @oauth_application if defined?(@oauth_application)
- 1391
@oauth_application = begin
- 3623
client_id = param_or_nil("client_id")
- 3623
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
- 871
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
- 845
token = fetch_access_token_from_authorization_header
end
- 871
return if token.nil? || token.empty?
- 689
token
end
- 13
def fetch_access_token_from_authorization_header(token_type = oauth_token_type)
- 897
value = request.env["HTTP_AUTHORIZATION"]
- 897
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
- 1183
return @authorization_token if defined?(@authorization_token)
# check if there is a token
- 377
access_token = fetch_access_token
- 377
return unless access_token
- 234
@authorization_token = oauth_grant_by_token(access_token)
end
- 13
def require_oauth_authorization(*scopes)
- 351
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)
- 31447
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
- 3554
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)
- 68134
path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str")
- 68134
return super unless File.exist?(path)
- 2541
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)
- 11242
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 = {})
- 815
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) &&
- 78
(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)
- 4
params = {
- 6
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)
- 205
html = +'<nav aria-label="Pagination"><ul class="pagination">'
- 205
html << oauth_management_pagination_link(paginated_ds.prev_page, label: oauth_management_pagination_previous_button)
- 205
html << oauth_management_pagination_link(paginated_ds.current_page - 1) unless paginated_ds.first_page?
- 205
html << oauth_management_pagination_link(paginated_ds.current_page, label: paginated_ds.current_page, current: true)
- 205
html << oauth_management_pagination_link(paginated_ds.current_page + 1) unless paginated_ds.last_page?
- 205
html << oauth_management_pagination_link(paginated_ds.next_page, label: oauth_management_pagination_next_button)
- 205
html << "</ul></nav>"
end
- 13
def oauth_management_pagination_link(page, label: page, current: false, classes: "")
- 681
classes += " disabled" if current || !page
- 681
classes += " active" if current
- 681
if page
- 337
params = URI.encode_www_form(request.GET.merge("page" => page))
- 337
href = "#{request.path}?#{params}"
- 221
<<-HTML
- 116
<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
- 232
<<-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|
- 546
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 = {})
- 143
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
- 20
{
- 32
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
- 55
{
- 88
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?
- 759
case request.path
when revoke_path
- 130
!json_request?
else
- 977
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
- 4
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).select do |k, _|
- 1352
VALID_METADATA_KEYS.include?(k)
end
- 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 = oauth_application = 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]
- 5
SELF_ISSUED_DEFAULT_APPLICATION_PARAMS = {
- 8
"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
- 8442
next unless is_authorization_server?
- 8442
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)
- 33891
define_method(meth) { |*args| translate(meth, value, *args) }
- 2444
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)
- 4018
def __insert_and_return__(dataset, _pkey, params)
- 1023
dataset.returning.insert(params).first
end
else
- 1785
def __insert_and_return__(dataset, pkey, params)
- 354
id = dataset.insert(params)
- 338
if params.key?(pkey)
# mysql returns 0 when the primary key is a varchar.
- 48
id = params[pkey]
end
- 338
dataset.where(pkey => id).first
end
end
- 5803
if dataset.supports_returning?(:update)
- 4018
def __update_and_return__(dataset, params)
- 791
dataset.returning.update(params).first
end
else
- 1785
def __update_and_return__(dataset, params)
- 438
dataset.update(params)
- 422
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)
- 504
find_params = params.select { |key, _| unique_columns.include?(key) }
- 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