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