doorkeeper's spec with token through http header
My goal is creating secure API between iOS and Rails4. So I've been trying doorkeeper-gem for a while. But I'm wasting time for testing and configuration now. In detail, the problem is doorkeeper_for method and token transferring through HTTP header. How using http request parameter was successful, but sending token through parameter is not good. So I want to send token with HTTP header, but doorkeeper does not see where request.header["My_TOKEN_PLACE"].
Circumstance
Now, I have api_controller.rb, communities_controller.rb, communities_controller_spec.rb, doorkeeper.rb.
api_controller.rb
class ApiController < ApplicationController
before_action :create_token
doorkeeper_for :all, scopes: [:app]
respond_to :json, handler: :jbuilder
private
def error_handle
raise 'Failed.'
end
def create_token
params[:access_token] = request.headers["HTTP_AUTHENTICATION"]
# this does not read by doorkeeper
end
end
and communities_controller.rb
class CommunitiesController < ApiController
def show
#community = Community.find params[:id]
end
def search
Query.create q: #search_form.q if #search_form.q.present?
community_search = Community.search title_or_description_cont: #search_form.q
#communities = community_search.result(distinct: true).page params[:page]
end
end
and communities_controller_spec.rb
require 'spec_helper'
describe CommunitiesController do
let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "http://app.com") }
let(:user){ create :user }
let!(:access_token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "app" }
before(:each) do
request.env["HTTP_ACCEPT"] = 'application/json'
end
describe "#show" do
let(:community) { create :community }
before do
request.headers["HTTP_AUTHENTICATION"] = access_token.token
get :show, id: community
end
it { expect(response).to be_success }
it { expect(response.status).to be 200 }
end
describe "#search" do
before { get :search, access_token: access_token.token }
it { expect(response).to be_success }
it { expect(response.status).to be 200 }
end
end
and config/initializer/doorkeeper.rb
Doorkeeper.configure do
orm :active_record
resource_owner_authenticator do
User.find id: session[:user_id]
default_scopes :app
end
and result of rspec communities_controller.rb is here.
/Users/shogochiai/Documents/anyll% be rspec spec/controllers/communities_controller_spec.rb
FF..
Failures:
1) CommunitiesController#show should be success
Failure/Error: it { expect(response).to be_success }
expected success? to return true, got false
# ./spec/controllers/communities_controller_spec.rb:20:in `block (3 levels) in <top(required)>'
2) CommunitiesController#show should equal 200
Failure/Error: it { expect(response.status).to be 200 }
expected #<Fixnum:401> => 200
got #<Fixnum:803> => 401
Compared using equal?, which compares object identity,
but expected and actual are not the same object. Use
`expect(actual).to eq(expected)` if you don't care about
object identity in this example.
# ./spec/controllers/communities_controller_spec.rb:21:in `block (3 levels) in <top(required)>'
Finished in 0.33439 seconds
4 examples, 2 failures
Failed examples:
rspec ./spec/controllers/communities_controller_spec.rb:20 # CommunitiesController#show should be success
rspec ./spec/controllers/communities_controller_spec.rb:21 # CommunitiesController#show should equal 200
Randomized with seed 18521
It seems
before do
request.headers["HTTP_AUTHENTICATION"] = access_token.token
get :show, id: community
end
is not authenticated
and
before { get :search, access_token: access_token.token }
is authenticated.
Supplementary
I did pp debug in controller, pp request and pp responce result had have a pair of key-value that "HTTP_AUTHENTICATION": "xrfui24j53iji34.....(some Hash value)".
Related
I have this api endpoint wot get all the blogs from my database that works id the user pass an api_key. This works correctly and now I'm trying to testing this endpoint.
Routes:
Rails.application.routes.draw do
get 'blogs', to: 'blogs#index'
end
Blogs controller:
class BlogsController < ApplicationController
def index
if params[:api_key]
user = User.find_by(api_key: params[:api_key])
if user.present?
#blogs = Blog.all
return render json: #blogs, status: :ok
end
end
render json: { error: "Unauthorized!" }, status: :bad_request
end
end
I'm new to rspec and tests in general, I watched a couple videos and tutorials and this is what I have so far:
spec/requests/blogs_spec.rb
require 'rails_helper'
RSpec.describe 'Blogs API', type: :request do
let!(:blogs) { Blog.limit(10) }
describe 'GET /blogs' do
before { get '/blogs' }
it 'returns status code 400' do
expect(response).to have_http_status(400)
end
context 'when the request is valid' do
before { get '/blogs', params: { api_key: '123123'} }
it 'returns status code 400' do
expect(response).to have_http_status(200)
end
end
end
end
I can't seem to make the last test work and I don't know why. My guess is that I'm not passing api_key correctly, but I don't know how
1) Blogs API GET /blogs when the request is valid returns status code 400
Failure/Error: expect(response).to have_http_status(200)
expected the response to have status code 200 but it was 400
# ./spec/requests/blogs_spec.rb:28:in `block (4 levels) in <top (required)>'
Ok, so accordingly to your question + comments, I can assume you are running your tests within test environment, but you are expecting to find a User existing in development database.
FactoryBot
You might wanna use FactoryBot to create records for your testing suite.
Add to your Gemfile:
group :development, :test do
gem 'factory_bot_rails'
end
In rails_helper.rb, add:
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
Now you should create your User factory. Create a new file spec/factories/user.rb with the following:
FactoryBot.define do
factory :user do
api_key { '123123' }
# You should define every any other required attributes here so record can be created
end
end
Finally, in your spec file:
....
context 'when the request is valid' do
before { get '/blogs', params: { api_key: user.api_key} }
let!(:user) { create(:user) }
it 'returns status code 200' do
expect(response).to have_http_status(200)
end
end
...
Now your test should pass. Notice that in testing database there is no Blog created also, so:
let!(:blogs) { Blog.limit(10) }
Will return an empty array. You will need to create a Blog factory too, and create blogs like:
let!(:blogs) { create_list(:blog, 2) }
Bonus
As soon as you start improving your tests, you may wanna take a look at Faker and Database Cleaner for ActiveRecord
I am trying to get devise and devise-jwt gems to work so I can implement Authorization into my API only Rails app.
I have installed both devise and devise-jwt gems.
I followed the instructions on this blog post:
https://medium.com/#mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03
I implemented the requests specs the author included in his post, and I can't get them to pass. If I put a byebug into the session controller, I see that it's saying the "User needs to sign in or sign up before continuing."
Any thoughts on what I'm doing incorrectly?
Here are the relevant files:
routes.rb
Rails.application.routes.draw do
namespace :api, path: '', defaults: {format: :json} do
namespace :v1 do
devise_for :users,
path: '',
path_names: {
sign_in: 'signin',
sign_out: 'signout',
registration: 'signup'
}
...
end
end
controllers/api/v1/sessions_controller.rb
class API::V1::SessionsController < Devise::SessionsController
respond_to :json
private
def respond_with(resource, _opts = {})
render json: resource
end
def respond_to_on_destroy
head :no_content
end
end
models/user.rb
class User < ApplicationRecord
devise :confirmable, :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :jwt_authenticatable, jwt_revocation_strategy: JwtBlacklist
...
end
models/jwt_blacklist.rb
class JwtBlacklist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Blacklist
self.table_name = 'jwt_blacklist'
end
config/initializers/devise.rb
Devise.setup do |config|
# Setup for devise JWT token authentication
config.jwt do |jwt|
jwt.secret = Rails.application.secret_key_base
jwt.dispatch_requests = [
['POST', %r{^*/signin$}]
]
jwt.revocation_requests = [
['DELETE', %r{^*/signout$}]
]
jwt.expiration_time = 1.day.to_i
end
config.navigational_formats = []
...
end
spec/request/authentication_spec.rb
require 'rails_helper'
describe 'POST /v1/signin', type: :request do
let(:user) { create(:user) }
let(:url) { '/v1/signin' }
let(:params) do
{
user: {
email: user.email,
password: user.password
}
}
end
context 'when params are correct' do
before do
post url, params: params
end
it 'returns 200' do
expect(response).to have_http_status(200)
end
it 'returns JTW token in authorization header' do
expect(response.headers['Authorization']).to be_present
end
it 'returns valid JWT token' do
decoded_token = decoded_jwt_token_from_response(response)
expect(decoded_token.first['sub']).to be_present
end
end
context 'when login params are incorrect' do
before { post url }
it 'returns unathorized status' do
expect(response.status).to eq 401
end
end
end
describe 'DELETE /v1/signout', type: :request do
let(:url) { '/v1/signout' }
it 'returns 204, no content' do
delete url
expect(response).to have_http_status(204)
end
end
I would expect the tests to pass, but I get the following errors:
Test Failures
Failures:
1) POST /v1/signin when params are correct returns 200
Failure/Error: expect(response).to have_http_status(200)
expected the response to have status code 200 but it was 401
# ./spec/request/authentication_spec.rb:21:in `block (3 levels) in <top (required)>'
2) POST /v1/signin when params are correct returns JTW token in authorization header
Failure/Error: expect(response.headers['Authorization']).to be_present
expected `nil.present?` to return true, got false
# ./spec/request/authentication_spec.rb:25:in `block (3 levels) in <top (required)>'
3) POST /v1/signin when params are correct returns valid JWT token
Failure/Error: decoded_token = decoded_jwt_token_from_response(response)
NoMethodError:
undefined method `decoded_jwt_token_from_response' for #<RSpec::ExampleGroups::POSTV1Signin::WhenParamsAreCorrect:0x00007fec3d3ae158>
# ./spec/request/authentication_spec.rb:29:in `block (3 levels) in <top (required)>'
Finished in 0.76386 seconds (files took 3.31 seconds to load)
5 examples, 3 failures
Failed examples:
rspec ./spec/request/authentication_spec.rb:20 # POST /v1/signin when params are correct returns 200
rspec ./spec/request/authentication_spec.rb:24 # POST /v1/signin when params are correct returns JTW token in authorization header
rspec ./spec/request/authentication_spec.rb:28 # POST /v1/signin when params are correct returns valid JWT token
I don't know if you found a solution; but I leave an approach I've made; It might helpfull.
Taking special attetion to the problem, The solution was to change:
decoded_token = decoded_jwt_token_from_response(response)
To:
decoded_token = JWT.decode(response.headers['authorization'].split(' ').last, Rails.application.credentials.jwt_secret, true)
Beacuse I din't find any in the documentation or other place and I chose to decode with method provided by JWT.
Also if you see I handle the requests in a different way, but I think that is not a problem at all.
require 'rails_helper'
require "json"
RSpec.describe "POST /login", type: :request do
let(:user) { User.create!( username: 'usertest',
email: 'usertest#email.com',
password: 'passwordtest123',
password_confirmation: 'passwordtest123') }
let(:url) { '/users/login' }
let(:params) do
{
user: {
login: user.email,
password: user.password
}
}
end
context 'when params are correct' do
before do
post url, params: params.to_json, headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
end
it 'returns 200' do
expect(response).to have_http_status(200)
end
it 'returns JTW token in authorization header' do
expect(response.headers['authorization']).to be_present
end
it 'returns valid JWT token' do
token_from_request = response.headers['Authorization'].split(' ').last
decoded_token = JWT.decode(token_from_request, Rails.application.credentials.jwt_secret, true)
expect(decoded_token.first['sub']).to be_present
end
end
context 'when login params are incorrect' do
before { post url }
it 'returns unathorized status' do
expect(response.status).to eq 401
end
end
end
RSpec.describe 'DELETE /logout', type: :request do
let(:url) { '/users/logout' }
it 'returns 204, no content' do
delete url
expect(response).to have_http_status(204)
end
end
RSpec.describe 'POST /signup', type: :request do
let(:url) { '/users/signup' }
let(:params) do
{
user: {
username: 'usertest2',
email: 'usertest2#email.com',
password: 'passwordtest123',
password_confirmation: 'passwordtest123'
}
}
end
context 'when user is unauthenticated' do
before {
post url,
params: params.to_json,
headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
}
it 'returns 201' do
expect(response.status).to eq 201
end
it 'returns a new user' do
expect(response).to have_http_status :created
end
end
context 'when user already exists' do
before do
post url,
params: params.to_json,
headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
post url,
params: params.to_json,
headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
end
it 'returns bad request status' do
expect(response.status).to eq 400
end
it 'returns validation errors' do
expect(response_body['errors'].first['title']).to eq('Bad Request')
end
end
end
PD: I leave the spec code for register, with a couple differences (requests, url, username params in User model (that's is why I use the login param y the login request), I made all in a sigle spec.rb file, ...) to https://medium.com/#mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03. Kepp that in mind.
I believe you need to use the helper sign_in user before making the request for it to be authorized. Check https://github.com/heartcombo/devise, Controller tests
I am upgrading a legacy project to rails 5 and among the rspec tests that are failing I have one that says:
Failure/Error: expect(response).to be_redirect
expected `#<ActionDispatch::TestResponse:0x00007fbe5fde51f0 #mon_owner=nil, #mon_count=0, #mon_mutex=#<Thread::...ch::Http::Headers:0x00007fbe5fdde9e0 #req=#<ActionController::TestRequest:0x00007fbe5fde5358 ...>>>>.redirect?` to return true, got false
# ./spec/controllers/search_controller_spec.rb:86:in `block (3 levels) in <top (required)>'
I am using devise gem to authenticate clients.
The tests are as follows:
describe SearchController do
before(:each) do
#client = FactoryGirl.create(:client)
end
describe "authenticated search" do
# a bunch of tests
end
describe "unauthenticated search" do
it "requires a user to be authenticated" do
get :search, params: { q: "tec" }, format: :json
expect(response).to be_redirect # FAILING HERE
end
end
end
If I run the test manually and go to /search?q=tec I get redirected to the sign_in page. The search_controller.rb has a before_action :authenticate_client!
I tried adding sign_out #client before the search but it didn't work.
Also tried current_client.reload but didn't recognize current_client.
In the authenticated search tests there is a call to stub_authenticate_client that has the following code:
def stub_authenticate_client(client)
allow(request.env['warden']).to receive(:authenticate!) { client }
allow(controller).to receive(:current_client) { client }
end
in case that is useful to solve this issue.
I also tried creating a stub_logout_client method like this:
def stub_logout_client(client)
allow(request.env['warden']).to receive(:authenticate!) { nil }
allow(controller).to receive(:current_client) { nil }
end
and calling it at the beginning of the test, but it is still passing the before_action authenticate_client!
Also tried what it was suggested here, but didn't work
The search controller that is being tested:
class SearchController < ClientApplicationController
before_action :authenticate_client!
def search
limit = params[:limit] ? params[:limit].to_i : 10
query = params[:q].to_s.downcase.strip
results = {}
if params[:report]
results[:this_report] = Report.where("SOME QUERY")
end
render json: results
end
end
Thank you!
The problem is related to the be_redirect check. Changed the test to check for content in the response and that solved it, like this:
describe "unauthenticated search" do
it "requires a user to be authenticated" do
get :search, params: { q: "tec" }, format: :json
expect(response.body).to have_content("content from the page I render")
end
end
I have a basic Rails API built with Accounts and Users. All of the account specs pass when I remove...
before_action :authenticate_user!
But with that in place, I'm having trouble getting the specs to pass.
# Note `json` is a custom helper to parse JSON responses
RSpec.describe 'Account API', type: :request do
# test data
let!(:user) { create(:user) }
let!(:accounts) { create_list(:account, 10, user_id: user.id) }
let(:account_id) { accounts.first.id }
# GET /accounts
describe 'GET /accounts' do
# HTTP request before examples
before do
get '/accounts'
request.headers.merge! user.create_new_auth_token
end
it 'returns accounts' do
expect(json).not_to be_empty
expect(json.size).to eq(10)
end
it 'returns status code 200' do
expect(response).to have_http_status(200)
end
end
end
As you can see I attempted to merge the auth token with this line in a before do block...
request.headers.merge! user.create_new_auth_token
But that is not working. Instead I get json.size == 1 and http_status of 401 unauthorized.
I was able to fix it by declaring a variable at the top...
let(:auth_headers) { create(:user).create_new_auth_token }
And in my specs using it like this...
before { get '/accounts', headers: auth_headers }
I have a Rails 5 API only app and using knock to do JWT authenticate.
After complete the model and model spec, I start to do the request spec.
But I have no idea how to complete the authentication inside the request spec in the right way,
My users controller,
module V1
class UsersController < ApplicationController
before_action :authenticate_user, except: [:create]
end
end
Application controller,
class ApplicationController < ActionController::API
include Knock::Authenticable
include ActionController::Serialization
end
My stupidest solution (call the get token request to get the JWT before the rest request),
context 'when the request contains an authentication header' do
it 'should return the user info' do
user = create(:user)
post '/user_token', params: {"auth": {"email": user.email, "password": user.password }}
body = response.body
puts body # {"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0ODgxMDgxMDYsInN1YiI6MX0.GDBHPzbivclJfwSTswXhDkV0TCFCybJFDrjBnLIfN3Q"}
# use the retrieved JWT for future requests
end
end
Any advice is appreciated.
def authenticated_header(user)
token = Knock::AuthToken.new(payload: { sub: user.id }).token
{ 'Authorization': "Bearer #{token}" }
end
describe 'GET /users?me=true' do
URL = '/v1/users?me=true'
AUTH_URL = '/user_token'
context 'when the request with NO authentication header' do
it 'should return unauth for retrieve current user info before login' do
get URL
expect(response).to have_http_status(:unauthorized)
end
end
context 'when the request contains an authentication header' do
it 'should return the user info' do
user = create(:user)
get URL, headers: authenticated_header(user)
puts response.body
end
end
end
With the help of Lorem's answer, I was able to implement something similar for my request spec. Sharing it here for others to see an alternate implementation.
# spec/requests/locations_spec.rb
require 'rails_helper'
RSpec.describe 'Locations API' do
let!(:user) { create(:user) }
let!(:locations) { create_list(:location, 10, user_id: user.id) }
describe 'GET /locations' do
it 'reponds with invalid request without JWT' do
get '/locations'
expect(response).to have_http_status(401)
expect(response.body).to match(/Invalid Request/)
end
it 'responds with JSON with JWT' do
jwt = confirm_and_login_user(user)
get '/locations', headers: { "Authorization" => "Bearer #{jwt}" }
expect(response).to have_http_status(200)
expect(json.size).to eq(10)
end
end
end
confirm_and_login_user(user) is defined in a request_spec_helper which is included as a module in rails_helper.rb:
# spec/support/request_spec_helper.rb
module RequestSpecHelper
def json
JSON.parse(response.body)
end
def confirm_and_login_user(user)
get '/users/confirm', params: {token: user.confirmation_token}
post '/users/login', params: {email: user.email, password: 'password'}
return json['auth_token']
end
end
I'm using the jwt gem for generating my tokens as described in this SitePoint tutorial (https://www.sitepoint.com/introduction-to-using-jwt-in-rails/)
Lorem's answer mostly worked for me. I got unrecognized keyword setting headers: on the get. I modified the authenticated_header method and put it in support/api_helper.rb so I could reuse it. The modification is to merge the auth token into request.headers.
# spec/support/api_helper.rb
module ApiHelper
def authenticated_header(request, user)
token = Knock::AuthToken.new(payload: { sub: user.id }).token
request.headers.merge!('Authorization': "Bearer #{token}")
end
end
In each spec file testing the api, I include api_helper.rb. And I call authenticated_header just before the get statement when testing the case of valid authentication...
# spec/controllers/api/v2/search_controller_spec.rb
RSpec.describe API::V2::SearchController, type: :controller do
include ApiHelper
...
describe '#search_by_id' do
context 'with an unauthenticated user' do
it 'returns unauthorized' do
get :search_by_id, params: { "id" : "123" }
expect(response).to be_unauthorized
end
end
context 'with an authenticated user' do
let(:user) { create(:user) }
it 'renders json listing resource with id' do
expected_result = { id: 123, title: 'Resource 123' }
authenticated_header(request, user)
get :search_by_id, params: { "id" : "123" }
expect(response).to be_successful
expect(JSON.parse(response.body)).to eq expected_result
end
end
The key lines in this second test are...
authenticated_header(request, user)
get :search_by_id, params: { "id" : "123" }