├── spec ├── dummy │ ├── public │ │ ├── favicon.ico │ │ ├── apple-touch-icon.png │ │ ├── apple-touch-icon-precomposed.png │ │ ├── robots.txt │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── app │ │ ├── assets │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── models │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── application_record.rb │ │ │ └── user.rb │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── application_controller.rb │ │ │ ├── devise │ │ │ │ └── api │ │ │ │ │ └── customized_tokens_controller.rb │ │ │ └── home_controller.rb │ │ ├── helpers │ │ │ ├── home_helper.rb │ │ │ └── application_helper.rb │ │ └── views │ │ │ ├── devise │ │ │ ├── mailer │ │ │ │ ├── password_change.html.erb │ │ │ │ ├── confirmation_instructions.html.erb │ │ │ │ ├── unlock_instructions.html.erb │ │ │ │ ├── email_changed.html.erb │ │ │ │ └── reset_password_instructions.html.erb │ │ │ ├── shared │ │ │ │ ├── _error_messages.html.erb │ │ │ │ └── _links.html.erb │ │ │ ├── unlocks │ │ │ │ └── new.html.erb │ │ │ ├── passwords │ │ │ │ ├── new.html.erb │ │ │ │ └── edit.html.erb │ │ │ ├── confirmations │ │ │ │ └── new.html.erb │ │ │ ├── sessions │ │ │ │ └── new.html.erb │ │ │ └── registrations │ │ │ │ ├── new.html.erb │ │ │ │ └── edit.html.erb │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── bin │ │ ├── rake │ │ ├── rails │ │ ├── setup │ │ └── bundle │ ├── config │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── routes.rb │ │ ├── initializers │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── permissions_policy.rb │ │ │ ├── assets.rb │ │ │ ├── inflections.rb │ │ │ ├── content_security_policy.rb │ │ │ └── devise.rb │ │ ├── database.yml │ │ ├── locales │ │ │ ├── en.yml │ │ │ ├── devise_api.en.yml │ │ │ └── devise.en.yml │ │ ├── application.rb │ │ ├── storage.yml │ │ ├── puma.rb │ │ └── environments │ │ │ ├── test.rb │ │ │ ├── development.rb │ │ │ └── production.rb │ ├── config.ru │ ├── Rakefile │ └── db │ │ ├── migrate │ │ ├── 20230113213619_create_devise_api_tables.rb │ │ └── 20230113190806_devise_create_users.rb │ │ └── schema.rb ├── supports │ ├── orm │ │ └── active_record.rb │ ├── dependencies │ │ └── factory_bot.rb │ └── helpers │ │ ├── body_parser_helper.rb │ │ └── devise_api_token_helper.rb ├── factories │ ├── users_factory.rb │ └── devise_api_tokens_factory.rb ├── services │ ├── tokens_service │ │ ├── create_spec.rb │ │ ├── refresh_spec.rb │ │ └── revoke_spec.rb │ └── resource_owner_service │ │ ├── sign_in_spec.rb │ │ ├── sign_up_spec.rb │ │ └── authenticate_spec.rb ├── devise │ ├── api_spec.rb │ └── api │ │ ├── configuration_spec.rb │ │ └── responses │ │ ├── token_response_spec.rb │ │ └── error_response_spec.rb ├── routing │ ├── default_routes_spec.rb │ └── customized_routes_spec.rb ├── spec_helper.rb └── requests │ ├── authentication_spec.rb │ └── tokens_spec.rb ├── .rspec ├── CHANGELOG.md ├── lib └── devise │ ├── api │ ├── version.rb │ ├── rails │ │ ├── engine.rb │ │ └── routes.rb │ ├── generators │ │ ├── templates │ │ │ └── migration.rb.erb │ │ └── install_generator.rb │ ├── configuration.rb │ ├── responses │ │ ├── token_response.rb │ │ └── error_response.rb │ ├── token.rb │ └── controllers │ │ └── helpers.rb │ └── api.rb ├── bin ├── setup └── console ├── sig └── devise │ └── api.rbs ├── Rakefile ├── .gitattributes ├── app ├── services │ └── devise │ │ └── api │ │ ├── base_service.rb │ │ ├── tokens_service │ │ ├── revoke.rb │ │ ├── refresh.rb │ │ └── create.rb │ │ └── resource_owner_service │ │ ├── authenticate.rb │ │ ├── sign_up.rb │ │ └── sign_in.rb └── controllers │ └── devise │ └── api │ └── tokens_controller.rb ├── .rubocop.yml ├── .github └── workflows │ ├── test.yml │ └── rubocop.yml ├── .gitignore ├── LICENSE ├── config └── locales │ └── en.yml ├── Gemfile ├── devise-api.gemspec ├── CODE_OF_CONDUCT.md ├── Gemfile.lock └── README.md /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /spec/dummy/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [0.0.0] - 2023-01-09 4 | 5 | - Initial release 6 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/home_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HomeHelper 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /lib/devise/api/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module Api 5 | VERSION = '0.2.0' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | primary_abstract_class 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require_relative '../config/boot' 5 | require 'rake' 6 | Rake.application.run 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /sig/devise/api.rbs: -------------------------------------------------------------------------------- 1 | module Devise 2 | module Api 3 | VERSION: String 4 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/mailer/password_change.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

We're contacting you to notify you that your password has been changed.

4 | -------------------------------------------------------------------------------- /spec/supports/orm/active_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # load schema to in memory sqlite 4 | ActiveRecord::Migration.verbose = false 5 | load Rails.root.join('db/schema.rb').to_s 6 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | APP_PATH = File.expand_path('../config/application', __dir__) 5 | require_relative '../config/boot' 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative 'application' 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /spec/factories/users_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :user do 5 | email { Faker::Internet.email } 6 | password { Faker::Internet.password } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative 'config/environment' 6 | 7 | run Rails.application 8 | Rails.application.load_server 9 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/devise/api/customized_tokens_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module Api 5 | class CustomizedTokensController < Devise::Api::TokensController 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/devise/api/rails/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module Api 5 | module Rails 6 | class Engine < ::Rails::Engine 7 | isolate_namespace Devise::Api 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:rspec) 7 | 8 | require 'rubocop/rake_task' 9 | 10 | RuboCop::RakeTask.new 11 | 12 | task default: %i[rspec rubocop] 13 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) 4 | 5 | require 'bundler/setup' # Set up gems listed in the Gemfile. 6 | 7 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/mailer/confirmation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome <%= @email %>!

2 | 3 |

You can confirm your account email through the link below:

4 | 5 |

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

6 | -------------------------------------------------------------------------------- /spec/supports/dependencies/factory_bot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'factory_bot' 4 | RSpec.configure do |config| 5 | config.include FactoryBot::Syntax::Methods 6 | 7 | config.before(:suite) do 8 | FactoryBot.find_definitions 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/supports/helpers/body_parser_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BodyParserHelper 4 | def parsed_body 5 | JSON.parse(response.body, object_class: OpenStruct) 6 | end 7 | end 8 | 9 | RSpec.configuration.include BodyParserHelper, type: :request 10 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require_relative 'config/application' 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /spec/services/tokens_service/create_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Devise::Api::TokensService::Create do 6 | it 'inherits from Devise::Api::BaseService' do 7 | expect(described_class).to be < Devise::Api::BaseService 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/services/tokens_service/refresh_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Devise::Api::TokensService::Refresh do 6 | it 'inherits from Devise::Api::BaseService' do 7 | expect(described_class).to be < Devise::Api::BaseService 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/services/tokens_service/revoke_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Devise::Api::TokensService::Revoke do 6 | it 'inherits from Devise::Api::BaseService' do 7 | expect(described_class).to be < Devise::Api::BaseService 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/services/resource_owner_service/sign_in_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Devise::Api::ResourceOwnerService::SignIn do 6 | it 'inherits from Devise::Api::BaseService' do 7 | expect(described_class).to be < Devise::Api::BaseService 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/services/resource_owner_service/sign_up_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Devise::Api::ResourceOwnerService::SignUp do 6 | it 'inherits from Devise::Api::BaseService' do 7 | expect(described_class).to be < Devise::Api::BaseService 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class HomeController < ApplicationController 4 | skip_before_action :verify_authenticity_token, raise: false 5 | before_action :authenticate_devise_api_token! 6 | 7 | def index 8 | render json: { success: true } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/services/resource_owner_service/authenticate_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Devise::Api::ResourceOwnerService::Authenticate do 6 | it 'inherits from Devise::Api::BaseService' do 7 | expect(described_class).to be < Devise::Api::BaseService 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 5 | 6 | # Defines the root path route ("/") 7 | # root "articles#index" 8 | 9 | devise_for :users 10 | get :home, to: 'home#index' 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/mailer/unlock_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

4 | 5 |

Click the link below to unlock your account:

6 | 7 |

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

8 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/mailer/email_changed.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @email %>!

2 | 3 | <% if @resource.try(:unconfirmed_email?) %> 4 |

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

5 | <% else %> 6 |

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

7 | <% end %> 8 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ApplicationRecord 4 | # Include default devise modules. Others available are: 5 | # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable 6 | devise :database_authenticatable, :registerable, 7 | :recoverable, :rememberable, :validatable, 8 | :confirmable, :lockable, :trackable, :api 9 | end 10 | -------------------------------------------------------------------------------- /app/services/devise/api/base_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry/monads/all' 4 | require 'dry-initializer' 5 | require 'dry-types' 6 | 7 | module Types 8 | include Dry.Types() 9 | end 10 | 11 | module Devise 12 | module Api 13 | class BaseService 14 | extend Dry::Initializer 15 | 16 | include Dry::Monads 17 | include Dry::Monads::Do 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'devise/api' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag "application" %> 10 | 11 | 12 | 13 |

<%= notice %>

14 |

<%= alert %>

15 | <%= yield %> 16 | 17 | 18 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Someone has requested a link to change your password. You can do this through the link below.

4 | 5 |

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

6 | 7 |

If you didn't request this, please ignore this email.

8 |

Your password won't change until you access the link above and create a new one.

9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of 6 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported 7 | # notations and behaviors. 8 | Rails.application.config.filter_parameters += %i[ 9 | passw secret token _key crypt salt certificate otp ssn 10 | ] 11 | -------------------------------------------------------------------------------- /spec/supports/helpers/devise_api_token_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DeviseApiTokenHelper 4 | def authentication_headers_for(resource_owner, token = nil, token_type = :access_token) 5 | token = FactoryBot.create(:devise_api_token, resource_owner: resource_owner) if token.blank? 6 | token_value = token.send(token_type) 7 | 8 | { 'Authorization': "Bearer #{token_value}" } 9 | end 10 | end 11 | 12 | RSpec.configuration.include DeviseApiTokenHelper, type: :request 13 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Define an application-wide HTTP permissions policy. For further 3 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 4 | # 5 | # Rails.application.config.permissions_policy do |f| 6 | # f.camera :none 7 | # f.gyroscope :none 8 | # f.microphone :none 9 | # f.usb :none 10 | # f.fullscreen :self 11 | # f.payment :self, "https://secure.example.com" 12 | # end 13 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/shared/_error_messages.html.erb: -------------------------------------------------------------------------------- 1 | <% if resource.errors.any? %> 2 |
3 |

4 | <%= I18n.t("errors.messages.not_saved", 5 | count: resource.errors.count, 6 | resource: resource.class.model_name.human.downcase) 7 | %> 8 |

9 | 14 |
15 | <% end %> 16 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/unlocks/new.html.erb: -------------------------------------------------------------------------------- 1 |

Resend unlock instructions

2 | 3 | <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 9 |
10 | 11 |
12 | <%= f.submit "Resend unlock instructions" %> 13 |
14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |

Forgot your password?

2 | 3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 9 |
10 | 11 |
12 | <%= f.submit "Send me reset password instructions" %> 13 |
14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Version of your assets, change this if you want to expire all your assets. 6 | Rails.application.config.assets.version = '1.0' 7 | 8 | # Add additional assets to the asset load path. 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /spec/factories/devise_api_tokens_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :devise_api_token, class: 'Devise::Api::Token' do 5 | association :resource_owner, factory: :user 6 | access_token { SecureRandom.hex(32) } 7 | refresh_token { SecureRandom.hex(32) } 8 | expires_in { 1.hour.to_i } 9 | 10 | trait :access_token_expired do 11 | created_at { 2.hours.ago } 12 | end 13 | 14 | trait :refresh_token_expired do 15 | created_at { 2.months.ago } 16 | end 17 | 18 | trait :revoked do 19 | revoked_at { 5.minutes.ago } 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20230113213619_create_devise_api_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateDeviseApiTables < ActiveRecord::Migration[7.0] 4 | def change 5 | create_table :devise_api_tokens do |t| 6 | t.belongs_to :resource_owner, null: false, polymorphic: true, index: true 7 | t.string :access_token, null: false, index: true 8 | t.string :refresh_token, null: true, index: true 9 | t.integer :expires_in, null: false 10 | t.datetime :revoked_at, null: true 11 | t.string :previous_refresh_token, null: true, index: true 12 | 13 | t.timestamps 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.7 3 | NewCops: disable 4 | SuggestExtensions: false 5 | 6 | Style/StringLiterals: 7 | Enabled: true 8 | EnforcedStyle: single_quotes 9 | 10 | Style/StringLiteralsInInterpolation: 11 | Enabled: true 12 | EnforcedStyle: single_quotes 13 | 14 | Layout/LineLength: 15 | Max: 120 16 | 17 | Style/Documentation: 18 | Enabled: false 19 | 20 | Metrics/MethodLength: 21 | Enabled: true 22 | CountComments: false 23 | Max: 30 24 | 25 | Metrics/AbcSize: 26 | Max: 30 27 | 28 | Metrics/BlockLength: 29 | Max: 30 30 | Exclude: 31 | - 'spec/**/*_spec.rb' 32 | - 'spec/dummy/db/**/*' 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | name: Ruby ${{ matrix.ruby }} 9 | strategy: 10 | matrix: 11 | ruby: 12 | - '2.7.7' 13 | - '3.0.5' 14 | - '3.1.3' 15 | - '3.2.0' 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby }} 23 | bundler-cache: false 24 | 25 | - name: Install dependencies 26 | run: bundle install 27 | 28 | - name: Run the tests 29 | run: bundle exec rake rspec 30 | -------------------------------------------------------------------------------- /spec/devise/api_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Devise::Api do 6 | it 'has a version number' do 7 | expect(Devise::Api::VERSION).not_to be nil 8 | end 9 | 10 | it 'has a configuration for api extension' do 11 | expect(Devise.api.class).to eq Devise::Api::Configuration 12 | end 13 | 14 | it 'has a default configuration for api extension' do 15 | config = Devise::Api::Configuration.new 16 | 17 | allow(Devise).to receive(:api).and_return(config) 18 | expect(Devise.api).to eq config 19 | end 20 | 21 | it 'added to devise modules' do 22 | expect(Devise::ALL).to include :api 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/confirmations/new.html.erb: -------------------------------------------------------------------------------- 1 |

Resend confirmation instructions

2 | 3 | <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> 9 |
10 | 11 |
12 | <%= f.submit "Resend confirmation instructions" %> 13 |
14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: rubocop 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | name: Ruby ${{ matrix.ruby }} 9 | strategy: 10 | matrix: 11 | ruby: 12 | - '2.7.7' 13 | - '3.0.5' 14 | - '3.1.3' 15 | - '3.2.0' 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby }} 23 | bundler-cache: false 24 | 25 | - name: Install dependencies 26 | run: bundle install 27 | 28 | - name: Run the rubocop 29 | run: bundle exec rubocop --config .rubocop.yml --parallel 30 | -------------------------------------------------------------------------------- /app/services/devise/api/tokens_service/revoke.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module Api 5 | module TokensService 6 | class Revoke < Devise::Api::BaseService 7 | option :devise_api_token, optional: true 8 | 9 | def call 10 | return Success(devise_api_token) if devise_api_token.blank? 11 | return Success(devise_api_token) if devise_api_token.revoked? || devise_api_token.expired? 12 | return Success(devise_api_token) if devise_api_token.update(revoked_at: Time.zone.now) 13 | 14 | Failure(error: :devise_api_token_revoke_error, record: devise_api_token) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/devise/api/rails/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActionDispatch 4 | module Routing 5 | class Mapper 6 | protected 7 | 8 | def devise_api(mapping, controllers) 9 | controller = controllers.fetch(:tokens, 'devise/api/tokens') 10 | path = mapping.path_names.fetch(:tokens, 'tokens') 11 | 12 | resource :tokens, only: [], controller: controller, path: path do 13 | collection do 14 | post :revoke, as: :revoke 15 | post :refresh, as: :refresh 16 | post :sign_up, as: :sign_up 17 | post :sign_in, as: :sign_in 18 | get :info, as: :info 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new inflection rules using the following format. Inflections 5 | # are locale specific, and you may define rules for as many different 6 | # locales as you wish. All of these examples are active by default: 7 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 8 | # inflect.plural /^(ox)$/i, "\\1en" 9 | # inflect.singular /^(ox)en/i, "\\1" 10 | # inflect.irregular "person", "people" 11 | # inflect.uncountable %w( fish sheep ) 12 | # end 13 | 14 | # These inflection rules are supported but not enabled by default: 15 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 16 | # inflect.acronym "RESTful" 17 | # end 18 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |

Log in

2 | 3 | <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> 4 |
5 | <%= f.label :email %>
6 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 7 |
8 | 9 |
10 | <%= f.label :password %>
11 | <%= f.password_field :password, autocomplete: "current-password" %> 12 |
13 | 14 | <% if devise_mapping.rememberable? %> 15 |
16 | <%= f.check_box :remember_me %> 17 | <%= f.label :remember_me %> 18 |
19 | <% end %> 20 | 21 |
22 | <%= f.submit "Log in" %> 23 |
24 | <% end %> 25 | 26 | <%= render "devise/shared/links" %> 27 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/routing/default_routes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Default routes' do 6 | it 'routes to /users/tokens/revoke' do 7 | expect(post: '/users/tokens/revoke').to route_to('devise/api/tokens#revoke') 8 | end 9 | 10 | it 'routes to /users/tokens/refresh' do 11 | expect(post: '/users/tokens/refresh').to route_to('devise/api/tokens#refresh') 12 | end 13 | 14 | it 'routes to /users/tokens/info' do 15 | expect(get: '/users/tokens/info').to route_to('devise/api/tokens#info') 16 | end 17 | 18 | it 'routes to /users/tokens/sign_in' do 19 | expect(post: '/users/tokens/sign_in').to route_to('devise/api/tokens#sign_in') 20 | end 21 | 22 | it 'routes to /users/tokens/sign_up' do 23 | expect(post: '/users/tokens/sign_up').to route_to('devise/api/tokens#sign_up') 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/services/devise/api/tokens_service/refresh.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module Api 5 | module TokensService 6 | class Refresh < Devise::Api::BaseService 7 | option :devise_api_token, type: Types.Instance(Devise.api.base_token_model.constantize) 8 | option :resource_owner, default: proc { devise_api_token.resource_owner } 9 | 10 | def call 11 | return Failure(error: :expired_refresh_token) if devise_api_token.refresh_token_expired? 12 | 13 | devise_api_token = yield create_devise_api_token 14 | Success(devise_api_token) 15 | end 16 | 17 | private 18 | 19 | def create_devise_api_token 20 | Devise::Api::TokensService::Create.new(resource_owner: resource_owner, 21 | previous_refresh_token: devise_api_token.refresh_token).call 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Change your password

2 | 3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | <%= f.hidden_field :reset_password_token %> 6 | 7 |
8 | <%= f.label :password, "New password" %>
9 | <% if @minimum_password_length %> 10 | (<%= @minimum_password_length %> characters minimum)
11 | <% end %> 12 | <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %> 13 |
14 | 15 |
16 | <%= f.label :password_confirmation, "Confirm new password" %>
17 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %> 18 |
19 | 20 |
21 | <%= f.submit "Change my password" %> 22 |
23 | <% end %> 24 | 25 | <%= render "devise/shared/links" %> 26 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/registrations/new.html.erb: -------------------------------------------------------------------------------- 1 |

Sign up

2 | 3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 9 |
10 | 11 |
12 | <%= f.label :password %> 13 | <% if @minimum_password_length %> 14 | (<%= @minimum_password_length %> characters minimum) 15 | <% end %>
16 | <%= f.password_field :password, autocomplete: "new-password" %> 17 |
18 | 19 |
20 | <%= f.label :password_confirmation %>
21 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %> 22 |
23 | 24 |
25 | <%= f.submit "Sign up" %> 26 |
27 | <% end %> 28 | 29 | <%= render "devise/shared/links" %> 30 | -------------------------------------------------------------------------------- /app/services/devise/api/resource_owner_service/authenticate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module Api 5 | module ResourceOwnerService 6 | class Authenticate < Devise::Api::BaseService 7 | option :params, type: Types::Hash 8 | option :resource_class, type: Types::Class 9 | 10 | def call 11 | resource = resource_class.find_for_authentication(params.slice(*resource_class.authentication_keys)) 12 | return Failure(error: :invalid_email, record: nil) if resource.blank? 13 | return Failure(error: :invalid_authentication, record: resource) unless authenticate!(resource) 14 | 15 | Success(resource) 16 | end 17 | 18 | private 19 | 20 | def authenticate!(resource) 21 | resource.valid_for_authentication? do 22 | resource.valid_password?(params[:password]) 23 | end && resource.active_for_authentication? 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # "true": "foo" 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | .idea/ 11 | 12 | .rspec_status 13 | 14 | # Dummy application 15 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 16 | # 17 | # If you find yourself ignoring temporary files generated by your text editor 18 | # or operating system, you probably want to add a global ignore instead: 19 | # git config --global core.excludesfile '~/.gitignore_global' 20 | 21 | # Ignore bundler config. 22 | /spec/dummy/.bundle 23 | 24 | # Ignore the default SQLite database. 25 | /spec/dummy/db/*.sqlite3 26 | /spec/dummy/db/*.sqlite3-* 27 | 28 | # Ignore all logfiles and tempfiles. 29 | /spec/dummy/log/* 30 | /spec/dummy/tmp/* 31 | !/spec/dummy/log/.keep 32 | !/spec/dummy/tmp/.keep 33 | 34 | # Ignore pidfiles, but keep the directory. 35 | /spec/dummy/tmp/pids/* 36 | !/spec/dummy/tmp/pids/ 37 | !/spec/dummy/tmp/pids/.keep 38 | 39 | 40 | /spec/dummy/public/assets 41 | 42 | # Ignore master key for decrypting credentials and more. 43 | /spec/dummy/config/master.key 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 nejdetkadir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/services/devise/api/resource_owner_service/sign_up.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module Api 5 | module ResourceOwnerService 6 | class SignUp < Devise::Api::BaseService 7 | option :params, type: Types::Hash 8 | option :resource_class, type: Types::Class 9 | 10 | def call 11 | ActiveRecord::Base.transaction do 12 | resource_owner = yield create_resource_owner 13 | devise_api_token = yield call_create_devise_api_token_service(resource_owner) 14 | 15 | Success(devise_api_token) 16 | end 17 | end 18 | 19 | private 20 | 21 | def create_resource_owner 22 | resource_owner = resource_class.new(params) 23 | 24 | return Success(resource_owner) if resource_owner.save 25 | 26 | Failure(error: :resource_owner_create_error, record: resource_owner) 27 | end 28 | 29 | def call_create_devise_api_token_service(resource_owner) 30 | Devise::Api::TokensService::Create.new(resource_owner: resource_owner).call 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['RAILS_ENV'] = 'test' 4 | 5 | $LOAD_PATH.unshift File.dirname(__FILE__) 6 | 7 | require 'devise/api' 8 | require 'dummy/config/environment' 9 | require 'pry' 10 | require 'awesome_print' 11 | require 'database_cleaner' 12 | require 'rspec/rails' 13 | 14 | Dir["#{File.dirname(__FILE__)}/supports/**/*.rb"].sort.each { |f| require f } 15 | 16 | RSpec.configure do |config| 17 | # Enable flags like --only-failures and --next-failure 18 | config.example_status_persistence_file_path = '.rspec_status' 19 | 20 | # Disable RSpec exposing methods globally on `Module` and `main` 21 | config.disable_monkey_patching! 22 | 23 | config.expect_with :rspec do |c| 24 | c.syntax = :expect 25 | end 26 | 27 | config.include RSpec::Rails::RequestExampleGroup, type: :request 28 | 29 | config.before do 30 | DatabaseCleaner.start 31 | end 32 | 33 | config.after do 34 | DatabaseCleaner.clean 35 | end 36 | 37 | config.infer_spec_type_from_file_location! 38 | end 39 | 40 | # For generators 41 | require 'rails/generators/test_case' 42 | require 'devise/api/generators/install_generator' 43 | -------------------------------------------------------------------------------- /app/services/devise/api/resource_owner_service/sign_in.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module Api 5 | module ResourceOwnerService 6 | class SignIn < Devise::Api::BaseService 7 | option :params, type: Types::Hash 8 | option :resource_class, type: Types::Class 9 | 10 | def call 11 | resource_owner = yield call_authenticate_service 12 | devise_api_token = yield call_create_devise_api_token_service(resource_owner) 13 | resource_owner.reset_failed_attempts! if resource_owner.class.supported_devise_modules.lockable? 14 | 15 | Success(devise_api_token) 16 | end 17 | 18 | private 19 | 20 | def call_authenticate_service 21 | Devise::Api::ResourceOwnerService::Authenticate.new(params: params, 22 | resource_class: resource_class).call 23 | end 24 | 25 | def call_create_devise_api_token_service(resource_owner) 26 | Devise::Api::TokensService::Create.new(resource_owner: resource_owner).call 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'boot' 4 | 5 | require 'rails/all' 6 | 7 | # Require the gems listed in Gemfile, including any gems 8 | # you've limited to :test, :development, or :production. 9 | Bundler.require(*Rails.groups) 10 | 11 | module Dummy 12 | class Application < Rails::Application 13 | # Initialize configuration defaults for originally generated Rails version. 14 | config.load_defaults 7.0 15 | 16 | # Configuration for the application, engines, and railties goes here. 17 | # 18 | # These settings can be overridden in specific environments using the files 19 | # in config/environments, which are processed later. 20 | # 21 | # config.time_zone = "Central Time (US & Canada)" 22 | # config.eager_load_paths << Rails.root.join("extras") 23 | 24 | I18n.load_path += Dir[Rails.root.join('config/locales/**/*.yml').to_s] 25 | config.i18n.default_locale = :en 26 | 27 | # Don't generate system test files. 28 | config.generators.system_tests = nil 29 | 30 | config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/devise/api/generators/templates/migration.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateDeviseApiTables < ActiveRecord::Migration<%= migration_version %> 4 | def change 5 | # Use Active Record's configured type for primary and foreign keys 6 | primary_key_type, foreign_key_type = primary_and_foreign_key_types 7 | 8 | create_table :devise_api_tokens, id: primary_key_type do |t| 9 | t.belongs_to :resource_owner, null: false, polymorphic: true, index: true, type: foreign_key_type 10 | t.string :access_token, null: false, index: true 11 | t.string :refresh_token, null: true, index: true 12 | t.integer :expires_in, null: false 13 | t.datetime :revoked_at, null: true 14 | t.string :previous_refresh_token, null: true, index: true 15 | 16 | t.timestamps 17 | end 18 | end 19 | 20 | private 21 | 22 | def primary_and_foreign_key_types 23 | config = Rails.configuration.generators 24 | setting = config.options[config.orm][:primary_key_type] 25 | primary_key_type = setting || :primary_key 26 | foreign_key_type = setting || :bigint 27 | [primary_key_type, foreign_key_type] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'fileutils' 5 | 6 | # path to your application root. 7 | APP_ROOT = File.expand_path('..', __dir__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | FileUtils.chdir APP_ROOT do 14 | # This script is a way to set up or update your development environment automatically. 15 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 16 | # Add necessary setup steps to this file. 17 | 18 | puts '== Installing dependencies ==' 19 | system! 'gem install bundler --conservative' 20 | system('bundle check') || system!('bundle install') 21 | 22 | # puts "\n== Copying sample files ==" 23 | # unless File.exist?("config/database.yml") 24 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 25 | # end 26 | 27 | puts "\n== Preparing database ==" 28 | system! 'bin/rails db:prepare' 29 | 30 | puts "\n== Removing old logs and tempfiles ==" 31 | system! 'bin/rails log:clear tmp:clear' 32 | 33 | puts "\n== Restarting application server ==" 34 | system! 'bin/rails restart' 35 | end 36 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/devise_api.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | devise: 3 | api: 4 | error_response: 5 | invalid_authentication: "Email or password is invalid" 6 | invalid_token: "Invalid token" 7 | expired_token: "Token has expired" 8 | expired_refresh_token: "Refresh token has expired" 9 | revoked_token: "Token has been revoked" 10 | refresh_token_disabled: "Refresh token is disabled for this application" 11 | invalid_refresh_token: "Refresh token is invalid" 12 | invalid_email: "Email is invalid" 13 | invalid_resource_owner: "Resource owner is invalid" 14 | resource_owner_create_error: "Resource owner could not be created" 15 | devise_api_token_create_error: "Token could not be created" 16 | devise_api_token_revoke_error: "Token could not be revoked" 17 | lockable: 18 | locked: "Your account is locked" 19 | confirmable: 20 | unconfirmed: "You have to confirm your account before continuing" 21 | registerable: 22 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account" 23 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Define an application-wide content security policy. 5 | # See the Securing Rails Applications Guide for more information: 6 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 7 | 8 | # Rails.application.configure do 9 | # config.content_security_policy do |policy| 10 | # policy.default_src :self, :https 11 | # policy.font_src :self, :https, :data 12 | # policy.img_src :self, :https, :data 13 | # policy.object_src :none 14 | # policy.script_src :self, :https 15 | # policy.style_src :self, :https 16 | # # Specify URI for violation reports 17 | # # policy.report_uri "/csp-violation-report-endpoint" 18 | # end 19 | # 20 | # # Generate session nonces for permitted importmap and inline scripts 21 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 22 | # config.content_security_policy_nonce_directives = %w(script-src) 23 | # 24 | # # Report violations without enforcing the policy. 25 | # # config.content_security_policy_report_only = true 26 | # end 27 | -------------------------------------------------------------------------------- /spec/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | devise: 3 | api: 4 | error_response: 5 | invalid_authentication: "Email or password is invalid" 6 | invalid_token: "Invalid token" 7 | expired_token: "Token has expired" 8 | expired_refresh_token: "Refresh token has expired" 9 | revoked_token: "Token has been revoked" 10 | refresh_token_disabled: "Refresh token is disabled for this application" 11 | sign_up_disabled: "Sign up is disabled for this application" 12 | invalid_refresh_token: "Refresh token is invalid" 13 | invalid_email: "Email is invalid" 14 | invalid_resource_owner: "Resource owner is invalid" 15 | resource_owner_create_error: "Resource owner could not be created" 16 | devise_api_token_create_error: "Token could not be created" 17 | devise_api_token_revoke_error: "Token could not be revoked" 18 | lockable: 19 | locked: "Your account is locked" 20 | confirmable: 21 | unconfirmed: "You have to confirm your account before continuing" 22 | registerable: 23 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account" 24 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/shared/_links.html.erb: -------------------------------------------------------------------------------- 1 | <%- if controller_name != 'sessions' %> 2 | <%= link_to "Log in", new_session_path(resource_name) %>
3 | <% end %> 4 | 5 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %> 6 | <%= link_to "Sign up", new_registration_path(resource_name) %>
7 | <% end %> 8 | 9 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> 10 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
11 | <% end %> 12 | 13 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> 14 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
15 | <% end %> 16 | 17 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> 18 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
19 | <% end %> 20 | 21 | <%- if devise_mapping.omniauthable? %> 22 | <%- resource_class.omniauth_providers.each do |provider| %> 23 | <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %>
24 | <% end %> 25 | <% end %> 26 | -------------------------------------------------------------------------------- /lib/devise/api/generators/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/generators' 4 | require 'rails/generators/active_record' 5 | require 'rails/generators/active_model' 6 | 7 | module Devise 8 | module Api 9 | module Generators 10 | class InstallGenerator < ::Rails::Generators::Base 11 | include ::Rails::Generators::Migration 12 | source_root File.expand_path('templates', __dir__) 13 | desc 'Generates a migration to add the required fields to the your devise model' 14 | namespace 'devise_api:install' 15 | 16 | def install 17 | migration_template( 18 | 'migration.rb.erb', 19 | 'db/migrate/create_devise_api_tables.rb', 20 | migration_version: migration_version 21 | ) 22 | 23 | copy_file locale_source, locale_destination 24 | end 25 | 26 | def self.next_migration_number(path) 27 | ActiveRecord::Generators::Base.next_migration_number(path) 28 | end 29 | 30 | private 31 | 32 | def locale_source 33 | File.expand_path('../../../../config/locales/en.yml', __dir__) 34 | end 35 | 36 | def locale_destination 37 | 'config/locales/devise_api.en.yml' 38 | end 39 | 40 | def migration_version 41 | "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/routing/customized_routes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Customized routes' do 6 | # create and customize routes for devise_for :users 7 | before :all do 8 | Rails.application.routes.disable_clear_and_finalize = true 9 | 10 | Rails.application.routes.clear! 11 | 12 | Rails.application.routes.draw do 13 | devise_for :users, controllers: { tokens: 'devise/api/customized_tokens' }, path: 'accounts' 14 | end 15 | end 16 | 17 | after :all do 18 | Rails.application.routes.clear! 19 | 20 | load File.expand_path('../dummy/config/routes.rb', __dir__) 21 | end 22 | 23 | it 'routes to /accounts/tokens/refresh' do 24 | expect(post: '/accounts/tokens/refresh').to route_to('devise/api/customized_tokens#refresh') 25 | end 26 | 27 | it 'routes to /accounts/tokens/revoke' do 28 | expect(post: '/accounts/tokens/revoke').to route_to('devise/api/customized_tokens#revoke') 29 | end 30 | 31 | it 'routes to /accounts/tokens/info' do 32 | expect(get: '/accounts/tokens/info').to route_to('devise/api/customized_tokens#info') 33 | end 34 | 35 | it 'routes to /accounts/tokens/sign_in' do 36 | expect(post: '/accounts/tokens/sign_in').to route_to('devise/api/customized_tokens#sign_in') 37 | end 38 | 39 | it 'routes to /accounts/tokens/sign_up' do 40 | expect(post: '/accounts/tokens/sign_up').to route_to('devise/api/customized_tokens#sign_up') 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/devise/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'devise' 4 | require 'active_support/concern' 5 | require_relative 'api/configuration' 6 | require_relative 'api/version' 7 | require_relative 'api/controllers/helpers' 8 | require_relative 'api/responses/error_response' 9 | require_relative 'api/responses/token_response' 10 | require_relative 'api/generators/install_generator' 11 | 12 | # rubocop:disable Style/ClassVars 13 | module Devise 14 | mattr_accessor :api 15 | @@api = Devise::Api::Configuration.new 16 | 17 | module Models 18 | module Api 19 | extend ActiveSupport::Concern 20 | 21 | included do 22 | has_many :access_tokens, 23 | class_name: Devise.api.config.base_token_model, 24 | dependent: :destroy, 25 | as: :resource_owner 26 | end 27 | 28 | class_methods do 29 | def supported_devise_modules 30 | devise_modules.inquiry 31 | end 32 | end 33 | end 34 | end 35 | 36 | module Api; end 37 | 38 | add_module :api, 39 | strategy: false, 40 | controller: :tokens, 41 | route: { api: %i[revoke refresh sign_up sign_in info] } 42 | end 43 | # rubocop:enable Style/ClassVars 44 | 45 | ActiveSupport.on_load(:action_controller) do 46 | include Devise::Api::Controllers::Helpers 47 | end 48 | 49 | require_relative 'api/token' 50 | require_relative 'api/rails/engine' 51 | require_relative 'api/rails/routes' 52 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20230113190806_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DeviseCreateUsers < ActiveRecord::Migration[7.0] 4 | def change 5 | create_table :users do |t| 6 | ## Database authenticatable 7 | t.string :email, null: false, default: '' 8 | t.string :encrypted_password, null: false, default: '' 9 | 10 | ## Recoverable 11 | t.string :reset_password_token 12 | t.datetime :reset_password_sent_at 13 | 14 | ## Rememberable 15 | t.datetime :remember_created_at 16 | 17 | ## Trackable 18 | t.integer :sign_in_count, default: 0, null: false 19 | t.datetime :current_sign_in_at 20 | t.datetime :last_sign_in_at 21 | t.string :current_sign_in_ip 22 | t.string :last_sign_in_ip 23 | 24 | ## Confirmable 25 | t.string :confirmation_token 26 | t.datetime :confirmed_at 27 | t.datetime :confirmation_sent_at 28 | t.string :unconfirmed_email # Only if using reconfirmable 29 | 30 | ## Lockable 31 | t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 32 | t.string :unlock_token # Only if unlock strategy is :email or :both 33 | t.datetime :locked_at 34 | 35 | t.timestamps null: false 36 | end 37 | 38 | add_index :users, :email, unique: true 39 | add_index :users, :reset_password_token, unique: true 40 | add_index :users, :confirmation_token, unique: true 41 | add_index :users, :unlock_token, unique: true 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/services/devise/api/tokens_service/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module Api 5 | module TokensService 6 | class Create < Devise::Api::BaseService 7 | option :resource_owner 8 | option :previous_refresh_token, type: Types::String | Types::Nil, default: proc { nil } 9 | 10 | def call 11 | return Failure(error: :invalid_resource_owner) unless resource_owner.respond_to?(:access_tokens) 12 | 13 | devise_api_token = yield create_devise_api_token 14 | 15 | Success(devise_api_token) 16 | end 17 | 18 | private 19 | 20 | def authenticate_service 21 | Devise::Api::ResourceOwnerService::Authenticate.new(params: params, 22 | resource_class: resource_class).call 23 | end 24 | 25 | def create_devise_api_token 26 | devise_api_token = resource_owner.access_tokens.new(params) 27 | 28 | return Success(devise_api_token) if devise_api_token.save 29 | 30 | Failure(error: :devise_api_token_create_error, record: devise_api_token) 31 | end 32 | 33 | def params 34 | { 35 | access_token: Devise.api.config.base_token_model.constantize.generate_uniq_access_token(resource_owner), 36 | refresh_token: Devise.api.config.base_token_model.constantize.generate_uniq_refresh_token(resource_owner), 37 | expires_in: Devise.api.config.access_token.expires_in, 38 | revoked_at: nil, 39 | previous_refresh_token: previous_refresh_token 40 | } 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/dummy/app/views/devise/registrations/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit <%= resource_name.to_s.humanize %>

2 | 3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 9 |
10 | 11 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> 12 |
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
13 | <% end %> 14 | 15 |
16 | <%= f.label :password %> (leave blank if you don't want to change it)
17 | <%= f.password_field :password, autocomplete: "new-password" %> 18 | <% if @minimum_password_length %> 19 |
20 | <%= @minimum_password_length %> characters minimum 21 | <% end %> 22 |
23 | 24 |
25 | <%= f.label :password_confirmation %>
26 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %> 27 |
28 | 29 |
30 | <%= f.label :current_password %> (we need your current password to confirm your changes)
31 | <%= f.password_field :current_password, autocomplete: "current-password" %> 32 |
33 | 34 |
35 | <%= f.submit "Update" %> 36 |
37 | <% end %> 38 | 39 |

Cancel my account

40 | 41 |

Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

42 | 43 | <%= link_to "Back", :back %> 44 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Puma can serve each request in a thread from an internal thread pool. 4 | # The `threads` method setting takes two numbers: a minimum and maximum. 5 | # Any libraries that use thread pools should be configured to match 6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 7 | # and maximum; this matches the default thread size of Active Record. 8 | # 9 | max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) 10 | min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } 11 | threads min_threads_count, max_threads_count 12 | 13 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 14 | # terminating a worker in development environments. 15 | # 16 | worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' 17 | 18 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 19 | # 20 | port ENV.fetch('PORT', 3000) 21 | 22 | # Specifies the `environment` that Puma will run in. 23 | # 24 | environment ENV.fetch('RAILS_ENV', 'development') 25 | 26 | # Specifies the `pidfile` that Puma will use. 27 | pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid') 28 | 29 | # Specifies the number of `workers` to boot in clustered mode. 30 | # Workers are forked web server processes. If using threads and workers together 31 | # the concurrency of the application would be max `threads` * `workers`. 32 | # Workers do not work on JRuby or Windows (both of which do not support 33 | # processes). 34 | # 35 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 36 | 37 | # Use the `preload_app!` method when specifying a `workers` number. 38 | # This directive tells Puma to first boot the application and load code 39 | # before forking the application. This takes advantage of Copy On Write 40 | # process behavior so workers use less memory. 41 | # 42 | # preload_app! 43 | 44 | # Allow puma to be restarted by `bin/rails restart` command. 45 | plugin :tmp_restart 46 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | # Specify your gem's dependencies in devise-api.gemspec 7 | gemspec 8 | 9 | # Rake is a Make-like program implemented in Ruby [https://github.com/ruby/rake] 10 | gem 'rake', '~> 13.0' 11 | 12 | # Behaviour Driven Development for Ruby [https://github.com/rspec/rspec-metagem] 13 | gem 'rspec', '~> 3.0' 14 | 15 | # RuboCop is a Ruby code style checking and code formatting tool [https://github.com/rubocop/rubocop] 16 | gem 'rubocop', '~> 1.21' 17 | 18 | # Pretty print Ruby objects with proper indentation and colors [https://github.com/awesome-print/awesome_print] 19 | gem 'awesome_print' 20 | 21 | # Pry is a runtime developer console and IRB alternative with powerful introspection capabilities [https://github.com/pry/pry] 22 | gem 'pry', '~> 0.14.1' 23 | 24 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] 25 | gem 'sprockets-rails' 26 | 27 | # Use sqlite3 as the database for Active Record 28 | gem 'sqlite3', '~> 1.4' 29 | 30 | # Use the Puma web server [https://github.com/puma/puma] 31 | gem 'puma', '~> 5.0' 32 | 33 | # A library for setting up Ruby objects as test data [https://github.com/thoughtbot/factory_bot] 34 | gem 'factory_bot', '~> 6.2', '>= 6.2.1' 35 | 36 | # A library for generating fake data such as names, addresses, and phone numbers [https://github.com/faker-ruby/faker] 37 | gem 'faker', '~> 3.1' 38 | 39 | # Strategies for cleaning databases in Ruby. Can be used to ensure a clean state for testing [https://github.com/DatabaseCleaner/database_cleaner] 40 | gem 'database_cleaner', '~> 2.0', '>= 2.0.1' 41 | 42 | # RSpec for Rails 5+ [https://github.com/rspec/rspec-rails] 43 | gem 'rspec-rails', '~> 6.0', '>= 6.0.1' 44 | 45 | # RSpec runner and formatters [https://github.com/rspec/rspec-core] 46 | gem 'rspec-core' 47 | 48 | # Common code needed by the other RSpec gems. Not intended for direct use [https://github.com/rspec/rspec-support] 49 | gem 'rspec-support' 50 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | # The test environment is used exclusively to run your application's 6 | # test suite. You never need to work with it otherwise. Remember that 7 | # your test database is "scratch space" for the test suite and is wiped 8 | # and recreated between test runs. Don't rely on the data there! 9 | 10 | Rails.application.configure do 11 | # Settings specified here will take precedence over those in config/application.rb. 12 | 13 | # Turn false under Spring and add config.action_view.cache_template_loading = true. 14 | config.cache_classes = true 15 | 16 | # Eager loading loads your whole application. When running a single test locally, 17 | # this probably isn't necessary. It's a good idea to do in a continuous integration 18 | # system, or in some way before deploying your code. 19 | config.eager_load = ENV['CI'].present? 20 | 21 | # Configure public file server for tests with Cache-Control for performance. 22 | config.public_file_server.enabled = true 23 | config.public_file_server.headers = { 24 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 25 | } 26 | 27 | # Show full error reports and disable caching. 28 | config.consider_all_requests_local = true 29 | config.action_controller.perform_caching = false 30 | config.cache_store = :null_store 31 | 32 | # Raise exceptions instead of rendering exception templates. 33 | config.action_dispatch.show_exceptions = false 34 | 35 | # Disable request forgery protection in test environment. 36 | config.action_controller.allow_forgery_protection = false 37 | 38 | # Print deprecation notices to the stderr. 39 | config.active_support.deprecation = :stderr 40 | 41 | # Raise exceptions for disallowed deprecations. 42 | config.active_support.disallowed_deprecation = :raise 43 | 44 | # Tell Active Support which deprecation messages to disallow. 45 | config.active_support.disallowed_deprecation_warnings = [] 46 | 47 | # Raises error for missing translations. 48 | # config.i18n.raise_on_missing_translations = true 49 | 50 | # Annotate rendered view with file names. 51 | # config.action_view.annotate_rendered_view_with_filenames = true 52 | 53 | config.action_mailer.raise_delivery_errors = false 54 | end 55 | -------------------------------------------------------------------------------- /devise-api.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/devise/api/version' 4 | 5 | # rubocop:disable Layout/LineLength 6 | Gem::Specification.new do |spec| 7 | spec.name = 'devise-api' 8 | spec.version = Devise::Api::VERSION 9 | spec.authors = ['nejdetkadir'] 10 | spec.email = ['nejdetkadir.550@gmail.com'] 11 | 12 | spec.summary = "The devise-api gem is a convenient way to add authentication to your Ruby on Rails application using the devise gem. 13 | It provides support for access tokens and refresh tokens, which allow you to authenticate API requests and 14 | keep the user's session active for a longer period of time on the client side. It can be installed by adding the gem to your Gemfile, 15 | running migrations, and adding the :api module to your devise model. The gem is fully configurable, 16 | allowing you to set things like token expiration times and token generators." 17 | 18 | spec.description = spec.summary 19 | spec.homepage = "https://github.com/nejdetkadir/#{spec.name}" 20 | spec.license = 'MIT' 21 | spec.required_ruby_version = '>= 2.7.0' 22 | 23 | spec.metadata['homepage_uri'] = spec.homepage 24 | spec.metadata['source_code_uri'] = spec.homepage 25 | spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md" 26 | 27 | # Specify which files should be added to the gem when it is released. 28 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 29 | spec.files = Dir.chdir(__dir__) do 30 | `git ls-files -z`.split("\x0").reject do |f| 31 | (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)}) 32 | end 33 | end 34 | spec.bindir = 'exe' 35 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 36 | spec.require_paths = ['lib'] 37 | 38 | # Uncomment to register a new dependency of your gem 39 | spec.add_dependency 'devise', '>= 4.7.2' 40 | spec.add_dependency 'dry-configurable', '~> 1.0', '>= 1.0.1' 41 | spec.add_dependency 'dry-initializer', '>= 3.1.1' 42 | spec.add_dependency 'dry-monads', '>= 1.6.0' 43 | spec.add_dependency 'dry-types', '>= 1.7.0' 44 | spec.add_dependency 'rails', '>= 6.0.0' 45 | 46 | # For more information and examples about making a new gem, check out our 47 | # guide at: https://bundler.io/guides/creating_gem.html 48 | end 49 | # rubocop:enable Layout/LineLength 50 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | Rails.application.configure do 6 | # Settings specified here will take precedence over those in config/application.rb. 7 | 8 | # In the development environment your application's code is reloaded any time 9 | # it changes. This slows down response time but is perfect for development 10 | # since you don't have to restart the web server when you make code changes. 11 | config.cache_classes = false 12 | 13 | # Do not eager load code on boot. 14 | config.eager_load = false 15 | 16 | # Show full error reports. 17 | config.consider_all_requests_local = true 18 | 19 | # Enable server timing 20 | config.server_timing = true 21 | 22 | # Enable/disable caching. By default caching is disabled. 23 | # Run rails dev:cache to toggle caching. 24 | if Rails.root.join('tmp/caching-dev.txt').exist? 25 | config.action_controller.perform_caching = true 26 | config.action_controller.enable_fragment_cache_logging = true 27 | 28 | config.cache_store = :memory_store 29 | config.public_file_server.headers = { 30 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 31 | } 32 | else 33 | config.action_controller.perform_caching = false 34 | 35 | config.cache_store = :null_store 36 | end 37 | 38 | # Print deprecation notices to the Rails logger. 39 | config.active_support.deprecation = :log 40 | 41 | # Raise exceptions for disallowed deprecations. 42 | config.active_support.disallowed_deprecation = :raise 43 | 44 | # Tell Active Support which deprecation messages to disallow. 45 | config.active_support.disallowed_deprecation_warnings = [] 46 | 47 | # Raise an error on page load if there are pending migrations. 48 | config.active_record.migration_error = :page_load 49 | 50 | # Highlight code that triggered database queries in logs. 51 | config.active_record.verbose_query_logs = true 52 | 53 | # Suppress logger output for asset requests. 54 | config.assets.quiet = true 55 | 56 | # Raises error for missing translations. 57 | # config.i18n.raise_on_missing_translations = true 58 | 59 | # Annotate rendered view with file names. 60 | # config.action_view.annotate_rendered_view_with_filenames = true 61 | 62 | # Uncomment if you wish to allow Action Cable access from any origin. 63 | # config.action_cable.disable_request_forgery_protection = true 64 | end 65 | -------------------------------------------------------------------------------- /lib/devise/api/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry-configurable' 4 | 5 | module Devise 6 | module Api 7 | class Configuration 8 | include Dry::Configurable 9 | 10 | setting :access_token, reader: true do 11 | setting :expires_in, default: 1.hour, reader: true 12 | setting :expires_in_infinite, default: proc { |_resource_owner| false }, reader: true 13 | setting :generator, default: proc { |_resource_owner| ::Devise.friendly_token(60) }, reader: true 14 | end 15 | 16 | setting :refresh_token, reader: true do 17 | setting :enabled, default: true, reader: true 18 | setting :expires_in, default: 1.week, reader: true 19 | setting :generator, default: proc { |_resource_owner| ::Devise.friendly_token(60) }, reader: true 20 | setting :expires_in_infinite, default: proc { |_resource_owner| false }, reader: true 21 | end 22 | 23 | setting :sign_up, reader: true do 24 | setting :enabled, default: true, reader: true 25 | setting :extra_fields, default: [], reader: true 26 | end 27 | 28 | setting :authorization, reader: true do 29 | setting :key, default: 'Authorization', reader: true 30 | setting :scheme, default: 'Bearer', reader: true 31 | setting :location, default: :both, reader: true # :header or :params or :both 32 | setting :params_key, default: 'access_token', reader: true 33 | end 34 | 35 | setting :base_token_model, default: 'Devise::Api::Token', reader: true 36 | setting :base_controller, default: '::DeviseController', reader: true 37 | 38 | setting :after_successful_sign_in, default: proc { |_resource_owner, _token, _request| }, reader: true 39 | setting :after_successful_sign_up, default: proc { |_resource_owner, _token, _request| }, reader: true 40 | setting :after_successful_refresh, default: proc { |_resource_owner, _token, _request| }, reader: true 41 | setting :after_successful_revoke, default: proc { |_resource_owner, _token, _request| }, reader: true 42 | 43 | setting :before_sign_in, default: proc { |_params, _request, _resource_class| }, reader: true 44 | setting :before_sign_up, default: proc { |_params, _request, _resource_class| }, reader: true 45 | setting :before_refresh, default: proc { |_token, _request| }, reader: true 46 | setting :before_revoke, default: proc { |_token, _request| }, reader: true 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/devise/api/responses/token_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module Api 5 | module Responses 6 | class TokenResponse 7 | attr_reader :request, :token, :action, :resource_owner 8 | 9 | ACTIONS = %i[ 10 | sign_in 11 | sign_up 12 | refresh 13 | revoke 14 | info 15 | ].freeze 16 | 17 | ACTIONS.each do |act| 18 | define_method("#{act}_action?") do 19 | action.eql?(act) 20 | end 21 | end 22 | 23 | def initialize(request, token:, action:) 24 | @request = request 25 | @token = token 26 | @action = action 27 | @resource_owner = token&.resource_owner 28 | end 29 | 30 | def body 31 | return {} if revoke_action? 32 | return signed_up_body if sign_up_action? 33 | return info_body if info_action? 34 | 35 | default_body 36 | end 37 | 38 | def default_body 39 | { 40 | token: token.access_token, 41 | refresh_token: Devise.api.config.refresh_token.enabled ? token.refresh_token : nil, 42 | expires_in: token.expires_in, 43 | token_type: ::Devise.api.config.authorization.scheme, 44 | resource_owner: default_resource_owner 45 | }.compact 46 | end 47 | 48 | def default_resource_owner 49 | keys_to_extract = %i[id email created_at updated_at] 50 | if Devise.api.config.sign_up.extra_fields.present? 51 | keys_to_extract |= Devise.api.config.sign_up.extra_fields.map(&:to_sym) 52 | end 53 | 54 | resource_owner.slice(*keys_to_extract) 55 | end 56 | 57 | def status 58 | return :created if sign_up_action? 59 | return :no_content if revoke_action? 60 | 61 | :ok 62 | end 63 | 64 | private 65 | 66 | def signed_up_body 67 | return default_body unless resource_owner.class.supported_devise_modules.confirmable? 68 | 69 | message = resource_owner.confirmed? ? nil : I18n.t('devise.api.error_response.registerable.signed_up_but_unconfirmed') 70 | 71 | default_body.merge(confirmable: { confirmed: resource_owner.confirmed?, message: message }.compact) 72 | end 73 | 74 | def info_body 75 | default_body[:resource_owner] 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is auto-generated from the current state of the database. Instead 4 | # of editing this file, please use the migrations feature of Active Record to 5 | # incrementally modify your database, and then regenerate this schema definition. 6 | # 7 | # This file is the source Rails uses to define your schema when running `bin/rails 8 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 9 | # be faster and is potentially less error prone than running all of your 10 | # migrations from scratch. Old migrations may fail to apply correctly if those 11 | # migrations use external dependencies or application code. 12 | # 13 | # It's strongly recommended that you check this file into your version control system. 14 | 15 | ActiveRecord::Schema[7.0].define(version: 20_230_113_213_619) do 16 | create_table 'devise_api_tokens', force: :cascade do |t| 17 | t.string 'resource_owner_type', null: false 18 | t.integer 'resource_owner_id', null: false 19 | t.string 'access_token', null: false 20 | t.string 'refresh_token' 21 | t.integer 'expires_in', null: false 22 | t.datetime 'revoked_at' 23 | t.string 'previous_refresh_token' 24 | t.datetime 'created_at', null: false 25 | t.datetime 'updated_at', null: false 26 | t.index ['access_token'], name: 'index_devise_api_tokens_on_access_token' 27 | t.index ['previous_refresh_token'], name: 'index_devise_api_tokens_on_previous_refresh_token' 28 | t.index ['refresh_token'], name: 'index_devise_api_tokens_on_refresh_token' 29 | t.index %w[resource_owner_type resource_owner_id], name: 'index_devise_api_tokens_on_resource_owner' 30 | end 31 | 32 | create_table 'users', force: :cascade do |t| 33 | t.string 'email', default: '', null: false 34 | t.string 'encrypted_password', default: '', null: false 35 | t.string 'reset_password_token' 36 | t.datetime 'reset_password_sent_at' 37 | t.datetime 'remember_created_at' 38 | t.integer 'sign_in_count', default: 0, null: false 39 | t.datetime 'current_sign_in_at' 40 | t.datetime 'last_sign_in_at' 41 | t.string 'current_sign_in_ip' 42 | t.string 'last_sign_in_ip' 43 | t.string 'confirmation_token' 44 | t.datetime 'confirmed_at' 45 | t.datetime 'confirmation_sent_at' 46 | t.string 'unconfirmed_email' 47 | t.integer 'failed_attempts', default: 0, null: false 48 | t.string 'unlock_token' 49 | t.datetime 'locked_at' 50 | t.datetime 'created_at', null: false 51 | t.datetime 'updated_at', null: false 52 | t.index ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true 53 | t.index ['email'], name: 'index_users_on_email', unique: true 54 | t.index ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true 55 | t.index ['unlock_token'], name: 'index_users_on_unlock_token', unique: true 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/devise/api/token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_record' 4 | 5 | module Devise 6 | module Api 7 | class Token < ::ActiveRecord::Base 8 | self.table_name = 'devise_api_tokens' 9 | 10 | # associations 11 | belongs_to :resource_owner, 12 | polymorphic: true, 13 | optional: false 14 | belongs_to :previous_refresh, 15 | class_name: Devise.api.config.base_token_model, 16 | foreign_key: :previous_refresh_token, 17 | primary_key: :refresh_token, 18 | optional: true 19 | has_many :refreshes, 20 | class_name: Devise.api.config.base_token_model, 21 | foreign_key: :previous_refresh_token, 22 | primary_key: :refresh_token 23 | 24 | # validations 25 | validates :access_token, presence: true, uniqueness: true 26 | validates :refresh_token, 27 | presence: true, 28 | uniqueness: true, 29 | if: -> { Devise.api.config.refresh_token.enabled } 30 | validates :expires_in, 31 | presence: true, 32 | numericality: { greater_than: 0 }, 33 | unless: -> { Devise.api.config.access_token.expires_in_infinite.call(resource_owner) } 34 | 35 | def revoked? 36 | revoked_at.present? 37 | end 38 | 39 | def active? 40 | !inactive? 41 | end 42 | 43 | def inactive? 44 | revoked? || expired? 45 | end 46 | 47 | def expired? 48 | return false if Devise.api.config.access_token.expires_in_infinite.call(resource_owner) 49 | 50 | !!(expires_in && Time.now.utc > expires_at) 51 | end 52 | 53 | def refresh_token_expired? 54 | return false if Devise.api.config.refresh_token.expires_in_infinite.call(resource_owner) 55 | 56 | Time.now.utc > refresh_token_expires_at 57 | end 58 | 59 | def self.generate_uniq_access_token(resource_owner) 60 | loop do 61 | token = Devise.api.config.access_token.generator.call(resource_owner) 62 | 63 | break token unless Devise.api.config.base_token_model.constantize.exists?(access_token: token) 64 | end 65 | end 66 | 67 | def self.generate_uniq_refresh_token(resource_owner) 68 | return nil unless Devise.api.config.refresh_token.enabled 69 | 70 | loop do 71 | token = Devise.api.config.refresh_token.generator.call(resource_owner) 72 | 73 | break token unless Devise.api.config.base_token_model.constantize.exists?(refresh_token: token) 74 | end 75 | end 76 | 77 | private 78 | 79 | def expires_at 80 | created_at + expires_in.seconds 81 | end 82 | 83 | def refresh_token_expires_at 84 | created_at + Devise.api.config.refresh_token.expires_in.seconds 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | Rails.application.configure do 6 | # Settings specified here will take precedence over those in config/application.rb. 7 | 8 | # Code is not reloaded between requests. 9 | config.cache_classes = true 10 | 11 | # Eager load code on boot. This eager loads most of Rails and 12 | # your application in memory, allowing both threaded web servers 13 | # and those relying on copy on write to perform better. 14 | # Rake tasks automatically ignore this option for performance. 15 | config.eager_load = true 16 | 17 | # Full error reports are disabled and caching is turned on. 18 | config.consider_all_requests_local = false 19 | config.action_controller.perform_caching = true 20 | 21 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 22 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 23 | # config.require_master_key = true 24 | 25 | # Disable serving static files from the `/public` folder by default since 26 | # Apache or NGINX already handles this. 27 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 28 | 29 | # Compress CSS using a preprocessor. 30 | # config.assets.css_compressor = :sass 31 | 32 | # Do not fallback to assets pipeline if a precompiled asset is missed. 33 | config.assets.compile = false 34 | 35 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 36 | # config.asset_host = "http://assets.example.com" 37 | 38 | # Specifies the header that your server uses for sending files. 39 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 40 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | # config.force_ssl = true 44 | 45 | # Include generic and useful information about system operation, but avoid logging too much 46 | # information to avoid inadvertent exposure of personally identifiable information (PII). 47 | config.log_level = :info 48 | 49 | # Prepend all log lines with the following tags. 50 | config.log_tags = [:request_id] 51 | 52 | # Use a different cache store in production. 53 | # config.cache_store = :mem_cache_store 54 | 55 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 56 | # the I18n.default_locale when a translation cannot be found). 57 | config.i18n.fallbacks = true 58 | 59 | # Don't log any deprecations. 60 | config.active_support.report_deprecations = false 61 | 62 | # Use default logging formatter so that PID and timestamp are not suppressed. 63 | config.log_formatter = ::Logger::Formatter.new 64 | 65 | # Use a different logger for distributed setups. 66 | # require "syslog/logger" 67 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 68 | 69 | if ENV['RAILS_LOG_TO_STDOUT'].present? 70 | logger = ActiveSupport::Logger.new($stdout) 71 | logger.formatter = config.log_formatter 72 | config.logger = ActiveSupport::TaggedLogging.new(logger) 73 | end 74 | 75 | # Do not dump schema after migrations. 76 | config.active_record.dump_schema_after_migration = false 77 | end 78 | -------------------------------------------------------------------------------- /lib/devise/api/controllers/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/concern' 4 | 5 | module Devise 6 | module Api 7 | module Controllers 8 | module Helpers 9 | extend ActiveSupport::Concern 10 | 11 | def authenticate_devise_api_token! 12 | if current_devise_api_token.blank? 13 | error_response = Devise::Api::Responses::ErrorResponse.new(request, error: :invalid_token, 14 | resource_class: resource_class) 15 | 16 | return render json: error_response.body, status: error_response.status 17 | end 18 | 19 | if current_devise_api_token.expired? 20 | error_response = Devise::Api::Responses::ErrorResponse.new(request, error: :expired_token, 21 | resource_class: resource_class) 22 | 23 | return render json: error_response.body, status: error_response.status 24 | end 25 | 26 | return unless current_devise_api_token.revoked? 27 | 28 | error_response = Devise::Api::Responses::ErrorResponse.new(request, error: :revoked_token, 29 | resource_class: resource_class) 30 | 31 | render json: error_response.body, status: error_response.status 32 | end 33 | 34 | def current_devise_api_refresh_token 35 | token = find_devise_api_token 36 | 37 | Devise.api.config.base_token_model.constantize.find_by(refresh_token: token) 38 | end 39 | 40 | def current_devise_api_token 41 | return @current_devise_api_token if defined?(@current_devise_api_token) 42 | 43 | token = find_devise_api_token 44 | devise_api_token_model = Devise.api.config.base_token_model.constantize 45 | @current_devise_api_token = devise_api_token_model.find_by(access_token: token) 46 | end 47 | 48 | def current_devise_api_user 49 | current_devise_api_token&.resource_owner 50 | end 51 | 52 | private 53 | 54 | def resource_class 55 | current_devise_api_user&.class 56 | end 57 | 58 | def extract_devise_api_token_from_params 59 | params[Devise.api.config.authorization.params_key] 60 | end 61 | 62 | def extract_devise_api_token_from_headers 63 | token = request.headers[Devise.api.config.authorization.key] 64 | unless token.blank? 65 | token = begin 66 | token.gsub(/^#{Devise.api.config.authorization.scheme} /, 67 | '') 68 | rescue StandardError 69 | token 70 | end 71 | end 72 | token 73 | end 74 | 75 | def find_devise_api_token 76 | case Devise.api.config.authorization.location 77 | when :header 78 | extract_devise_api_token_from_headers 79 | when :params 80 | extract_devise_api_token_from_params 81 | when :both 82 | extract_devise_api_token_from_params || extract_devise_api_token_from_headers 83 | else 84 | raise ArgumentError, 'Invalid authorization location, must be :header, :params or :both' 85 | end 86 | end 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require 'rubygems' 12 | 13 | # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Layout/LineLength 14 | m = Module.new do 15 | module_function 16 | 17 | def invoked_as_script? 18 | File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__) 19 | end 20 | 21 | def env_var_version 22 | ENV['BUNDLER_VERSION'] 23 | end 24 | 25 | def cli_arg_version 26 | return unless invoked_as_script? # don't want to hijack other binstubs 27 | return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update` 28 | 29 | bundler_version = nil 30 | update_index = nil 31 | ARGV.each_with_index do |a, i| 32 | bundler_version = a if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | 35 | bundler_version = Regexp.last_match(1) 36 | update_index = i 37 | end 38 | bundler_version 39 | end 40 | 41 | def gemfile 42 | gemfile = ENV['BUNDLE_GEMFILE'] 43 | return gemfile if gemfile && !gemfile.empty? 44 | 45 | File.expand_path('../Gemfile', __dir__) 46 | end 47 | 48 | def lockfile 49 | lockfile = 50 | case File.basename(gemfile) 51 | when 'gems.rb' then gemfile.sub(/\.rb$/, gemfile) 52 | else "#{gemfile}.lock" 53 | end 54 | File.expand_path(lockfile) 55 | end 56 | 57 | def lockfile_version 58 | return unless File.file?(lockfile) 59 | 60 | lockfile_contents = File.read(lockfile) 61 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 62 | 63 | Regexp.last_match(1) 64 | end 65 | 66 | def bundler_requirement 67 | @bundler_requirement ||= 68 | env_var_version || 69 | cli_arg_version || 70 | bundler_requirement_for(lockfile_version) 71 | end 72 | 73 | def bundler_requirement_for(version) 74 | return "#{Gem::Requirement.default}.a" unless version 75 | 76 | bundler_gem_version = Gem::Version.new(version) 77 | 78 | bundler_gem_version.approximate_recommendation 79 | end 80 | 81 | def load_bundler! 82 | ENV['BUNDLE_GEMFILE'] ||= gemfile 83 | 84 | activate_bundler 85 | end 86 | 87 | def activate_bundler 88 | gem_error = activation_error_handling do 89 | gem 'bundler', bundler_requirement 90 | end 91 | return if gem_error.nil? 92 | 93 | require_error = activation_error_handling do 94 | require 'bundler/version' 95 | end 96 | if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 97 | return 98 | end 99 | 100 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 101 | exit 42 102 | end 103 | 104 | def activation_error_handling 105 | yield 106 | nil 107 | rescue StandardError, LoadError => e 108 | e 109 | end 110 | end 111 | # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Layout/LineLength 112 | 113 | m.load_bundler! 114 | 115 | load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script? 116 | -------------------------------------------------------------------------------- /spec/devise/api/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Devise::Api::Configuration do 6 | let(:config) { described_class.new } 7 | 8 | it 'extens from dry-configurable' do 9 | expect(described_class).to be < Dry::Configurable 10 | end 11 | 12 | context 'default settings' do 13 | context 'access_token' do 14 | it 'expires_in is 1.hour' do 15 | expect(config.access_token.expires_in).to eq 1.hour 16 | end 17 | 18 | it 'expires_in_infinite is false' do 19 | expect(config.access_token.expires_in_infinite.call).to eq false 20 | end 21 | 22 | it 'generator returns a token string with using Devise.friendly_token' do 23 | allow(Devise).to receive(:friendly_token).with(60).and_return('token') 24 | expect(config.access_token.generator.call).to eq 'token' 25 | expect(Devise).to have_received(:friendly_token).with(60) 26 | expect(config.access_token.generator).to be_a Proc 27 | end 28 | end 29 | 30 | context 'refresh_token' do 31 | it 'enabled is true' do 32 | expect(config.refresh_token.enabled).to eq true 33 | end 34 | 35 | it 'expires_in is 1.week' do 36 | expect(config.refresh_token.expires_in).to eq 1.week 37 | end 38 | 39 | it 'expires_in_infinite is false' do 40 | expect(config.refresh_token.expires_in_infinite.call).to eq false 41 | end 42 | 43 | it 'generator returns a token string with using Devise.friendly_token' do 44 | allow(Devise).to receive(:friendly_token).with(60).and_return('token') 45 | expect(config.refresh_token.generator.call).to eq 'token' 46 | expect(Devise).to have_received(:friendly_token).with(60) 47 | expect(config.refresh_token.generator).to be_a Proc 48 | end 49 | end 50 | 51 | context 'sign_up' do 52 | it 'enabled is true' do 53 | expect(config.sign_up.enabled).to eq true 54 | end 55 | 56 | it 'extra_fields is an empty array' do 57 | expect(config.sign_up.extra_fields).to eq [] 58 | end 59 | end 60 | 61 | context 'authorization' do 62 | it 'key is Authorization' do 63 | expect(config.authorization.key).to eq 'Authorization' 64 | end 65 | 66 | it 'scheme is Bearer' do 67 | expect(config.authorization.scheme).to eq 'Bearer' 68 | end 69 | 70 | it 'location is both' do 71 | expect(config.authorization.location).to eq :both 72 | end 73 | 74 | it 'params_key is access_token' do 75 | expect(config.authorization.params_key).to eq 'access_token' 76 | end 77 | end 78 | 79 | context 'base classes' do 80 | it 'base_token_model is Devise::Api::Token' do 81 | expect(config.base_token_model).to eq 'Devise::Api::Token' 82 | end 83 | 84 | it 'base_controller is DeviseController' do 85 | expect(config.base_controller).to eq '::DeviseController' 86 | end 87 | end 88 | 89 | context 'after callbacks' do 90 | it 'after_successful_sign_in is a proc' do 91 | expect(config.after_successful_sign_in).to be_a Proc 92 | end 93 | 94 | it 'after_successful_sign_up is a proc' do 95 | expect(config.after_successful_sign_up).to be_a Proc 96 | end 97 | 98 | it 'after_successful_refresh is a proc' do 99 | expect(config.after_successful_refresh).to be_a Proc 100 | end 101 | 102 | it 'after_successful_revoke is a proc' do 103 | expect(config.after_successful_revoke).to be_a Proc 104 | end 105 | end 106 | 107 | context 'before callbacks' do 108 | it 'before_sign_in is a proc' do 109 | expect(config.before_sign_in).to be_a Proc 110 | end 111 | 112 | it 'before_sign_up is a proc' do 113 | expect(config.before_sign_up).to be_a Proc 114 | end 115 | 116 | it 'before_refresh is a proc' do 117 | expect(config.before_refresh).to be_a Proc 118 | end 119 | 120 | it 'before_revoke is a proc' do 121 | expect(config.before_revoke).to be_a Proc 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/devise/api/responses/error_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module Api 5 | module Responses 6 | class ErrorResponse 7 | attr_reader :request, :error, :record, :resource_class 8 | 9 | ERROR_TYPES = %i[ 10 | invalid_token 11 | expired_token 12 | expired_refresh_token 13 | revoked_token 14 | refresh_token_disabled 15 | sign_up_disabled 16 | invalid_refresh_token 17 | invalid_email 18 | invalid_resource_owner 19 | resource_owner_create_error 20 | devise_api_token_create_error 21 | devise_api_token_revoke_error 22 | invalid_authentication 23 | ].freeze 24 | 25 | ERROR_TYPES.each do |error_type| 26 | method_name = error_type.end_with?('_error') ? error_type : "#{error_type}_error" 27 | 28 | define_method("#{method_name}?") do 29 | error.eql?(error_type) 30 | end 31 | end 32 | 33 | def initialize(request, error:, record: nil, resource_class: nil) 34 | @request = request 35 | @error = error 36 | @record = record 37 | @resource_class = resource_class 38 | end 39 | 40 | def body 41 | { 42 | error: error, 43 | error_description: error_description, 44 | lockable: devise_lockable_info, 45 | confirmable: devise_confirmable_info 46 | }.compact 47 | end 48 | 49 | def status 50 | return :unauthorized if unauthorized_status? 51 | return :bad_request if bad_request_status? 52 | 53 | :unprocessable_entity 54 | end 55 | 56 | private 57 | 58 | # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity 59 | def error_description 60 | return [I18n.t("devise.api.error_response.#{error}")] if record.blank? 61 | if invalid_authentication_error? && devise_lockable_info.present? && record.access_locked? 62 | return [I18n.t('devise.api.error_response.lockable.locked')] 63 | end 64 | if invalid_authentication_error? && devise_confirmable_info.present? && !record.confirmed? 65 | return [I18n.t('devise.api.error_response.confirmable.unconfirmed')] 66 | end 67 | return [I18n.t('devise.api.error_response.invalid_authentication')] if invalid_authentication_error? 68 | 69 | record.errors.full_messages 70 | end 71 | # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity 72 | 73 | def devise_lockable_info 74 | unless resource_class.present? && 75 | resource_class.supported_devise_modules.lockable? && 76 | invalid_authentication_error? 77 | return nil 78 | end 79 | 80 | unlock_at = record.access_locked? ? record.locked_at + ::Devise.unlock_in : nil 81 | 82 | { 83 | locked: record.access_locked?, 84 | max_attempts: ::Devise.maximum_attempts, 85 | failed_attemps: record.failed_attempts, 86 | locked_at: record.locked_at, 87 | unlock_at: unlock_at 88 | }.compact 89 | end 90 | 91 | def devise_confirmable_info 92 | unless resource_class.present? && 93 | resource_class.supported_devise_modules.confirmable? && 94 | invalid_authentication_error? 95 | return nil 96 | end 97 | 98 | { 99 | confirmed: record.confirmed?, 100 | confirmation_sent_at: record.confirmed? ? nil : record.confirmation_sent_at 101 | }.compact 102 | end 103 | 104 | def unauthorized_status? 105 | invalid_token_error? || 106 | expired_token_error? || 107 | expired_refresh_token_error? || 108 | revoked_token_error? || 109 | invalid_authentication_error? 110 | end 111 | 112 | def bad_request_status? 113 | invalid_email_error? || 114 | invalid_refresh_token_error? || 115 | refresh_token_disabled_error? || 116 | sign_up_disabled_error? || 117 | invalid_resource_owner_error? 118 | end 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/heartcombo/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your email address has been successfully confirmed." 7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid %{authentication_keys} or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account is locked." 15 | not_found_in_database: "Invalid %{authentication_keys} or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your email address before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock instructions" 26 | email_changed: 27 | subject: "Email Changed" 28 | password_change: 29 | subject: "Password Changed" 30 | omniauth_callbacks: 31 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 32 | success: "Successfully authenticated from %{kind} account." 33 | passwords: 34 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 35 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 36 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 37 | updated: "Your password has been changed successfully. You are now signed in." 38 | updated_not_active: "Your password has been changed successfully." 39 | registrations: 40 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." 41 | signed_up: "Welcome! You have signed up successfully." 42 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 43 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 44 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." 45 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." 46 | updated: "Your account has been updated successfully." 47 | updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." 48 | sessions: 49 | signed_in: "Signed in successfully." 50 | signed_out: "Signed out successfully." 51 | already_signed_out: "Signed out successfully." 52 | unlocks: 53 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." 54 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." 55 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 56 | errors: 57 | messages: 58 | already_confirmed: "was already confirmed, please try signing in" 59 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 60 | expired: "has expired, please request a new one" 61 | not_found: "not found" 62 | not_locked: "was not locked" 63 | not_saved: 64 | one: "1 error prohibited this %{resource} from being saved:" 65 | other: "%{count} errors prohibited this %{resource} from being saved:" 66 | -------------------------------------------------------------------------------- /spec/requests/authentication_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Authentication', type: :request do 6 | describe 'GET /home' do 7 | context 'when the token is valid and on the header' do 8 | let(:user) { create(:user) } 9 | let(:devise_api_token) { create(:devise_api_token, resource_owner: user) } 10 | 11 | before do 12 | get home_path, headers: authentication_headers_for(user, devise_api_token), as: :json 13 | end 14 | 15 | it 'returns the correct response' do 16 | expect(response).to have_http_status(:success) 17 | expect(JSON.parse(response.body)).to eql({ 'success' => true }) 18 | end 19 | end 20 | 21 | context 'when the token is valid and on the url param' do 22 | let(:user) { create(:user) } 23 | let(:devise_api_token) { create(:devise_api_token, resource_owner: user) } 24 | 25 | before do 26 | get home_path(access_token: devise_api_token.access_token), as: :json 27 | end 28 | 29 | it 'returns the correct response' do 30 | expect(response).to have_http_status(:success) 31 | expect(JSON.parse(response.body)).to eql({ 'success' => true }) 32 | end 33 | end 34 | 35 | context 'when the token is invalid and on the header' do 36 | let(:user) { create(:user) } 37 | let(:devise_api_token) { build(:devise_api_token, resource_owner: user) } 38 | 39 | before do 40 | get home_path, headers: authentication_headers_for(user, devise_api_token), as: :json 41 | end 42 | 43 | it 'returns http unauthorized' do 44 | expect(response).to have_http_status(:unauthorized) 45 | end 46 | 47 | it 'returns an error response' do 48 | expect(parsed_body.error).to eq 'invalid_token' 49 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.invalid_token')]) 50 | end 51 | 52 | it 'does not return the authenticated resource owner' do 53 | expect(parsed_body.id).to be_nil 54 | expect(parsed_body.email).to be_nil 55 | expect(parsed_body.created_at).to be_nil 56 | expect(parsed_body.updated_at).to be_nil 57 | end 58 | end 59 | 60 | context 'when the token is invalid and on the url param' do 61 | before do 62 | get home_path(access_token: 'invalid'), as: :json 63 | end 64 | 65 | it 'returns http unauthorized' do 66 | expect(response).to have_http_status(:unauthorized) 67 | end 68 | 69 | it 'returns an error response' do 70 | expect(parsed_body.error).to eq 'invalid_token' 71 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.invalid_token')]) 72 | end 73 | 74 | it 'does not return the authenticated resource owner' do 75 | expect(parsed_body.id).to be_nil 76 | expect(parsed_body.email).to be_nil 77 | expect(parsed_body.created_at).to be_nil 78 | expect(parsed_body.updated_at).to be_nil 79 | end 80 | end 81 | 82 | context 'when the token is expired' do 83 | let(:user) { create(:user) } 84 | let(:devise_api_token) { create(:devise_api_token, :access_token_expired, resource_owner: user) } 85 | 86 | before do 87 | get home_path, headers: authentication_headers_for(user, devise_api_token), as: :json 88 | end 89 | 90 | it 'returns http unauthorized' do 91 | expect(response).to have_http_status(:unauthorized) 92 | end 93 | 94 | it 'returns an error response' do 95 | expect(parsed_body.error).to eq 'expired_token' 96 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.expired_token')]) 97 | end 98 | 99 | it 'does not return the authenticated resource owner' do 100 | expect(parsed_body.id).to be_nil 101 | expect(parsed_body.email).to be_nil 102 | expect(parsed_body.created_at).to be_nil 103 | expect(parsed_body.updated_at).to be_nil 104 | end 105 | end 106 | 107 | context 'when the token is revoked' do 108 | let(:user) { create(:user) } 109 | let(:devise_api_token) { create(:devise_api_token, :revoked, resource_owner: user) } 110 | 111 | before do 112 | get home_path, headers: authentication_headers_for(user, devise_api_token), as: :json 113 | end 114 | 115 | it 'returns http unauthorized' do 116 | expect(response).to have_http_status(:unauthorized) 117 | end 118 | 119 | it 'returns an error response' do 120 | expect(parsed_body.error).to eq 'revoked_token' 121 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.revoked_token')]) 122 | end 123 | 124 | it 'does not return the authenticated resource owner' do 125 | expect(parsed_body.id).to be_nil 126 | expect(parsed_body.email).to be_nil 127 | expect(parsed_body.created_at).to be_nil 128 | expect(parsed_body.updated_at).to be_nil 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at nejdetkadir.550@gmail.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /spec/devise/api/responses/token_response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Devise::Api::Responses::TokenResponse do 6 | let(:resource_owner) do 7 | FactoryBot.build( 8 | :user, 9 | id: 1, 10 | email: 'test@development.com', 11 | created_at: Time.now, 12 | updated_at: Time.now 13 | ) 14 | end 15 | let(:token) do 16 | FactoryBot.build( 17 | :devise_api_token, 18 | resource_owner: resource_owner, 19 | access_token: 'access_token', 20 | refresh_token: 'refresh_token', 21 | expires_in: 3600 22 | ) 23 | end 24 | 25 | context 'action types' do 26 | it 'has a list of actions' do 27 | expect(described_class::ACTIONS).to eq(%i[sign_in sign_up refresh revoke info]) 28 | end 29 | 30 | it 'has a method for each action' do 31 | expect(described_class.new(nil, token: token, action: :sign_in)).to respond_to(:sign_in_action?) 32 | expect(described_class.new(nil, token: token, action: :sign_up)).to respond_to(:sign_up_action?) 33 | expect(described_class.new(nil, token: token, action: :refresh)).to respond_to(:refresh_action?) 34 | expect(described_class.new(nil, token: token, action: :revoke)).to respond_to(:revoke_action?) 35 | expect(described_class.new(nil, token: token, action: :info)).to respond_to(:info_action?) 36 | end 37 | end 38 | 39 | context 'sign in' do 40 | let(:token_response) { described_class.new(nil, token: token, action: :sign_in) } 41 | 42 | it 'returns the correct body' do 43 | expect(token_response.body).to eq({ 44 | token: 'access_token', 45 | refresh_token: 'refresh_token', 46 | expires_in: 3600, 47 | token_type: 'Bearer', 48 | resource_owner: { 49 | id: 1, 50 | email: 'test@development.com', 51 | created_at: resource_owner.created_at, 52 | updated_at: resource_owner.updated_at 53 | }.stringify_keys 54 | }) 55 | end 56 | 57 | it 'returns the correct status' do 58 | expect(token_response.status).to eq(:ok) 59 | end 60 | end 61 | 62 | context 'sign up' do 63 | let(:token_response) { described_class.new(nil, token: token, action: :sign_up) } 64 | 65 | it 'returns the correct body' do 66 | allow(resource_owner).to receive(:confirmed?).and_return(true) 67 | expect(token_response.body).to eq({ 68 | token: 'access_token', 69 | refresh_token: 'refresh_token', 70 | expires_in: 3600, 71 | token_type: 'Bearer', 72 | resource_owner: { 73 | id: 1, 74 | email: 'test@development.com', 75 | created_at: resource_owner.created_at, 76 | updated_at: resource_owner.updated_at 77 | }.stringify_keys, 78 | confirmable: { 79 | confirmed: true 80 | } 81 | }) 82 | end 83 | 84 | it 'returns the correct status' do 85 | expect(token_response.status).to eq(:created) 86 | end 87 | end 88 | 89 | context 'refresh' do 90 | let(:token_response) { described_class.new(nil, token: token, action: :refresh) } 91 | 92 | it 'returns the correct body' do 93 | expect(token_response.body).to eq({ 94 | token: 'access_token', 95 | refresh_token: 'refresh_token', 96 | expires_in: 3600, 97 | token_type: 'Bearer', 98 | resource_owner: { 99 | id: 1, 100 | email: 'test@development.com', 101 | created_at: resource_owner.created_at, 102 | updated_at: resource_owner.updated_at 103 | }.stringify_keys 104 | }) 105 | end 106 | 107 | it 'returns the correct status' do 108 | expect(token_response.status).to eq(:ok) 109 | end 110 | end 111 | 112 | context 'revoke' do 113 | let(:token_response) { described_class.new(nil, token: token, action: :revoke) } 114 | 115 | it 'returns the correct body' do 116 | expect(token_response.body).to eq({}) 117 | end 118 | 119 | it 'returns the correct status' do 120 | expect(token_response.status).to eq(:no_content) 121 | end 122 | end 123 | 124 | context 'info' do 125 | let(:token_response) { described_class.new(nil, token: token, action: :info) } 126 | 127 | it 'returns the correct body' do 128 | expect(token_response.body).to eq( 129 | { 130 | id: 1, 131 | email: 'test@development.com', 132 | created_at: resource_owner.created_at, 133 | updated_at: resource_owner.updated_at 134 | }.stringify_keys 135 | ) 136 | end 137 | 138 | it 'returns the correct status' do 139 | expect(token_response.status).to eq(:ok) 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | devise-api (0.1.3) 5 | devise (>= 4.7.2) 6 | dry-configurable (~> 1.0, >= 1.0.1) 7 | dry-initializer (>= 3.1.1) 8 | dry-monads (>= 1.6.0) 9 | dry-types (>= 1.7.0) 10 | rails (>= 6.0.0) 11 | 12 | GEM 13 | remote: https://rubygems.org/ 14 | specs: 15 | actioncable (7.0.4) 16 | actionpack (= 7.0.4) 17 | activesupport (= 7.0.4) 18 | nio4r (~> 2.0) 19 | websocket-driver (>= 0.6.1) 20 | actionmailbox (7.0.4) 21 | actionpack (= 7.0.4) 22 | activejob (= 7.0.4) 23 | activerecord (= 7.0.4) 24 | activestorage (= 7.0.4) 25 | activesupport (= 7.0.4) 26 | mail (>= 2.7.1) 27 | net-imap 28 | net-pop 29 | net-smtp 30 | actionmailer (7.0.4) 31 | actionpack (= 7.0.4) 32 | actionview (= 7.0.4) 33 | activejob (= 7.0.4) 34 | activesupport (= 7.0.4) 35 | mail (~> 2.5, >= 2.5.4) 36 | net-imap 37 | net-pop 38 | net-smtp 39 | rails-dom-testing (~> 2.0) 40 | actionpack (7.0.4) 41 | actionview (= 7.0.4) 42 | activesupport (= 7.0.4) 43 | rack (~> 2.0, >= 2.2.0) 44 | rack-test (>= 0.6.3) 45 | rails-dom-testing (~> 2.0) 46 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 47 | actiontext (7.0.4) 48 | actionpack (= 7.0.4) 49 | activerecord (= 7.0.4) 50 | activestorage (= 7.0.4) 51 | activesupport (= 7.0.4) 52 | globalid (>= 0.6.0) 53 | nokogiri (>= 1.8.5) 54 | actionview (7.0.4) 55 | activesupport (= 7.0.4) 56 | builder (~> 3.1) 57 | erubi (~> 1.4) 58 | rails-dom-testing (~> 2.0) 59 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 60 | activejob (7.0.4) 61 | activesupport (= 7.0.4) 62 | globalid (>= 0.3.6) 63 | activemodel (7.0.4) 64 | activesupport (= 7.0.4) 65 | activerecord (7.0.4) 66 | activemodel (= 7.0.4) 67 | activesupport (= 7.0.4) 68 | activestorage (7.0.4) 69 | actionpack (= 7.0.4) 70 | activejob (= 7.0.4) 71 | activerecord (= 7.0.4) 72 | activesupport (= 7.0.4) 73 | marcel (~> 1.0) 74 | mini_mime (>= 1.1.0) 75 | activesupport (7.0.4) 76 | concurrent-ruby (~> 1.0, >= 1.0.2) 77 | i18n (>= 1.6, < 2) 78 | minitest (>= 5.1) 79 | tzinfo (~> 2.0) 80 | ast (2.4.2) 81 | awesome_print (1.9.2) 82 | bcrypt (3.1.18) 83 | builder (3.2.4) 84 | coderay (1.1.3) 85 | concurrent-ruby (1.1.10) 86 | crass (1.0.6) 87 | database_cleaner (2.0.1) 88 | database_cleaner-active_record (~> 2.0.0) 89 | database_cleaner-active_record (2.0.1) 90 | activerecord (>= 5.a) 91 | database_cleaner-core (~> 2.0.0) 92 | database_cleaner-core (2.0.1) 93 | date (3.3.3) 94 | devise (4.8.1) 95 | bcrypt (~> 3.0) 96 | orm_adapter (~> 0.1) 97 | railties (>= 4.1.0) 98 | responders 99 | warden (~> 1.2.3) 100 | diff-lcs (1.5.0) 101 | dry-configurable (1.0.1) 102 | dry-core (~> 1.0, < 2) 103 | zeitwerk (~> 2.6) 104 | dry-core (1.0.0) 105 | concurrent-ruby (~> 1.0) 106 | zeitwerk (~> 2.6) 107 | dry-inflector (1.0.0) 108 | dry-initializer (3.1.1) 109 | dry-logic (1.5.0) 110 | concurrent-ruby (~> 1.0) 111 | dry-core (~> 1.0, < 2) 112 | zeitwerk (~> 2.6) 113 | dry-monads (1.6.0) 114 | concurrent-ruby (~> 1.0) 115 | dry-core (~> 1.0, < 2) 116 | zeitwerk (~> 2.6) 117 | dry-types (1.7.0) 118 | concurrent-ruby (~> 1.0) 119 | dry-core (~> 1.0, < 2) 120 | dry-inflector (~> 1.0, < 2) 121 | dry-logic (>= 1.4, < 2) 122 | zeitwerk (~> 2.6) 123 | erubi (1.12.0) 124 | factory_bot (6.2.1) 125 | activesupport (>= 5.0.0) 126 | faker (3.1.0) 127 | i18n (>= 1.8.11, < 2) 128 | globalid (1.0.0) 129 | activesupport (>= 5.0) 130 | i18n (1.12.0) 131 | concurrent-ruby (~> 1.0) 132 | json (2.6.3) 133 | loofah (2.19.1) 134 | crass (~> 1.0.2) 135 | nokogiri (>= 1.5.9) 136 | mail (2.8.0) 137 | mini_mime (>= 0.1.1) 138 | net-imap 139 | net-pop 140 | net-smtp 141 | marcel (1.0.2) 142 | method_source (1.0.0) 143 | mini_mime (1.1.2) 144 | mini_portile2 (2.8.1) 145 | minitest (5.17.0) 146 | net-imap (0.3.4) 147 | date 148 | net-protocol 149 | net-pop (0.1.2) 150 | net-protocol 151 | net-protocol (0.2.1) 152 | timeout 153 | net-smtp (0.3.3) 154 | net-protocol 155 | nio4r (2.5.8) 156 | nokogiri (1.13.10) 157 | mini_portile2 (~> 2.8.0) 158 | racc (~> 1.4) 159 | orm_adapter (0.5.0) 160 | parallel (1.22.1) 161 | parser (3.2.0.0) 162 | ast (~> 2.4.1) 163 | pry (0.14.2) 164 | coderay (~> 1.1) 165 | method_source (~> 1.0) 166 | puma (5.6.5) 167 | nio4r (~> 2.0) 168 | racc (1.6.2) 169 | rack (2.2.5) 170 | rack-test (2.0.2) 171 | rack (>= 1.3) 172 | rails (7.0.4) 173 | actioncable (= 7.0.4) 174 | actionmailbox (= 7.0.4) 175 | actionmailer (= 7.0.4) 176 | actionpack (= 7.0.4) 177 | actiontext (= 7.0.4) 178 | actionview (= 7.0.4) 179 | activejob (= 7.0.4) 180 | activemodel (= 7.0.4) 181 | activerecord (= 7.0.4) 182 | activestorage (= 7.0.4) 183 | activesupport (= 7.0.4) 184 | bundler (>= 1.15.0) 185 | railties (= 7.0.4) 186 | rails-dom-testing (2.0.3) 187 | activesupport (>= 4.2.0) 188 | nokogiri (>= 1.6) 189 | rails-html-sanitizer (1.4.4) 190 | loofah (~> 2.19, >= 2.19.1) 191 | railties (7.0.4) 192 | actionpack (= 7.0.4) 193 | activesupport (= 7.0.4) 194 | method_source 195 | rake (>= 12.2) 196 | thor (~> 1.0) 197 | zeitwerk (~> 2.5) 198 | rainbow (3.1.1) 199 | rake (13.0.6) 200 | regexp_parser (2.6.1) 201 | responders (3.0.1) 202 | actionpack (>= 5.0) 203 | railties (>= 5.0) 204 | rexml (3.2.5) 205 | rspec (3.12.0) 206 | rspec-core (~> 3.12.0) 207 | rspec-expectations (~> 3.12.0) 208 | rspec-mocks (~> 3.12.0) 209 | rspec-core (3.12.0) 210 | rspec-support (~> 3.12.0) 211 | rspec-expectations (3.12.2) 212 | diff-lcs (>= 1.2.0, < 2.0) 213 | rspec-support (~> 3.12.0) 214 | rspec-mocks (3.12.2) 215 | diff-lcs (>= 1.2.0, < 2.0) 216 | rspec-support (~> 3.12.0) 217 | rspec-rails (6.0.1) 218 | actionpack (>= 6.1) 219 | activesupport (>= 6.1) 220 | railties (>= 6.1) 221 | rspec-core (~> 3.11) 222 | rspec-expectations (~> 3.11) 223 | rspec-mocks (~> 3.11) 224 | rspec-support (~> 3.11) 225 | rspec-support (3.12.0) 226 | rubocop (1.42.0) 227 | json (~> 2.3) 228 | parallel (~> 1.10) 229 | parser (>= 3.1.2.1) 230 | rainbow (>= 2.2.2, < 4.0) 231 | regexp_parser (>= 1.8, < 3.0) 232 | rexml (>= 3.2.5, < 4.0) 233 | rubocop-ast (>= 1.24.1, < 2.0) 234 | ruby-progressbar (~> 1.7) 235 | unicode-display_width (>= 1.4.0, < 3.0) 236 | rubocop-ast (1.24.1) 237 | parser (>= 3.1.1.0) 238 | ruby-progressbar (1.11.0) 239 | sprockets (4.2.0) 240 | concurrent-ruby (~> 1.0) 241 | rack (>= 2.2.4, < 4) 242 | sprockets-rails (3.4.2) 243 | actionpack (>= 5.2) 244 | activesupport (>= 5.2) 245 | sprockets (>= 3.0.0) 246 | sqlite3 (1.6.0-arm64-darwin) 247 | thor (1.2.1) 248 | timeout (0.3.1) 249 | tzinfo (2.0.5) 250 | concurrent-ruby (~> 1.0) 251 | unicode-display_width (2.4.2) 252 | warden (1.2.9) 253 | rack (>= 2.0.9) 254 | websocket-driver (0.7.5) 255 | websocket-extensions (>= 0.1.0) 256 | websocket-extensions (0.1.5) 257 | zeitwerk (2.6.6) 258 | 259 | PLATFORMS 260 | arm64-darwin-21 261 | arm64-darwin-22 262 | arm64-darwin-23 263 | 264 | DEPENDENCIES 265 | awesome_print 266 | database_cleaner (~> 2.0, >= 2.0.1) 267 | devise-api! 268 | factory_bot (~> 6.2, >= 6.2.1) 269 | faker (~> 3.1) 270 | pry (~> 0.14.1) 271 | puma (~> 5.0) 272 | rake (~> 13.0) 273 | rspec (~> 3.0) 274 | rspec-core 275 | rspec-rails (~> 6.0, >= 6.0.1) 276 | rspec-support 277 | rubocop (~> 1.21) 278 | sprockets-rails 279 | sqlite3 (~> 1.4) 280 | 281 | BUNDLED WITH 282 | 2.4.3 283 | -------------------------------------------------------------------------------- /app/controllers/devise/api/tokens_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # rubocop:disable Metrics/ClassLength 4 | module Devise 5 | module Api 6 | class TokensController < Devise.api.config.base_controller.constantize 7 | wrap_parameters false 8 | skip_before_action :verify_authenticity_token, raise: false 9 | before_action :authenticate_devise_api_token!, only: %i[info] 10 | 11 | respond_to :json 12 | 13 | # rubocop:disable Metrics/AbcSize 14 | def sign_up 15 | unless Devise.api.config.sign_up.enabled 16 | error_response = Devise::Api::Responses::ErrorResponse.new(request, error: :sign_up_disabled, 17 | resource_class: resource_class) 18 | 19 | return render json: error_response.body, status: error_response.status 20 | end 21 | 22 | Devise.api.config.before_sign_up.call(sign_up_params, request, resource_class) 23 | 24 | service = Devise::Api::ResourceOwnerService::SignUp.new(params: sign_up_params, 25 | resource_class: resource_class).call 26 | 27 | if service.success? 28 | token = service.success 29 | 30 | call_devise_trackable!(token.resource_owner) 31 | 32 | token_response = Devise::Api::Responses::TokenResponse.new(request, token: token, action: __method__) 33 | 34 | Devise.api.config.after_successful_sign_up.call(token.resource_owner, token, request) 35 | 36 | return render json: token_response.body, status: token_response.status 37 | end 38 | 39 | error_response = Devise::Api::Responses::ErrorResponse.new(request, 40 | resource_class: resource_class, 41 | **service.failure) 42 | 43 | render json: error_response.body, status: error_response.status 44 | end 45 | # rubocop:enable Metrics/AbcSize 46 | 47 | # rubocop:disable Metrics/AbcSize 48 | def sign_in 49 | Devise.api.config.before_sign_in.call(sign_in_params, request, resource_class) 50 | 51 | service = Devise::Api::ResourceOwnerService::SignIn.new(params: sign_in_params, 52 | resource_class: resource_class).call 53 | 54 | if service.success? 55 | token = service.success 56 | 57 | call_devise_trackable!(token.resource_owner) 58 | 59 | token_response = Devise::Api::Responses::TokenResponse.new(request, token: service.success, 60 | action: __method__) 61 | 62 | Devise.api.config.after_successful_sign_in.call(token.resource_owner, token, request) 63 | 64 | return render json: token_response.body, status: token_response.status 65 | end 66 | 67 | error_response = Devise::Api::Responses::ErrorResponse.new(request, 68 | resource_class: resource_class, 69 | **service.failure) 70 | 71 | render json: error_response.body, status: error_response.status 72 | end 73 | # rubocop:enable Metrics/AbcSize 74 | 75 | def info 76 | token_response = Devise::Api::Responses::TokenResponse.new(request, token: current_devise_api_token, 77 | action: __method__) 78 | 79 | render json: token_response.body, status: token_response.status 80 | end 81 | 82 | # rubocop:disable Metrics/AbcSize 83 | def revoke 84 | Devise.api.config.before_revoke.call(current_devise_api_token, request) 85 | 86 | service = Devise::Api::TokensService::Revoke.new(devise_api_token: current_devise_api_token).call 87 | 88 | if service.success? 89 | token_response = Devise::Api::Responses::TokenResponse.new(request, token: service.success, 90 | action: __method__) 91 | 92 | Devise.api.config.after_successful_revoke.call(service.success&.resource_owner, service.success, request) 93 | 94 | return render json: token_response.body, status: token_response.status 95 | end 96 | 97 | error_response = Devise::Api::Responses::ErrorResponse.new(request, 98 | resource_class: resource_class, 99 | **service.failure) 100 | 101 | render json: error_response.body, status: error_response.status 102 | end 103 | # rubocop:enable Metrics/AbcSize 104 | 105 | # rubocop:disable Metrics/AbcSize 106 | def refresh 107 | unless Devise.api.config.refresh_token.enabled 108 | error_response = Devise::Api::Responses::ErrorResponse.new(request, 109 | resource_class: resource_class, 110 | error: :refresh_token_disabled) 111 | 112 | return render json: error_response.body, status: error_response.status 113 | end 114 | 115 | if current_devise_api_refresh_token.blank? 116 | error_response = Devise::Api::Responses::ErrorResponse.new(request, error: :invalid_token, 117 | resource_class: resource_class) 118 | 119 | return render json: error_response.body, status: error_response.status 120 | end 121 | 122 | if current_devise_api_refresh_token.revoked? 123 | error_response = Devise::Api::Responses::ErrorResponse.new(request, error: :revoked_token, 124 | resource_class: resource_class) 125 | 126 | return render json: error_response.body, status: error_response.status 127 | end 128 | 129 | Devise.api.config.before_refresh.call(current_devise_api_refresh_token, request) 130 | 131 | service = Devise::Api::TokensService::Refresh.new(devise_api_token: current_devise_api_refresh_token).call 132 | 133 | if service.success? 134 | token_response = Devise::Api::Responses::TokenResponse.new(request, token: service.success, 135 | action: __method__) 136 | 137 | Devise.api.config.after_successful_refresh.call(service.success.resource_owner, service.success, request) 138 | 139 | return render json: token_response.body, status: token_response.status 140 | end 141 | 142 | error_response = Devise::Api::Responses::ErrorResponse.new(request, 143 | resource_class: resource_class, 144 | **service.failure) 145 | 146 | render json: error_response.body, status: error_response.status 147 | end 148 | # rubocop:enable Metrics/AbcSize 149 | 150 | private 151 | 152 | def sign_up_params 153 | params.permit(*Devise.api.config.sign_up.extra_fields, *resource_class.authentication_keys, 154 | *::Devise::ParameterSanitizer::DEFAULT_PERMITTED_ATTRIBUTES[:sign_up]).to_h 155 | end 156 | 157 | def sign_in_params 158 | params.permit(*resource_class.authentication_keys, 159 | *::Devise::ParameterSanitizer::DEFAULT_PERMITTED_ATTRIBUTES[:sign_in]).to_h 160 | end 161 | 162 | def call_devise_trackable!(resource_owner) 163 | return unless resource_class.supported_devise_modules.trackable? 164 | 165 | resource_owner.update_tracked_fields!(request) 166 | end 167 | 168 | def current_devise_api_refresh_token 169 | return @current_devise_api_refresh_token if defined?(@current_devise_api_refresh_token) 170 | 171 | token = find_devise_api_token 172 | devise_api_token_model = Devise.api.config.base_token_model.constantize 173 | @current_devise_api_refresh_token = devise_api_token_model.find_by(refresh_token: token) 174 | end 175 | end 176 | end 177 | end 178 | # rubocop:enable Metrics/ClassLength 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/devise-api.svg)](https://badge.fury.io/rb/devise-api) 2 | ![test](https://github.com/nejdetkadir/devise-api/actions/workflows/test.yml/badge.svg?branch=main) 3 | ![rubocop](https://github.com/nejdetkadir/devise-api/actions/workflows/rubocop.yml/badge.svg?branch=main) 4 | [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop) 5 | ![Ruby Version](https://img.shields.io/badge/ruby_version->=_2.7.0-blue.svg) 6 | 7 | # Devise API 8 | The devise-api gem is a convenient way to add authentication to your Ruby on Rails application using the devise gem. It provides support for access tokens and refresh tokens, which allow you to authenticate API requests and keep the user's session active for a longer period of time on the client side. It can be installed by adding the gem to your Gemfile, running migrations, and adding the :api module to your devise model. The gem is fully configurable, allowing you to set things like token expiration times and token generators. 9 | 10 | Here's how it works: 11 | 12 | - When a user logs in to your Rails application, the `devise-api` gem generates an access token and a refresh token. 13 | - The access token is included in the API request headers and is used to authenticate the user on each subsequent request. 14 | - The refresh token is stored on the client side (e.g. in a browser cookie or on a mobile device) and is used to obtain a new access token when the original access token expires. 15 | - This allows the user to remain logged in and make API requests without having to constantly re-enter their login credentials. 16 | 17 | Overall, the `devise-api` gem is a useful tool for adding secure authentication to your Ruby on Rails application. 18 | 19 | ## Installation 20 | 21 | Install the gem and add to the application's Gemfile by executing: 22 | ```bash 23 | $ bundle add devise-api 24 | ``` 25 | 26 | Or add the following line to the application's Gemfile: 27 | ```ruby 28 | gem 'devise-api', github: 'nejdetkadir/devise-api', branch: 'main' 29 | ``` 30 | 31 | If bundler is not being used to manage dependencies, install the gem by executing: 32 | ```bash 33 | gem install devise-api 34 | ``` 35 | 36 | After that, you need to generate relevant migrations and locales by executing: 37 | ```bash 38 | $ rails generate devise_api:install 39 | ``` 40 | 41 | This will introduce two changes: 42 | - Locale files in `config/locales/devise_api.en.yml` 43 | - Migration file in `db/migrate` to create devise api tokens table 44 | 45 | Now you're ready to run the migrations: 46 | ```bash 47 | $ rails db:migrate 48 | ``` 49 | 50 | Finally, you need to add `:api` module to your devise model. For example: 51 | ```ruby 52 | class User < ApplicationRecord 53 | devise :database_authenticatable, 54 | :registerable, 55 | :recoverable, 56 | :rememberable, 57 | :validatable, 58 | :api # <--- Add this module 59 | end 60 | ``` 61 | 62 | Your user model is now ready to use `devise-api` gem. It will draw routes for token authenticatable and token refreshable. 63 | 64 | | Prefix | Verb | URI Pattern | Controller#Action | 65 | |--------|------|------------|--------------------------| 66 | | revoke_user_tokens | POST | /users/tokens/revoke | devise/api/tokens#revoke | 67 | | refresh_user_tokens | POST | /users/tokens/refresh | devise/api/tokens#refresh | 68 | | sign_up_user_tokens | POST | /users/tokens/sign_up | devise/api/tokens#sign_up | 69 | | sign_in_user_tokens | POST | /users/tokens/sign_in | devise/api/tokens#sign_in | 70 | | info_user_tokens | GET | /users/tokens/info | devise/api/tokens#info | 71 | 72 | ### You can look up the [example requests](#example-api-requests). 73 | 74 | ## Configuration 75 | 76 | `devise-api` is a full configurable gem. You can configure it to your needs. Here is a basic usage example: 77 | 78 | ```ruby 79 | # config/initializers/devise.rb 80 | Devise.setup do |config| 81 | config.api.configure do |api| 82 | # Access Token 83 | api.access_token.expires_in = 1.hour 84 | api.access_token.expires_in_infinite = ->(_resource_owner) { false } 85 | api.access_token.generator = ->(_resource_owner) { Devise.friendly_token(60) } 86 | 87 | 88 | # Refresh Token 89 | api.refresh_token.enabled = true 90 | api.refresh_token.expires_in = 1.week 91 | api.refresh_token.generator = ->(_resource_owner) { Devise.friendly_token(60) } 92 | api.refresh_token.expires_in_infinite = ->(_resource_owner) { false } 93 | 94 | # Sign up 95 | api.sign_up.enabled = true 96 | api.sign_up.extra_fields = [] 97 | 98 | # Authorization 99 | api.authorization.key = 'Authorization' 100 | api.authorization.scheme = 'Bearer' 101 | api.authorization.location = :both # :header or :params or :both 102 | api.authorization.params_key = 'access_token' 103 | 104 | 105 | # Base classes 106 | api.base_token_model = 'Devise::Api::Token' 107 | api.base_controller = '::DeviseController' 108 | 109 | 110 | # After successful callbacks 111 | api.after_successful_sign_in = ->(_resource_owner, _token, _request) { } 112 | api.after_successful_sign_up = ->(_resource_owner, _token, _request) { } 113 | api.after_successful_refresh = ->(_resource_owner, _token, _request) { } 114 | api.after_successful_revoke = ->(_resource_owner, _token, _request) { } 115 | 116 | 117 | # Before callbacks 118 | api.before_sign_in = ->(_params, _request, _resource_class) { } 119 | api.before_sign_up = ->(_params, _request, _resource_class) { } 120 | api.before_refresh = ->(_token_model, _request) { } 121 | api.before_revoke = ->(_token_model, _request) { } 122 | end 123 | end 124 | ``` 125 | 126 | ## Routes 127 | 128 | You can configure the tokens routes with the orginally `devise_for` method. For example: 129 | ```ruby 130 | # config/routes.rb 131 | Rails.application.routes.draw do 132 | devise_for :customers, 133 | controllers: { tokens: 'customers/api/tokens' } 134 | end 135 | ``` 136 | 137 | ## Usage 138 | `devise-api` module works with `:lockable` and `:confirmable` modules. It also works with `:trackable` module. 139 | 140 | `devise-api` provides a set of controllers and helpers to help you implement authentication in your Rails application. Here's a quick overview of the available controllers and helpers: 141 | 142 | - [Devise::Api::TokensController](https://github.com/nejdetkadir/devise-api/tree/main/app/controllers/devise/api/tokens_controller.rb) - This controller is responsible for generating access tokens and refresh tokens. It also provides actions for refreshing access tokens and revoking refresh tokens. 143 | 144 | - [Devise::Api::Token](https://github.com/nejdetkadir/devise-api/tree/main/lib/devise/api/token.rb) - This model is responsible for storing access tokens and refresh tokens in the database. 145 | 146 | - [Devise::Api::Responses::ErrorResponse](https://github.com/nejdetkadir/devise-api/tree/main/lib/devise/api/responses/error_response.rb) - This class is responsible for generating error responses. It also provides a set of error types and helpers to help you implement error responses in your Rails application. 147 | 148 | - [Devise::Api::Responses::TokenResponse](https://github.com/nejdetkadir/devise-api/tree/main/lib/devise/api/responses/token_response.rb) - This class is responsible for generating token responses. It also provides actions for generating access tokens and refresh tokens for each action. 149 | 150 | ## Overriding Responses 151 | You can prepend your decorators to the response classes to override the default responses. For example: 152 | ```ruby 153 | # app/lib/devise/api/responses/token_response_decorator.rb 154 | module Devise::Api::Responses::TokenResponseDecorator 155 | def body 156 | return default_body.merge({ roles: resource_owner.roles }) 157 | end 158 | end 159 | ``` 160 | 161 | Then you need to load and prepend your decorator to the response class. For example: 162 | 163 | ```ruby 164 | # config/initializers/devise.rb 165 | require 'devise/api/responses/token_response_decorator' # Either do this or autoload the lib directory 166 | 167 | Devise.setup do |config| 168 | end 169 | 170 | Devise::Api::Responses::TokenResponse.prepend Devise::Api::Responses::TokenResponseDecorator 171 | ``` 172 | 173 | ## Using helpers 174 | `devise-api` provides a set of helpers to help you implement authentication in your Rails application. Here's a quick overview of the available helpers: 175 | 176 | Example: 177 | ```ruby 178 | # app/controllers/api/v1/orders_controller.rb 179 | class Api::V1::OrdersController < YourBaseController 180 | skip_before_action :verify_authenticity_token, raise: false 181 | before_action :authenticate_devise_api_token! 182 | 183 | def index 184 | render json: current_devise_api_user.orders, status: :ok 185 | end 186 | 187 | def show 188 | devise_api_token = current_devise_api_token 189 | render json: devise_api_token.resource_owner.orders.find(params[:id]), status: :ok 190 | end 191 | end 192 | ``` 193 | 194 | ## Using devise base services 195 | `devise-api` provides a set of base services to help you implement authentication in your Rails application. Here's a quick overview of the available services: 196 | 197 | - [Devise::Api::BaseService](https://github.com/nejdetkadir/devise-api/tree/main/app/services/devise/api/base_service.rb) - This service is useful for creating and updating resources. It is inherited by the following gems. 198 | - [dry-monads](https://dry-rb.org/gems/dry-monads) 199 | - [dry-types](https://dry-rb.org/gems/dry-types) 200 | - [dry-initializer](https://dry-rb.org/gems/dry-initializer) 201 | 202 | You can create a service by inheriting the `Devise::Api::BaseService` class. For example: 203 | ```ruby 204 | # app/services/devise/api/tokens_service/v2/create.rb 205 | module Devise::Api::TokensService::V2 206 | class Create < Devise::Api::BaseService 207 | option :params, type: Types::Hash, reader: true 208 | option :resource_class, type: Types::Class, reader: true 209 | 210 | def call 211 | ... 212 | 213 | Success(resource) 214 | end 215 | end 216 | end 217 | ``` 218 | 219 | Then you can call the service in your controller. For example: 220 | ```ruby 221 | # app/controllers/api/v1/tokens_controller.rb 222 | class Api::V1::TokensController < YourBaseController 223 | skip_before_action :verify_authenticity_token, raise: false 224 | 225 | def create 226 | service = Devise::Api::TokensService::V2::Create.call(params: params, resource_class: Customer || resource_class) 227 | if service.success? 228 | render json: service.success, status: :created 229 | else 230 | render json: service.failure, status: :unprocessable_entity 231 | end 232 | end 233 | end 234 | ``` 235 | 236 | ## Example API requests 237 | 238 | ### Sign in 239 | ```curl 240 | curl --location --request POST 'http://127.0.0.1:3000/users/tokens/sign_in' \ 241 | --header 'Content-Type: application/json' \ 242 | --data-raw '{ 243 | "email": "test@development.com", 244 | "password": "123456" 245 | }' 246 | ``` 247 | 248 | ### Sign up 249 | ```curl 250 | curl --location --request POST 'http://127.0.0.1:3000/users/tokens/sign_up' \ 251 | --header 'Content-Type: application/json' \ 252 | --data-raw '{ 253 | "email": "test@development.com", 254 | "password": "123456" 255 | }' 256 | ``` 257 | 258 | ### Refresh token 259 | ```curl 260 | curl --location --request POST 'http://127.0.0.1:3000/users/tokens/refresh' \ 261 | --header 'Authorization: Bearer ' 262 | ``` 263 | 264 | ### Revoke 265 | ```curl 266 | curl --location --request POST 'http://127.0.0.1:3000/users/tokens/revoke' \ 267 | --header 'Authorization: Bearer ' 268 | ``` 269 | 270 | ### Info 271 | ```curl 272 | curl --location --request GET 'http://127.0.0.1:3000/users/tokens/info' \ 273 | --header 'Authorization: Bearer ' 274 | ``` 275 | 276 | ## Development 277 | 278 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 279 | 280 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 281 | 282 | ## Contributing 283 | 284 | Bug reports and pull requests are welcome on GitHub at https://github.com/nejdetkadir/devise-api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/nejdetkadir/devise-api/blob/main/CODE_OF_CONDUCT.md). 285 | 286 | ## License 287 | 288 | The gem is available as open source under the terms of the [MIT License](LICENSE). 289 | 290 | ## Code of Conduct 291 | 292 | Everyone interacting in the Devise::Api project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nejdetkadir/devise-api/blob/main/CODE_OF_CONDUCT.md). 293 | -------------------------------------------------------------------------------- /spec/devise/api/responses/error_response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Devise::Api::Responses::ErrorResponse do 6 | context 'error types' do 7 | it 'has a list of error types' do 8 | expect(described_class::ERROR_TYPES).to eq %i[ 9 | invalid_token 10 | expired_token 11 | expired_refresh_token 12 | revoked_token 13 | refresh_token_disabled 14 | sign_up_disabled 15 | invalid_refresh_token 16 | invalid_email 17 | invalid_resource_owner 18 | resource_owner_create_error 19 | devise_api_token_create_error 20 | devise_api_token_revoke_error 21 | invalid_authentication 22 | ] 23 | end 24 | 25 | it 'defines a helper method for each error type' do 26 | expect(described_class.new(nil, error: :invalid_token)).to respond_to(:invalid_token_error?) 27 | expect(described_class.new(nil, error: :expired_token)).to respond_to(:expired_token_error?) 28 | expect(described_class.new(nil, error: :expired_refresh_token)).to respond_to(:expired_refresh_token_error?) 29 | expect(described_class.new(nil, error: :revoked_token)).to respond_to(:revoked_token_error?) 30 | expect(described_class.new(nil, error: :refresh_token_disabled)).to respond_to(:refresh_token_disabled_error?) 31 | expect(described_class.new(nil, error: :sign_up_disabled)).to respond_to(:sign_up_disabled_error?) 32 | expect(described_class.new(nil, error: :invalid_refresh_token)).to respond_to(:invalid_refresh_token_error?) 33 | expect(described_class.new(nil, error: :invalid_email)).to respond_to(:invalid_email_error?) 34 | expect(described_class.new(nil, error: :invalid_resource_owner)).to respond_to(:invalid_resource_owner_error?) 35 | expect(described_class.new(nil, error: :resource_owner_create_error)).to respond_to(:resource_owner_create_error?) 36 | expect(described_class.new(nil, 37 | error: :devise_api_token_create_error)).to respond_to(:devise_api_token_create_error?) 38 | expect(described_class.new(nil, 39 | error: :devise_api_token_revoke_error)).to respond_to(:devise_api_token_revoke_error?) 40 | expect(described_class.new(nil, error: :invalid_authentication)).to respond_to(:invalid_authentication_error?) 41 | end 42 | end 43 | 44 | context 'invalid token error response' do 45 | let(:error_response) { described_class.new(nil, error: :invalid_token) } 46 | 47 | it 'has a status of 401' do 48 | expect(error_response.status).to eq :unauthorized 49 | end 50 | 51 | it 'has a body with an error and error description' do 52 | allow(I18n).to receive(:t).with('devise.api.error_response.invalid_token').and_return('Invalid token') 53 | 54 | expect(error_response.body).to eq( 55 | error: :invalid_token, 56 | error_description: ['Invalid token'] 57 | ) 58 | 59 | expect(I18n).to have_received(:t).with('devise.api.error_response.invalid_token') 60 | end 61 | end 62 | 63 | context 'expired token error response' do 64 | let(:error_response) { described_class.new(nil, error: :expired_token) } 65 | 66 | it 'has a status of 401' do 67 | expect(error_response.status).to eq :unauthorized 68 | end 69 | 70 | it 'has a body with an error and error description' do 71 | allow(I18n).to receive(:t).with('devise.api.error_response.expired_token').and_return('Expired token') 72 | 73 | expect(error_response.body).to eq( 74 | error: :expired_token, 75 | error_description: ['Expired token'] 76 | ) 77 | 78 | expect(I18n).to have_received(:t).with('devise.api.error_response.expired_token') 79 | end 80 | end 81 | 82 | context 'expired refresh token error response' do 83 | let(:error_response) { described_class.new(nil, error: :expired_refresh_token) } 84 | 85 | it 'has a status of 401' do 86 | expect(error_response.status).to eq :unauthorized 87 | end 88 | 89 | it 'has a body with an error and error description' do 90 | allow(I18n).to receive(:t) 91 | .with('devise.api.error_response.expired_refresh_token') 92 | .and_return('Expired refresh token') 93 | 94 | expect(error_response.body).to eq( 95 | error: :expired_refresh_token, 96 | error_description: ['Expired refresh token'] 97 | ) 98 | 99 | expect(I18n).to have_received(:t).with('devise.api.error_response.expired_refresh_token') 100 | end 101 | end 102 | 103 | context 'revoked token error response' do 104 | let(:error_response) { described_class.new(nil, error: :revoked_token) } 105 | 106 | it 'has a status of 401' do 107 | expect(error_response.status).to eq :unauthorized 108 | end 109 | 110 | it 'has a body with an error and error description' do 111 | allow(I18n).to receive(:t).with('devise.api.error_response.revoked_token').and_return('Revoked token') 112 | 113 | expect(error_response.body).to eq( 114 | error: :revoked_token, 115 | error_description: ['Revoked token'] 116 | ) 117 | 118 | expect(I18n).to have_received(:t).with('devise.api.error_response.revoked_token') 119 | end 120 | end 121 | 122 | context 'refresh token disabled error response' do 123 | let(:error_response) { described_class.new(nil, error: :refresh_token_disabled) } 124 | 125 | it 'has a status of 400' do 126 | expect(error_response.status).to eq :bad_request 127 | end 128 | 129 | it 'has a body with an error and error description' do 130 | allow(I18n).to receive(:t) 131 | .with('devise.api.error_response.refresh_token_disabled') 132 | .and_return('Refresh token disabled') 133 | 134 | expect(error_response.body).to eq( 135 | error: :refresh_token_disabled, 136 | error_description: ['Refresh token disabled'] 137 | ) 138 | 139 | expect(I18n).to have_received(:t).with('devise.api.error_response.refresh_token_disabled') 140 | end 141 | end 142 | 143 | context 'sign up disabled error response' do 144 | let(:error_response) { described_class.new(nil, error: :sign_up_disabled) } 145 | 146 | it 'has a status of 400' do 147 | expect(error_response.status).to eq :bad_request 148 | end 149 | 150 | it 'has a body with an error and error description' do 151 | allow(I18n).to receive(:t) 152 | .with('devise.api.error_response.sign_up_disabled') 153 | .and_return('Sign up is disabled') 154 | 155 | expect(error_response.body).to eq( 156 | error: :sign_up_disabled, 157 | error_description: ['Sign up is disabled'] 158 | ) 159 | 160 | expect(I18n).to have_received(:t).with('devise.api.error_response.sign_up_disabled') 161 | end 162 | end 163 | 164 | context 'invalid refresh token error response' do 165 | let(:error_response) { described_class.new(nil, error: :invalid_refresh_token) } 166 | 167 | it 'has a status of 400' do 168 | expect(error_response.status).to eq :bad_request 169 | end 170 | 171 | it 'has a body with an error and error description' do 172 | allow(I18n).to receive(:t) 173 | .with('devise.api.error_response.invalid_refresh_token') 174 | .and_return('Invalid refresh token') 175 | 176 | expect(error_response.body).to eq( 177 | error: :invalid_refresh_token, 178 | error_description: ['Invalid refresh token'] 179 | ) 180 | 181 | expect(I18n).to have_received(:t).with('devise.api.error_response.invalid_refresh_token') 182 | end 183 | end 184 | 185 | context 'invalid email error response' do 186 | let(:error_response) { described_class.new(nil, error: :invalid_email) } 187 | 188 | it 'has a status of 400' do 189 | expect(error_response.status).to eq :bad_request 190 | end 191 | 192 | it 'has a body with an error and error description' do 193 | allow(I18n).to receive(:t) 194 | .with('devise.api.error_response.invalid_email') 195 | .and_return('Invalid email') 196 | 197 | expect(error_response.body).to eq( 198 | error: :invalid_email, 199 | error_description: ['Invalid email'] 200 | ) 201 | 202 | expect(I18n).to have_received(:t).with('devise.api.error_response.invalid_email') 203 | end 204 | end 205 | 206 | context 'invalid resource owner error response' do 207 | let(:error_response) { described_class.new(nil, error: :invalid_resource_owner) } 208 | 209 | it 'has a status of 400' do 210 | expect(error_response.status).to eq :bad_request 211 | end 212 | 213 | it 'has a body with an error and error description' do 214 | allow(I18n).to receive(:t) 215 | .with('devise.api.error_response.invalid_resource_owner') 216 | .and_return('Invalid resource owner') 217 | 218 | expect(error_response.body).to eq( 219 | error: :invalid_resource_owner, 220 | error_description: ['Invalid resource owner'] 221 | ) 222 | 223 | expect(I18n).to have_received(:t).with('devise.api.error_response.invalid_resource_owner') 224 | end 225 | end 226 | 227 | context 'resource owner create error response' do 228 | let(:record) { double('record', errors: double('errors', full_messages: ['error message'])) } 229 | let(:error_response) { described_class.new(nil, error: :resource_owner_create_error, record: record) } 230 | 231 | it 'has a status of 422' do 232 | expect(error_response.status).to eq :unprocessable_entity 233 | end 234 | 235 | it 'has a body with an error and error description' do 236 | expect(error_response.body).to eq( 237 | error: :resource_owner_create_error, 238 | error_description: ['error message'] 239 | ) 240 | 241 | expect(record).to have_received(:errors) 242 | expect(record.errors).to have_received(:full_messages) 243 | end 244 | end 245 | 246 | context 'devise api token create error response' do 247 | let(:record) { double('record', errors: double('errors', full_messages: ['error message'])) } 248 | let(:error_response) { described_class.new(nil, error: :devise_api_token_create_error, record: record) } 249 | 250 | it 'has a status of 422' do 251 | expect(error_response.status).to eq :unprocessable_entity 252 | end 253 | 254 | it 'has a body with an error and error description' do 255 | expect(error_response.body).to eq( 256 | error: :devise_api_token_create_error, 257 | error_description: ['error message'] 258 | ) 259 | 260 | expect(record).to have_received(:errors) 261 | expect(record.errors).to have_received(:full_messages) 262 | end 263 | end 264 | 265 | context 'devise api token revoke error response' do 266 | let(:record) { double('record', errors: double('errors', full_messages: ['error message'])) } 267 | let(:error_response) { described_class.new(nil, error: :devise_api_token_revoke_error, record: record) } 268 | 269 | it 'has a status of 422' do 270 | expect(error_response.status).to eq :unprocessable_entity 271 | end 272 | 273 | it 'has a body with an error and error description' do 274 | expect(error_response.body).to eq( 275 | error: :devise_api_token_revoke_error, 276 | error_description: ['error message'] 277 | ) 278 | 279 | expect(record).to have_received(:errors) 280 | expect(record.errors).to have_received(:full_messages) 281 | end 282 | end 283 | 284 | context 'invalid authentication error response' do 285 | context 'normal' do 286 | let(:error_response) { described_class.new(nil, error: :invalid_authentication) } 287 | 288 | it 'has a status of 401' do 289 | expect(error_response.status).to eq :unauthorized 290 | end 291 | 292 | it 'has a body with an error and error description' do 293 | allow(I18n).to receive(:t) 294 | .with('devise.api.error_response.invalid_authentication') 295 | .and_return('Invalid authentication') 296 | 297 | expect(error_response.body).to eq( 298 | error: :invalid_authentication, 299 | error_description: ['Invalid authentication'] 300 | ) 301 | 302 | expect(I18n).to have_received(:t).with('devise.api.error_response.invalid_authentication') 303 | end 304 | end 305 | end 306 | 307 | context 'with lockable' do 308 | let(:record) { double('record', access_locked?: false, failed_attempts: 0, locked_at: nil) } 309 | let(:resource_class) { double('resource_class', supported_devise_modules: [:lockable]) } 310 | let(:error_response) do 311 | described_class.new(nil, error: :invalid_authentication, record: record, resource_class: resource_class) 312 | end 313 | 314 | it 'has a body with an error and error description' do 315 | allow(resource_class.supported_devise_modules).to receive(:lockable?).and_return(true) 316 | allow(resource_class.supported_devise_modules).to receive(:confirmable?).and_return(false) 317 | allow(record).to receive(:confirmed?).and_return(false) 318 | allow(record).to receive(:access_locked?).and_return(false) 319 | allow(I18n).to receive(:t) 320 | .with('devise.api.error_response.invalid_authentication') 321 | .and_return('Invalid authentication') 322 | 323 | expect(error_response.body).to eq( 324 | error: :invalid_authentication, 325 | error_description: ['Invalid authentication'], 326 | lockable: { 327 | locked: false, 328 | max_attempts: ::Devise.maximum_attempts, 329 | failed_attemps: 0 330 | } 331 | ) 332 | 333 | expect(I18n).to have_received(:t).with('devise.api.error_response.invalid_authentication') 334 | end 335 | end 336 | 337 | context 'with confirmable' do 338 | let(:record) { double('record', confirmed?: false, confirmation_sent_at: nil) } 339 | let(:resource_class) { double('resource_class', supported_devise_modules: [:confirmable]) } 340 | let(:error_response) do 341 | described_class.new(nil, error: :invalid_authentication, record: record, resource_class: resource_class) 342 | end 343 | 344 | it 'has a body with an error and error description' do 345 | allow(resource_class.supported_devise_modules).to receive(:lockable?).and_return(false) 346 | allow(resource_class.supported_devise_modules).to receive(:confirmable?).and_return(true) 347 | allow(record).to receive(:confirmed?).and_return(false) 348 | allow(record).to receive(:access_locked?).and_return(false) 349 | allow(I18n).to receive(:t) 350 | .with('devise.api.error_response.confirmable.unconfirmed') 351 | .and_return('Unconfirmed') 352 | 353 | expect(error_response.body).to eq( 354 | error: :invalid_authentication, 355 | error_description: ['Unconfirmed'], 356 | confirmable: { 357 | confirmed: false 358 | } 359 | ) 360 | 361 | expect(I18n).to have_received(:t).with('devise.api.error_response.confirmable.unconfirmed') 362 | end 363 | end 364 | end 365 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Assuming you have not yet modified this file, each configuration option below 4 | # is set to its default value. Note that some are commented out while others 5 | # are not: uncommented lines are intended to protect your configuration from 6 | # breaking changes in upgrades (i.e., in the event that future versions of 7 | # Devise change the default values for those options). 8 | # 9 | # Use this hook to configure devise mailer, warden hooks and so forth. 10 | # Many of these configuration options can be set straight in your model. 11 | Devise.setup do |config| 12 | # The secret key used by Devise. Devise uses this key to generate 13 | # random tokens. Changing this key will render invalid all existing 14 | # confirmation, reset password and unlock tokens in the database. 15 | # Devise will use the `secret_key_base` as its `secret_key` 16 | # by default. You can change it below and use your own secret key. 17 | # config.secret_key = 'secret_key' 18 | 19 | # ==> Controller configuration 20 | # Configure the parent class to the devise controllers. 21 | # config.parent_controller = 'DeviseController' 22 | 23 | # ==> Mailer Configuration 24 | # Configure the e-mail address which will be shown in Devise::Mailer, 25 | # note that it will be overwritten if you use your own mailer class 26 | # with default "from" parameter. 27 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' 28 | 29 | # Configure the class responsible to send e-mails. 30 | # config.mailer = 'Devise::Mailer' 31 | 32 | # Configure the parent class responsible to send e-mails. 33 | # config.parent_mailer = 'ActionMailer::Base' 34 | 35 | # ==> ORM configuration 36 | # Load and configure the ORM. Supports :active_record (default) and 37 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 38 | # available as additional gems. 39 | require 'devise/orm/active_record' 40 | 41 | # ==> Configuration for any authentication mechanism 42 | # Configure which keys are used when authenticating a user. The default is 43 | # just :email. You can configure it to use [:username, :subdomain], so for 44 | # authenticating a user, both parameters are required. Remember that those 45 | # parameters are used only when authenticating and not when retrieving from 46 | # session. If you need permissions, you should implement that in a before filter. 47 | # You can also supply a hash where the value is a boolean determining whether 48 | # or not authentication should be aborted when the value is not present. 49 | # config.authentication_keys = [:email] 50 | 51 | # Configure parameters from the request object used for authentication. Each entry 52 | # given should be a request method and it will automatically be passed to the 53 | # find_for_authentication method and considered in your model lookup. For instance, 54 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 55 | # The same considerations mentioned for authentication_keys also apply to request_keys. 56 | # config.request_keys = [] 57 | 58 | # Configure which authentication keys should be case-insensitive. 59 | # These keys will be downcased upon creating or modifying a user and when used 60 | # to authenticate or find a user. Default is :email. 61 | config.case_insensitive_keys = [:email] 62 | 63 | # Configure which authentication keys should have whitespace stripped. 64 | # These keys will have whitespace before and after removed upon creating or 65 | # modifying a user and when used to authenticate or find a user. Default is :email. 66 | config.strip_whitespace_keys = [:email] 67 | 68 | # Tell if authentication through request.params is enabled. True by default. 69 | # It can be set to an array that will enable params authentication only for the 70 | # given strategies, for example, `config.params_authenticatable = [:database]` will 71 | # enable it only for database (email + password) authentication. 72 | # config.params_authenticatable = true 73 | 74 | # Tell if authentication through HTTP Auth is enabled. False by default. 75 | # It can be set to an array that will enable http authentication only for the 76 | # given strategies, for example, `config.http_authenticatable = [:database]` will 77 | # enable it only for database authentication. 78 | # For API-only applications to support authentication "out-of-the-box", you will likely want to 79 | # enable this with :database unless you are using a custom strategy. 80 | # The supported strategies are: 81 | # :database = Support basic authentication with authentication key + password 82 | # config.http_authenticatable = false 83 | 84 | # If 401 status code should be returned for AJAX requests. True by default. 85 | # config.http_authenticatable_on_xhr = true 86 | 87 | # The realm used in Http Basic Authentication. 'Application' by default. 88 | # config.http_authentication_realm = 'Application' 89 | 90 | # It will change confirmation, password recovery and other workflows 91 | # to behave the same regardless if the e-mail provided was right or wrong. 92 | # Does not affect registerable. 93 | # config.paranoid = true 94 | 95 | # By default Devise will store the user in session. You can skip storage for 96 | # particular strategies by setting this option. 97 | # Notice that if you are skipping storage for all authentication paths, you 98 | # may want to disable generating routes to Devise's sessions controller by 99 | # passing skip: :sessions to `devise_for` in your config/routes.rb 100 | config.skip_session_storage = [:http_auth] 101 | 102 | # By default, Devise cleans up the CSRF token on authentication to 103 | # avoid CSRF token fixation attacks. This means that, when using AJAX 104 | # requests for sign in and sign up, you need to get a new CSRF token 105 | # from the server. You can disable this option at your own risk. 106 | # config.clean_up_csrf_token_on_authentication = true 107 | 108 | # When false, Devise will not attempt to reload routes on eager load. 109 | # This can reduce the time taken to boot the app but if your application 110 | # requires the Devise mappings to be loaded during boot time the application 111 | # won't boot properly. 112 | # config.reload_routes = true 113 | 114 | # ==> Configuration for :database_authenticatable 115 | # For bcrypt, this is the cost for hashing the password and defaults to 12. If 116 | # using other algorithms, it sets how many times you want the password to be hashed. 117 | # The number of stretches used for generating the hashed password are stored 118 | # with the hashed password. This allows you to change the stretches without 119 | # invalidating existing passwords. 120 | # 121 | # Limiting the stretches to just one in testing will increase the performance of 122 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 123 | # a value less than 10 in other environments. Note that, for bcrypt (the default 124 | # algorithm), the cost increases exponentially with the number of stretches (e.g. 125 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 126 | config.stretches = Rails.env.test? ? 1 : 12 127 | 128 | # Set up a pepper to generate the hashed password. 129 | # config.pepper = 'pepper_token' 130 | 131 | # Send a notification to the original email when the user's email is changed. 132 | # config.send_email_changed_notification = false 133 | 134 | # Send a notification email when the user's password is changed. 135 | # config.send_password_change_notification = false 136 | 137 | # ==> Configuration for :confirmable 138 | # A period that the user is allowed to access the website even without 139 | # confirming their account. For instance, if set to 2.days, the user will be 140 | # able to access the website for two days without confirming their account, 141 | # access will be blocked just in the third day. 142 | # You can also set it to nil, which will allow the user to access the website 143 | # without confirming their account. 144 | # Default is 0.days, meaning the user cannot access the website without 145 | # confirming their account. 146 | # config.allow_unconfirmed_access_for = 2.days 147 | 148 | # A period that the user is allowed to confirm their account before their 149 | # token becomes invalid. For example, if set to 3.days, the user can confirm 150 | # their account within 3 days after the mail was sent, but on the fourth day 151 | # their account can't be confirmed with the token any more. 152 | # Default is nil, meaning there is no restriction on how long a user can take 153 | # before confirming their account. 154 | # config.confirm_within = 3.days 155 | 156 | # If true, requires any email changes to be confirmed (exactly the same way as 157 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 158 | # db field (see migrations). Until confirmed, new email is stored in 159 | # unconfirmed_email column, and copied to email column on successful confirmation. 160 | config.reconfirmable = true 161 | 162 | # Defines which key will be used when confirming an account 163 | # config.confirmation_keys = [:email] 164 | 165 | # ==> Configuration for :rememberable 166 | # The time the user will be remembered without asking for credentials again. 167 | # config.remember_for = 2.weeks 168 | 169 | # Invalidates all the remember me tokens when the user signs out. 170 | config.expire_all_remember_me_on_sign_out = true 171 | 172 | # If true, extends the user's remember period when remembered via cookie. 173 | # config.extend_remember_period = false 174 | 175 | # Options to be passed to the created cookie. For instance, you can set 176 | # secure: true in order to force SSL only cookies. 177 | # config.rememberable_options = {} 178 | 179 | # ==> Configuration for :validatable 180 | # Range for password length. 181 | config.password_length = 6..128 182 | 183 | # Email regex used to validate email formats. It simply asserts that 184 | # one (and only one) @ exists in the given string. This is mainly 185 | # to give user feedback and not to assert the e-mail validity. 186 | config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ 187 | 188 | # ==> Configuration for :timeoutable 189 | # The time you want to timeout the user session without activity. After this 190 | # time the user will be asked for credentials again. Default is 30 minutes. 191 | # config.timeout_in = 30.minutes 192 | 193 | # ==> Configuration for :lockable 194 | # Defines which strategy will be used to lock an account. 195 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 196 | # :none = No lock strategy. You should handle locking by yourself. 197 | # config.lock_strategy = :failed_attempts 198 | 199 | # Defines which key will be used when locking and unlocking an account 200 | # config.unlock_keys = [:email] 201 | 202 | # Defines which strategy will be used to unlock an account. 203 | # :email = Sends an unlock link to the user email 204 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 205 | # :both = Enables both strategies 206 | # :none = No unlock strategy. You should handle unlocking by yourself. 207 | # config.unlock_strategy = :both 208 | 209 | # Number of authentication tries before locking an account if lock_strategy 210 | # is failed attempts. 211 | # config.maximum_attempts = 20 212 | 213 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 214 | # config.unlock_in = 1.hour 215 | 216 | # Warn on the last attempt before the account is locked. 217 | # config.last_attempt_warning = true 218 | 219 | # ==> Configuration for :recoverable 220 | # 221 | # Defines which key will be used when recovering the password for an account 222 | # config.reset_password_keys = [:email] 223 | 224 | # Time interval you can reset your password with a reset password key. 225 | # Don't put a too small interval or your users won't have the time to 226 | # change their passwords. 227 | config.reset_password_within = 6.hours 228 | 229 | # When set to false, does not sign a user in automatically after their password is 230 | # reset. Defaults to true, so a user is signed in automatically after a reset. 231 | # config.sign_in_after_reset_password = true 232 | 233 | # ==> Configuration for :encryptable 234 | # Allow you to use another hashing or encryption algorithm besides bcrypt (default). 235 | # You can use :sha1, :sha512 or algorithms from others authentication tools as 236 | # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 237 | # for default behavior) and :restful_authentication_sha1 (then you should set 238 | # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). 239 | # 240 | # Require the `devise-encryptable` gem when using anything other than bcrypt 241 | # config.encryptor = :sha512 242 | 243 | # ==> Scopes configuration 244 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 245 | # "users/sessions/new". It's turned off by default because it's slower if you 246 | # are using only default views. 247 | # config.scoped_views = false 248 | 249 | # Configure the default scope given to Warden. By default it's the first 250 | # devise role declared in your routes (usually :user). 251 | # config.default_scope = :user 252 | 253 | # Set this configuration to false if you want /users/sign_out to sign out 254 | # only the current scope. By default, Devise signs out all scopes. 255 | # config.sign_out_all_scopes = true 256 | 257 | # ==> Navigation configuration 258 | # Lists the formats that should be treated as navigational. Formats like 259 | # :html, should redirect to the sign in page when the user does not have 260 | # access, but formats like :xml or :json, should return 401. 261 | # 262 | # If you have any extra navigational formats, like :iphone or :mobile, you 263 | # should add them to the navigational formats lists. 264 | # 265 | # The "*/*" below is required to match Internet Explorer requests. 266 | # config.navigational_formats = ['*/*', :html] 267 | 268 | # The default HTTP method used to sign out a resource. Default is :delete. 269 | config.sign_out_via = :delete 270 | 271 | # ==> OmniAuth 272 | # Add a new OmniAuth provider. Check the wiki for more information on setting 273 | # up on your models and hooks. 274 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' 275 | 276 | # ==> Warden configuration 277 | # If you want to use other strategies, that are not supported by Devise, or 278 | # change the failure app, you can configure them inside the config.warden block. 279 | # 280 | # config.warden do |manager| 281 | # manager.intercept_401 = false 282 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 283 | # end 284 | 285 | # ==> Mountable engine configurations 286 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 287 | # is mountable, there are some extra configurations to be taken into account. 288 | # The following options are available, assuming the engine is mounted as: 289 | # 290 | # mount MyEngine, at: '/my_engine' 291 | # 292 | # The router that invoked `devise_for`, in the example above, would be: 293 | # config.router_name = :my_engine 294 | # 295 | # When using OmniAuth, Devise cannot automatically set OmniAuth path, 296 | # so you need to do it manually. For the users scope, it would be: 297 | # config.omniauth_path_prefix = '/my_engine/users/auth' 298 | 299 | # ==> Turbolinks configuration 300 | # If your app is using Turbolinks, Turbolinks::Controller needs to be included to make redirection work correctly: 301 | # 302 | # ActiveSupport.on_load(:devise_failure_app) do 303 | # include Turbolinks::Controller 304 | # end 305 | 306 | # ==> Configuration for :registerable 307 | 308 | # When set to false, does not sign a user in automatically after their password is 309 | # changed. Defaults to true, so a user is signed in automatically after changing a password. 310 | # config.sign_in_after_change_password = true 311 | end 312 | -------------------------------------------------------------------------------- /spec/requests/tokens_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Devise::Api::TokensController, type: :request do 6 | describe 'POST /users/tokens/sign_up' do 7 | context 'when the user is valid' do 8 | let(:params) { attributes_for(:user) } 9 | 10 | before do 11 | allow(Devise.api.config.after_successful_sign_up).to receive(:call).and_call_original 12 | allow(Devise.api.config.before_sign_up).to receive(:call).and_call_original 13 | 14 | post sign_up_user_tokens_path, params: params, as: :json 15 | end 16 | 17 | it 'returns http success' do 18 | expect(response).to have_http_status(:created) 19 | end 20 | 21 | it 'returns a token' do 22 | expect(parsed_body.token).to be_present 23 | expect(parsed_body.refresh_token).to be_present 24 | expect(parsed_body.expires_in).to eq(1.hour.to_i) 25 | expect(parsed_body.token_type).to eq('Bearer') 26 | expect(parsed_body.resource_owner).to be_present 27 | expect(parsed_body.resource_owner.id).to eq(User.last.id) 28 | expect(parsed_body.resource_owner.email).to eq(params[:email]) 29 | expect(parsed_body.resource_owner.created_at).to be_present 30 | expect(parsed_body.resource_owner.updated_at).to be_present 31 | end 32 | 33 | it 'creates a user' do 34 | expect(User.count).to eq(1) 35 | expect(User.last.email).to eq(params[:email]) 36 | end 37 | 38 | it 'creates a token' do 39 | expect(Devise::Api::Token.count).to eq(1) 40 | expect(Devise::Api::Token.last.access_token).to eq(parsed_body.token) 41 | expect(Devise::Api::Token.last.refresh_token).to eq(parsed_body.refresh_token) 42 | expect(Devise::Api::Token.last.expires_in).to eq 1.hour.to_i 43 | expect(Devise::Api::Token.last.resource_owner_id).to eq(User.last.id) 44 | expect(Devise::Api::Token.last.resource_owner_type).to eq('User') 45 | end 46 | 47 | it 'trackable is incremented' do 48 | expect(User.last.sign_in_count).to eq(1) 49 | expect(User.last.current_sign_in_at).to be_present 50 | expect(User.last.last_sign_in_at).to be_present 51 | expect(User.last.current_sign_in_ip).to be_present 52 | expect(User.last.last_sign_in_ip).to be_present 53 | end 54 | 55 | it 'calls the after_successful_sign_up and before_sign_up callbacks' do 56 | expect(Devise.api.config.after_successful_sign_up).to have_received(:call).once 57 | expect(Devise.api.config.before_sign_up).to have_received(:call).once 58 | end 59 | end 60 | 61 | context 'when the email is already taken' do 62 | let(:user) { create(:user) } 63 | let(:params) { attributes_for(:user, email: user.email) } 64 | 65 | before do 66 | allow(Devise.api.config.before_sign_up).to receive(:call).and_call_original 67 | 68 | post sign_up_user_tokens_path, params: params, as: :json 69 | end 70 | 71 | it 'returns http unprocessable entity' do 72 | expect(response).to have_http_status(:unprocessable_entity) 73 | end 74 | 75 | it 'returns an error' do 76 | expect(parsed_body.error).to eq 'resource_owner_create_error' 77 | expect(parsed_body.error_description).to include('Email has already been taken') 78 | end 79 | 80 | it 'does not create a user' do 81 | expect(User.count).to eq(1) 82 | end 83 | 84 | it 'does not create a token' do 85 | expect(Devise::Api::Token.count).to eq(0) 86 | end 87 | 88 | it 'calls the before_sign_up callback only' do 89 | expect(Devise.api.config.before_sign_up).to have_received(:call).once 90 | end 91 | end 92 | end 93 | 94 | describe 'POST /users/tokens/sign_in' do 95 | context 'when the user confirmed' do 96 | let(:user) { create(:user, password: 'pass123456') } 97 | let(:params) { { email: user.email, password: 'pass123456' } } 98 | 99 | before do 100 | user.confirm 101 | 102 | allow(Devise.api.config.after_successful_sign_in).to receive(:call).and_call_original 103 | allow(Devise.api.config.before_sign_in).to receive(:call).and_call_original 104 | 105 | post sign_in_user_tokens_path, params: params, as: :json 106 | end 107 | 108 | it 'returns http success' do 109 | expect(response).to have_http_status(:success) 110 | end 111 | 112 | it 'returns a token' do 113 | expect(parsed_body.token).to be_present 114 | expect(parsed_body.refresh_token).to be_present 115 | expect(parsed_body.expires_in).to eq(1.hour.to_i) 116 | expect(parsed_body.token_type).to eq('Bearer') 117 | expect(parsed_body.resource_owner).to be_present 118 | expect(parsed_body.resource_owner.id).to eq(user.id) 119 | expect(parsed_body.resource_owner.email).to eq(user.email) 120 | expect(parsed_body.resource_owner.created_at.to_date).to eq user.created_at.to_date 121 | expect(parsed_body.resource_owner.updated_at.to_date).to eq user.updated_at.to_date 122 | end 123 | 124 | it 'trackable is incremented' do 125 | expect(User.last.sign_in_count).to eq(1) 126 | expect(User.last.current_sign_in_at).to be_present 127 | expect(User.last.last_sign_in_at).to be_present 128 | expect(User.last.current_sign_in_ip).to be_present 129 | expect(User.last.last_sign_in_ip).to be_present 130 | end 131 | 132 | it 'calls the after_successful_sign_in and before_sign_in callbacks' do 133 | expect(Devise.api.config.after_successful_sign_in).to have_received(:call).once 134 | expect(Devise.api.config.before_sign_in).to have_received(:call).once 135 | end 136 | end 137 | 138 | context 'when the user is not confirmed' do 139 | let(:user) { create(:user, password: 'pass123456') } 140 | let(:params) { { email: user.email, password: 'pass123456' } } 141 | 142 | before do 143 | allow(Devise.api.config.before_sign_in).to receive(:call).and_call_original 144 | 145 | post sign_in_user_tokens_path, params: params, as: :json 146 | end 147 | 148 | it 'returns http unauthorized' do 149 | expect(response).to have_http_status(:unauthorized) 150 | end 151 | 152 | it 'returns an error response' do 153 | expect(parsed_body.error).to eq 'invalid_authentication' 154 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.confirmable.unconfirmed')]) 155 | expect(parsed_body.confirmable).to be_present 156 | expect(parsed_body.confirmable.confirmed).to eq false 157 | expect(parsed_body.confirmable.confirmation_sent_at.to_date).to eq user.confirmation_sent_at.to_date 158 | end 159 | 160 | it 'does not create a token' do 161 | expect(Devise::Api::Token.count).to eq(0) 162 | end 163 | 164 | it 'calls the before_sign_in callback only' do 165 | expect(Devise.api.config.before_sign_in).to have_received(:call).once 166 | end 167 | end 168 | 169 | context 'when the user is locked' do 170 | let(:user) { create(:user, password: 'pass123456') } 171 | let(:params) { { email: user.email, password: 'pass123456' } } 172 | 173 | before do 174 | user.lock_access! 175 | 176 | allow(Devise.api.config.before_sign_in).to receive(:call).and_call_original 177 | 178 | post sign_in_user_tokens_path, params: params, as: :json 179 | end 180 | 181 | it 'returns http unauthorized' do 182 | expect(response).to have_http_status(:unauthorized) 183 | end 184 | 185 | it 'returns an error response' do 186 | expect(parsed_body.error).to eq 'invalid_authentication' 187 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.lockable.locked')]) 188 | expect(parsed_body.lockable).to be_present 189 | expect(parsed_body.lockable.locked).to eq true 190 | expect(parsed_body.lockable.max_attempts).to eq Devise.maximum_attempts 191 | expect(parsed_body.lockable.failed_attemps).to be_present 192 | expect(parsed_body.lockable.locked_at.to_date).to eq user.locked_at.to_date 193 | end 194 | 195 | it 'does not create a token' do 196 | expect(Devise::Api::Token.count).to eq(0) 197 | end 198 | 199 | it 'calls the before_sign_in callback only' do 200 | expect(Devise.api.config.before_sign_in).to have_received(:call).once 201 | end 202 | end 203 | 204 | context 'when the user is not found' do 205 | let(:params) { { email: 'invalid@mail.com', password: 'pass123456' } } 206 | 207 | before do 208 | allow(Devise.api.config.before_sign_in).to receive(:call).and_call_original 209 | 210 | post sign_in_user_tokens_path, params: params, as: :json 211 | end 212 | 213 | it 'returns http bad request' do 214 | expect(response).to have_http_status(:bad_request) 215 | end 216 | 217 | it 'returns an error response' do 218 | expect(parsed_body.error).to eq 'invalid_email' 219 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.invalid_email')]) 220 | end 221 | 222 | it 'does not create a token' do 223 | expect(Devise::Api::Token.count).to eq(0) 224 | end 225 | 226 | it 'calls the before_sign_in callback only' do 227 | expect(Devise.api.config.before_sign_in).to have_received(:call).once 228 | end 229 | end 230 | 231 | context 'when the password is invalid' do 232 | let(:user) { create(:user, password: 'pass123456') } 233 | let(:params) { { email: user.email, password: 'invalid' } } 234 | 235 | before do 236 | user.confirm 237 | 238 | allow(Devise.api.config.before_sign_in).to receive(:call).and_call_original 239 | 240 | post sign_in_user_tokens_path, params: params, as: :json 241 | end 242 | 243 | it 'returns http unauthorized' do 244 | expect(response).to have_http_status(:unauthorized) 245 | end 246 | 247 | it 'returns an error response' do 248 | expect(parsed_body.error).to eq 'invalid_authentication' 249 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.invalid_authentication')]) 250 | expect(parsed_body.confirmable).to be_present 251 | expect(parsed_body.lockable).to be_present 252 | expect(parsed_body.lockable.locked).to eq false 253 | expect(parsed_body.lockable.max_attempts).to eq Devise.maximum_attempts 254 | expect(parsed_body.lockable.failed_attemps).to eq 1 255 | end 256 | 257 | it 'lockable is incremented' do 258 | expect(User.last.failed_attempts).to eq(1) 259 | expect(User.last.locked_at).to be_nil 260 | end 261 | 262 | it 'does not create a token' do 263 | expect(Devise::Api::Token.count).to eq(0) 264 | end 265 | 266 | it 'calls the before_sign_in callback only' do 267 | expect(Devise.api.config.before_sign_in).to have_received(:call).once 268 | end 269 | end 270 | end 271 | 272 | describe 'GET /users/tokens/info' do 273 | context 'when the token is valid and on the header' do 274 | let(:user) { create(:user) } 275 | let(:devise_api_token) { create(:devise_api_token, resource_owner: user) } 276 | 277 | before do 278 | get info_user_tokens_path, headers: authentication_headers_for(user, devise_api_token), as: :json 279 | end 280 | 281 | it 'returns http success' do 282 | expect(response).to have_http_status(:success) 283 | end 284 | 285 | it 'returns the authenticated resource owner' do 286 | expect(parsed_body.id).to eq(user.id) 287 | expect(parsed_body.email).to eq(user.email) 288 | expect(parsed_body.created_at.to_date).to eq user.created_at.to_date 289 | expect(parsed_body.updated_at.to_date).to eq user.updated_at.to_date 290 | end 291 | end 292 | 293 | context 'when the token is valid and on the url param' do 294 | let(:user) { create(:user) } 295 | let(:devise_api_token) { create(:devise_api_token, resource_owner: user) } 296 | 297 | before do 298 | get info_user_tokens_path(access_token: devise_api_token.access_token), as: :json 299 | end 300 | 301 | it 'returns http success' do 302 | expect(response).to have_http_status(:success) 303 | end 304 | 305 | it 'returns the authenticated resource owner' do 306 | expect(parsed_body.id).to eq(user.id) 307 | expect(parsed_body.email).to eq(user.email) 308 | expect(parsed_body.created_at.to_date).to eq user.created_at.to_date 309 | expect(parsed_body.updated_at.to_date).to eq user.updated_at.to_date 310 | end 311 | end 312 | 313 | context 'when the token is invalid and on the header' do 314 | let(:user) { create(:user) } 315 | let(:devise_api_token) { build(:devise_api_token, resource_owner: user) } 316 | 317 | before do 318 | get info_user_tokens_path, headers: authentication_headers_for(user, devise_api_token), as: :json 319 | end 320 | 321 | it 'returns http unauthorized' do 322 | expect(response).to have_http_status(:unauthorized) 323 | end 324 | 325 | it 'returns an error response' do 326 | expect(parsed_body.error).to eq 'invalid_token' 327 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.invalid_token')]) 328 | end 329 | 330 | it 'does not return the authenticated resource owner' do 331 | expect(parsed_body.id).to be_nil 332 | expect(parsed_body.email).to be_nil 333 | expect(parsed_body.created_at).to be_nil 334 | expect(parsed_body.updated_at).to be_nil 335 | end 336 | end 337 | 338 | context 'when the token is invalid and on the url param' do 339 | before do 340 | get info_user_tokens_path(access_token: 'invalid'), as: :json 341 | end 342 | 343 | it 'returns http unauthorized' do 344 | expect(response).to have_http_status(:unauthorized) 345 | end 346 | 347 | it 'returns an error response' do 348 | expect(parsed_body.error).to eq 'invalid_token' 349 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.invalid_token')]) 350 | end 351 | 352 | it 'does not return the authenticated resource owner' do 353 | expect(parsed_body.id).to be_nil 354 | expect(parsed_body.email).to be_nil 355 | expect(parsed_body.created_at).to be_nil 356 | expect(parsed_body.updated_at).to be_nil 357 | end 358 | end 359 | 360 | context 'when the token is expired' do 361 | let(:user) { create(:user) } 362 | let(:devise_api_token) { create(:devise_api_token, :access_token_expired, resource_owner: user) } 363 | 364 | before do 365 | get info_user_tokens_path, headers: authentication_headers_for(user, devise_api_token), as: :json 366 | end 367 | 368 | it 'returns http unauthorized' do 369 | expect(response).to have_http_status(:unauthorized) 370 | end 371 | 372 | it 'returns an error response' do 373 | expect(parsed_body.error).to eq 'expired_token' 374 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.expired_token')]) 375 | end 376 | 377 | it 'does not return the authenticated resource owner' do 378 | expect(parsed_body.id).to be_nil 379 | expect(parsed_body.email).to be_nil 380 | expect(parsed_body.created_at).to be_nil 381 | expect(parsed_body.updated_at).to be_nil 382 | end 383 | end 384 | 385 | context 'when the token is revoked' do 386 | let(:user) { create(:user) } 387 | let(:devise_api_token) { create(:devise_api_token, :revoked, resource_owner: user) } 388 | 389 | before do 390 | get info_user_tokens_path, headers: authentication_headers_for(user, devise_api_token), as: :json 391 | end 392 | 393 | it 'returns http unauthorized' do 394 | expect(response).to have_http_status(:unauthorized) 395 | end 396 | 397 | it 'returns an error response' do 398 | expect(parsed_body.error).to eq 'revoked_token' 399 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.revoked_token')]) 400 | end 401 | 402 | it 'does not return the authenticated resource owner' do 403 | expect(parsed_body.id).to be_nil 404 | expect(parsed_body.email).to be_nil 405 | expect(parsed_body.created_at).to be_nil 406 | expect(parsed_body.updated_at).to be_nil 407 | end 408 | end 409 | end 410 | 411 | describe 'POST /users/tokens/refresh' do 412 | context 'when the refresh token is valid and on the header' do 413 | let(:user) { create(:user) } 414 | let(:devise_api_token) { create(:devise_api_token, resource_owner: user) } 415 | 416 | before do 417 | allow(Devise.api.config.before_refresh).to receive(:call).and_call_original 418 | allow(Devise.api.config.after_successful_refresh).to receive(:call).and_call_original 419 | 420 | post refresh_user_tokens_path, headers: authentication_headers_for(user, devise_api_token, :refresh_token), 421 | as: :json 422 | end 423 | 424 | it 'returns http success' do 425 | expect(response).to have_http_status(:success) 426 | end 427 | 428 | it 'returns the new token' do 429 | expect(parsed_body.token).to be_present 430 | expect(parsed_body.refresh_token).to be_present 431 | expect(parsed_body.expires_in).to eq(1.hour.to_i) 432 | expect(parsed_body.token_type).to eq('Bearer') 433 | expect(parsed_body.resource_owner).to be_present 434 | expect(parsed_body.resource_owner.id).to eq(User.last.id) 435 | expect(parsed_body.resource_owner.email).to eq(user.email) 436 | expect(parsed_body.resource_owner.created_at).to be_present 437 | expect(parsed_body.resource_owner.updated_at).to be_present 438 | end 439 | 440 | it 'creates a new token' do 441 | expect(devise_api_token.refreshes.count).to eq(1) 442 | end 443 | 444 | it 'calls the before_refresh_token and after_successful_refresh callbacks' do 445 | expect(Devise.api.config.before_refresh).to have_received(:call).once 446 | expect(Devise.api.config.after_successful_refresh).to have_received(:call).once 447 | end 448 | end 449 | 450 | context 'when the refresh token is valid and on the url param' do 451 | let(:user) { create(:user) } 452 | let(:devise_api_token) { create(:devise_api_token, resource_owner: user) } 453 | 454 | before do 455 | allow(Devise.api.config.before_refresh).to receive(:call).and_call_original 456 | allow(Devise.api.config.after_successful_refresh).to receive(:call).and_call_original 457 | 458 | post refresh_user_tokens_path(access_token: devise_api_token.refresh_token), as: :json 459 | end 460 | 461 | it 'returns http success' do 462 | expect(response).to have_http_status(:success) 463 | end 464 | 465 | it 'returns the new token' do 466 | expect(parsed_body.token).to be_present 467 | expect(parsed_body.refresh_token).to be_present 468 | expect(parsed_body.expires_in).to eq(1.hour.to_i) 469 | expect(parsed_body.token_type).to eq('Bearer') 470 | expect(parsed_body.resource_owner).to be_present 471 | expect(parsed_body.resource_owner.id).to eq(User.last.id) 472 | expect(parsed_body.resource_owner.email).to eq(user.email) 473 | expect(parsed_body.resource_owner.created_at).to be_present 474 | expect(parsed_body.resource_owner.updated_at).to be_present 475 | end 476 | 477 | it 'creates a new token' do 478 | expect(devise_api_token.refreshes.count).to eq(1) 479 | end 480 | 481 | it 'calls the before_refresh_token and after_successful_refresh callbacks' do 482 | expect(Devise.api.config.before_refresh).to have_received(:call).once 483 | expect(Devise.api.config.after_successful_refresh).to have_received(:call).once 484 | end 485 | end 486 | 487 | context 'when the refresh token is invalid and on the header' do 488 | let(:user) { create(:user) } 489 | let(:devise_api_token) { build(:devise_api_token, resource_owner: user) } 490 | 491 | before do 492 | post refresh_user_tokens_path, headers: authentication_headers_for(user, devise_api_token, :refresh_token), 493 | as: :json 494 | end 495 | 496 | it 'returns http unauthorized' do 497 | expect(response).to have_http_status(:unauthorized) 498 | end 499 | 500 | it 'returns an error response' do 501 | expect(parsed_body.error).to eq 'invalid_token' 502 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.invalid_token')]) 503 | end 504 | 505 | it 'does not refresh the token' do 506 | expect(devise_api_token.refreshes.count).to eq(0) 507 | end 508 | end 509 | 510 | context 'when the refresh token is invalid and on the url param' do 511 | let(:user) { create(:user) } 512 | let(:devise_api_token) { build(:devise_api_token, resource_owner: user) } 513 | 514 | before do 515 | post refresh_user_tokens_path(access_token: devise_api_token.refresh_token), as: :json 516 | end 517 | 518 | it 'returns http unauthorized' do 519 | expect(response).to have_http_status(:unauthorized) 520 | end 521 | 522 | it 'returns an error response' do 523 | expect(parsed_body.error).to eq 'invalid_token' 524 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.invalid_token')]) 525 | end 526 | 527 | it 'does not refresh the token' do 528 | expect(devise_api_token.refreshes.count).to eq(0) 529 | end 530 | end 531 | 532 | context 'when the devise api token is expired' do 533 | let(:user) { create(:user) } 534 | let(:devise_api_token) { create(:devise_api_token, :refresh_token_expired, resource_owner: user) } 535 | 536 | before do 537 | post refresh_user_tokens_path, headers: authentication_headers_for(user, devise_api_token, :refresh_token), 538 | as: :json 539 | end 540 | 541 | it 'returns http unauthorized' do 542 | expect(response).to have_http_status(:unauthorized) 543 | end 544 | 545 | it 'returns an error response' do 546 | expect(parsed_body.error).to eq 'expired_refresh_token' 547 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.expired_refresh_token')]) 548 | end 549 | 550 | it 'does not refresh the token' do 551 | expect(devise_api_token.refreshes.count).to eq(0) 552 | end 553 | end 554 | 555 | context 'when the devise api token is revoked' do 556 | let(:user) { create(:user) } 557 | let(:devise_api_token) { create(:devise_api_token, :revoked, resource_owner: user) } 558 | 559 | before do 560 | post refresh_user_tokens_path, headers: authentication_headers_for(user, devise_api_token, :refresh_token), 561 | as: :json 562 | end 563 | 564 | it 'returns http unauthorized' do 565 | expect(response).to have_http_status(:unauthorized) 566 | end 567 | 568 | it 'returns an error response' do 569 | expect(parsed_body.error).to eq 'revoked_token' 570 | expect(parsed_body.error_description).to eq([I18n.t('devise.api.error_response.revoked_token')]) 571 | end 572 | 573 | it 'does not refresh the token' do 574 | expect(devise_api_token.refreshes.count).to eq(0) 575 | end 576 | end 577 | end 578 | 579 | describe 'POST /users/tokens/revoke' do 580 | context 'when the access token is valid and on the header' do 581 | let(:user) { create(:user) } 582 | let(:devise_api_token) { create(:devise_api_token, resource_owner: user) } 583 | 584 | before do 585 | allow(Devise.api.config.before_revoke).to receive(:call).and_call_original 586 | allow(Devise.api.config.after_successful_revoke).to receive(:call).and_call_original 587 | 588 | post revoke_user_tokens_path, headers: authentication_headers_for(user, devise_api_token), as: :json 589 | end 590 | 591 | it 'returns http no content' do 592 | expect(response).to have_http_status(:no_content) 593 | end 594 | 595 | it 'returns an empty response' do 596 | expect(response.body).to be_empty 597 | end 598 | 599 | it 'revokes the token' do 600 | expect(devise_api_token.reload.revoked?).to be_truthy 601 | end 602 | 603 | it 'calls the before_revoke_token and after_successful_revoke callbacks' do 604 | expect(Devise.api.config.before_revoke).to have_received(:call).once 605 | expect(Devise.api.config.after_successful_revoke).to have_received(:call).once 606 | end 607 | end 608 | 609 | context 'when the access token is valid and on the url param' do 610 | let(:user) { create(:user) } 611 | let(:devise_api_token) { create(:devise_api_token, resource_owner: user) } 612 | 613 | before do 614 | post revoke_user_tokens_path(access_token: devise_api_token.access_token), as: :json 615 | end 616 | 617 | it 'returns http no content' do 618 | expect(response).to have_http_status(:no_content) 619 | end 620 | 621 | it 'returns an empty response' do 622 | expect(response.body).to be_empty 623 | end 624 | 625 | it 'revokes the token' do 626 | expect(devise_api_token.reload.revoked?).to be_truthy 627 | end 628 | end 629 | 630 | context 'when the access token is invalid and on the header' do 631 | let(:user) { create(:user) } 632 | let(:devise_api_token) { build(:devise_api_token, resource_owner: user) } 633 | 634 | before do 635 | post revoke_user_tokens_path, headers: authentication_headers_for(user, devise_api_token), as: :json 636 | end 637 | 638 | it 'returns http no content' do 639 | expect(response).to have_http_status(:no_content) 640 | end 641 | 642 | it 'returns an empty response' do 643 | expect(response.body).to be_empty 644 | end 645 | end 646 | 647 | context 'when the access token is invalid and on the url param' do 648 | let(:user) { create(:user) } 649 | let(:devise_api_token) { build(:devise_api_token, resource_owner: user) } 650 | 651 | before do 652 | post revoke_user_tokens_path(access_token: devise_api_token.access_token), as: :json 653 | end 654 | 655 | it 'returns http no content' do 656 | expect(response).to have_http_status(:no_content) 657 | end 658 | 659 | it 'returns an empty response' do 660 | expect(response.body).to be_empty 661 | end 662 | end 663 | 664 | context 'when the access token is expired' do 665 | let(:user) { create(:user) } 666 | let(:devise_api_token) { create(:devise_api_token, :access_token_expired, resource_owner: user) } 667 | 668 | before do 669 | post revoke_user_tokens_path, headers: authentication_headers_for(user, devise_api_token), as: :json 670 | end 671 | 672 | it 'returns http no content' do 673 | expect(response).to have_http_status(:no_content) 674 | end 675 | 676 | it 'returns an empty response' do 677 | expect(response.body).to be_empty 678 | end 679 | end 680 | 681 | context 'when the access token is revoked' do 682 | let(:user) { create(:user) } 683 | let(:devise_api_token) { create(:devise_api_token, :revoked, resource_owner: user) } 684 | 685 | before do 686 | post revoke_user_tokens_path, headers: authentication_headers_for(user, devise_api_token), as: :json 687 | end 688 | 689 | it 'returns http no content' do 690 | expect(response).to have_http_status(:no_content) 691 | end 692 | 693 | it 'returns an empty response' do 694 | expect(response.body).to be_empty 695 | end 696 | end 697 | end 698 | end 699 | --------------------------------------------------------------------------------