Problem Statement
I'm Newbie in Rails and following this tutorial for setting up JWT based authentication in API, and working on an existing web application that uses Devise. My task at the moment is to add a JSON API to the application.
This rails project works fine for a web applications. However, Incase of API I'm getting empty resource while I still have value in params.
Environment
rails (6.1.4)
devise (4.8.0)
devise-jwt (0.9.0)
warden (1.2.9)
warden-jwt_auth (0.6.0)
Controller & Route
app/controllers/api/v1/users/registrations_controller.rb
class Api::V1::Users::RegistrationsController < Devise::RegistrationsController
respond_to :json
skip_before_action :verify_authenticity_token
# POST /resource
def create
super
end
private
def respond_with(resource, _opts = {})
if resource.persisted?
render json: {
status: { code: 200, message: "Signed up sucessfully." },
data: UserSerializer.new(resource).serializable_hash[:data][:attributes]
}
else
render json: {
status: { message: "User couldn't be created successfully. #{resource.errors.full_messages.to_sentence}" }
}, status: :unprocessable_entity
end
end
end
config/routes.rb
# For Web
devise_for :users, controllers: { registrations: "registrations" }
# Authentication
devise_scope :user do
get "/login" => "devise/sessions#new", as: :login
get "/logout" => "sessions#destroy", :as => :logout
get "/signup" => "registrations#new", :as => :signup
scope "my" do
get "profile", to: "registrations#edit"
put "profile/update", to: "registrations#update"
end
end
authenticated :user do
resources :dashboard, only: [:index] do
collection do
get :home
end
end
end
unauthenticated do
as :user do
root to: "devise/sessions#new", as: :unauthenticated_root
end
end
# For API
namespace :api do
namespace :v1 do
devise_for :users, path: '', path_names: {
sign_in: 'login',
sign_out: 'logout',
registration: 'signup'
},
controllers: {
sessions: 'api/v1/users/sessions',
registrations: 'api/v1/users/registrations'
}
end
end
Debugging information
app/controllers/api/v1/users/registrations_controller.rb
| 66: private
| 67: def respond_with(resource, _opts = {})
| 68: byebug
| => 69: if resource.persisted?
| 70: render json: {
| 71: status: { code: 200, message: "Signed up sucessfully." },
| 72: data: UserSerializer.new(resource).serializable_hash[:data][:attributes]
| 73: }
| (byebug) resource
| #<User
id: nil,
email: "",
first_name: "",
last_name: "",
role: "member",
created_at: nil,
updated_at: nil,
jti: nil
>
| (byebug) params
| #<ActionController::Parameters
{
"email"=>"test#test.com",
"first_name"=>"John",
"last_name"=>"Wick",
"password"=>"password",
"controller"=>"api/v1/users/registrations",
"action"=>"create",
"registration"=>{
"email"=>"test#test.com",
"first_name"=>"John",
"last_name"=>"Wick",
"password"=>"password"
}
} permitted: false>
Request
curl -X POST \
http://127.0.0.1:3000/api/v1/signup \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/json' \
-d '{
"email": "test#test.com",
"first_name": "John",
"last_name": "Wick",
"password": "password"
}'
Response
{
"status": {
"message": "User couldn't be created successfully. Email can't be blank and Password can't be blank"
}
}
Related
I've been stuck for days and searching but I cannot find a correct solution to logout from a devise session using JWT. I had a front made with react and everything works fine on login and searching, but when I logout the page if I don't make a refresh I can't login. I leave the code from devise session controller along side the application controller, route and my middeware build to use redux with my front (I'm working with React to). Thanks in advance and I you need something else, let me know.
Devise::SessionsController
# frozen_string_literal: true
class Api::SessionsController < Devise::SessionsController
respond_to :json, :html
# GET /resource/sign_in
# def new
# super
# end
# POST /resource/sign_in
# def create
# super
# end
# DELETE /resource/sign_out
# def destroy
# super
# end
# protected
private
def revoke_token(token)
# Decode JWT to get jti and exp values.
begin
secret = Rails.application.credentials.jwt_secret
jti = JWT.decode(token, secret, true, algorithm: 'HS256', verify_jti: true)[0]['jti']
exp = JWT.decode(token, secret, true, algorithm: 'HS256')[0]['exp']
user = User.find(JWT.decode(token, secret, true, algorithm: 'HS256')[0]['sub'])
sign_out user
# Add record to blacklist.
time_now = Time.zone.now.to_s.split(" UTC")[0]
sql_blacklist_jwt = "INSERT INTO jwt_blacklist (jti, exp, created_at, updated_at) VALUES ('#{ jti }', '#{ Time.at(exp) }', '#{time_now}', '#{time_now}');"
ActiveRecord::Base.connection.execute(sql_blacklist_jwt)
rescue JWT::ExpiredSignature, JWT::VerificationError, JWT::DecodeError
head :unauthorized
end
end
def respond_with(resource, _opts = {})
render json: resource
end
def respond_to_on_destroy
token = request.headers['Authorization'].split("Bearer ")[1]
revoke_token(token)
request.delete_header('Authorization')
render json: :ok
end
end
ApplicationController
class ApplicationController < ActionController::API
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :authenticate_user
protected
def configure_permitted_parameters
added_attrs = %i[username email password password_confirmation remember_me]
devise_parameter_sanitizer.permit(:sign_up, keys: added_attrs)
devise_parameter_sanitizer.permit(:account_update, keys: added_attrs)
end
private
def authenticate_user
if request.headers['Authorization'].present?
token = request.headers['Authorization'].split("Bearer ")[1]
begin
jwt_payload = JWT.decode(token, Rails.application.credentials.jwt_secret).first
#current_user_id = jwt_payload['sub']
rescue JWT::ExpiredSignature, JWT::VerificationError, JWT::DecodeError
head :unauthorized
end
end
end
def authenticate_user!(options = {})
head :unauthorized unless signed_in?
end
def current_user
#current_user ||= super || User.find(#current_user_id)
end
def signed_in?
#current_user_id.present?
end
end
routes.rb
Rails.application.routes.draw do
devise_for :users, skip: %i[registrations sessions passwords]
namespace :api do
devise_scope :user do
post 'signup', to: 'registrations#create'
post 'login', to: 'sessions#create'
delete 'logout', to: 'sessions#destroy'
get 'login', to: 'sessions#create'
end
resources :notes
resources :searches
get 'get_places', to: 'searches#get_places'
end
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
middleware.js
import * as constants from './constants';
import axios from 'axios';
import { logoutUser } from './actions/authActionCreators'
export const apiMiddleware = ({ dispatch, getState }) => next => action => {
if (action.type !== constants.API) return next(action);
dispatch({ type: constants.TOGGLE_LOADER });
const BASE_URL = 'http://localhost:3001';
const AUTH_TOKEN = getState().user.token;
if (AUTH_TOKEN)
axios.defaults.headers.common['Authorization'] = `Bearer ${AUTH_TOKEN}`;
const { url, method, success, data, postProcessSuccess, postProcessError } = action.payload;
console.log('AUTH_TOKEN '+AUTH_TOKEN);
console.log('url '+url);
axios({
method,
url: BASE_URL + url,
data: data ? data : null,
headers: {
'Content-Type': 'application/json', 'Accept': '*/*'
}
}).then((response) => {
dispatch({ type: constants.TOGGLE_LOADER });
if (success) dispatch(success(response));
if (postProcessSuccess) postProcessSuccess(response);
}).catch(error => {
dispatch({ type: constants.TOGGLE_LOADER });
if (typeof(error.response) === "undefined") {
console.warn(error);
postProcessError('An error has ocurred');
} else {
if (error.response && error.response.status === 403)
dispatch(logoutUser());
if (error.response.data.message) {
if (postProcessError) postProcessError(error.reponse.data.message);
}
}
})
};
Is there a “standard” approach to receiving (potentially nested) a JSON:API POST object in Rails?
The JSON:API spec uses the same format for GET / POST / PUT, etc, but rails seems to need *_attributes and accepts_nested_attributes_for. These seem incompatible.
I feel like what I'm doing must be somewhat common, yet I'm having trouble finding documentation. I'm wanting to use a React/Redux app that communicates with a Rails app using the JSON:API spec. I'm just not sure how to handle the nested associations.
You can active_model_serializer gem's Deserialization functionality.
From the docs of the gem:
class PostsController < ActionController::Base
def create
Post.create(create_params)
end
def create_params
ActiveModelSerializers::Deserialization.jsonapi_parse(params, only: [:title, :content, :author])
end
end
The above can work with the below JSON API payload:
document = {
'data' => {
'id' => 1,
'type' => 'post',
'attributes' => {
'title' => 'Title 1',
'date' => '2015-12-20'
},
'relationships' => {
'author' => {
'data' => {
'type' => 'user',
'id' => '2'
}
},
'second_author' => {
'data' => nil
},
'comments' => {
'data' => [{
'type' => 'comment',
'id' => '3'
},{
'type' => 'comment',
'id' => '4'
}]
}
}
}
}
The entire document can be parsed without specifying any options:
ActiveModelSerializers::Deserialization.jsonapi_parse(document)
#=>
# {
# title: 'Title 1',
# date: '2015-12-20',
# author_id: 2,
# second_author_id: nil
# comment_ids: [3, 4]
# }
I saw these longs threads/issues discussions #979 and #795 on JSON:API repo days ago, that aparently seems that JSON API don't have a true solution for accepts_nested_attributes_for.
I don't know if it is the better solution but the work around to that is disposing a route to your belongs_to and has_many/has_one associations.
Something like that:
Your routes.rb:
Rails.application.routes.draw do
resources :contacts do
resource :kind, only: [:show]
resource :kind, only: [:show], path: 'relationships/kind'
resource :phones, only: [:show]
resource :phones, only: [:show], path: 'relationships/phones'
resource :phone, only: [:update, :create, :destroy]
# These relationships routes is merely a suggestion of a best practice
resource :phone, only: [:update, :create, :destroy], path: 'relationships/phone'
resource :address, only: [:show, :update, :create, :destroy]
resource :address, only: [:show, :update, :create, :destroy], path: 'relationships/address'
end
root 'contacts#index'
end
Them implement your controllers.
The phones_controller.rb following the example above:
class PhonesController < ApplicationController
before_action :set_contacts
def update
phone = Phone.find(phone_params[:id])
if phone.update(phone_params)
render json: #contact.phones, status: :created, location: contact_phones_url(#contact.id)
else
render json: #contact.errors, status: :unprocessable_entity
end
end
# DELETE /contacts/1/phone
def destroy
phone = Phone.find(phone_params[:id])
phone.destroy
end
# POST contacts/1/phone
def create
#contact.phones << Phone.new(phone_params)
if #contact.save
render json: #contact.phones, status: :created, location: contact_phones_url(#contact.id)
else
render json: #contact.errors, status: :unprocessable_entity
end
end
# GET /contacts/1/phones
def show
render json: #contact.phones
end
private
# Use callbacks to share common setup or constraints between actions.
def set_contacts
#contact = Contact.find(params[:contact_id])
end
def phone_params
ActiveModelSerializers::Deserialization.jsonapi_parse(params)
end
end
By doing that you should be able to request a Phone POST normally through a contact separately, like so:
POST on http://localhost:3000/contacts/1/phone with the body:
{
"data": {
"type": "phones",
"attributes": {
"number": "(+55) 91111.2222"
}
}
}
The response or GET on http://localhost:3000/contacts/1/phones:
{
"data": [
{
"id": "40",
"type": "phones",
"attributes": {
"number": "(55) 91111.2222"
},
"relationships": {
"contact": {
"data": {
"id": "1",
"type": "contacts"
},
"links": {
"related": "http://localhost:3000/contacts/1"
}
}
}
}
]
}
Hope that answer
I am using devise_saml_authenticatable gem in my rails application to integrate it with external SSO, I have configured my application but I am getting Completed 401 Unauthorized in 119ms from devise/saml_sessions controller.
My config/initializers/devise.rb
config.saml_create_user = true
config.saml_update_user = true
config.saml_default_user_key = :email
config.saml_session_index_key = :session_index
config.saml_use_subject = true
config.idp_settings_adapter = CidpSettingsAdapter
IDP Settings Adapter
class CidpSettingsAdapter
def self.settings(idp_entity_id)
{
issuer: 'https://devidentity.greenfence.com/users/saml/metadata',
assertion_consumer_service_url: 'https://devidentity.greenfence.com/saml/consume',
assertion_consumer_service_binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
#assertion_consumer_logout_service_url: 'https://devidentity.greenfence.com/users/saml/sign_out',
idp_entity_id: 'https://cargill.identitynow.com',
authn_context: 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
idp_sso_target_url: 'https://prd02-useast1-sso.identitynow.com/sso/SSOPOST/metaAlias/cargill/idp',
idp_slo_target_url: 'https://prd02-useast1-sso.identitynow.com/sso/IDPSloPOST/metaAlias/cargill/idp',
security: {
authn_requests_signed: false,
logout_requests_signed: false,
logout_responses_signed: false,
metadata_signed: false,
digest_method: XMLSecurity::Document::SHA1,
signature_method: XMLSecurity::Document::RSA_SHA1
},
idp_cert: <<-CERT.chomp
-----BEGIN CERTIFICATE-----
MIIDQDCCAiigAwIBAgIEIZbEtDANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEOMAwGA1U
CBMFVGV4YXMxDzANBgNVBAcTBkF1c3RpbjESMBAGA1UEChMJU2FpbFBvaW50MR4wHAYDVQQDExVw
cmQwMi11c2Vhc3QxLWNhcmdpbGwwHhcNMTYwMTE5MDM0OTQwWhcNMjYwMTE2MDM0OTQwWjBiMQsw
CQYDVQQGEwJVUzEOMAwGA1UECBMFVGV4YXMxDzANBgNVBAcTBkF1c3RpbjESMBAGA1UEChMJU2Fp
bFBvaW50MR4wHAYDVQQDExVwcmQwMi11c2Vhc3QxLWNhcmdpbGwwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQCRlr1CRIYLomUqTt9Igdrs9dwSW45lLS7lRDh+7WAgIbqIRxLjDH0fJgMi
T14i2gZD+bKyv43epVi6DG8pWrP2qjf8/U1VTr2hMnLrty5ycB9c8DSSh8YSARRIRjxUKrETp70i
BspeMtA3+ZMEnrrz38WlU5zuctzRSr6Q75Yf96tIk1wO+EqRASiNUy+oe/+/LClvPiJLnwdUEnNY
SXgidUvAGxgM639yD0C4cKs++zimwUBcTOgdvPbSJhpG1/CoQcrrdPt78a1RxC3MJJBVG9015SW1
ZkQ5u5sJjFWPzvqd9POgszzc/cj9SjLnh4Y6BFbxZOqkg5Ghn9b8vaElAgMBAAEwDQYJKoZIhvcN=
-----END CERTIFICATE-----
CERT
}
end
end
My config/routes.rb
devise_scope :user do
get 'users/sign_out', to: 'devise/sessions#destroy'
get 'users/submit_verification_code', to: 'aws_cognito#submit_verification_code'
get 'users/request_verification_code', to: 'aws_cognito#request_verification_code'
scope 'users', controller: 'saml_sessions' do
get :new, path: 'saml/sign_in', as: :new_user_saml_session
post :create, path: 'saml/auth', as: :user_saml_session
get :destroy, path: 'saml/sign_out', as: :destroy_user_saml_session
get :metadata, path: 'saml/metadata', as: :metadata_user_saml_session
match :idp_sign_out, path: 'saml/idp_sign_out', via: [:get, :post]
get :sso_dashboard
end
post '/saml/consume' => 'saml_sessions#create'
end
Issue fixed by providing correct issuer name in CidpSettingsAdapter.
I have the following create action:
def create
episode = Episode.new(episode_params)
if episode.save
render json: episode, status: :created, location: episode
end
end
but when I test the following:
require 'test_helper'
class CreatingEpisodesTest < ActionDispatch::IntegrationTest
setup { host! 'api.example.com'}
test 'create episodes' do
post '/episodes',
{ episode:
{ title: 'Bananas', description: 'Learn about bananas.' }
}.to_json,
{ 'Accept' => Mime::JSON, 'Content-Type' => Mime::JSON.to_s }
assert_equal 200, response.status
assert_equal Mime::JSON, response.content_type
episode = json(response.body)
assert_equal "/episodes/#{episode[:id]}", response.location
end
end
I get the following error:
1) Error: CreatingEpisodesTest#test_create_episodes: NoMethodError: undefined method `episode_url' for #<API::EpisodesController:0x007f8e34519450> Did you mean? episode_params
app/controllers/api/episodes_controller.rb:11:in `create'
test/integration/creating_episodes_test.rb:7:in `block in #<class:CreatingEpisodesTest>'
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
I guest I miss something to send the location after I create the episode.
UPDATE: Route
Rails.application.routes.draw do
namespace :api, path: '/', constraints: { subdomain: 'api' } do
resources :zombies
resources :episodes
end
end
This is because of namespace, use [:api, episode] for location
I am trying to setup json based authentication on my current rails app. The app's authentication is currently handled by devise.
I have read couple of questions on stackoverflow, but I do not seem to get it working.
routes.rb:
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks", :sessions => 'users/sessions' }
SessionsController:
class Users:: SessionsController < Devise::SessionsController
def create
resource = warden.authenticate!(:scope => resource_name, :recall => :failure)
return sign_in_and_redirect(resource_name, resource)
end
def sign_in_and_redirect(resource_or_scope, resource=nil)
scope = Devise::Mapping.find_scope!(resource_or_scope)
resource ||= resource_or_scope
sign_in(scope, resource) unless warden.user(scope) == resource
return render :json => {:success => true, :redirect => stored_location_for(scope) || after_sign_in_path_for(resource)}
end
def failure
return render:json => {:success => false, :errors => ["Login failed."]}
end
end
Not sure where I have gone wrong. The call I am making is:
JSON:
Content-Type: application/json
Accept: application/json
data: {user: {password: mypass, email: some_email#gmail.com}}
The Error:
MultiJson::DecodeError (756: unexpected token at '{user: {password: mypass, email: some_email#gmail.com}}'):
2012-06-30T18:10:10+00:00 app[web.1]: vendor/bundle/ruby/1.9.1/gems/json-1.6.5/lib/json/common.rb:148:in `parse'
If you paste that JSON into http://jsonlint.com, you get the same error.. Instead, you should wrap your values in quotes:
{
"user": {
"password": "mypass",
"email": "some_email#gmail.com"
}
}