lib/rodauth/features/oauth.rb in rodauth-oauth-0.0.6 vs lib/rodauth/features/oauth.rb in rodauth-oauth-0.1.0

- old
+ new

@@ -41,11 +41,10 @@ SCOPES = %w[profile.read].freeze before "authorize" after "authorize" - after "authorize_failure" before "token" before "revoke" after "revoke" @@ -53,19 +52,17 @@ before "introspect" before "create_oauth_application" after "create_oauth_application" - error_flash "OAuth Authorization invalid parameters", "oauth_grant_valid_parameters" - error_flash "Please authorize to continue", "require_authorization" error_flash "There was an error registering your oauth application", "create_oauth_application" notice_flash "Your oauth application has been registered", "create_oauth_application" notice_flash "The oauth token has been revoked", "revoke_oauth_token" - view "oauth_authorize", "Authorize", "authorize" + view "authorize", "Authorize", "authorize" view "oauth_applications", "Oauth Applications", "oauth_applications" view "oauth_application", "Oauth Application", "oauth_application" view "new_oauth_application", "New Oauth Application", "new_oauth_application" view "oauth_tokens", "Oauth Tokens", "oauth_tokens" @@ -78,11 +75,11 @@ 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] + auth_value_method :oauth_valid_uri_schemes, %w[https] auth_value_method :oauth_scope_separator, " " # Application APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri client_secret].freeze @@ -146,13 +143,11 @@ auth_value_method :oauth_application_default_scope, SCOPES.first auth_value_method :oauth_application_scopes, SCOPES auth_value_method :oauth_token_type, "bearer" - auth_value_method :invalid_request, "Request is missing a required parameter" - auth_value_method :invalid_client, "Invalid client" - auth_value_method :unauthorized_client, "Unauthorized client" + auth_value_method :invalid_client_message, "Invalid client" auth_value_method :invalid_grant_type_message, "Invalid grant type" auth_value_method :invalid_grant_message, "Invalid grant" auth_value_method :invalid_scope_message, "Invalid scope" auth_value_method :invalid_url_message, "Invalid URL" @@ -193,15 +188,15 @@ SERVER_METADATA = OAuth::TtlStore.new def check_csrf? case request.path - when oauth_token_path, oauth_introspect_path + when token_path, introspect_path false - when oauth_revoke_path + when revoke_path !json_request? - when oauth_authorize_path, %r{/#{oauth_applications_path}} + when authorize_path, %r{/#{oauth_applications_path}} only_json? ? false : super else super end end @@ -231,11 +226,19 @@ def initialize(scope) @scope = scope end def scopes - (param_or_nil("scope") || oauth_application_default_scope).split(" ") + scope = request.params["scope"] + case scope + when Array + scope + when String + scope.split(" ") + when nil + [oauth_application_default_scope] + end end def redirect_uri param_or_nil("redirect_uri") || begin return unless oauth_application @@ -264,10 +267,12 @@ scheme, token = value.split(" ", 2) return unless scheme.downcase == oauth_token_type + return if token.empty? + token end def authorization_token return @authorization_token if defined?(@authorization_token) @@ -407,11 +412,11 @@ def introspection_request(token_type_hint, token) auth_url = URI(authorization_server_url) http = Net::HTTP.new(auth_url.host, auth_url.port) http.use_ssl = auth_url.scheme == "https" - request = Net::HTTP::Post.new(oauth_introspect_path) + request = Net::HTTP::Post.new(introspect_path) request["content-type"] = json_response_content_type request["accept"] = json_response_content_type request.body = JSON.dump({ "token_type_hint" => token_type_hint, "token" => token }) before_introspection_request(request) @@ -692,18 +697,18 @@ # if there's a previous oauth grant for the params combo, it means that this user has approved before. request.env["REQUEST_METHOD"] = "POST" end - def create_oauth_grant - create_params = { + def create_oauth_grant(create_params = {}) + create_params.merge!( 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_expires_in_column => Time.now + oauth_grant_expires_in, oauth_grants_scopes_column => scopes.join(oauth_scope_separator) - } + ) # Access Type flow if use_oauth_access_type? if (access_type = param_or_nil("access_type")) create_params[oauth_grants_access_type_column] = access_type @@ -733,10 +738,49 @@ rescue Sequel::UniqueConstraintViolation retry end end + def do_authorize(redirect_url, query_params = [], fragment_params = []) + case param("response_type") + when "token" + redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type? + + fragment_params.replace(_do_authorize_token.map { |k, v| "#{k}=#{v}" }) + when "code", "", nil + query_params.replace(_do_authorize_code.map { |k, v| "#{k}=#{v}" }) + end + + if param_or_nil("state") + if !fragment_params.empty? + fragment_params << "state=#{param('state')}" + else + query_params << "state=#{param('state')}" + end + end + + query_params << redirect_url.query if redirect_url.query + + redirect_url.query = query_params.join("&") unless query_params.empty? + redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty? + end + + def _do_authorize_code + { "code" => create_oauth_grant } + end + + def _do_authorize_token + 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) + + json_access_token_payload(oauth_token) + end + # Access Tokens def before_token require_oauth_application end @@ -758,31 +802,46 @@ end def create_oauth_token case param("grant_type") when "authorization_code" - create_oauth_token_from_authorization_code(oauth_application) + # fetch oauth grant + oauth_grant = db[oauth_grants_table].where( + oauth_grants_code_column => param("code"), + oauth_grants_redirect_uri_column => param("redirect_uri"), + oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column], + 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 + + 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_oauth_token_from_authorization_code(oauth_grant, create_params) when "refresh_token" - create_oauth_token_from_token(oauth_application) + # fetch oauth token + oauth_token = oauth_token_by_refresh_token(param("refresh_token")) + + 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 + } + create_oauth_token_from_token(oauth_token, update_params) else redirect_response_error("invalid_grant") end end - 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"), - oauth_grants_redirect_uri_column => param("redirect_uri"), - 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 - + def create_oauth_token_from_authorization_code(oauth_grant, create_params) # PKCE if use_oauth_pkce? if oauth_grant[oauth_grants_code_challenge_column] code_verifier = param_or_nil("code_verifier") @@ -790,40 +849,25 @@ 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] - } - # 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) should_generate_refresh_token = !use_oauth_access_type? || oauth_grant[oauth_grants_access_type_column] == "offline" generate_oauth_token(create_params, should_generate_refresh_token) end - def create_oauth_token_from_token(oauth_application) - # fetch oauth token - oauth_token = oauth_token_by_refresh_token(param("refresh_token")) + def create_oauth_token_from_token(oauth_token, update_params) + redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application) - redirect_response_error("invalid_grant") unless oauth_token && token_from_application?(oauth_token, oauth_application) - token = oauth_unique_id_generator - 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 - } - 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 @@ -993,11 +1037,11 @@ def authorization_required if accepts_json? throw_json_response_error(authorization_required_error_status, "invalid_client") else set_redirect_error_flash(require_authorization_error_flash) - redirect(oauth_authorize_path) + redirect(authorize_path) end end def check_valid_uri?(uri) URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri) @@ -1073,11 +1117,11 @@ # Server metadata def oauth_server_metadata_body(path) issuer = base_url - issuer += "/#{path}" if issuer + issuer += "/#{path}" if path responses_supported = %w[code] response_modes_supported = %w[query] grant_types_supported = %w[authorization_code] @@ -1086,32 +1130,34 @@ response_modes_supported << "fragment" grant_types_supported << "implicit" end { issuer: issuer, - authorization_endpoint: oauth_authorize_url, - token_endpoint: oauth_token_url, + authorization_endpoint: authorize_url, + token_endpoint: token_url, registration_endpoint: "#{base_url}/#{oauth_applications_path}", scopes_supported: oauth_application_scopes, response_types_supported: responses_supported, response_modes_supported: response_modes_supported, grant_types_supported: grant_types_supported, token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post], service_documentation: oauth_metadata_service_documentation, ui_locales_supported: oauth_metadata_ui_locales_supported, op_policy_uri: oauth_metadata_op_policy_uri, op_tos_uri: oauth_metadata_op_tos_uri, - revocation_endpoint: oauth_revoke_url, + revocation_endpoint: revoke_url, revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic - introspection_endpoint: oauth_introspect_url, + introspection_endpoint: introspect_url, introspection_endpoint_auth_methods_supported: %w[client_secret_basic], code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil) } end - # /oauth-token - route(:oauth_token) do |r| + # /token + route(:token) do |r| + next unless is_authorization_server? + before_token r.post do catch_error do validate_oauth_token_params @@ -1126,12 +1172,14 @@ throw_json_response_error(invalid_oauth_response_status, "invalid_request") end end - # /oauth-introspect - route(:oauth_introspect) do |r| + # /introspect + route(:introspect) do |r| + next unless is_authorization_server? + before_introspect r.post do catch_error do validate_oauth_introspect_params @@ -1157,12 +1205,14 @@ throw_json_response_error(invalid_oauth_response_status, "invalid_request") end end - # /oauth-revoke - route(:oauth_revoke) do |r| + # /revoke + route(:revoke) do |r| + next unless is_authorization_server? + before_revoke r.post do catch_error do validate_oauth_revoke_params @@ -1186,12 +1236,14 @@ redirect_response_error("invalid_request", request.referer || "/") end end - # /oauth-authorize - route(:oauth_authorize) do |r| + # /authorize + route(:authorize) do |r| + next unless is_authorization_server? + require_account validate_oauth_grant_params try_approval_prompt if use_oauth_access_type? && request.get? before_authorize @@ -1199,42 +1251,14 @@ r.get do authorize_view end r.post do - code = nil - query_params = [] - fragment_params = [] + redirect_url = URI.parse(redirect_uri) transaction do - case param("response_type") - when "token" - redirect_response_error("invalid_request") 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) - - 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}" - else - redirect_response_error("invalid_request") - end - after_authorize + do_authorize(redirect_url) end - - redirect_url = URI.parse(redirect_uri) - query_params << "state=#{param('state')}" if param_or_nil("state") - query_params << redirect_url.query if redirect_url.query - redirect_url.query = query_params.join("&") unless query_params.empty? - redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty? - redirect(redirect_url.to_s) end end end end