lib/rodauth/features/oauth.rb in rodauth-oauth-0.0.2 vs lib/rodauth/features/oauth.rb in rodauth-oauth-0.0.3
- old
+ new
@@ -31,12 +31,10 @@
using(SuffixExtensions)
end
SCOPES = %w[profile.read].freeze
- depends :login
-
before "authorize"
after "authorize"
after "authorize_failure"
before "token"
@@ -62,17 +60,21 @@
view "new_oauth_application", "New Oauth Application", "new_oauth_application"
view "oauth_tokens", "Oauth Tokens", "oauth_tokens"
auth_value_method :json_response_content_type, "application/json"
- auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
+ auth_value_method :oauth_grant_expires_in, 60 * 5 # 60 minutes
auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
- auth_value_method :use_oauth_implicit_grant_type, false
+ auth_value_method :use_oauth_implicit_grant_type?, false
+ auth_value_method :use_oauth_pkce?, true
+ auth_value_method :use_oauth_access_type?, true
auth_value_method :oauth_require_pkce, false
auth_value_method :oauth_pkce_challenge_method, "S256"
+ auth_value_method :oauth_valid_uri_schemes, %w[http https]
+
# URL PARAMS
# Authorize / token
%w[
grant_type code refresh_token client_id client_secret scope
@@ -166,47 +168,61 @@
:oauth_unique_id_generator,
:secret_matches?,
:secret_hash
)
+ auth_value_methods(:only_json?)
+
redirect(:oauth_application) do |id|
"/#{oauth_applications_path}/#{id}"
end
redirect(:require_authorization) do
if logged_in?
oauth_authorize_path
- else
+ elsif respond_to?(:login_redirect)
login_redirect
+ else
+ default_redirect
end
end
- auth_value_method :json_request_accept_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
- auth_methods(:json_request?)
+ auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
def check_csrf?
case request.path
when oauth_token_path
false
when oauth_revoke_path
!json_request?
+ when oauth_authorize_path, %r{/#{oauth_applications_path}}
+ only_json? ? false : super
else
super
end
end
# Overrides logged_in?, so that a valid authorization token also authnenticates a request
def logged_in?
super || authorization_token
end
- def json_request?
- return @json_request if defined?(@json_request)
+ def accepts_json?
+ return true if only_json?
- @json_request = request.get_header("HTTP_ACCEPT") =~ json_request_accept_regexp
+ (accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
end
+ unless method_defined?(:json_request?)
+ # copied from the jwt feature
+ def json_request?
+ return @json_request if defined?(@json_request)
+
+ @json_request = request.content_type =~ json_request_regexp
+ end
+ end
+
attr_reader :oauth_application
def initialize(scope)
@scope = scope
end
@@ -273,11 +289,11 @@
def require_oauth_authorization(*scopes)
authorization_required unless authorization_token
scopes << oauth_application_default_scope if scopes.empty?
- token_scopes = authorization_token[:scopes].split(",")
+ token_scopes = authorization_token[oauth_tokens_scopes_column].split(",")
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
end
# /oauth-applications routes
@@ -424,10 +440,22 @@
else
db[oauth_tokens_table].where(oauth_tokens_refresh_token_column => token)
end
end
+ def json_access_token_payload(oauth_token)
+ payload = {
+ "access_token" => oauth_token[oauth_tokens_token_column],
+ "token_type" => oauth_token_type.downcase,
+ "expires_in" => oauth_token_expires_in
+ }
+ if oauth_token[oauth_tokens_refresh_token_column]
+ payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column]
+ end
+ payload
+ end
+
# Oauth Application
def oauth_application_params
@oauth_application_params ||= oauth_application_required_params.each_with_object({}) do |param, params|
value = request.params[__send__(:"oauth_application_#{param}_param")]
@@ -442,11 +470,13 @@
def validate_oauth_application_params
oauth_application_params.each do |key, value|
if key == oauth_application_homepage_url_param ||
key == oauth_application_redirect_uri_param
- set_field_error(key, invalid_url_message) unless URI::DEFAULT_PARSER.make_regexp(%w[http https]).match?(value)
+ unless URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(value)
+ set_field_error(key, invalid_url_message)
+ end
elsif key == oauth_application_scopes_param
value.each do |scope|
set_field_error(key, invalid_scope_message) unless oauth_application_scopes.include?(scope)
@@ -507,19 +537,22 @@
!raised && id
end
# Authorize
+ def before_authorize
+ require_account
+ end
def validate_oauth_grant_params
unless oauth_application && check_valid_redirect_uri? && check_valid_access_type? &&
check_valid_approval_prompt? && check_valid_response_type?
redirect_response_error("invalid_request")
end
redirect_response_error("invalid_scope") unless check_valid_scopes?
- validate_pkce_challenge_params
+ validate_pkce_challenge_params if use_oauth_pkce?
end
def try_approval_prompt
approval_prompt = param_or_nil(approval_prompt_param)
@@ -546,22 +579,28 @@
oauth_grants_code_column => oauth_unique_id_generator,
oauth_grants_expires_in_column => Time.now + oauth_grant_expires_in,
oauth_grants_scopes_column => scopes.join(",")
}
- if (access_type = param_or_nil(access_type_param))
- create_params[oauth_grants_access_type_column] = access_type
+ # Access Type flow
+ if use_oauth_access_type?
+ if (access_type = param_or_nil(access_type_param))
+ create_params[oauth_grants_access_type_column] = access_type
+ end
end
# PKCE flow
- if (code_challenge = param_or_nil(code_challenge_param))
- code_challenge_method = param_or_nil(code_challenge_method_param)
+ if use_oauth_pkce?
- create_params[oauth_grants_code_challenge_column] = code_challenge
- create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
- elsif oauth_require_pkce
- redirect_response_error("code_challenge_required")
+ if (code_challenge = param_or_nil(code_challenge_param))
+ code_challenge_method = param_or_nil(code_challenge_method_param)
+
+ create_params[oauth_grants_code_challenge_column] = code_challenge
+ create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
+ elsif oauth_require_pkce
+ redirect_response_error("code_challenge_required")
+ end
end
ds = db[oauth_grants_table]
begin
@@ -611,89 +650,105 @@
redirect_response_error("invalid_request") unless secret_matches?(oauth_application, client_secret)
end
case param(grant_type_param)
when "authorization_code"
+ create_oauth_token_from_authorization_code(oauth_application)
+ when "refresh_token"
+ create_oauth_token_from_token(oauth_application)
+ else
+ redirect_response_error("invalid_grant")
+ end
+ end
- # fetch oauth grant
- oauth_grant = db[oauth_grants_table].where(
- oauth_grants_code_column => param(code_param),
- oauth_grants_redirect_uri_column => param(redirect_uri_param),
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
- oauth_grants_revoked_at_column => nil
- ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
- .first
+ def create_oauth_token_from_authorization_code(oauth_application)
+ # fetch oauth grant
+ oauth_grant = db[oauth_grants_table].where(
+ oauth_grants_code_column => param(code_param),
+ oauth_grants_redirect_uri_column => param(redirect_uri_param),
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
+ oauth_grants_revoked_at_column => nil
+ ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
+ .for_update
+ .first
- redirect_response_error("invalid_grant") unless oauth_grant
+ redirect_response_error("invalid_grant") unless oauth_grant
- # PKCE
+ # PKCE
+ if use_oauth_pkce?
if oauth_grant[oauth_grants_code_challenge_column]
code_verifier = param_or_nil(code_verifier_param)
unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
redirect_response_error("invalid_request")
end
elsif oauth_require_pkce
redirect_response_error("code_challenge_required")
end
+ end
- create_params = {
- oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
- oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
- oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
- oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
- }
+ create_params = {
+ oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
+ oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
+ oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
+ oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
+ }
- # revoke oauth grant
- db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
- .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
+ # revoke oauth grant
+ db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
+ .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
- generate_oauth_token(create_params, oauth_grant[oauth_grants_access_type_column] == "offline")
+ should_generate_refresh_token = !use_oauth_access_type? ||
+ oauth_grant[oauth_grants_access_type_column] == "offline"
- when "refresh_token"
- # fetch oauth token
- oauth_token = oauth_token_by_refresh_token(param(refresh_token_param)).where(
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column]
- ).where(oauth_grants_revoked_at_column => nil).first
+ generate_oauth_token(create_params, should_generate_refresh_token)
+ end
- redirect_response_error("invalid_grant") unless oauth_token
+ def create_oauth_token_from_token(oauth_application)
+ # fetch oauth token
+ oauth_token = oauth_token_by_refresh_token(param(refresh_token_param)).where(
+ oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column]
+ ).where(oauth_grants_revoked_at_column => nil).for_update.first
- token = oauth_unique_id_generator
+ redirect_response_error("invalid_grant") unless oauth_token
- update_params = {
- oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
- oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
- }
+ token = oauth_unique_id_generator
- if oauth_tokens_token_hash_column
- update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
- else
- update_params[oauth_tokens_token_column] = token
- end
+ update_params = {
+ oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
+ oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
+ }
- ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
+ if oauth_tokens_token_hash_column
+ update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
+ else
+ update_params[oauth_tokens_token_column] = token
+ end
- oauth_token = begin
- if ds.supports_returning?(:update)
- ds.returning.update(update_params)
- else
- ds.update(update_params)
- ds.first
- end
- rescue Sequel::UniqueConstraintViolation
- retry
- end
+ ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
- oauth_token[oauth_tokens_token_column] = token
- oauth_token
- else
- redirect_response_error("invalid_grant")
+ oauth_token = begin
+ if ds.supports_returning?(:update)
+ ds.returning.update(update_params)
+ else
+ ds.update(update_params)
+ ds.first
+ end
+ rescue Sequel::UniqueConstraintViolation
+ retry
end
+
+ oauth_token[oauth_tokens_token_column] = token
+ oauth_token
end
# Token revocation
+ def before_revoke
+ require_account
+ end
+
TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
def validate_oauth_revoke_params
# check if valid token hint type
redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(token_type_hint)
@@ -717,11 +772,11 @@
oauth_tokens_oauth_application_id_column => db[oauth_applications_table].where(
oauth_applications_client_id_column => param(client_id_param),
oauth_applications_account_id_column => account_id
).select(oauth_applications_id_column)
)
- ).first
+ ).for_update.first
redirect_response_error("invalid_request") unless oauth_token
update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
@@ -747,11 +802,11 @@
end
# Response helpers
def redirect_response_error(error_code, redirect_url = request.referer || default_redirect)
- if json_request?
+ if accepts_json?
throw_json_response_error(invalid_oauth_response_status, error_code)
else
redirect_url = URI.parse(redirect_url)
query_params = []
@@ -781,11 +836,11 @@
end
payload = { "error" => code }
payload["error_description"] = send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message")
json_payload = _json_response_body(payload)
response["Content-Type"] ||= json_response_content_type
- response["WWW-Authenticate"] = "Bearer" if status == 401
+ response["WWW-Authenticate"] = oauth_token_type if status == 401
response.write(json_payload)
request.halt
end
unless method_defined?(:_json_response_body)
@@ -797,11 +852,11 @@
end
end
end
def authorization_required
- if json_request?
+ if accepts_json?
throw_json_response_error(authorization_required_error_status, "invalid_client")
else
set_redirect_error_flash(require_authorization_error_flash)
redirect(require_authorization_redirect)
end
@@ -818,27 +873,31 @@
end
ACCESS_TYPES = %w[offline online].freeze
def check_valid_access_type?
+ return true unless use_oauth_access_type?
+
access_type = param_or_nil(access_type_param)
!access_type || ACCESS_TYPES.include?(access_type)
end
APPROVAL_PROMPTS = %w[force auto].freeze
def check_valid_approval_prompt?
+ return true unless use_oauth_access_type?
+
approval_prompt = param_or_nil(approval_prompt_param)
!approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
end
def check_valid_response_type?
response_type = param_or_nil(response_type_param)
return true if response_type.nil? || response_type == "code"
- return use_oauth_implicit_grant_type if response_type == "token"
+ return use_oauth_implicit_grant_type? if response_type == "token"
false
end
# PKCE
@@ -871,32 +930,25 @@
end
end
# /oauth-token
route(:oauth_token) do |r|
+ before_token
+
r.post do
catch_error do
validate_oauth_token_params
oauth_token = nil
transaction do
- before_token
oauth_token = create_oauth_token
after_token
end
response.status = 200
response["Content-Type"] ||= json_response_content_type
- json_response = {
- "token" => oauth_token[oauth_tokens_token_column],
- "token_type" => oauth_token_type,
- "expires_in" => oauth_token_expires_in
- }
-
- json_response["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[:refresh_token]
-
- json_payload = _json_response_body(json_response)
+ json_payload = _json_response_body(json_access_token_payload(oauth_token))
response.write(json_payload)
request.halt
end
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
@@ -904,24 +956,24 @@
end
# /oauth-revoke
route(:oauth_revoke) do |r|
require_account
+ before_revoke
# access-token
r.post do
catch_error do
validate_oauth_revoke_params
oauth_token = nil
transaction do
- before_revoke
oauth_token = revoke_oauth_token
after_revoke
end
- if json_request?
+ if accepts_json?
response.status = 200
response["Content-Type"] ||= json_response_content_type
json_response = {
"token" => oauth_token[oauth_tokens_token_column],
"refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
@@ -942,39 +994,39 @@
# /oauth-authorize
route(:oauth_authorize) do |r|
require_account
validate_oauth_grant_params
- try_approval_prompt if request.get?
+ try_approval_prompt if use_oauth_access_type? && request.get?
+ before_authorize
+
r.get do
authorize_view
end
r.post do
code = nil
query_params = []
fragment_params = []
transaction do
- before_authorize
case param(response_type_param)
when "token"
- redirect_response_error("invalid_request", redirect_uri) unless use_oauth_implicit_grant_type
+ redirect_response_error("invalid_request", redirect_uri) unless use_oauth_implicit_grant_type?
create_params = {
oauth_tokens_account_id_column => account_id,
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
oauth_tokens_scopes_column => scopes
}
oauth_token = generate_oauth_token(create_params, false)
- fragment_params << ["access_token=#{oauth_token[oauth_tokens_token_column]}"]
- fragment_params << ["token_type=#{oauth_token_type}"]
- fragment_params << ["expires_in=#{oauth_token_expires_in}"]
+ token_payload = json_access_token_payload(oauth_token)
+ fragment_params.replace(token_payload.map { |k, v| "#{k}=#{v}" })
when "code", "", nil
code = create_oauth_grant
- query_params << ["code=#{code}"]
+ query_params << "code=#{code}"
else
redirect_response_error("invalid_request")
end
after_authorize
end