├── 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 |
10 | <% resource.errors.full_messages.each do |message| %>
11 | - <%= message %>
12 | <% end %>
13 |
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 | [](https://badge.fury.io/rb/devise-api)
2 | 
3 | 
4 | [](https://github.com/rubocop/rubocop)
5 | 
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 |
--------------------------------------------------------------------------------