All Files ( 96.39% covered at 380.62 hits/line )
30 files in total.
2714 relevant lines,
2616 lines covered and
98 lines missed.
(
96.39%
)
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_application_management, :OauthApplicationManagement) do
- 15
depends :oauth_management_base, :oauth_token_revocation
- 15
before "create_oauth_application"
- 15
after "create_oauth_application"
- 15
error_flash "There was an error registering your oauth application", "create_oauth_application"
- 15
notice_flash "Your oauth application has been registered", "create_oauth_application"
- 15
view "oauth_applications", "Oauth Applications", "oauth_applications"
- 15
view "oauth_application", "Oauth Application", "oauth_application"
- 15
view "new_oauth_application", "New Oauth Application", "new_oauth_application"
- 15
view "oauth_application_oauth_grants", "Oauth Application Grants", "oauth_application_oauth_grants"
# Application
- 15
APPLICATION_REQUIRED_PARAMS = %w[name scopes homepage_url redirect_uri client_secret].freeze
- 15
auth_value_method :oauth_application_required_params, APPLICATION_REQUIRED_PARAMS
- 15
(APPLICATION_REQUIRED_PARAMS + %w[description client_id]).each do |param|
- 105
auth_value_method :"oauth_application_#{param}_param", param
end
- 15
translatable_method :oauth_applications_name_label, "Name"
- 15
translatable_method :oauth_applications_description_label, "Description"
- 15
translatable_method :oauth_applications_scopes_label, "Default scopes"
- 15
translatable_method :oauth_applications_contacts_label, "Contacts"
- 15
translatable_method :oauth_applications_tos_uri_label, "Terms of service"
- 15
translatable_method :oauth_applications_policy_uri_label, "Policy"
- 15
translatable_method :oauth_applications_jwks_label, "JSON Web Keys"
- 15
translatable_method :oauth_applications_jwks_uri_label, "JSON Web Keys URI"
- 15
translatable_method :oauth_applications_homepage_url_label, "Homepage URL"
- 15
translatable_method :oauth_applications_redirect_uri_label, "Redirect URI"
- 15
translatable_method :oauth_applications_client_secret_label, "Client Secret"
- 15
translatable_method :oauth_applications_client_id_label, "Client ID"
- 15
%w[type token refresh_token expires_in revoked_at].each do |param|
- 75
translatable_method :"oauth_grants_#{param}_label", param.gsub("_", " ").capitalize
end
- 15
button "Register", "oauth_application"
- 15
button "Revoke", "oauth_grant_revoke"
- 15
auth_value_method :oauth_applications_oauth_grants_path, "oauth-grants"
- 15
auth_value_method :oauth_applications_route, "oauth-applications"
- 15
auth_value_method :oauth_applications_per_page, 20
- 15
auth_value_method :oauth_applications_id_pattern, Integer
- 15
auth_value_method :oauth_grants_per_page, 20
- 15
translatable_method :invalid_url_message, "Invalid URL"
- 15
translatable_method :null_error_message, "is not filled"
- 15
translatable_method :oauth_no_applications_text, "No oauth applications yet!"
- 15
translatable_method :oauth_no_grants_text, "No oauth grants yet!"
- 15
auth_value_methods(
:oauth_application_path
)
- 15
def oauth_applications_path(opts = {})
- 1299
route_path(oauth_applications_route, opts)
end
- 15
def oauth_application_path(id)
- 255
"#{oauth_applications_path}/#{id}"
end
# /oauth-applications routes
- 15
def load_oauth_application_management_routes
- 315
request.on(oauth_applications_route) do
- 315
check_csrf if check_csrf?
- 315
require_account
- 315
request.get "new" do
- 45
new_oauth_application_view
end
- 270
request.on(oauth_applications_id_pattern) do |id|
- 105
oauth_application = db[oauth_applications_table]
- 18
.where(oauth_applications_id_column => id)
- 18
.where(oauth_applications_account_id_column => account_id)
- 18
.first
- 105
next unless oauth_application
- 90
scope.instance_variable_set(:@oauth_application, oauth_application)
- 90
request.is do
- 30
request.get do
- 30
oauth_application_view
end
end
- 60
request.on(oauth_applications_oauth_grants_path) do
- 60
page = Integer(param_or_nil("page") || 1)
- 60
per_page = per_page_param(oauth_grants_per_page)
- 60
oauth_grants = db[oauth_grants_table]
- 9
.where(oauth_grants_oauth_application_id_column => id)
- 9
.order(Sequel.desc(oauth_grants_id_column))
- 60
scope.instance_variable_set(:@oauth_grants, oauth_grants.paginate(page, per_page))
- 60
request.is do
- 60
request.get do
- 60
oauth_application_oauth_grants_view
end
end
end
end
- 165
request.is do
- 165
request.get do
- 105
page = Integer(param_or_nil("page") || 1)
- 105
per_page = per_page_param(oauth_applications_per_page)
- 147
scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table]
- 18
.where(oauth_applications_account_id_column => account_id)
- 18
.order(Sequel.desc(oauth_applications_id_column))
- 18
.paginate(page, per_page))
- 105
oauth_applications_view
end
- 60
request.post do
- 60
catch_error do
- 60
validate_oauth_application_params
- 30
transaction do
- 30
before_create_oauth_application
- 30
id = create_oauth_application
- 30
after_create_oauth_application
- 30
set_notice_flash create_oauth_application_notice_flash
- 30
redirect "#{request.path}/#{id}"
end
end
- 30
set_error_flash create_oauth_application_error_flash
- 30
new_oauth_application_view
end
end
end
end
- 15
private
- 15
def oauth_application_params
- 240
@oauth_application_params ||= oauth_application_required_params.each_with_object({}) do |param, params|
- 300
value = request.params[__send__(:"oauth_application_#{param}_param")]
- 300
if value && !value.empty?
- 195
params[param] = value
else
- 105
set_field_error(param, null_error_message)
end
end
end
- 15
def validate_oauth_application_params
- 60
oauth_application_params.each do |key, value|
- 195
if key == oauth_application_homepage_url_param
- 45
set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
- 147
elsif key == oauth_application_redirect_uri_param
- 45
if value.respond_to?(:each)
- 15
value.each do |uri|
- 30
next if uri.empty?
- 30
set_field_error(key, invalid_url_message) unless check_valid_no_fragment_uri?(uri)
end
else
- 30
set_field_error(key, invalid_url_message) unless check_valid_no_fragment_uri?(value)
end
- 102
elsif key == oauth_application_scopes_param
- 30
value.each do |scope|
- 60
set_field_error(key, oauth_invalid_scope_message) unless oauth_application_scopes.include?(scope)
end
end
end
- 60
throw :rodauth_error if @field_errors && !@field_errors.empty?
end
- 15
def create_oauth_application
- 8
create_params = {
- 19
oauth_applications_account_id_column => account_id,
- 3
oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
- 3
oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
- 3
oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
- 3
oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
}
- 30
redirect_uris = oauth_application_params[oauth_application_redirect_uri_param]
- 30
redirect_uris = redirect_uris.to_a.reject(&:empty?).join(" ") if redirect_uris.respond_to?(:each)
- 30
create_params[oauth_applications_redirect_uri_column] = redirect_uris unless redirect_uris.empty?
# set client ID/secret pairs
- 30
set_client_secret(create_params, oauth_application_params[oauth_application_client_secret_param])
- 30
if create_params[oauth_applications_scopes_column]
- 30
create_params[oauth_applications_scopes_column] = create_params[oauth_applications_scopes_column].join(oauth_scope_separator)
end
- 30
rescue_from_uniqueness_error do
- 30
create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
- 30
db[oauth_applications_table].insert(create_params)
end
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_assertion_base, :OauthAssertionBase) do
- 15
depends :oauth_base
- 15
auth_value_methods(
:assertion_grant_type?,
:client_assertion_type?,
:assertion_grant_type,
:client_assertion_type
)
- 15
private
- 15
def validate_token_params
- 120
return super unless assertion_grant_type?
- 60
redirect_response_error("invalid_grant") unless param_or_nil("assertion")
end
- 15
def require_oauth_application
- 240
if assertion_grant_type?
- 60
@oauth_application = __send__(:"require_oauth_application_from_#{assertion_grant_type}_assertion_issuer", param("assertion"))
- 177
elsif client_assertion_type?
- 162
@oauth_application = __send__(:"require_oauth_application_from_#{client_assertion_type}_assertion_subject",
- 24
param("client_assertion"))
- 90
if (client_id = param_or_nil("client_id")) &&
- 3
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.
- 30
redirect_response_error("invalid_grant")
end
else
- 45
super
end
end
- 15
def account_from_bearer_assertion_subject(subject)
- 60
__insert_or_do_nothing_and_return__(
- 9
db[accounts_table],
- 9
account_id_column,
- 9
[login_column],
- 9
login_column => subject
)
end
- 15
def create_token(grant_type)
- 75
return super unless assertion_grant_type?(grant_type) && supported_grant_type?(grant_type)
- 60
account = __send__(:"account_from_#{assertion_grant_type}_assertion", param("assertion"))
- 60
redirect_response_error("invalid_grant") unless account
- 60
grant_scopes = if param_or_nil("scope")
- 30
redirect_response_error("invalid_scope") unless check_valid_scopes?
- 15
scopes
else
- 30
@oauth_application[oauth_applications_scopes_column]
end
- 12
grant_params = {
- 30
oauth_grants_type_column => grant_type,
- 6
oauth_grants_account_id_column => account[account_id_column],
- 6
oauth_grants_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
- 6
oauth_grants_scopes_column => grant_scopes
}
- 45
generate_token(grant_params, false)
end
- 87
def assertion_grant_type?(grant_type = param("grant_type"))
- 435
grant_type.start_with?("urn:ietf:params:oauth:grant-type:")
end
- 51
def client_assertion_type?(client_assertion_type = param("client_assertion_type"))
- 180
client_assertion_type.start_with?("urn:ietf:params:oauth:client-assertion-type:")
end
- 39
def assertion_grant_type(grant_type = param("grant_type"))
- 120
grant_type.delete_prefix("urn:ietf:params:oauth:grant-type:").tr("-", "_")
end
- 42
def client_assertion_type(assertion_type = param("client_assertion_type"))
- 135
assertion_type.delete_prefix("urn:ietf:params:oauth:client-assertion-type:").tr("-", "_")
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_authorization_code_grant, :OauthAuthorizationCodeGrant) do
- 15
depends :oauth_authorize_base
- 15
auth_value_method :oauth_response_mode, "form_post"
- 15
def oauth_grant_types_supported
- 3579
super | %w[authorization_code]
end
- 15
def oauth_response_types_supported
- 1497
super | %w[code]
end
- 15
def oauth_response_modes_supported
- 1407
super | %w[query form_post]
end
- 15
private
- 15
def validate_authorize_params
- 1914
super
- 1719
response_mode = param_or_nil("response_mode")
- 1719
redirect_response_error("invalid_request") if response_mode && !oauth_response_modes_supported.include?(response_mode)
end
- 15
def validate_token_params
- 1242
redirect_response_error("invalid_request") if param_or_nil("grant_type") == "authorization_code" && !param_or_nil("code")
- 1242
super
end
- 60
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
- 627
response_mode ||= oauth_response_mode
- 627
redirect_response_error("invalid_request") unless response_mode.nil? || supported_response_mode?(response_mode)
- 627
response_type = param_or_nil("response_type")
- 627
redirect_response_error("invalid_request") unless response_type.nil? || supported_response_type?(response_type)
- 627
case response_type
when "code", nil
- 300
response_params.replace(_do_authorize_code)
end
- 612
response_params["state"] = param("state") if param_or_nil("state")
- 612
[response_params, response_mode]
end
- 15
def _do_authorize_code
- 132
create_params = {
- 357
oauth_grants_type_column => "authorization_code",
- 93
oauth_grants_account_id_column => account_id
}
- 492
{ "code" => create_oauth_grant(create_params) }
end
- 15
def authorize_response(params, mode)
- 360
redirect_url = URI.parse(redirect_uri)
- 360
case mode
when "query"
- 765
params = params.map { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v)}" }
- 330
params << redirect_url.query if redirect_url.query
- 330
redirect_url.query = params.join("&")
- 330
redirect(redirect_url.to_s)
when "form_post"
- 36
scope.view layout: false, inline: <<-FORM
<html>
<head><title>Authorized</title></head>
<body onload="javascript:document.forms[0].submit()">
<form method="post" action="#{redirect_uri}">
#{
- 6
params.map do |name, value|
- 30
"<input type=\"hidden\" name=\"#{scope.h(name)}\" value=\"#{scope.h(value)}\" />"
end.join
}
- 6
<input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
</form>
</body>
</html>
FORM
end
end
- 15
def _redirect_response_error(redirect_url, query_params)
- 390
response_mode = param_or_nil("response_mode") || oauth_response_mode
- 390
case response_mode
when "form_post"
- 15
response["Content-Type"] = "text/html"
- 18
response.write <<-FORM
<html>
<head><title></title></head>
<body onload="javascript:document.forms[0].submit()">
<form method="post" action="#{redirect_uri}">
#{
- 3
query_params.map do |name, value|
- 30
"<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
end.join
}
</form>
</body>
</html>
FORM
- 15
request.halt
else
- 375
super
end
end
- 15
def create_token(grant_type)
- 1032
return super unless supported_grant_type?(grant_type, "authorization_code")
- 196
grant_params = {
- 533
oauth_grants_code_column => param("code"),
- 141
oauth_grants_redirect_uri_column => param("redirect_uri"),
- 141
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
}
- 732
create_token_from_authorization_code(grant_params)
end
- 15
def check_valid_response_type?
- 1215
response_type = param_or_nil("response_type")
- 1215
response_type == "code" || response_type == "none" || super
end
- 15
def oauth_server_metadata_body(*)
- 240
super.tap do |data|
- 240
data[:authorization_endpoint] = authorize_url
end
end
end
end
# frozen_string_literal: true
- 15
require "ipaddr"
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_authorize_base, :OauthAuthorizeBase) do
- 15
depends :oauth_base
- 15
before "authorize"
- 15
after "authorize"
- 15
view "authorize", "Authorize", "authorize"
- 15
view "authorize_error", "Authorize Error", "authorize_error"
- 15
button "Authorize", "oauth_authorize"
- 15
button "Back to Client Application", "oauth_authorize_post"
- 15
auth_value_method :use_oauth_access_type?, false
- 15
auth_value_method :oauth_grants_access_type_column, :access_type
- 15
translatable_method :authorize_page_lead, "The application %<name>s would like to access your data"
- 15
translatable_method :oauth_grants_scopes_label, "Scopes"
- 15
translatable_method :oauth_applications_contacts_label, "Contacts"
- 15
translatable_method :oauth_applications_tos_uri_label, "Terms of service URL"
- 15
translatable_method :oauth_applications_policy_uri_label, "Policy URL"
- 15
translatable_method :oauth_unsupported_response_type_message, "Unsupported response type"
- 15
translatable_method :oauth_authorize_parameter_required, "Invalid or missing '%<parameter>s'"
# /authorize
- 15
auth_server_route(:authorize) do |r|
- 2271
require_authorizable_account
- 2139
before_authorize_route
- 2139
validate_authorize_params
- 1719
r.get do
- 1047
authorize_view
end
- 672
r.post do
- 672
params, mode = transaction do
- 672
before_authorize
- 672
do_authorize
end
- 642
authorize_response(params, mode)
end
end
- 15
def check_csrf?
- 9255
case request.path
- 1812
when authorize_path
- 2271
only_json? ? false : super
else
- 6984
super
end
end
- 15
def authorize_scopes
- 1047
scopes || begin
- 120
oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)
end
end
- 15
private
- 15
def validate_authorize_params
- 1899
redirect_authorize_error("client_id") unless oauth_application
- 1869
redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
- 1869
if (redirect_uri = param_or_nil("redirect_uri"))
- 420
normalized_redirect_uri = normalize_redirect_uri_for_comparison(redirect_uri)
- 420
redirect_authorize_error("redirect_uri") unless redirect_uris.include?(normalized_redirect_uri)
- 1446
elsif redirect_uris.size > 1
- 15
redirect_authorize_error("redirect_uri")
end
- 1839
redirect_response_error("unsupported_response_type") unless check_valid_response_type?
- 1794
redirect_response_error("invalid_request") unless check_valid_access_type? && check_valid_approval_prompt?
- 1794
try_approval_prompt if use_oauth_access_type? && request.get?
- 1794
redirect_response_error("invalid_scope") if (request.post? || param_or_nil("scope")) && !check_valid_scopes?
end
- 15
def check_valid_response_type?
- 45
false
end
- 15
ACCESS_TYPES = %w[offline online].freeze
- 15
def check_valid_access_type?
- 1794
return true unless use_oauth_access_type?
- 45
access_type = param_or_nil("access_type")
- 45
!access_type || ACCESS_TYPES.include?(access_type)
end
- 15
APPROVAL_PROMPTS = %w[force auto].freeze
- 15
def check_valid_approval_prompt?
- 1794
return true unless use_oauth_access_type?
- 45
approval_prompt = param_or_nil("approval_prompt")
- 45
!approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
end
- 15
def try_approval_prompt
- 30
approval_prompt = param_or_nil("approval_prompt")
- 30
return unless approval_prompt && approval_prompt == "auto"
- 14
return if db[oauth_grants_table].where(
oauth_grants_account_id_column => account_id,
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"
- 4
).count.zero?
# if there's a previous oauth grant for the params combo, it means that this user has approved before.
- 15
request.env["REQUEST_METHOD"] = "POST"
end
- 27
def redirect_authorize_error(parameter, referer = request.referer || default_redirect)
- 60
error_message = oauth_authorize_parameter_required(parameter: parameter)
- 60
if accepts_json?
status_code = oauth_invalid_response_status
throw_json_response_error(status_code, "invalid_request", error_message)
else
- 60
scope.instance_variable_set(:@error, error_message)
- 60
scope.instance_variable_set(:@back_url, referer)
- 60
return_response(authorize_error_view)
end
end
- 15
def authorization_required
- 360
if accepts_json?
- 345
throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
else
- 15
set_redirect_error_flash(require_authorization_error_flash)
- 15
redirect(authorize_path)
end
end
- 15
def do_authorize(*args); end
- 15
def authorize_response(params, mode); end
- 150
def create_token_from_authorization_code(grant_params, should_generate_refresh_token = !use_oauth_access_type?, oauth_grant: nil)
# fetch oauth grant
- 687
oauth_grant ||= valid_locked_oauth_grant(grant_params)
- 627
should_generate_refresh_token ||= oauth_grant[oauth_grants_access_type_column] == "offline"
- 627
generate_token(oauth_grant, should_generate_refresh_token)
end
- 15
def create_oauth_grant(create_params = {})
- 552
create_params[oauth_grants_oauth_application_id_column] ||= oauth_application[oauth_applications_id_column]
- 552
create_params[oauth_grants_redirect_uri_column] ||= redirect_uri
- 552
create_params[oauth_grants_expires_in_column] ||= Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in)
- 552
create_params[oauth_grants_scopes_column] ||= scopes.join(oauth_scope_separator)
- 552
if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
- 30
create_params[oauth_grants_access_type_column] = access_type
end
- 552
ds = db[oauth_grants_table]
- 552
create_params[oauth_grants_code_column] = oauth_unique_id_generator
- 552
if oauth_reuse_access_token
- 120
unique_conds = Hash[oauth_grants_unique_columns.map { |column| [column, create_params[column]] }]
- 30
valid_grant = valid_oauth_grant_ds(unique_conds).select(oauth_grants_id_column).first
- 30
if valid_grant
- 30
create_params[oauth_grants_id_column] = valid_grant[oauth_grants_id_column]
- 30
rescue_from_uniqueness_error do
- 30
__insert_or_update_and_return__(
ds,
- 3
oauth_grants_id_column,
- 3
[oauth_grants_id_column],
create_params
)
end
- 30
return create_params[oauth_grants_code_column]
end
end
- 522
rescue_from_uniqueness_error do
- 567
if __one_oauth_token_per_account
- 228
__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
- 339
__insert_and_return__(ds, oauth_grants_id_column, create_params)
end
end
- 507
create_params[oauth_grants_code_column]
end
- 15
def normalize_redirect_uri_for_comparison(redirect_uri)
- 420
uri = URI(redirect_uri)
- 420
return redirect_uri unless uri.scheme == "http" && uri.port
- 60
hostname = uri.hostname
# https://www.rfc-editor.org/rfc/rfc8252#section-7.3
# ignore (potentially ephemeral) port number for native clients per RFC8252
- 16
begin
- 60
ip = IPAddr.new(hostname)
- 30
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.
- 30
uri.port = nil if hostname == "localhost"
end
- 60
uri.to_s
end
end
end
# frozen_string_literal: true
- 15
require "time"
- 15
require "base64"
- 15
require "securerandom"
- 15
require "cgi"
- 15
require "digest/sha2"
- 15
require "rodauth/version"
- 15
require "rodauth/oauth"
- 15
require "rodauth/oauth/database_extensions"
- 15
require "rodauth/oauth/http_extensions"
- 15
module Rodauth
- 15
Feature.define(:oauth_base, :OauthBase) do
- 15
include OAuth::HTTPExtensions
- 15
EMPTY_HASH = {}.freeze
- 15
auth_value_methods(:http_request)
- 15
auth_value_methods(:http_request_cache)
- 15
before "token"
- 15
error_flash "Please authorize to continue", "require_authorization"
- 15
error_flash "You are not authorized to revoke this token", "revoke_unauthorized_account"
- 15
button "Cancel", "oauth_cancel"
- 15
auth_value_method :json_response_content_type, "application/json"
- 15
auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
- 15
auth_value_method :oauth_access_token_expires_in, 60 * 60 # 60 minutes
- 15
auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
- 15
auth_value_method :oauth_unique_id_generation_retries, 3
- 15
auth_value_method :oauth_token_endpoint_auth_methods_supported, %w[client_secret_basic client_secret_post]
- 15
auth_value_method :oauth_grant_types_supported, %w[refresh_token]
- 15
auth_value_method :oauth_response_types_supported, []
- 15
auth_value_method :oauth_response_modes_supported, []
- 15
auth_value_method :oauth_valid_uri_schemes, %w[https]
- 15
auth_value_method :oauth_scope_separator, " "
# OAuth Grants
- 15
auth_value_method :oauth_grants_table, :oauth_grants
- 15
auth_value_method :oauth_grants_id_column, :id
- 11
%i[
account_id oauth_application_id type
redirect_uri code scopes
expires_in revoked_at
token refresh_token
- 4
].each do |column|
- 150
auth_value_method :"oauth_grants_#{column}_column", column
end
# Enables Token Hash
- 15
auth_value_method :oauth_grants_token_hash_column, :token
- 15
auth_value_method :oauth_grants_refresh_token_hash_column, :refresh_token
# Access Token reuse
- 15
auth_value_method :oauth_reuse_access_token, false
- 15
auth_value_method :oauth_applications_table, :oauth_applications
- 15
auth_value_method :oauth_applications_id_column, :id
- 11
%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
- 4
].each do |column|
- 300
auth_value_method :"oauth_applications_#{column}_column", column
end
# Enables client secret Hash
- 15
auth_value_method :oauth_applications_client_secret_hash_column, :client_secret
- 15
auth_value_method :oauth_authorization_required_error_status, 401
- 15
auth_value_method :oauth_invalid_response_status, 400
- 15
auth_value_method :oauth_already_in_use_response_status, 409
# Feature options
- 15
auth_value_method :oauth_application_scopes, []
- 15
auth_value_method :oauth_token_type, "bearer"
- 15
auth_value_method :oauth_refresh_token_protection_policy, "rotation" # can be: none, sender_constrained, rotation
- 15
translatable_method :oauth_invalid_client_message, "Invalid client"
- 15
translatable_method :oauth_invalid_grant_type_message, "Invalid grant type"
- 15
translatable_method :oauth_invalid_grant_message, "Invalid grant"
- 15
translatable_method :oauth_invalid_scope_message, "Invalid scope"
- 15
translatable_method :oauth_unsupported_token_type_message, "Invalid token type hint"
- 15
translatable_method :oauth_already_in_use_message, "error generating unique token"
- 15
auth_value_method :oauth_already_in_use_error_code, "invalid_request"
- 15
auth_value_method :oauth_invalid_grant_type_error_code, "unsupported_grant_type"
- 15
auth_value_method :is_authorization_server?, true
- 15
auth_value_methods(:only_json?)
- 15
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
# METADATA
- 15
auth_value_method :oauth_metadata_service_documentation, nil
- 15
auth_value_method :oauth_metadata_ui_locales_supported, nil
- 15
auth_value_method :oauth_metadata_op_policy_uri, nil
- 15
auth_value_method :oauth_metadata_op_tos_uri, nil
- 15
auth_value_methods(
:fetch_access_token,
:secret_hash,
:generate_token_hash,
:secret_matches?,
:authorization_server_url,
:oauth_unique_id_generator,
:oauth_grants_unique_columns,
:require_authorizable_account,
:oauth_account_ds,
:oauth_application_ds
)
# /token
- 15
auth_server_route(:token) do |r|
- 1782
require_oauth_application
- 1512
before_token_route
- 1512
r.post do
- 1512
catch_error do
- 1512
validate_token_params
- 1467
oauth_grant = nil
- 1467
transaction do
- 1467
before_token
- 1467
oauth_grant = create_token(param("grant_type"))
end
- 927
json_response_success(json_access_token_payload(oauth_grant))
end
throw_json_response_error(oauth_invalid_response_status, "invalid_request")
end
end
- 15
def load_oauth_server_metadata_route(issuer = nil)
- 180
request.on(".well-known") do
- 180
request.get("oauth-authorization-server") do
- 180
json_response_success(oauth_server_metadata_body(issuer), true)
end
end
end
- 15
def check_csrf?
- 8865
case request.path
- 1734
when token_path
- 1782
false
else
- 7083
super
end
end
- 15
def oauth_token_subject
- 120
return unless authorization_token
- 120
authorization_token[oauth_grants_account_id_column] ||
- 9
db[oauth_applications_table].where(
- 3
oauth_applications_id_column => authorization_token[oauth_grants_oauth_application_id_column]
- 3
).select_map(oauth_applications_client_id_column).first
end
- 15
def current_oauth_account
- 120
account_id = authorization_token[oauth_grants_account_id_column]
- 120
return unless account_id
- 90
oauth_account_ds(account_id).first
end
- 15
def current_oauth_application
- 135
oauth_application_ds(authorization_token[oauth_grants_oauth_application_id_column]).first
end
- 15
def accepts_json?
- 1515
return true if only_json?
- 1500
(accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
end
# copied from the jwt feature
- 15
def json_request?
- 300
return super if features.include?(:jsonn)
- 300
return @json_request if defined?(@json_request)
- 300
@json_request = request.content_type =~ json_request_regexp
end
- 15
def scopes
- 6321
scope = request.params["scope"]
- 6321
case scope
when Array
- 2328
scope
when String
- 3648
scope.split(" ")
end
end
- 15
def redirect_uri
- 3741
param_or_nil("redirect_uri") || begin
- 2736
return unless oauth_application
- 2736
redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
- 2736
redirect_uris.size == 1 ? redirect_uris.first : nil
end
end
- 15
def oauth_application
- 30282
return @oauth_application if defined?(@oauth_application)
- 600
@oauth_application = begin
- 2244
client_id = param_or_nil("client_id")
- 2244
return unless client_id
- 2169
db[oauth_applications_table].filter(oauth_applications_client_id_column => client_id).first
end
end
- 15
def fetch_access_token
- 912
if (token = request.params["access_token"])
- 33
if request.post? && !(request.content_type.start_with?("application/x-www-form-urlencoded") &&
request.params.size == 1)
return
end
else
- 882
value = request.env["HTTP_AUTHORIZATION"]
- 882
return unless value && !value.empty?
- 777
scheme, token = value.split(" ", 2)
- 777
return unless scheme.downcase == oauth_token_type
end
- 807
return if token.nil? || token.empty?
- 702
token
end
- 15
def authorization_token
- 1080
return @authorization_token if defined?(@authorization_token)
# check if there is a token
- 375
access_token = fetch_access_token
- 375
return unless access_token
- 210
@authorization_token = oauth_grant_by_token(access_token)
end
- 15
def require_oauth_authorization(*scopes)
- 360
authorization_required unless authorization_token
- 165
token_scopes = authorization_token[oauth_grants_scopes_column].split(oauth_scope_separator)
- 360
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
end
- 15
def use_date_arithmetic?
- 4683
true
end
# override
- 15
def translate(key, default, args = EMPTY_HASH)
- 24660
return i18n_translate(key, default, **args) if features.include?(:i18n)
# do not attempt to translate by default
- 120
return default if args.nil?
- 120
default % args
end
- 15
def post_configure
- 4698
super
- 4698
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.
- 4698
return unless is_authorization_server?
- 4488
self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
# Check whether we can reutilize db entries for the same account / application pair
- 4488
one_oauth_token_per_account = db.indexes(oauth_grants_table).values.any? do |definition|
- 23638
definition[:unique] &&
- 5361
definition[:columns] == oauth_grants_unique_columns
end
- 5817
self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
end
- 15
private
- 15
def oauth_account_ds(account_id)
- 267
account_ds(account_id)
end
- 15
def oauth_application_ds(oauth_application_id)
- 135
db[oauth_applications_table].where(oauth_applications_id_column => oauth_application_id)
end
- 15
def require_authorizable_account
- 2556
require_account
end
- 15
def rescue_from_uniqueness_error(&block)
- 2079
retries = oauth_unique_id_generation_retries
- 556
begin
- 2151
transaction(savepoint: :only, &block)
- 96
rescue Sequel::UniqueConstraintViolation
- 120
redirect_response_error("already_in_use") if retries.zero?
- 90
retries -= 1
- 90
retry
end
end
# OAuth Token Unique/Reuse
- 15
def oauth_grants_unique_columns
- 6182
[
- 17737
oauth_grants_oauth_application_id_column,
- 5373
oauth_grants_account_id_column,
- 5373
oauth_grants_scopes_column
]
end
- 15
def authorization_server_url
- 1506
base_url
end
- 15
def template_path(page)
- 52587
path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str")
- 52587
return super unless File.exist?(path)
- 2058
path
end
# to be used internally. Same semantics as require account, must:
# fetch an authorization basic header
# parse client id and secret
#
- 15
def require_oauth_application
- 1917
@oauth_application = if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
# client_secret_basic
- 450
require_oauth_application_from_client_secret_basic(token)
- 1467
elsif (client_id = param_or_nil("client_id"))
- 1392
if (client_secret = param_or_nil("client_secret"))
# client_secret_post
- 1062
require_oauth_application_from_client_secret_post(client_id, client_secret)
else
# none
- 330
require_oauth_application_from_none(client_id)
end
else
- 75
authorization_required
end
end
- 15
def require_oauth_application_from_client_secret_basic(token)
- 450
client_id, client_secret = Base64.decode64(token).split(/:/, 2)
- 450
authorization_required unless client_id
- 450
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
- 333
authorization_required unless supports_auth_method?(oauth_application,
- 204
"client_secret_basic") && secret_matches?(oauth_application, client_secret)
- 435
oauth_application
end
- 15
def require_oauth_application_from_client_secret_post(client_id, client_secret)
- 1062
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
- 781
authorization_required unless supports_auth_method?(oauth_application,
- 488
"client_secret_post") && secret_matches?(oauth_application, client_secret)
- 1047
oauth_application
end
- 15
def require_oauth_application_from_none(client_id)
- 330
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
- 330
authorization_required unless supports_auth_method?(oauth_application, "none")
- 240
oauth_application
end
- 15
def supports_auth_method?(oauth_application, auth_method)
- 1737
supported_auth_methods = if oauth_application[oauth_applications_token_endpoint_auth_method_column]
- 165
oauth_application[oauth_applications_token_endpoint_auth_method_column].split(/ +/)
else
- 1572
oauth_token_endpoint_auth_methods_supported
end
- 1737
supported_auth_methods.include?(auth_method)
end
- 15
def require_oauth_application_from_account
- 15
ds = db[oauth_applications_table]
- 3
.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)
- 15
@oauth_application = ds.qualify.first
- 15
return if @oauth_application
set_redirect_error_flash revoke_unauthorized_account_error_flash
redirect request.referer || "/"
end
- 15
def secret_matches?(oauth_application, secret)
- 1482
if oauth_applications_client_secret_hash_column
- 1482
BCrypt::Password.new(oauth_application[oauth_applications_client_secret_hash_column]) == secret
else
oauth_application[oauth_applications_client_secret_column] == secret
end
end
- 15
def set_client_secret(params, secret)
- 570
if oauth_applications_client_secret_hash_column
- 570
params[oauth_applications_client_secret_hash_column] = secret_hash(secret)
else
params[oauth_applications_client_secret_column] = secret
end
end
- 15
def secret_hash(secret)
- 570
password_hash(secret)
end
- 15
def oauth_unique_id_generator
- 2619
SecureRandom.urlsafe_base64(32)
end
- 15
def generate_token_hash(token)
- 150
Base64.urlsafe_encode64(Digest::SHA256.digest(token))
end
- 15
def grant_from_application?(oauth_grant, oauth_application)
- 255
oauth_grant[oauth_grants_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
end
- 15
def password_hash(password)
- 570
return super if features.include?(:login_password_requirements_base)
BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
end
- 15
def generate_token(grant_params = {}, should_generate_refresh_token = true)
- 792
if grant_params[oauth_grants_id_column] && (oauth_reuse_access_token &&
(
- 60
if oauth_grants_token_hash_column
- 30
grant_params[oauth_grants_token_hash_column]
else
- 30
grant_params[oauth_grants_token_column]
end
))
- 30
return grant_params
end
- 204
update_params = {
- 555
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in),
- 147
oauth_grants_code_column => nil
}
- 762
rescue_from_uniqueness_error do
- 762
access_token = _generate_access_token(update_params)
- 762
refresh_token = _generate_refresh_token(update_params) if should_generate_refresh_token
- 762
oauth_grant = store_token(grant_params, update_params)
- 762
return unless oauth_grant
- 762
oauth_grant[oauth_grants_token_column] = access_token
- 762
oauth_grant[oauth_grants_refresh_token_column] = refresh_token if refresh_token
- 762
oauth_grant
end
end
- 15
def _generate_access_token(params = {})
- 420
token = oauth_unique_id_generator
- 420
if oauth_grants_token_hash_column
- 45
params[oauth_grants_token_hash_column] = generate_token_hash(token)
else
- 375
params[oauth_grants_token_column] = token
end
- 420
token
end
- 15
def _generate_refresh_token(params)
- 642
token = oauth_unique_id_generator
- 642
if oauth_grants_refresh_token_hash_column
- 45
params[oauth_grants_refresh_token_hash_column] = generate_token_hash(token)
else
- 597
params[oauth_grants_refresh_token_column] = token
end
- 642
token
end
- 15
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
- 15
def store_token(grant_params, update_params = {})
- 762
ds = db[oauth_grants_table]
- 762
if __one_oauth_token_per_account
- 102
to_update_if_null = [
- 204
oauth_grants_token_column,
oauth_grants_token_hash_column,
oauth_grants_refresh_token_column,
oauth_grants_refresh_token_hash_column
].compact.map do |attribute|
- 216
[
- 432
attribute,
(
- 648
if ds.respond_to?(:supports_insert_conflict?) && ds.supports_insert_conflict?
- 324
Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], Sequel[:excluded][attribute])
else
- 324
Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], update_params[attribute])
end
)
]
end
- 306
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.
- 306
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
- 456
if oauth_reuse_access_token
- 72
unique_conds = Hash[oauth_grants_unique_columns.map { |column| [column, update_params[column]] }]
- 18
valid_token_ds = valid_oauth_grant_ds(unique_conds)
- 18
if oauth_grants_token_hash_column
- 9
valid_token_ds.exclude(oauth_grants_token_hash_column => nil)
else
- 9
valid_token_ds.exclude(oauth_grants_token_column => nil)
end
- 18
valid_token = valid_token_ds.first
- 18
return valid_token if valid_token
end
- 456
if grant_params[oauth_grants_id_column]
- 357
__update_and_return__(ds.where(oauth_grants_id_column => grant_params[oauth_grants_id_column]), update_params)
else
- 99
__insert_and_return__(ds, oauth_grants_id_column, grant_params.merge(update_params))
end
end
end
- 15
def valid_locked_oauth_grant(grant_params = nil)
- 732
oauth_grant = valid_oauth_grant_ds(grant_params).for_update.first
- 732
redirect_response_error("invalid_grant") unless oauth_grant
- 672
oauth_grant
end
- 15
def valid_oauth_grant_ds(grant_params = nil)
- 1572
ds = db[oauth_grants_table]
- 309
.where(Sequel[oauth_grants_table][oauth_grants_revoked_at_column] => nil)
- 309
.where(Sequel.expr(Sequel[oauth_grants_table][oauth_grants_expires_in_column]) >= Sequel::CURRENT_TIMESTAMP)
- 1572
ds = ds.where(grant_params) if grant_params
- 1572
ds
end
- 15
def oauth_grant_by_token_ds(token)
- 405
ds = valid_oauth_grant_ds
- 405
if oauth_grants_token_hash_column
- 15
ds.where(Sequel[oauth_grants_table][oauth_grants_token_hash_column] => generate_token_hash(token))
else
- 390
ds.where(Sequel[oauth_grants_table][oauth_grants_token_column] => token)
end
end
- 15
def oauth_grant_by_token(token)
- 315
oauth_grant_by_token_ds(token).first
end
- 15
def oauth_grant_by_refresh_token_ds(token, revoked: false)
- 450
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.
#
- 540
ds = ds.where(Sequel.date_add(oauth_grants_expires_in_column,
- 450
seconds: (oauth_refresh_token_expires_in - oauth_access_token_expires_in)) >= Sequel::CURRENT_TIMESTAMP)
- 450
ds = if oauth_grants_refresh_token_hash_column
- 45
ds.where(oauth_grants_refresh_token_hash_column => generate_token_hash(token))
else
- 405
ds.where(oauth_grants_refresh_token_column => token)
end
- 450
ds = ds.where(oauth_grants_revoked_at_column => nil) unless revoked
- 450
ds
end
- 15
def oauth_grant_by_refresh_token(token, **kwargs)
- 75
oauth_grant_by_refresh_token_ds(token, **kwargs).first
end
- 15
def json_access_token_payload(oauth_grant)
- 268
payload = {
- 731
"access_token" => oauth_grant[oauth_grants_token_column],
- 195
"token_type" => oauth_token_type,
- 195
"expires_in" => oauth_access_token_expires_in
}
- 1002
payload["refresh_token"] = oauth_grant[oauth_grants_refresh_token_column] if oauth_grant[oauth_grants_refresh_token_column]
- 1002
payload
end
# Access Tokens
- 15
def validate_token_params
- 1437
unless (grant_type = param_or_nil("grant_type"))
- 30
redirect_response_error("invalid_request")
end
- 1407
redirect_response_error("invalid_request") if grant_type == "refresh_token" && !param_or_nil("refresh_token")
end
- 15
def create_token(grant_type)
- 450
redirect_response_error("invalid_request") unless supported_grant_type?(grant_type, "refresh_token")
- 375
refresh_token = param("refresh_token")
# fetch potentially revoked oauth token
- 375
oauth_grant = oauth_grant_by_refresh_token_ds(refresh_token, revoked: true).for_update.first
- 450
update_params = { oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
- 72
seconds: oauth_access_token_expires_in) }
- 375
if !oauth_grant || oauth_grant[oauth_grants_revoked_at_column]
- 180
redirect_response_error("invalid_grant")
- 192
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.
- 90
refresh_token = _generate_refresh_token(update_params)
end
- 195
update_params[oauth_grants_oauth_application_id_column] = oauth_grant[oauth_grants_oauth_application_id_column]
- 195
oauth_grant = create_token_from_token(oauth_grant, update_params)
- 180
oauth_grant[oauth_grants_refresh_token_column] = refresh_token
- 180
oauth_grant
end
- 15
def create_token_from_token(oauth_grant, update_params)
- 195
redirect_response_error("invalid_grant") unless grant_from_application?(oauth_grant, oauth_application)
- 195
rescue_from_uniqueness_error do
- 240
oauth_grants_ds = db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
- 240
access_token = _generate_access_token(update_params)
- 240
oauth_grant = __update_and_return__(oauth_grants_ds, update_params)
- 180
oauth_grant[oauth_grants_token_column] = access_token
- 180
oauth_grant
end
end
- 15
def supported_grant_type?(grant_type, expected_grant_type = grant_type)
- 1812
return false unless grant_type == expected_grant_type
- 1407
grant_types_supported = if oauth_application[oauth_applications_grant_types_column]
- 15
oauth_application[oauth_applications_grant_types_column].split(/ +/)
else
- 1392
oauth_grant_types_supported
end
- 1407
grant_types_supported.include?(grant_type)
end
- 15
def supported_response_type?(response_type, expected_response_type = response_type)
- 672
return false unless response_type == expected_response_type
- 672
response_types_supported = if oauth_application[oauth_applications_grant_types_column]
oauth_application[oauth_applications_response_types_column].split(/ +/)
else
- 672
oauth_response_types_supported
end
- 672
response_types = response_type.split(/ +/)
- 672
(response_types - response_types_supported).empty?
end
- 15
def supported_response_mode?(response_mode, expected_response_mode = response_mode)
- 657
return false unless response_mode == expected_response_mode
- 657
response_modes_supported = if oauth_application[oauth_applications_response_modes_column]
oauth_application[oauth_applications_response_modes_column].split(/ +/)
else
- 657
oauth_response_modes_supported
end
- 657
response_modes_supported.include?(response_mode)
end
- 15
def oauth_server_metadata_body(path = nil)
- 240
issuer = base_url
- 240
issuer += "/#{path}" if path
- 64
{
- 128
issuer: issuer,
- 45
token_endpoint: token_url,
- 45
scopes_supported: oauth_application_scopes,
- 45
response_types_supported: oauth_response_types_supported,
- 45
response_modes_supported: oauth_response_modes_supported,
- 45
grant_types_supported: oauth_grant_types_supported,
- 45
token_endpoint_auth_methods_supported: oauth_token_endpoint_auth_methods_supported,
- 45
service_documentation: oauth_metadata_service_documentation,
- 45
ui_locales_supported: oauth_metadata_ui_locales_supported,
- 45
op_policy_uri: oauth_metadata_op_policy_uri,
- 45
op_tos_uri: oauth_metadata_op_tos_uri
}
end
- 216
def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
- 1035
if accepts_json?
- 615
status_code = if respond_to?(:"oauth_#{error_code}_response_status")
- 15
send(:"oauth_#{error_code}_response_status")
else
- 600
oauth_invalid_response_status
end
- 615
throw_json_response_error(status_code, error_code)
else
- 420
redirect_url = URI.parse(redirect_url)
- 420
query_params = []
- 504
query_params << if respond_to?(:"oauth_#{error_code}_error_code")
- 30
["error", send(:"oauth_#{error_code}_error_code")]
else
- 390
["error", error_code]
end
- 420
if respond_to?(:"oauth_#{error_code}_message")
- 240
message = send(:"oauth_#{error_code}_message")
- 240
query_params << ["error_description", CGI.escape(message)]
end
- 420
state = param_or_nil("state")
- 420
query_params << ["state", state] if state
- 420
_redirect_response_error(redirect_url, query_params)
end
end
- 15
def _redirect_response_error(redirect_url, query_params)
- 990
query_params = query_params.map { |k, v| "#{k}=#{v}" }
- 375
query_params << redirect_url.query if redirect_url.query
- 375
redirect_url.query = query_params.join("&")
- 375
redirect(redirect_url.to_s)
end
- 15
def json_response_success(body, cache = false)
- 1557
response.status = 200
- 1557
response["Content-Type"] ||= json_response_content_type
- 1557
if cache
# defaulting to 1-day for everyone, for now at least
- 285
max_age = 60 * 60 * 24
- 285
response["Cache-Control"] = "private, max-age=#{max_age}"
else
- 1272
response["Cache-Control"] = "no-store"
- 1272
response["Pragma"] = "no-cache"
end
- 1557
json_payload = _json_response_body(body)
- 1557
return_response(json_payload)
end
- 15
def throw_json_response_error(status, error_code, message = nil)
- 1980
set_response_error_status(status)
- 1980
code = if respond_to?(:"oauth_#{error_code}_error_code")
- 45
send(:"oauth_#{error_code}_error_code")
else
- 1935
error_code
end
- 1980
payload = { "error" => code }
- 1980
payload["error_description"] = message || (send(:"oauth_#{error_code}_message") if respond_to?(:"oauth_#{error_code}_message"))
- 1980
json_payload = _json_response_body(payload)
- 1980
response["Content-Type"] ||= json_response_content_type
- 1980
response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
- 1980
return_response(json_payload)
end
- 15
def _json_response_body(hash)
- 4077
return super if features.include?(:json)
- 4077
if request.respond_to?(:convert_to_json)
request.send(:convert_to_json, hash)
else
- 4077
JSON.dump(hash)
end
end
- 15
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
- 15
def authorization_required
- 240
throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
end
- 15
def check_valid_scopes?
- 1689
return false unless scopes
- 1689
(scopes - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
end
- 15
def check_valid_uri?(uri)
- 7560
URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
end
- 15
def check_valid_no_fragment_uri?(uri)
- 2325
check_valid_uri?(uri) && URI.parse(uri).fragment.nil?
end
# Resource server mode
- 15
def authorization_server_metadata
- 60
auth_url = URI(authorization_server_url).dup
- 60
auth_url.path = "/.well-known/oauth-authorization-server"
- 60
http_request_with_cache(auth_url)
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_client_credentials_grant, :OauthClientCredentialsGrant) do
- 15
depends :oauth_base
- 15
def oauth_grant_types_supported
- 105
super | %w[client_credentials]
end
- 15
private
- 15
def create_token(grant_type)
- 90
return super unless supported_grant_type?(grant_type, "client_credentials")
- 75
grant_scopes = scopes
- 75
grant_scopes = if grant_scopes
- 15
redirect_response_error("invalid_scope") unless check_valid_scopes?
- 15
grant_scopes.join(oauth_scope_separator)
else
- 60
oauth_application[oauth_applications_scopes_column]
end
- 20
grant_params = {
- 52
oauth_grants_type_column => "client_credentials",
- 12
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
- 12
oauth_grants_scopes_column => grant_scopes
}
- 75
generate_token(grant_params, false)
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_device_code_grant, :OauthDeviceCodeGrant) do
- 15
depends :oauth_authorize_base
- 15
before "device_authorization"
- 15
before "device_verification"
- 15
notice_flash "The device is verified", "device_verification"
- 15
error_flash "No device to authorize with the given user code", "user_code_not_found"
- 15
view "device_verification", "Device Verification", "device_verification"
- 15
view "device_search", "Device Search", "device_search"
- 15
button "Verify", "oauth_device_verification"
- 15
button "Search", "oauth_device_search"
- 15
auth_value_method :oauth_grants_user_code_column, :user_code
- 15
auth_value_method :oauth_grants_last_polled_at_column, :last_polled_at
- 15
translatable_method :oauth_device_search_page_lead, "Insert the user code from the device you'd like to authorize."
- 15
translatable_method :oauth_device_verification_page_lead, "The device with user code %<user_code>s would like to access your data."
- 15
translatable_method :oauth_expired_token_message, "the device code has expired"
- 15
translatable_method :oauth_access_denied_message, "the authorization request has been denied"
- 15
translatable_method :oauth_authorization_pending_message, "the authorization request is still pending"
- 15
translatable_method :oauth_slow_down_message, "authorization request is still pending but poll interval should be increased"
- 15
auth_value_method :oauth_device_code_grant_polling_interval, 5 # seconds
- 15
auth_value_method :oauth_device_code_grant_user_code_size, 8 # characters
- 15
%w[user_code].each do |param|
- 15
auth_value_method :"oauth_grant_#{param}_param", param
end
- 15
translatable_method :oauth_grant_user_code_label, "User code"
- 15
auth_value_methods(
:generate_user_code
)
# /device-authorization
- 15
auth_server_route(:device_authorization) do |r|
- 30
require_oauth_application
- 30
before_device_authorization_route
- 30
r.post do
- 30
user_code = generate_user_code
- 30
device_code = transaction do
- 30
before_device_authorization
- 30
create_oauth_grant(
- 3
oauth_grants_type_column => "device_code",
- 3
oauth_grants_user_code_column => user_code
)
end
- 30
json_response_success \
"device_code" => device_code,
"user_code" => user_code,
- 3
"verification_uri" => device_url,
- 3
"verification_uri_complete" => device_url(user_code: user_code),
- 3
"expires_in" => oauth_grant_expires_in,
- 3
"interval" => oauth_device_code_grant_polling_interval
end
end
# /device
- 15
auth_server_route(:device) do |r|
- 315
require_authorizable_account
- 300
before_device_route
- 300
r.get do
- 255
if (user_code = param_or_nil("user_code"))
- 90
oauth_grant = valid_oauth_grant_ds(oauth_grants_user_code_column => user_code).first
- 90
unless oauth_grant
- 45
set_redirect_error_flash user_code_not_found_error_flash
- 45
redirect device_path
end
- 45
scope.instance_variable_set(:@oauth_grant, oauth_grant)
- 45
device_verification_view
else
- 165
device_search_view
end
end
- 45
r.post do
- 45
catch_error do
- 45
unless (user_code = param_or_nil("user_code")) && !user_code.empty?
- 15
set_redirect_error_flash oauth_invalid_grant_message
- 15
redirect device_path
end
- 30
transaction do
- 30
before_device_verification
- 30
create_token("device_code")
end
end
- 30
set_notice_flash device_verification_notice_flash
- 30
redirect device_path
end
end
- 15
def check_csrf?
- 705
case request.path
- 138
when device_authorization_path
- 30
false
else
- 675
super
end
end
- 15
def oauth_grant_types_supported
- 165
super | %w[urn:ietf:params:oauth:grant-type:device_code]
end
- 15
private
- 15
def generate_user_code
- 30
user_code_size = oauth_device_code_grant_user_code_size
- 22
SecureRandom.random_number(36**user_code_size)
- 3
.to_s(36) # 0 to 9, a to z
- 3
.upcase
- 11
.rjust(user_code_size, "0")
end
# TODO: think about removing this and recommend PKCE
- 15
def supports_auth_method?(oauth_application, auth_method)
- 195
return super unless auth_method == "none"
- 165
request.path == device_authorization_path || request.params.key?("device_code") || super
end
- 15
def create_token(grant_type)
- 180
if supported_grant_type?(grant_type, "urn:ietf:params:oauth:grant-type:device_code")
- 180
oauth_grant = db[oauth_grants_table].where(
- 27
oauth_grants_type_column => "device_code",
- 27
oauth_grants_code_column => param("device_code"),
- 27
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
- 27
).for_update.first
- 150
throw_json_response_error(oauth_invalid_response_status, "invalid_grant") unless oauth_grant
- 135
now = Time.now
- 135
if oauth_grant[oauth_grants_user_code_column].nil?
- 22
return create_token_from_authorization_code(
- 3
{ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] },
oauth_grant: oauth_grant
- 8
)
end
- 105
if oauth_grant[oauth_grants_revoked_at_column]
- 30
throw_json_response_error(oauth_invalid_response_status, "access_denied")
- 72
elsif oauth_grant[oauth_grants_expires_in_column] < now
- 15
throw_json_response_error(oauth_invalid_response_status, "expired_token")
else
- 60
last_polled_at = oauth_grant[oauth_grants_last_polled_at_column]
- 60
if last_polled_at && convert_timestamp(last_polled_at) + oauth_device_code_grant_polling_interval > now
- 15
throw_json_response_error(oauth_invalid_response_status, "slow_down")
else
- 45
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
- 18
.update(oauth_grants_last_polled_at_column => Sequel::CURRENT_TIMESTAMP)
- 45
throw_json_response_error(oauth_invalid_response_status, "authorization_pending")
end
end
- 27
elsif grant_type == "device_code"
# fetch oauth grant
- 30
rs = valid_oauth_grant_ds(
- 3
oauth_grants_user_code_column => param("user_code")
- 3
).update(oauth_grants_user_code_column => nil, oauth_grants_type_column => "device_code")
- 30
return unless rs.positive?
else
super
end
end
- 15
def validate_token_params
- 165
grant_type = param_or_nil("grant_type")
- 165
if grant_type == "urn:ietf:params:oauth:grant-type:device_code" && !param_or_nil("device_code")
- 15
redirect_response_error("invalid_request")
end
- 150
super
end
- 15
def store_token(grant_params, update_params = {})
- 30
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[oauth_grants_account_id_column] = account_id
super(grant_params, update_params)
end
- 15
def oauth_server_metadata_body(*)
- 15
super.tap do |data|
- 15
data[:device_authorization_endpoint] = device_authorization_url
end
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_dynamic_client_registration, :OauthDynamicClientRegistration) do
- 15
depends :oauth_base
- 15
before "register"
- 15
auth_value_method :oauth_client_registration_required_params, %w[redirect_uris client_name]
- 15
PROTECTED_APPLICATION_ATTRIBUTES = %w[account_id client_id].freeze
# /register
- 15
auth_server_route(:register) do |r|
- 1185
before_register_route
- 1185
validate_client_registration_params
- 540
r.post do
- 540
response_params = transaction do
- 540
before_register
- 540
do_register
end
- 540
response.status = 201
- 540
response["Content-Type"] = json_response_content_type
- 540
response["Cache-Control"] = "no-store"
- 540
response["Pragma"] = "no-cache"
- 540
response.write(_json_response_body(response_params))
end
end
- 15
def check_csrf?
- 1185
case request.path
- 234
when register_path
- 1185
false
else
super
end
end
- 15
private
- 15
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
- 15
def validate_client_registration_params
- 1185
oauth_client_registration_required_params.each do |required_param|
- 2310
unless request.params.key?(required_param)
- 60
register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
end
end
- 1125
@oauth_application_params = request.params.each_with_object({}) do |(key, value), params|
- 13530
case key
when "redirect_uris"
- 1125
if value.is_a?(Array)
- 1110
value = value.each do |uri|
- 2070
unless check_valid_no_fragment_uri?(uri)
- 30
register_throw_json_response_error("invalid_redirect_uri",
- 3
register_invalid_uri_message(uri))
end
- 213
end.join(" ")
else
- 15
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
end
- 1080
key = oauth_applications_redirect_uri_column
when "token_endpoint_auth_method"
- 585
unless oauth_token_endpoint_auth_methods_supported.include?(value)
- 15
register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
end
# verify if in range
- 570
key = oauth_applications_token_endpoint_auth_method_column
when "grant_types"
- 615
if value.is_a?(Array)
- 600
value = value.each do |grant_type|
- 1110
unless oauth_grant_types_supported.include?(grant_type)
- 30
register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(grant_type, value))
end
- 111
end.join(" ")
else
- 15
register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
end
- 570
key = oauth_applications_grant_types_column
when "response_types"
- 615
if value.is_a?(Array)
- 600
grant_types = request.params["grant_types"] || oauth_grant_types_supported
- 600
value = value.each do |response_type|
- 615
unless oauth_response_types_supported.include?(response_type)
- 30
register_throw_json_response_error("invalid_client_metadata",
- 3
register_invalid_response_type_message(response_type))
end
- 585
validate_client_registration_response_type(response_type, grant_types)
- 108
end.join(" ")
else
- 15
register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
end
- 555
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"
- 5040
register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value)) unless check_valid_uri?(value)
- 4965
case key
when "client_uri"
- 1050
key = "homepage_url"
when "jwks_uri"
- 945
if request.params.key?("jwks")
- 15
register_throw_json_response_error("invalid_client_metadata",
register_invalid_jwks_param_message(key, "jwks"))
end
end
- 4950
key = __send__(:"oauth_applications_#{key}_column")
when "jwks"
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash)
if request.params.key?("jwks_uri")
register_throw_json_response_error("invalid_client_metadata",
register_invalid_jwks_param_message(key, "jwks_uri"))
end
key = oauth_applications_jwks_column
value = JSON.dump(value)
when "scope"
- 1035
scopes = value.split(" ") - oauth_application_scopes
- 1035
register_throw_json_response_error("invalid_client_metadata", register_invalid_scopes_message(value)) unless scopes.empty?
- 1005
key = oauth_applications_scopes_column
# verify if in range
when "contacts"
- 1005
register_throw_json_response_error("invalid_client_metadata", register_invalid_contacts_message(value)) unless value.is_a?(Array)
- 990
value = value.join(" ")
- 990
key = oauth_applications_contacts_column
when "client_name"
- 1065
key = oauth_applications_name_column
else
- 2445
if respond_to?(:"oauth_applications_#{key}_column")
- 2430
if PROTECTED_APPLICATION_ATTRIBUTES.include?(key)
- 15
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
end
- 2415
property = :"oauth_applications_#{key}_column"
- 2415
key = __send__(property)
- 12
elsif !db[oauth_applications_table].columns.include?(key.to_sym)
- 15
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
end
end
- 13200
params[key] = value
end
end
- 15
def validate_client_registration_response_type(response_type, grant_types)
- 555
case response_type
when "code"
- 525
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"
- 30
unless grant_types.include?("implicit")
register_throw_json_response_error("invalid_client_metadata",
register_invalid_response_type_for_grant_type_message(response_type, "implicit"))
end
when "none"
if grant_types.include?("implicit") || grant_types.include?("authorization_code")
register_throw_json_response_error("invalid_client_metadata", register_invalid_response_type_message(response_type))
end
end
end
- 66
def do_register(return_params = request.params.dup)
- 540
applications_ds = db[oauth_applications_table]
- 540
application_columns = applications_ds.columns
# set defaults
- 540
create_params = @oauth_application_params
- 540
create_params[oauth_applications_scopes_column] ||= return_params["scopes"] = oauth_application_scopes.join(" ")
- 540
if create_params[oauth_applications_grant_types_column] ||= begin
- 240
return_params["grant_types"] = %w[authorization_code] # rubocop:disable Lint/AssignmentInCondition
- 240
"authorization_code"
end
- 540
create_params[oauth_applications_token_endpoint_auth_method_column] ||= begin
- 240
return_params["token_endpoint_auth_method"] = "client_secret_basic"
- 240
"client_secret_basic"
end
end
- 540
create_params[oauth_applications_response_types_column] ||= begin
- 240
return_params["response_types"] = %w[code]
- 240
"code"
end
- 540
rescue_from_uniqueness_error do
- 540
client_id = oauth_unique_id_generator
- 540
create_params[oauth_applications_client_id_column] = client_id
- 540
return_params["client_id"] = client_id
- 540
return_params["client_id_issued_at"] = Time.now.utc.iso8601
- 540
if create_params.key?(oauth_applications_client_secret_column)
- 15
set_client_secret(create_params, create_params[oauth_applications_client_secret_column])
- 15
return_params.delete("client_secret")
else
- 525
client_secret = oauth_unique_id_generator
- 525
set_client_secret(create_params, client_secret)
- 525
return_params["client_secret"] = client_secret
- 525
return_params["client_secret_expires_at"] = 0
- 9750
create_params.delete_if { |k, _| !application_columns.include?(k) }
end
- 540
applications_ds.insert(create_params)
end
- 540
return_params
end
- 15
def register_throw_json_response_error(code, message)
- 645
throw_json_response_error(oauth_invalid_response_status, code, message)
end
- 15
def register_required_param_message(key)
- 75
"The param '#{key}' is required by this server."
end
- 15
def register_invalid_param_message(key)
- 45
"The param '#{key}' is not supported by this server."
end
- 15
def register_invalid_client_metadata_message(key, value)
- 225
"The value '#{value}' is not supported by this server for param '#{key}'."
end
- 15
def register_invalid_contacts_message(contacts)
- 15
"The contacts '#{contacts}' are not allowed by this server."
end
- 15
def register_invalid_uri_message(uri)
- 195
"The '#{uri}' URL is not allowed by this server."
end
- 15
def register_invalid_jwks_param_message(key1, key2)
- 15
"The param '#{key1}' cannot be accepted together with param '#{key2}'."
end
- 15
def register_invalid_scopes_message(scopes)
- 30
"The given scopes (#{scopes}) are not allowed by this server."
end
- 15
def register_oauth_invalid_grant_type_message(grant_type)
"The grant type #{grant_type} is not allowed by this server."
end
- 15
def register_invalid_response_type_message(response_type)
- 30
"The response type #{response_type} is not allowed by this server."
end
- 15
def register_invalid_response_type_for_grant_type_message(response_type, grant_type)
- 15
"The grant type '#{grant_type}' must be registered for the response " \
"type '#{response_type}' to be allowed."
end
- 15
def oauth_server_metadata_body(*)
- 30
super.tap do |data|
- 30
data[:registration_endpoint] = register_url
end
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_grant_management, :OauthTokenManagement) do
- 15
depends :oauth_management_base, :oauth_token_revocation
- 15
view "oauth_grants", "My Oauth Grants", "oauth_grants"
- 15
button "Revoke", "oauth_grant_revoke"
- 15
auth_value_method :oauth_grants_path, "oauth-grants"
- 15
%w[type token refresh_token expires_in revoked_at].each do |param|
- 75
translatable_method :"oauth_grants_#{param}_label", param.gsub("_", " ").capitalize
end
- 15
translatable_method :oauth_no_grants_text, "No oauth grants yet!"
- 15
auth_value_method :oauth_grants_route, "oauth-grants"
- 15
auth_value_method :oauth_grants_id_pattern, Integer
- 15
auth_value_method :oauth_grants_per_page, 20
- 15
auth_value_methods(
:oauth_grant_path
)
- 15
def oauth_grants_path(opts = {})
- 885
route_path(oauth_grants_route, opts)
end
- 15
def oauth_grant_path(id)
- 366
"#{oauth_grants_path}/#{id}"
end
- 15
def load_oauth_grant_management_routes
- 126
request.on(oauth_grants_route) do
- 126
check_csrf if check_csrf?
- 126
require_account
- 126
request.post(oauth_grants_id_pattern) do |id|
- 11
db[oauth_grants_table]
.where(oauth_grants_id_column => id)
.where(oauth_grants_account_id_column => account_id)
- 4
.update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
- 15
set_notice_flash revoke_oauth_grant_notice_flash
- 15
redirect oauth_grants_path || "/"
end
- 111
request.is do
- 111
request.get do
- 111
page = Integer(param_or_nil("page") || 1)
- 111
per_page = per_page_param(oauth_grants_per_page)
- 165
scope.instance_variable_set(:@oauth_grants, db[oauth_grants_table]
- 24
.select(Sequel[oauth_grants_table].*, Sequel[oauth_applications_table][oauth_applications_name_column])
- 51
.join(oauth_applications_table, Sequel[oauth_grants_table][oauth_grants_oauth_application_id_column] =>
- 24
Sequel[oauth_applications_table][oauth_applications_id_column])
- 24
.where(Sequel[oauth_grants_table][oauth_grants_account_id_column] => account_id)
- 24
.where(oauth_grants_revoked_at_column => nil)
- 24
.order(Sequel.desc(oauth_grants_id_column))
- 24
.paginate(page, per_page))
- 111
oauth_grants_view
end
end
end
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_implicit_grant, :OauthImplicitGrant) do
- 15
depends :oauth_authorize_base
- 15
def oauth_grant_types_supported
- 462
super | %w[implicit]
end
- 15
def oauth_response_types_supported
- 402
super | %w[token]
end
- 15
def oauth_response_modes_supported
- 327
super | %w[fragment]
end
- 15
private
- 72
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
- 297
response_type = param("response_type")
- 297
return super unless response_type == "token" && supported_response_type?(response_type)
- 45
response_mode ||= "fragment"
- 45
redirect_response_error("invalid_request") unless supported_response_mode?(response_mode)
- 30
oauth_grant = _do_authorize_token
- 30
response_params.replace(json_access_token_payload(oauth_grant))
- 30
response_params["state"] = param("state") if param_or_nil("state")
- 30
[response_params, response_mode]
end
- 15
def _do_authorize_token(grant_params = {})
- 12
grant_params = {
- 30
oauth_grants_type_column => "implicit",
- 6
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
- 6
oauth_grants_scopes_column => scopes,
- 6
oauth_grants_account_id_column => account_id
- 6
}.merge(grant_params)
- 45
generate_token(grant_params, false)
end
- 15
def _redirect_response_error(redirect_url, query_params)
- 30
response_types = param("response_type").split(/ +/)
- 30
return super if response_types.empty? || response_types == %w[code]
- 75
query_params = query_params.map { |k, v| "#{k}=#{v}" }
- 30
redirect_url.fragment = query_params.join("&")
- 30
redirect(redirect_url.to_s)
end
- 15
def authorize_response(params, mode)
- 282
return super unless mode == "fragment"
- 267
redirect_url = URI.parse(redirect_uri)
- 891
params = params.map { |k, v| "#{k}=#{v}" }
- 267
params << redirect_url.query if redirect_url.query
- 267
redirect_url.fragment = params.join("&")
- 267
redirect(redirect_url.to_s)
end
- 15
def check_valid_response_type?
- 639
return true if param_or_nil("response_type") == "token"
- 534
super
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
require "rodauth/oauth/http_extensions"
- 15
module Rodauth
- 15
Feature.define(:oauth_jwt, :OauthJwt) do
- 15
depends :oauth_jwt_base, :oauth_jwt_jwks
- 15
auth_value_method :oauth_jwt_access_tokens, true
- 15
def require_oauth_authorization(*scopes)
- 282
return super unless oauth_jwt_access_tokens
- 282
authorization_required unless authorization_token
- 267
token_scopes = authorization_token["scope"].split(" ")
- 534
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
end
- 15
def oauth_token_subject
- 399
return super unless oauth_jwt_access_tokens
- 399
return unless authorization_token
- 399
authorization_token["sub"]
end
- 15
def current_oauth_account
- 192
subject = oauth_token_subject
- 192
return if subject == authorization_token["client_id"]
- 177
oauth_account_ds(subject).first
end
- 15
def current_oauth_application
- 218
db[oauth_applications_table].where(
- 42
oauth_applications_client_id_column => authorization_token["client_id"]
- 106
).first
end
- 15
private
- 15
def authorization_token
- 1941
return super unless oauth_jwt_access_tokens
- 1941
return @authorization_token if defined?(@authorization_token)
- 116
@authorization_token = begin
- 432
access_token = fetch_access_token
- 432
return unless access_token
- 417
jwt_claims = jwt_decode(access_token)
- 417
return unless jwt_claims
- 417
return unless jwt_claims["sub"]
- 417
return unless jwt_claims["aud"]
- 417
jwt_claims
end
end
# /token
- 15
def create_token_from_token(_grant, update_params)
- 120
oauth_grant = super
- 120
if oauth_jwt_access_tokens
- 120
access_token = _generate_jwt_access_token(oauth_grant)
- 120
oauth_grant[oauth_grants_token_column] = access_token
end
- 120
oauth_grant
end
- 15
def generate_token(_grant_params = {}, should_generate_refresh_token = true)
- 477
oauth_grant = super
- 477
if oauth_jwt_access_tokens
- 462
access_token = _generate_jwt_access_token(oauth_grant)
- 462
oauth_grant[oauth_grants_token_column] = access_token
end
- 477
oauth_grant
end
- 15
def _generate_jwt_access_token(oauth_grant)
- 612
claims = jwt_claims(oauth_grant)
# one of the points of using jwt is avoiding database lookups, so we put here all relevant
# token data.
- 612
claims[:scope] = oauth_grant[oauth_grants_scopes_column]
- 612
jwt_encode(claims)
end
- 15
def _generate_access_token(*)
- 597
return super unless oauth_jwt_access_tokens
end
- 15
def jwt_claims(oauth_grant)
- 1059
issued_at = Time.now.to_i
- 284
{
- 772
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.
- 204
sub: jwt_subject(oauth_grant),
- 204
client_id: oauth_application[oauth_applications_client_id_column],
- 204
exp: issued_at + oauth_access_token_expires_in,
- 204
aud: oauth_jwt_audience
}
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
require "rodauth/oauth/http_extensions"
- 15
module Rodauth
- 15
Feature.define(:oauth_jwt_base, :OauthJwtBase) do
- 15
depends :oauth_base
- 15
auth_value_method :oauth_application_jwt_public_key_param, "jwt_public_key"
- 15
auth_value_method :oauth_application_jwks_param, "jwks"
- 15
auth_value_method :oauth_jwt_keys, {}
- 15
auth_value_method :oauth_jwt_public_keys, {}
- 15
auth_value_method :oauth_jwt_jwe_keys, {}
- 15
auth_value_method :oauth_jwt_jwe_public_keys, {}
- 15
auth_value_method :oauth_jwt_jwe_copyright, nil
- 15
auth_value_methods(
:jwt_encode,
:jwt_decode,
:jwt_decode_no_key,
:generate_jti,
:oauth_jwt_issuer,
:oauth_jwt_audience
)
- 15
private
- 15
def oauth_jwt_issuer
# The JWT MUST contain an "iss" (issuer) claim that contains a
# unique identifier for the entity that issued the JWT.
- 2343
@oauth_jwt_issuer ||= authorization_server_url
end
- 15
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.
- 1059
@oauth_jwt_audience ||= if is_authorization_server?
- 804
oauth_application[oauth_applications_client_id_column]
else
metadata = authorization_server_metadata
return unless metadata
metadata[:token_endpoint]
end
end
- 15
def grant_from_application?(grant_or_claims, oauth_application)
- 135
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
- 78
def jwt_subject(oauth_grant, client_application = oauth_application)
- 1089
account_id = oauth_grant[oauth_grants_account_id_column]
- 1089
return account_id.to_s if account_id
- 45
client_application[oauth_applications_client_id_column]
end
- 15
def oauth_server_metadata_body(path = nil)
- 105
metadata = super
- 105
metadata.merge! \
- 18
token_endpoint_auth_signing_alg_values_supported: oauth_jwt_keys.keys.uniq
- 105
metadata
end
- 15
def _jwt_key
- 303
@_jwt_key ||= (oauth_application_jwks(oauth_application) if oauth_application)
end
- 15
def _jwt_public_key
@_jwt_public_key ||= if oauth_application
oauth_application_jwks(oauth_application)
else
_jwt_key
end
end
# Resource Server only!
#
# returns the jwks set from the authorization server.
- 15
def auth_server_jwks_set
- 60
metadata = authorization_server_metadata
- 60
return unless metadata && (jwks_uri = metadata[:jwks_uri])
- 60
jwks_uri = URI(jwks_uri)
- 60
http_request_with_cache(jwks_uri)
end
- 15
def generate_jti(payload)
# Use the key and iat to create a unique key per request to prevent replay attacks
- 385
jti_raw = [
- 1070
payload[:aud] || payload["aud"],
- 300
payload[:iat] || payload["iat"]
- 300
].join(":").to_s
- 1458
Digest::SHA256.hexdigest(jti_raw)
end
- 15
def verify_jti(jti, claims)
- 369
generate_jti(claims) == jti
end
- 15
def verify_aud(expected_aud, aud)
- 762
expected_aud == aud
end
- 15
def oauth_application_jwks(oauth_application)
- 1140
jwks = oauth_application[oauth_applications_jwks_column]
- 1140
if jwks
- 588
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
- 588
return jwks
end
- 552
jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
- 552
return unless jwks_uri
- 30
jwks_uri = URI(jwks_uri)
- 30
http_request_with_cache(jwks_uri)
end
- 15
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 jwk_export(key)
- 9
JSON::JWK.new(key)
end
- 3
def jwt_encode(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]],
signing_algorithm: oauth_jwt_keys.keys.first)
- 219
payload[:jti] = generate_jti(payload)
- 219
jwt = JSON::JWT.new(payload)
- 219
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
- 219
key = key.first if key.is_a?(Array)
- 219
jwk = JSON::JWK.new(key || "")
- 219
jwt = jwt.sign(jwk, signing_algorithm)
- 219
jwt.kid = jwk.thumbprint
- 237
if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
- 6
jwk = JSON::JWK.new(jwk)
- 6
jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
- 6
jwe.to_s
- 213
elsif jwe_key
- 3
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
- 3
algorithm = encryption_algorithm.to_sym if encryption_algorithm
- 3
meth = encryption_method.to_sym if encryption_method
- 3
jwt.encrypt(jwe_key, algorithm, meth)
else
- 210
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,
**
)
- 198
jws_key = jws_key.first if jws_key.is_a?(Array)
- 198
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
- 198
claims = if is_authorization_server?
- 186
if jwks
- 69
jwks = jwks[:keys] if jwks.is_a?(Hash)
- 69
enc_algs = [jws_encryption_algorithm].compact
- 69
enc_meths = [jws_encryption_method].compact
- 141
sig_algs = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
- 69
sig_algs = sig_algs.compact.map(&:to_sym)
# JWKs may be set up without a KID, when there's a single one
- 69
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
- 66
jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
- 60
jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
end
- 63
jws
- 117
elsif jws_key
- 117
JSON::JWT.decode(token, jws_key)
end
- 12
elsif (jwks = auth_server_jwks_set)
- 12
JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
end
- 192
now = Time.now
- 192
if verify_claims && (
(!claims[:exp] || Time.at(claims[:exp]) < now) &&
(claims[:nbf] && Time.at(claims[:nbf]) < now) &&
(claims[:iat] && Time.at(claims[:iat]) < now) &&
(verify_iss && claims[:iss] != oauth_jwt_issuer) &&
(verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
(verify_jti && !verify_jti(claims[:jti], claims))
)
return
end
- 192
claims
rescue JSON::JWT::Exception
- 6
nil
end
- 3
def jwt_decode_no_key(token)
- 24
jws = JSON::JWT.decode(token, :skip_verification)
- 24
[jws.to_h, jws.header]
end
- 9
elsif defined?(JWT)
# ruby-jwt
- 12
require "rodauth/oauth/jwe_extensions" if defined?(JWE)
- 12
auth_value_method :oauth_jwt_jws_algorithms_supported, %w[
HS256 HS384 HS512 HS512256
RS256 RS384 RS512
ED25519
ES256 ES384 ES512
PS256 PS384 PS512
]
- 12
if defined?(JWE)
- 12
auth_value_methods(
:oauth_jwt_jwe_algorithms_supported,
:oauth_jwt_jwe_encryption_methods_supported
)
- 12
def oauth_jwt_jwe_algorithms_supported
- 216
JWE::VALID_ALG
end
- 12
def oauth_jwt_jwe_encryption_methods_supported
- 216
JWE::VALID_ENC
end
else
auth_value_method :oauth_jwt_jwe_algorithms_supported, []
auth_value_method :oauth_jwt_jwe_encryption_methods_supported, []
end
- 12
def jwk_export(key)
- 36
JWT::JWK.new(key).export
end
- 12
def jwt_encode(payload,
- 117
signing_algorithm: oauth_jwt_keys.keys.first, **)
- 870
headers = {}
- 870
key = oauth_jwt_keys[signing_algorithm] || _jwt_key
- 870
key = key.first if key.is_a?(Array)
- 870
case key
when OpenSSL::PKey::PKey
- 582
jwk = JWT::JWK.new(key)
- 582
headers[:kid] = jwk.kid
- 582
key = jwk.keypair
end
# @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
- 870
payload[:jti] = generate_jti(payload)
- 870
JWT.encode(payload, key, signing_algorithm, headers)
end
- 12
if defined?(JWE)
- 12
def jwt_encode_with_jwe(
payload,
jwks: nil,
- 204
encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
- 204
encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
- 210
jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm, encryption_method]],
**args
)
- 870
token = jwt_encode_without_jwe(payload, **args)
- 870
return token unless encryption_algorithm && encryption_method
- 60
if jwks && jwks.any? { |k| k[:use] == "enc" }
- 24
JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
- 9
elsif jwe_key
- 12
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
- 3
params = {
- 6
zip: "DEF",
copyright: oauth_jwt_jwe_copyright
}
- 12
params[:enc] = encryption_method if encryption_method
- 12
params[:alg] = encryption_algorithm if encryption_algorithm
- 12
JWE.encrypt(token, jwe_key, **params)
else
token
end
end
- 12
alias_method :jwt_encode_without_jwe, :jwt_encode
- 12
alias_method :jwt_encode, :jwt_encode_with_jwe
end
- 12
def jwt_decode(
token,
jwks: nil,
- 171
jws_algorithm: oauth_jwt_public_keys.keys.first || oauth_jwt_keys.keys.first,
- 183
jws_key: oauth_jwt_keys[jws_algorithm] || _jwt_key,
verify_claims: true,
verify_jti: true,
verify_iss: true,
verify_aud: true
)
- 777
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.
#
- 777
verify_claims_params = if verify_claims
- 183
{
- 366
verify_iss: verify_iss,
- 177
iss: oauth_jwt_issuer,
# can't use stock aud verification, as it's dependent on the client application id
verify_aud: false,
- 729
verify_jti: (verify_jti ? method(:verify_jti) : false),
verify_iat: true
}
else
- 48
{}
end
# decode jwt
- 777
claims = if is_authorization_server?
- 729
if jwks
- 264
jwks = jwks[:keys] if jwks.is_a?(Hash)
# JWKs may be set up without a KID, when there's a single one
- 264
if jwks.size == 1 && !jwks[0][:kid]
- 12
key = jwks[0]
- 12
algo = key[:alg]
- 12
key = JWT::JWK.import(key).keypair
- 12
JWT.decode(token, key, true, algorithms: [algo], **verify_claims_params).first
else
- 540
algorithms = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
- 252
JWT.decode(token, nil, true, algorithms: algorithms, jwks: { keys: jwks }, **verify_claims_params).first
end
- 462
elsif jws_key
- 465
JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
end
- 48
elsif (jwks = auth_server_jwks_set)
- 144
algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
- 48
JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
end
- 765
return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
- 765
claims
rescue JWT::DecodeError, JWT::JWKError
- 12
nil
end
- 12
if defined?(JWE)
- 12
def jwt_decode_with_jwe(
token,
jwks: nil,
- 183
jws_encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
- 183
jws_encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
- 192
jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_keys.values.first,
**args
)
- 1125
token = if jwks && jwks.any? { |k| k[:use] == "enc" }
- 36
JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
- 750
elsif jwe_key
- 36
jwe_key = jwe_key.first if jwe_key.is_a?(Array)
- 36
JWE.decrypt(token, jwe_key)
else
- 717
token
end
- 765
jwt_decode_without_jwe(token, jwks: jwks, **args)
rescue JWE::DecodeError => e
- 24
jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
end
- 12
alias_method :jwt_decode_without_jwe, :jwt_decode
- 12
alias_method :jwt_decode, :jwt_decode_with_jwe
end
- 12
def jwt_decode_no_key(token)
- 96
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 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
# :nocov:
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_jwt_bearer_grant, :OauthJwtBearerGrant) do
- 15
depends :oauth_assertion_base, :oauth_jwt
- 15
auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
- 15
auth_value_methods(
:require_oauth_application_from_jwt_bearer_assertion_issuer,
:require_oauth_application_from_jwt_bearer_assertion_subject,
:account_from_jwt_bearer_assertion
)
- 15
def oauth_token_endpoint_auth_methods_supported
- 45
if oauth_applications_client_secret_hash_column.nil?
- 15
super | %w[client_secret_jwt private_key_jwt urn:ietf:params:oauth:client-assertion-type:jwt-bearer]
else
- 30
super | %w[private_key_jwt]
end
end
- 15
def oauth_grant_types_supported
- 120
super | %w[urn:ietf:params:oauth:grant-type:jwt-bearer]
end
- 15
private
- 15
def require_oauth_application_from_jwt_bearer_assertion_issuer(assertion)
- 45
claims = jwt_assertion(assertion)
- 45
return unless claims
- 42
db[oauth_applications_table].where(
- 6
oauth_applications_client_id_column => claims["iss"]
- 18
).first
end
- 15
def require_oauth_application_from_jwt_bearer_assertion_subject(assertion)
- 120
claims, header = jwt_decode_no_key(assertion)
- 120
client_id = claims["sub"]
- 120
case header["alg"]
when "none"
# do not accept jwts with no alg set
- 15
authorization_required
when /\AHS/
- 45
require_oauth_application_from_client_secret_jwt(client_id, assertion, header["alg"])
else
- 60
require_oauth_application_from_private_key_jwt(client_id, assertion)
end
end
- 15
def require_oauth_application_from_client_secret_jwt(client_id, assertion, alg)
- 45
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
- 45
authorization_required unless oauth_application && supports_auth_method?(oauth_application, "client_secret_jwt")
- 30
client_secret = oauth_application[oauth_applications_client_secret_column]
- 30
claims = jwt_assertion(assertion, jws_key: client_secret, jws_algorithm: alg)
- 30
authorization_required unless claims && claims["iss"] == client_id
- 30
oauth_application
end
- 15
def require_oauth_application_from_private_key_jwt(client_id, assertion)
- 60
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
- 60
authorization_required unless oauth_application && supports_auth_method?(oauth_application, "private_key_jwt")
- 45
jwks = oauth_application_jwks(oauth_application)
- 45
claims = jwt_assertion(assertion, jwks: jwks)
- 45
authorization_required unless claims
- 45
oauth_application
end
- 15
def account_from_jwt_bearer_assertion(assertion)
- 45
claims = jwt_assertion(assertion)
- 45
return unless claims
- 45
account_from_bearer_assertion_subject(claims["sub"])
end
- 15
def jwt_assertion(assertion, **kwargs)
- 165
claims = jwt_decode(assertion, verify_iss: false, verify_aud: false, verify_jti: false, **kwargs)
- 165
return unless claims && verify_aud(request.url, claims["aud"])
- 165
claims
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
require "rodauth/oauth/http_extensions"
- 15
module Rodauth
- 15
Feature.define(:oauth_jwt_jwks, :OauthJwtJwks) do
- 15
depends :oauth_jwt_base
- 15
auth_value_methods(:jwks_set)
- 15
auth_server_route(:jwks) do |r|
- 45
before_jwks_route
- 45
r.get do
- 45
json_response_success({ keys: jwks_set }, true)
end
end
- 15
private
- 15
def oauth_server_metadata_body(path = nil)
- 105
metadata = super
- 105
metadata.merge!(jwks_uri: jwks_url)
- 105
metadata
end
- 15
def jwks_set
- 45
@jwks_set ||= [
*(
- 45
unless oauth_jwt_public_keys.empty?
- 90
oauth_jwt_public_keys.flat_map { |algo, pkeys| Array(pkeys).map { |pkey| jwk_export(pkey).merge(use: "sig", alg: algo) } }
end
),
*(
- 45
unless oauth_jwt_jwe_public_keys.empty?
- 15
oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
- 15
Array(pkeys).map do |pkey|
- 15
jwk_export(pkey).merge(use: "enc", alg: algo)
end
end
end
)
- 6
].compact
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_jwt_secured_authorization_request, :OauthJwtSecuredAuthorizationRequest) do
- 15
ALLOWED_REQUEST_URI_CONTENT_TYPES = %w[application/jose application/oauth-authz-req+jwt].freeze
- 15
depends :oauth_authorize_base, :oauth_jwt_base
- 15
auth_value_method :oauth_require_request_uri_registration, false
- 15
auth_value_method :oauth_request_object_signing_alg_allow_none, false
- 15
auth_value_method :oauth_applications_request_uris_column, :request_uris
- 15
auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
- 15
auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
- 15
auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
- 15
translatable_method :oauth_invalid_request_object_message, "request object is invalid"
- 15
auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
- 15
private
# /authorize
- 15
def validate_authorize_params
- 540
request_object = param_or_nil("request")
- 540
request_uri = param_or_nil("request_uri")
- 540
return super unless (request_object || request_uri) && oauth_application
- 450
if request_uri
- 120
request_uri = CGI.unescape(request_uri)
- 120
redirect_response_error("invalid_request_uri") unless supported_request_uri?(request_uri, oauth_application)
- 60
response = http_request(request_uri)
- 60
unless response.code.to_i == 200 && ALLOWED_REQUEST_URI_CONTENT_TYPES.include?(response["content-type"])
redirect_response_error("invalid_request_uri")
end
- 60
request_object = response.body
end
- 104
request_sig_enc_opts = {
- 283
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
- 75
jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
- 75
jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
- 75
}.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"
jwks = nil
- 390
elsif (jwks = oauth_application_jwks(oauth_application))
- 300
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
else
- 90
redirect_response_error("invalid_request_object")
end
- 300
claims = jwt_decode(request_object,
jwks: jwks,
verify_jti: false,
verify_iss: false,
verify_aud: false,
**request_sig_enc_opts)
- 300
redirect_response_error("invalid_request_object") unless claims
- 270
if (iss = claims["iss"]) && (iss != oauth_application[oauth_applications_client_id_column])
- 15
redirect_response_error("invalid_request_object")
end
- 255
if (aud = claims["aud"]) && !verify_aud(aud, oauth_jwt_issuer)
- 15
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].
- 240
claims.delete("iss")
- 240
audience = claims.delete("aud")
- 240
redirect_response_error("invalid_request_object") if audience && audience != oauth_jwt_issuer
- 240
claims.each do |k, v|
- 1485
request.params[k.to_s] = v
end
- 240
super
end
- 15
def supported_request_uri?(request_uri, oauth_application)
- 120
return false unless check_valid_uri?(request_uri)
- 90
request_uris = oauth_application[oauth_applications_request_uris_column]
- 150
request_uris.nil? || request_uris.split(oauth_scope_separator).one? { |uri| request_uri.start_with?(uri) }
end
- 15
def oauth_server_metadata_body(*)
super.tap do |data|
data[:request_parameter_supported] = true
data[:request_uri_parameter_supported] = true
data[:require_request_uri_registration] = oauth_require_request_uri_registration
end
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_management_base, :OauthManagementBase) do
- 15
depends :oauth_authorize_base
- 15
button "Previous", "oauth_management_pagination_previous"
- 15
button "Next", "oauth_management_pagination_next"
- 15
def oauth_management_pagination_links(paginated_ds)
- 216
html = +'<nav aria-label="Pagination"><ul class="pagination">'
- 216
html << oauth_management_pagination_link(paginated_ds.prev_page, label: oauth_management_pagination_previous_button)
- 216
html << oauth_management_pagination_link(paginated_ds.current_page - 1) unless paginated_ds.first_page?
- 216
html << oauth_management_pagination_link(paginated_ds.current_page, label: paginated_ds.current_page, current: true)
- 216
html << oauth_management_pagination_link(paginated_ds.current_page + 1) unless paginated_ds.last_page?
- 216
html << oauth_management_pagination_link(paginated_ds.next_page, label: oauth_management_pagination_next_button)
- 216
html << "</ul></nav>"
end
- 15
def oauth_management_pagination_link(page, label: page, current: false, classes: "")
- 720
classes += " disabled" if current || !page
- 720
classes += " active" if current
- 720
if page
- 360
params = request.GET.merge("page" => page).map do |k, v|
- 576
v ? "#{CGI.escape(String(k))}=#{CGI.escape(String(v))}" : CGI.escape(String(k))
- 81
end.join("&")
- 360
href = "#{request.path}?#{params}"
- 276
<<-HTML
- 84
<li class="page-item #{classes}" #{'aria-current="page"' if current}>
- 36
<a class="page-link" href="#{href}" tabindex="-1" aria-disabled="#{current || !page}">
#{label}
</a>
</li>
HTML
else
- 282
<<-HTML
- 78
<li class="page-item #{classes}">
<span class="page-link">
#{label}
#{'<span class="sr-only">(current)</span>' if current}
</span>
</li>
HTML
end
end
- 15
def post_configure
- 99
super
# TODO: remove this in v1, when resource-server mode does not load all of the provider features.
- 99
return unless db
- 99
db.extension :pagination
end
- 15
private
- 15
def per_page_param(default_per_page)
- 276
per_page = param_or_nil("per_page")
- 276
return default_per_page unless per_page
- 72
per_page = per_page.to_i
- 72
return default_per_page if per_page <= 0
- 72
[per_page, default_per_page].min
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_pkce, :OauthPkce) do
- 15
depends :oauth_authorization_code_grant
- 15
auth_value_method :oauth_require_pkce, true
- 15
auth_value_method :oauth_pkce_challenge_method, "S256"
- 15
auth_value_method :oauth_grants_code_challenge_column, :code_challenge
- 15
auth_value_method :oauth_grants_code_challenge_method_column, :code_challenge_method
- 15
auth_value_method :oauth_code_challenge_required_error_code, "invalid_request"
- 15
translatable_method :oauth_code_challenge_required_message, "code challenge required"
- 15
auth_value_method :oauth_unsupported_transform_algorithm_error_code, "invalid_request"
- 15
translatable_method :oauth_unsupported_transform_algorithm_message, "transform algorithm not supported"
- 15
private
- 15
def supports_auth_method?(oauth_application, auth_method)
- 90
return super unless auth_method == "none"
- 60
request.params.key?("code_verifier") || super
end
- 15
def validate_authorize_params
- 60
validate_pkce_challenge_params
- 45
super
end
- 15
def create_oauth_grant(create_params = {})
# PKCE flow
- 15
if (code_challenge = param_or_nil("code_challenge"))
- 15
code_challenge_method = param_or_nil("code_challenge_method") || oauth_pkce_challenge_method
- 15
create_params[oauth_grants_code_challenge_column] = code_challenge
- 15
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
end
- 15
super
end
- 15
def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
- 90
oauth_grant ||= valid_locked_oauth_grant(grant_params)
- 90
if oauth_grant[oauth_grants_code_challenge_column]
- 75
code_verifier = param_or_nil("code_verifier")
- 75
redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
- 12
elsif oauth_require_pkce
- 15
redirect_response_error("code_challenge_required")
end
- 30
super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
end
- 15
def validate_pkce_challenge_params
- 60
if param_or_nil("code_challenge")
- 30
challenge_method = param_or_nil("code_challenge_method")
- 30
redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
else
- 30
return unless oauth_require_pkce
- 15
redirect_response_error("code_challenge_required")
end
end
- 15
def check_valid_grant_challenge?(grant, verifier)
- 60
challenge = grant[oauth_grants_code_challenge_column]
- 60
case grant[oauth_grants_code_challenge_method_column]
when "plain"
- 15
challenge == verifier
when "S256"
- 30
generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier))
- 30
generated_challenge.delete_suffix!("=") while generated_challenge.end_with?("=")
- 30
challenge == generated_challenge
else
- 15
redirect_response_error("unsupported_transform_algorithm")
end
end
- 15
def oauth_server_metadata_body(*)
- 15
super.tap do |data|
- 15
data[:code_challenge_methods_supported] = oauth_pkce_challenge_method
end
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_resource_indicators, :OauthResourceIndicators) do
- 15
depends :oauth_authorize_base
- 15
auth_value_method :oauth_grants_resource_column, :resource
- 15
def resource_indicators
- 600
return @resource_indicators if defined?(@resource_indicators)
- 150
resources = param_or_nil("resource")
- 150
return unless resources
- 150
if json_request? || param_or_nil("request") # signed request
- 30
resources = Array(resources)
else
- 120
query = if request.form_data?
- 75
request.body.rewind
- 75
request.body.read
else
- 45
request.query_string
end
# resource query param does not conform to rack parsing rules
- 120
resources = URI.decode_www_form(query).each_with_object([]) do |(k, v), memo|
- 630
memo << v if k == "resource"
end
end
- 150
@resource_indicators = resources
end
- 15
def require_oauth_authorization(*)
- 105
super
# done so to support token-in-grant-db, jwt, and resource-server mode
- 90
token_indicators = authorization_token[oauth_grants_resource_column] || authorization_token["resource"]
- 90
return unless token_indicators
- 75
token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
- 150
authorization_required unless token_indicators.any? { |resource| base_url.start_with?(resource) }
end
- 15
private
- 15
def validate_token_params
- 60
super
- 60
return unless resource_indicators
- 60
resource_indicators.each do |resource|
- 60
redirect_response_error("invalid_target") unless check_valid_no_fragment_uri?(resource)
end
end
- 15
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
- 15
module IndicatorAuthorizationCodeGrant
- 15
private
- 15
def validate_authorize_params
- 90
super
- 90
return unless resource_indicators
- 90
resource_indicators.each do |resource|
- 90
redirect_response_error("invalid_target") unless check_valid_no_fragment_uri?(resource)
end
end
- 15
def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
- 60
return super unless resource_indicators
- 60
oauth_grant ||= valid_locked_oauth_grant(grant_params)
- 60
redirect_response_error("invalid_target") unless oauth_grant[oauth_grants_resource_column]
- 60
grant_indicators = oauth_grant[oauth_grants_resource_column]
- 60
grant_indicators = grant_indicators.split(" ") if grant_indicators.is_a?(String)
- 60
redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
# update ownership
- 45
if grant_indicators != resource_indicators
- 15
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
- 45
super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
end
- 15
def create_oauth_grant(create_params = {})
- 15
create_params[oauth_grants_resource_column] = resource_indicators.join(" ") if resource_indicators
- 15
super
end
end
- 15
module IndicatorIntrospection
- 15
def json_token_introspect_payload(grant)
- 15
return super unless grant[oauth_grants_id_column]
- 15
payload = super
- 15
token_indicators = grant[oauth_grants_resource_column]
- 15
token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
- 15
payload[:aud] = token_indicators
- 15
payload
end
- 15
def introspection_request(*)
- 45
payload = super
- 45
payload[oauth_grants_resource_column] = payload["aud"] if payload["aud"]
- 45
payload
end
end
- 15
module IndicatorJwt
- 15
def jwt_claims(*)
- 15
return super unless resource_indicators
- 15
super.merge(aud: resource_indicators)
end
- 15
def jwt_decode(token, verify_aud: true, **args)
- 45
claims = super(token, verify_aud: false, **args)
- 45
return claims unless verify_aud
- 30
return unless claims["aud"] && claims["aud"].one? { |aud| request.url.starts_with?(aud) }
- 15
claims
end
end
- 15
def self.included(rodauth)
- 225
super
- 225
rodauth.send(:include, IndicatorAuthorizationCodeGrant) if rodauth.features.include?(:oauth_authorization_code_grant)
- 225
rodauth.send(:include, IndicatorIntrospection) if rodauth.features.include?(:oauth_token_introspection)
- 225
rodauth.send(:include, IndicatorJwt) if rodauth.features.include?(:oauth_jwt)
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_resource_server, :OauthResourceServer) do
- 15
depends :oauth_token_introspection
- 15
auth_value_method :is_authorization_server?, false
- 15
auth_value_methods(
:before_introspection_request
)
- 15
def authorization_token
- 270
return @authorization_token if defined?(@authorization_token)
# check if there is a token
- 135
access_token = fetch_access_token
- 135
return unless access_token
# where in resource server, NOT the authorization server.
- 105
payload = introspection_request("access_token", access_token)
- 105
return unless payload["active"]
- 90
@authorization_token = payload
end
- 15
def require_oauth_authorization(*scopes)
- 135
authorization_required unless authorization_token
- 90
aux_scopes = authorization_token["scope"]
- 90
token_scopes = if aux_scopes
- 90
aux_scopes.split(oauth_scope_separator)
else
[]
end
- 180
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
end
- 15
private
- 15
def introspection_request(token_type_hint, token)
- 105
introspect_url = URI("#{authorization_server_url}#{introspect_path}")
- 105
response = http_request(introspect_url, { "token_type_hint" => token_type_hint, "token" => token }) do |request|
- 105
before_introspection_request(request)
end
- 105
JSON.parse(response.body)
end
- 15
def before_introspection_request(request); end
end
end
# frozen_string_literal: true
- 15
require "onelogin/ruby-saml"
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_saml_bearer_grant, :OauthSamlBearerGrant) do
- 15
depends :oauth_assertion_base
- 15
auth_value_method :oauth_saml_cert_fingerprint, "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
- 15
auth_value_method :oauth_saml_cert, nil
- 15
auth_value_method :oauth_saml_cert_fingerprint_algorithm, nil
- 15
auth_value_method :oauth_saml_name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
- 15
auth_value_method :oauth_saml_security_authn_requests_signed, true
- 15
auth_value_method :oauth_saml_security_metadata_signed, true
- 15
auth_value_method :oauth_saml_security_digest_method, XMLSecurity::Document::SHA1
- 15
auth_value_method :oauth_saml_security_signature_method, XMLSecurity::Document::RSA_SHA1
- 15
auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
- 15
auth_value_methods(
:require_oauth_application_from_saml2_bearer_assertion_issuer,
:require_oauth_application_from_saml2_bearer_assertion_subject,
:account_from_saml2_bearer_assertion
)
- 15
def oauth_grant_types_supported
- 30
super | %w[urn:ietf:params:oauth:grant-type:saml2-bearer]
end
- 15
private
- 15
def require_oauth_application_from_saml2_bearer_assertion_issuer(assertion)
- 15
saml = saml_assertion(assertion)
- 15
return unless saml
- 14
db[oauth_applications_table].where(
oauth_applications_homepage_url_column => saml.issuers
- 4
).first
end
- 15
def require_oauth_application_from_saml2_bearer_assertion_subject(assertion)
- 15
saml = saml_assertion(assertion)
- 15
return unless saml
- 14
db[oauth_applications_table].where(
oauth_applications_client_id_column => saml.nameid
- 4
).first
end
- 15
def account_from_saml2_bearer_assertion(assertion)
- 15
saml = saml_assertion(assertion)
- 15
return unless saml
- 15
account_from_bearer_assertion_subject(saml.nameid)
end
- 15
def saml_assertion(assertion)
- 45
settings = OneLogin::RubySaml::Settings.new
- 45
settings.idp_cert = oauth_saml_cert
- 45
settings.idp_cert_fingerprint = oauth_saml_cert_fingerprint
- 45
settings.idp_cert_fingerprint_algorithm = oauth_saml_cert_fingerprint_algorithm
- 45
settings.name_identifier_format = oauth_saml_name_identifier_format
- 45
settings.security[:authn_requests_signed] = oauth_saml_security_authn_requests_signed
- 45
settings.security[:metadata_signed] = oauth_saml_security_metadata_signed
- 45
settings.security[:digest_method] = oauth_saml_security_digest_method
- 45
settings.security[:signature_method] = oauth_saml_security_signature_method
- 45
response = OneLogin::RubySaml::Response.new(assertion, settings: settings, skip_recipient_check: true)
# 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, ...
- 45
redirect_response_error("invalid_grant") unless response.is_valid?
# In order to issue an access token response as described in OAuth 2.0
# [RFC6749] or to rely on an Assertion for client authentication, the
# authorization server MUST validate the Assertion according to the
# criteria below.
# 1. The Assertion's <Issuer> element MUST contain a unique identifier
# for the entity that issued the Assertion.
- 45
redirect_response_error("invalid_grant") unless response.issuers.size == 1
# 2. in addition to the URI references
# discussed there, the token endpoint URL of the authorization
# server MAY be used as a URI that identifies the authorization
# server as an intended audience. The authorization server MUST
# reject any Assertion that does not contain its own identity as
# the intended audience.
- 45
redirect_response_error("invalid_grant") if response.audiences && !response.audiences.include?(token_url)
- 45
response
end
- 15
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
- 15
require "rodauth/oauth"
- 15
require "rodauth/oauth/http_extensions"
- 15
module Rodauth
- 15
Feature.define(:oauth_token_introspection, :OauthTokenIntrospection) do
- 15
depends :oauth_base
- 15
before "introspect"
- 15
auth_value_methods(
:resource_owner_identifier
)
# /introspect
- 15
auth_server_route(:introspect) do |r|
- 195
require_oauth_application_for_introspect
- 195
before_introspect_route
- 195
r.post do
- 195
catch_error do
- 195
validate_introspect_params
- 165
token_type_hint = param_or_nil("token_type_hint")
- 165
before_introspect
- 165
oauth_grant = case token_type_hint
when "access_token", nil
- 150
if features.include?(:oauth_jwt) && oauth_jwt_access_tokens
- 45
jwt_decode(param("token"))
else
- 105
oauth_grant_by_token(param("token"))
end
when "refresh_token"
- 15
oauth_grant_by_refresh_token(param("token"))
end
- 165
oauth_grant ||= oauth_grant_by_refresh_token(param("token")) if token_type_hint.nil?
- 165
json_response_success(json_token_introspect_payload(oauth_grant))
end
throw_json_response_error(oauth_invalid_response_status, "invalid_request")
end
end
# Token introspect
- 54
def validate_introspect_params(token_hint_types = %w[access_token refresh_token].freeze)
# check if valid token hint type
- 195
if param_or_nil("token_type_hint") && !token_hint_types.include?(param("token_type_hint"))
- 15
redirect_response_error("unsupported_token_type")
end
- 180
redirect_response_error("invalid_request") unless param_or_nil("token")
end
- 15
def json_token_introspect_payload(grant_or_claims)
- 165
return { active: false } unless grant_or_claims
- 135
if grant_or_claims["sub"]
# JWT
- 12
{
- 24
active: true,
- 6
scope: grant_or_claims["scope"],
- 6
client_id: grant_or_claims["client_id"],
- 6
username: resource_owner_identifier(grant_or_claims),
token_type: "access_token",
- 6
exp: grant_or_claims["exp"],
- 6
iat: grant_or_claims["iat"],
- 6
nbf: grant_or_claims["nbf"],
- 6
sub: grant_or_claims["sub"],
- 6
aud: grant_or_claims["aud"],
- 6
iss: grant_or_claims["iss"],
- 6
jti: grant_or_claims["jti"]
}
else
- 24
{
- 48
active: true,
- 15
scope: grant_or_claims[oauth_grants_scopes_column],
- 15
client_id: oauth_application[oauth_applications_client_id_column],
- 15
username: resource_owner_identifier(grant_or_claims),
- 15
token_type: oauth_token_type,
- 15
exp: grant_or_claims[oauth_grants_expires_in_column].to_i
}
end
end
- 15
def check_csrf?
- 255
case request.path
- 48
when introspect_path
- 195
false
else
- 60
super
end
end
- 15
private
- 15
def require_oauth_application_for_introspect
- 195
(token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Bearer (.*)\Z/, 1]))
- 195
return require_oauth_application unless token
- 30
oauth_application = current_oauth_application
- 30
authorization_required unless oauth_application
- 30
@oauth_application = oauth_application
end
- 15
def oauth_server_metadata_body(*)
- 30
super.tap do |data|
- 30
data[:introspection_endpoint] = introspect_url
- 30
data[:introspection_endpoint_auth_methods_supported] = %w[client_secret_basic]
end
end
- 15
def resource_owner_identifier(grant_or_claims)
- 135
if (account_id = grant_or_claims[oauth_grants_account_id_column])
- 75
account_ds(account_id).select(login_column).first[login_column]
- 60
elsif (app_id = grant_or_claims[oauth_grants_oauth_application_id_column])
- 11
db[oauth_applications_table].where(oauth_applications_id_column => app_id)
.select(oauth_applications_name_column)
- 4
.first[oauth_applications_name_column]
- 45
elsif (subject = grant_or_claims["sub"])
# JWT
- 45
if subject == grant_or_claims["client_id"]
- 11
db[oauth_applications_table].where(oauth_applications_client_id_column => subject)
.select(oauth_applications_name_column)
- 4
.first[oauth_applications_name_column]
else
- 30
account_ds(subject).select(login_column).first[login_column]
end
end
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oauth_token_revocation, :OauthTokenRevocation) do
- 15
depends :oauth_base
- 15
before "revoke"
- 15
after "revoke"
- 15
notice_flash "The oauth grant has been revoked", "revoke_oauth_grant"
# /revoke
- 15
auth_server_route(:revoke) do |r|
- 150
if logged_in?
- 15
require_account
- 15
require_oauth_application_from_account
else
- 135
require_oauth_application
end
- 150
before_revoke_route
- 150
r.post do
- 150
catch_error do
- 150
validate_revoke_params
- 105
oauth_grant = nil
- 105
transaction do
- 105
before_revoke
- 105
oauth_grant = revoke_oauth_grant
- 60
after_revoke
end
- 60
if accepts_json?
- 12
json_payload = {
- 30
"revoked_at" => convert_timestamp(oauth_grant[oauth_grants_revoked_at_column])
}
- 45
if param("token_type_hint") == "refresh_token"
- 30
json_payload["refresh_token"] = oauth_grant[oauth_grants_refresh_token_column]
else
- 15
json_payload["token"] = oauth_grant[oauth_grants_token_column]
end
- 45
json_response_success json_payload
else
- 15
set_notice_flash revoke_oauth_grant_notice_flash
- 15
redirect request.referer || "/"
end
end
- 30
redirect_response_error("invalid_request", request.referer || "/")
end
end
- 45
def validate_revoke_params(token_hint_types = %w[access_token refresh_token].freeze)
- 150
token_hint = param_or_nil("token_type_hint")
- 150
if features.include?(:oauth_jwt) && oauth_jwt_access_tokens && (!token_hint || token_hint == "access_token")
# JWT access tokens can't be revoked
- 30
throw(:rodauth_error)
end
# check if valid token hint type
- 120
redirect_response_error("unsupported_token_type") if token_hint && !token_hint_types.include?(token_hint)
- 105
redirect_response_error("invalid_request") unless param_or_nil("token")
end
- 15
def check_csrf?
- 1179
case request.path
- 240
when revoke_path
- 150
!json_request?
else
- 1029
super
end
end
- 15
private
- 15
def revoke_oauth_grant
- 105
token = param("token")
- 105
if param("token_type_hint") == "refresh_token"
- 30
oauth_grant = oauth_grant_by_refresh_token(token)
- 30
token_column = oauth_grants_refresh_token_column
else
- 90
oauth_grant = oauth_grant_by_token_ds(token).where(
- 12
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
- 12
).first
- 75
token_column = oauth_grants_token_column
end
- 105
redirect_response_error("invalid_request") unless oauth_grant
- 60
redirect_response_error("invalid_request") unless grant_from_application?(oauth_grant, oauth_application)
- 60
update_params = { oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
- 60
ds = db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
- 60
oauth_grant = __update_and_return__(ds, update_params)
- 60
oauth_grant[token_column] = token
- 60
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
- 15
def oauth_server_metadata_body(*)
- 30
super.tap do |data|
- 30
data[:revocation_endpoint] = revoke_url
- 30
data[:revocation_endpoint_auth_methods_supported] = nil # because it's client_secret_basic
end
end
end
end
# frozen_string_literal: true
- 15
require "rodauth/oauth"
- 15
module Rodauth
- 15
Feature.define(:oidc, :Oidc) do
# https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
- 4
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
- 12
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
].freeze
- 12
REQUIRED_METADATA_KEYS = %i[
issuer
authorization_endpoint
token_endpoint
jwks_uri
response_types_supported
subject_types_supported
id_token_signing_alg_values_supported
].freeze
- 15
depends :account_expiration, :oauth_jwt, :oauth_jwt_jwks, :oauth_authorization_code_grant
- 15
auth_value_method :oauth_application_scopes, %w[openid]
- 11
%i[
subject_type application_type sector_identifier_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
- 4
].each do |column|
- 135
auth_value_method :"oauth_applications_#{column}_column", column
end
- 15
%i[nonce acr claims_locales claims].each do |column|
- 60
auth_value_method :"oauth_grants_#{column}_column", column
end
- 15
auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
- 15
auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
- 15
translatable_method :oauth_invalid_scope_message, "The Access Token expired"
- 15
auth_value_method :oauth_prompt_login_cookie_key, "_rodauth_oauth_prompt_login"
- 15
auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
- 15
auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
- 15
auth_value_methods(
:oauth_acr_values_supported,
:get_oidc_account_last_login_at,
:oidc_authorize_on_prompt_none?,
:get_oidc_param,
:get_additional_param,
:require_acr_value_phr,
:require_acr_value_phrh,
:require_acr_value,
:json_webfinger_payload
)
# /userinfo
- 15
auth_server_route(:userinfo) do |r|
- 135
r.on method: %i[get post] do
- 135
catch_error do
- 135
claims = authorization_token
- 135
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless claims
- 135
oauth_scopes = claims["scope"].split(" ")
- 135
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
- 135
account = db[accounts_table].where(account_id_column => claims["sub"]).first
- 135
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless account
- 135
oauth_scopes.delete("openid")
- 135
oidc_claims = { "sub" => claims["sub"] }
- 135
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
- 135
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless @oauth_application
- 135
oauth_grant = valid_oauth_grant_ds(
- 24
oauth_grants_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
- 24
oauth_grants_account_id_column => account[account_id_column]
- 24
).first
- 135
claims_locales = oauth_grant[oauth_grants_claims_locales_column] if oauth_grant
- 135
if (claims = oauth_grant[oauth_grants_claims_column])
- 15
claims = JSON.parse(claims)
- 15
if (userinfo_essential_claims = claims["userinfo"])
- 15
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
- 135
fill_with_account_claims(oidc_claims, account, oauth_scopes, claims_locales)
- 135
if (algo = @oauth_application[oauth_applications_userinfo_signed_response_alg_column])
- 8
params = {
- 19
jwks: oauth_application_jwks(@oauth_application),
- 3
encryption_algorithm: @oauth_application[oauth_applications_userinfo_encrypted_response_alg_column],
- 3
encryption_method: @oauth_application[oauth_applications_userinfo_encrypted_response_enc_column]
- 3
}.compact
- 30
jwt = jwt_encode(
- 3
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.
- 3
iss: oauth_jwt_issuer,
- 3
aud: @oauth_application[oauth_applications_client_id_column]
),
signing_algorithm: algo,
**params
)
- 30
jwt_response_success(jwt)
else
- 105
json_response_success(oidc_claims)
end
end
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token")
end
end
- 15
def load_openid_configuration_route(alt_issuer = nil)
- 75
request.on(".well-known/openid-configuration") do
- 75
allow_cors(request)
- 60
request.is do
- 60
request.get do
- 60
json_response_success(openid_configuration_body(alt_issuer), cache: true)
end
end
end
end
- 15
def load_webfinger_route
- 30
request.on(".well-known/webfinger") do
- 30
request.get do
- 30
resource = param_or_nil("resource")
- 30
throw_json_response_error(400, "invalid_request") unless resource
- 15
response.status = 200
- 15
response["Content-Type"] ||= "application/jrd+json"
- 15
return_response(json_webfinger_payload)
end
end
end
- 15
def check_csrf?
- 4059
case request.path
- 768
when userinfo_path
- 135
false
else
- 3924
super
end
end
- 15
def oauth_response_types_supported
- 1032
grant_types = oauth_grant_types_supported
- 1032
oidc_response_types = %w[id_token none]
- 1032
oidc_response_types |= ["code id_token"] if grant_types.include?("authorization_code")
- 1032
oidc_response_types |= ["code token", "id_token token", "code id_token token"] if grant_types.include?("implicit")
- 1032
super | oidc_response_types
end
- 15
def current_oauth_account
- 15
subject_type = current_oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
- 15
return super unless subject_type == "pairwise"
end
- 15
private
- 15
if defined?(::I18n)
- 15
def before_authorize_route
- 1089
if (ui_locales = param_or_nil("ui_locales"))
- 15
ui_locales = ui_locales.split(" ").map(&:to_sym)
- 15
ui_locales &= ::I18n.available_locales
- 15
::I18n.locale = ui_locales.first unless ui_locales.empty?
end
- 1089
super
end
end
- 15
def oauth_acr_values_supported
- 129
acr_values = []
- 129
acr_values << "phrh" if features.include?(:webauthn_login)
- 129
acr_values << "phr" if respond_to?(:require_two_factor_authenticated)
- 129
acr_values
end
- 15
def oidc_authorize_on_prompt_none?(_account)
- 15
false
end
- 15
def validate_authorize_params
- 1089
if (max_age = param_or_nil("max_age"))
- 30
max_age = Integer(max_age)
- 30
redirect_response_error("invalid_request") unless max_age.positive?
- 30
if Time.now - get_oidc_account_last_login_at(session_value) > max_age
# force user to re-login
- 15
clear_session
- 15
set_session_value(login_redirect_session_key, request.fullpath)
- 15
redirect require_login_redirect
end
end
- 1074
if (claims = param_or_nil("claims"))
# The value is a JSON object listing the requested Claims.
- 30
claims = JSON.parse(claims)
- 30
claims.each do |_, individual_claims|
- 60
redirect_response_error("invalid_request") unless individual_claims.is_a?(Hash)
- 60
individual_claims.each do |_, claim|
- 90
redirect_response_error("invalid_request") unless claim.nil? || individual_claims.is_a?(Hash)
end
end
end
- 1074
sc = scopes
- 1074
if sc && sc.include?("offline_access")
- 45
sc.delete("offline_access")
# 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