GraphQL/ Rails 422 Unprocessable Entity - Saving token to Session - ruby-on-rails

I’m currently working on changing the rails backend of a project from REST to graphql and I’m running into an error with authentication following their tutorial - https://www.howtographql.com/graphql-ruby/4-authentication/
I’m using the GraphiQL engine to test all my requests and receiving error status 422 unprocessable Entity, User Must Exist
Which makes sense because the mutation I am executing is to create a new color - which has a belongs_to relationship to User.
About halfway through the page linked above ^^^ it says this:
With the token that the signinUser mutation provides, apps can
authenticate subsequent requests. There are a couple of ways this can
be done. In this tutorial, we are just going to use the built-in
session, since this doesn’t add any requirements to the client application. The GraphQL server should be able to get the token from
the session header on each request, detect what user it relates to,
and pass this information down to the resolvers.
I’m able to successfully return an auth token through the signinUser method like the docs show previously on the same page - the method that it posts to also saves the token to this supposed session in this method here (also from the same link posted above ^^^) :
def call(_obj, args, ctx)
input = args[:email]
return unless input
user = User.find_by email: input[:email]
return unless user
return unless user.authenticate(input[:password_digest])
crypt = ActiveSupport::MessageEncryptor.new(ENV["SECRET_BASE_KEY"])
token = crypt.encrypt_and_sign("user-id:#{ user.id }")
puts "please **********************************"
p ctx[:session]
ctx[:session][:token] = token
puts "please **********************************"
p ctx[:session]
OpenStruct.new({
user: user,
token: token
})
end
You’ll be able to see in my desperate struggle that I p’d out the session right before the method returns and not surprisingly saw that it contained the token for that users sign in.
However, when I proceeded to execute the mutation to create a color, my expectation was that the session would still contain that token and I’d be able to commit that color successfully. That was not the case and when I p’d out the session for this request, it return an empty hash.
I cant find any information about how the built in graphql session works - and I’m brand new to graphql in general.
My main questions would be - is the graphql session supposed to be caching token information? Why is it that the information is not carrying over to requests following signinUser ? Should I even bother with trying to use the auth in this tutorial since the docs claim that this authentication method is not a long term solution?
I know this is a lot but would really appreciate an extra brain on this.
Thanks in advance!
PS. I understand the tutorial uses links and I am using colors here - that is intentional and I have done my best to make sure that semantic differences were not causing any errors.
Rails version - 5.2.2 (using api only)
graphql - 1.7.4
graphiql rails - 1.4.4

Same as REST APIs GraphQL does not store any information between two subsequent requests, You have to pass authentication token returned in sign in mutation to all subsequent requests where you want current user-related information.
You should do something like below in graphql_controller.rb
class GraphqlController < ApplicationController
def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
current_user: current_user
}
result = GraphqlTutorialSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
end
private
# set current user here
def current_user
# you can token here
token = request.headers['Authorization']
return nil unless token
# find current user from this token
end
# Handle form data, JSON body, or a blank value
def ensure_hash(ambiguous_param)
# ...code
end
end

A colleague of mine pointed out that "session" is part of rails and should create a cookie that would be accessible from the next request.
I mentioned that I was using Rails version - 5.2.2 (using api only) - well when you use the -api flag when initializing a new rails project, it adds these lines to application.rb
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
# Only loads a smaller set of middleware suitable for API only apps.
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
Notice this line in particular - Middleware like session, flash, cookies can be added back manually.
I commented out config.api_only = true and this added cookies back to the application/ allowed me to make the next request with an existing user.
You can also add these lines I found from "Lysender" on his post - Rails 5 – API Only – Enable Cookies and Sessions if you'd prefer not to remove the api-only feature.
config.api_only = true
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore, key: '_coookie_name', expire_after: 30.days

Related

Authenticating docusign via Rails API (Omniauth + Devise) + JS Frontend

I'm trying to create an authentication flow using Auth Code Grant where I've added necessary omniauth strategy for Docusign to create /auth/docusign routes in Rails API only application.
Here are the steps followed
I'm issuing a request to the route from VueJS client.
window.open("http://localhost:4000/auth/docusign", "targetWindow", "width=350,height=250")
After user enters credentials and on successful login I'm calling the callback:
class SessionsController < Devise::SessionsController
def docusign
internal_destroy
#success = false
userinfo = request.env['omniauth.auth']
request_info = request.env['omniauth.params']
if userinfo
info = userinfo.info
cred = userinfo.credentials
user = User.find_by(email: info['email']) || User.find_by(id: session[:user_id])
if user
organization = user.organization
organization.organization_providers.where(provider_name: 'Docusign').destroy_all
OrganizationProvider.create(email: info['email'], token_expires_at: Time.at(cred['expires_at']), token_expires_at: Time.now, provider_name: 'Docusign', organization_id: organization.id, token: cred.token)
#success = true
end
end
render 'sessions/docusign'
end
end
I'd like to pass some params (which I'm accessing in the callback as request.env['omniauth.params']) for executing some backend tasks in the method.
When I try window.open("http://localhost:4000/auth/docusign?email='"+email+"'", "targetWindow", "width=350,height=250")
It says that the url doesn't match with any redirect urls
I have also tried passing in redirect_to('/auth/docusign', query: query) but on doing so, it doesn't open in a browser due to CORS.
I'm also trying to set it in session cookie, but since it's an API only server, I'm still working towards setting up cookie store.
Question
Which is the best way to achieve this? To pass some params in the callback and retrieve it.
Then the execution flow continues on the Rails server and the window serves a page with an appropriate response as per authentication status. However during this time, the client window which started the request is not aware of the authentication outcome.
Question
How can I communicate to the VueJS client that the authentication process is completed?
Question
Am I doing the above flow correctly or are there any better ways to achieve the same?
Thanks in advance
You need to log into your DocuSign Developer Account, Click on Admin and go on the left nav down to "API and Keys" where you can find the integration key you set. Did you set one?
If you did, you should find it and then add the redirectUri to the OAuth settings for that key (client ID in OAuth).
That is why DocuSign login tells you that the redirectURI doesn't match. You can add http://localhost:4000/auth to the list and that should work for your local env.
You cannot past custom variables on the redirectUri, it has to match exactly to the one you entered. If you need to pass values to it, there's a way to do that using state.
Here is how the URL should look, notice the &state= part of it:
https://account-d.docusign.com/oauth/auth?
response_type=code
&scope=YOUR_REQUESTED_SCOPES
&client_id=YOUR_INTEGRATION_KEY
&state=YOUR_CUSTOM_STATE
&redirect_uri=YOUR_REDIRECT_URI
&login_hint=YOUR_LOGIN_HINT
You can put whatever you want in there (URI encoded of course) and that value would come back to you when redirected back also with &state= parameter.
This solves the problem and allows you to pass arguments back to your redirect URI.

Adding path to autoload works - why?

I'm working on the front-end for our web app which is constructed using Angular.
The backend is a rails app with a modification to allow it to act as an API as well as a normal Rails application (pre-existing application here so it makes a-lot of sense to keep it)
I'm using JWT's on the front-end to validate a user but I also issue a refresh_token so they can get a new JWT after a certain amount of time. The code looks like this:
def delegation
user = User.user_from_refresh_token params[:refresh_token]
render json: {jwt: user.generate_auth_token}
end
This is setup as a POST in the route file:
post "auth/delegation"
So the line in question is this one:
user = User.user_from_refresh_token params[:refresh_token]
which would mean you'd expect the user.rb model to contain a method called user_from_refresh_token which I do:
def self.user_from_refresh_token token
refresh_token = RefreshToken.find_by!(token: token)
raise API::Unauthorized.new("This refresh token has been revoked") if refresh_token.revoked?
find(refresh_token.user_id)
end
But the error that I get when I call this is:
NoMethodError: undefined method `user_from_refresh_token' for User
Question 1 - How come I can't call this like it is? Shouldn't this just work?
Note:
I fixed this issue by modifying the autoloader to add this in:
config.autoload_paths += %W(#{config.root}/lib #{config.root}/model/concerns)
Question 2 - Why does this have to be done in order for the previous part to work?

Where is the Session Stored in Rails?

In Rails, I have implemented the below code for user auth (confirmed to be correct). However, I wanted to confirm my thinking for this strange session[:session_token]. is this the "cookie" that is stored in the browser?
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :current_user, :signed_in?
private
def current_user
#current_user ||= User.find_by_session_token(session[:session_token])
end
def signed_in?
!!current_user
end
def sign_in(user)
#current_user = user
session[:session_token] = user.reset_token!
end
def sign_out
current_user.try(:reset_token!)
session[:session_token] = nil
end
def require_signed_in!
redirect_to new_session_url unless signed_in?
end
end
My understanding so far of how this works is that whenever the browser/client sends a request to rails, the cookie (with the session[:session_token]) is also sent over, thus allowing the current_user method to find the user. Is my understanding correct? This is strange to me because there's a gap of knowledge of how exactly the browser/client gets access to the session cookie when we declare it in ApplicationController (Rails-side).
You are pretty much there. Although, I have a feeling you might be confusing apples with oranges...
Sessions:
Very often in dynamic web sites one would want to store user data between HTTP requests (because http is stateless and you can't otherwise associate a request to any other request), but you don't want that data to be readable and/or editable on the client-side inside of the URL (like.. yourwebsite.com/yourPage?cookie=12345&id=678), and so on..., because you don't want the client to play around with that data without passing through your server-side code.
One way to solve this problem is to store that data server-side, give it a "session_token"(as you called it), and let the client only know (and pass back at every http request) that token. This is how the session is implemented.
Cookies:
The most common technique for implementing sessions in Rails involve using cookies, which are small pieces of text placed on the user’s browser. Because cookies persist from one page to the next, they can store information (such as a session_token or whatever else you want) that can be used by the application to retrieve the logged-in user from the database.
Where is the Session Stored in Rails?
Using both of the above concepts I can now tell you that the default session store inside of Rails is CookieStore, which is about 4KB in size.
To put it simply...
def sign_in(user)
#current_user = user
session[:session_token] = user.reset_token!
end
...method that you defined places the user into a temporary session.
Then the idea is that the following...
def current_user
#current_user ||= User.find_by_session_token(session[:session_token])
end
...method would find and retrieve the user from the database corresponding to the session token and initialize it to a variable you specified.
Additional info:
You should also note that there is an important difference between Rails's session and cookies helper methods...
They both generate cookies, however, session[...] method generates temporary cookies, which should expire upon the browser exit, and cookies[...] method creates persistent cookies, which do not.
Additionally, I would suggest having a look at Section 2 of Ruby on Rails Security guide. You might find it useful.
Hope this helps you out.
Session is stored in server side. And,
Cookie is stored in client side (in browser cookie). And,
When client/browser send a request to rails server, every time cookies are sent to rails server.
When a session is set in rails server, like: session[:user_id] = 4,
Rails store it in server side.
Session is saved in server side like key value pair (like json object)
For each browser, Rails set a session identifier in cookie, so that, Rails can find the correct session information for a request.
Without session identifier in cookie, Rails do not know, what session belongs to what browser.
So, session will not work without cookie.
Edit: Explain: sessions are stored server side
Suppose, I am using your web application, and after login I will be redirected to home page.
I open login page, input username and password, and click login button.
The form is submitted to sessions#login action.
in sessions#login - you check username and password - and set session[:session_token]:
if username and password is correct
random_unique_identifier_string = #user.remember_token
session[:session_token] = random_unique_identifier_string
redirect_to root_url
end
When server run this code session[:session_token], server need an unique identifier for each browser session.
So, server generate an unique identifier for this browser, such as: abc123
Server set all session variables in a place (may be in some folder or in database), label this folder as abc123.
Now server send a cookie request to browser - to set cookie _ebook_session = abc123.
(I see, if my app name is ebook, in rails, cookie name is like: _ebook_session)
Now the page redirect to home page.
** Note: Everything above happen in single request **
Now, in my browser, I want to open some page that need authentication (suppose, dashboard page).
You added before_action: require_signed_in! in dashboard controller.
So, when I open dashboard page in my browser, browser by default send all cookies with every request. so _ebook_session cookie is sent to server. Your server gets the value of _ebook_session cookie is abc123. Now your application know we need to look in abc123 folder for session. Now you can get value of session[:session_token] from abc123 folder.
** I have explained second request above **
Each browser needs unique session identifier.
Important: _ebook_session cookie will be set in browser in first request. If we already have _ebook_session cookie set in a browser, we do not need to set it again, second, third and next requests in that specific browser.
I hope, you understand.

How to set variables across models for a single request in rails?

The scenario: I need to give models access to API tokens stored in the session.
Background: I have an API-driven rails 3 application utilizing DataMapper(DM) and a DM adapter to interface with the API. Each DM model has a corresponding REST-ish API endpoint much like you get with rails scaffolding. The API requires various headers for requests, including API tokens, keys, ids etc. The headers have nothing to do with the requested data, they exist for authorization and tracking purposes only. A number of these tokens are stored in the session. I want a clean way to make these API headers available to any model during a request.
Possible solutions:
1. Passing session variables from the controller to the models
The obvious answer is passing the tokens in a hash or other object from the controller to the models. A controller action might have the following: #user = User.find(params[:id], api_headers).
The problem is needing to override any model method to accept the additional api_headers object. Not counting methods defined by Rails and DataMapper, there are hundreds of methods already defined in the application models that would need to be rewritten. So I'm ruling out a rewrite, and this also doesn't seem like a good solution since it would require overriding a ridiculous number of DM methods like the User#find example above.
2. Some metaprogramming hack
I could catch any ArgumentError's on DM's base class and check if the last argument is the api_headers object, then set the values as instance variables and invoke the requested method. This thought exercise already has me cringing at dealing with optional arguments etc. If given long enough I could probably create a functional Frankenstein that should get me fired but probably wouldn't.
3. Use a singleton (current preferred solution)
In the application controller set a before_filter to dump the session-stored API headers into a singleton ApiHeaders object. Then any model making an API request can get that singleton with the required API headers.
An additional after_filter* on the application controller would set all attributes to nil on the ApiHeaders singleton at the end of the request to prevent leaking headers between requests.
This is currently my preferred solution but I don't like that the API header values could potentially carry over into other requests if the after_filter doesn't get invoked. I don't know in which scenarios this might happen (in an application error perhaps?) which raises concerns. All I know is the values don't necessarily die with the request.
4. Custom code
Drop support of DataMapper and the custom API adapter and manually make all API calls, passing through all required API headers. Besides the fact I don't have time for this level of rewrite, why use a framework at all if you have to throw a huge chunk out to support a custom auth scheme?
Summary
What's the cleanest way to get these pesky API tokens from the session into the bowels of the application where they can be sent with each API request? I'm hoping for a better solution than those listed above.
* An alias for after_action
I set the current user and the request information on my User model using the request_store gem which is just a tiny shim over thread local storage with a bit of clean-up.
This makes the information available from any of my models via the User class. I have User.current, User.request and User.location available wherever I need it.
Your controller just has to set User.current and User.request once it has authenticated the user.
Example User model:
# models/user.rb
require 'request_store'
class User
def self.current
RequestStore.store[:current_user]
end
def self.current=(user)
RequestStore.store[:current_user] = user
end
def self.request
RequestStore.store[:current_request]
end
def self.request=(request)
# stash the request so things like IP address and GEO-IP based location is available to other models
RequestStore.store[:current_request] = request
end
def self.location
# resolve the location just once per request
RequestStore.store[:current_location] ||= self.request.try(:location)
end
end
Use Thread.current, which is passed in from request to model (note, this breaks if, inside your request, you use sub-threads). You can store the attribute you want to share in a cattr_accessor or in rails cache:
in a cattr_accessor
class YourClass
cattr_accessor :my_var_hash
...
# and in your controller
# set the var
YourClass.my_var_hash = {} if YourClass.my_var_hash.nil?
YourClass.my_var_hash[Thread.current.object_id] = {}
YourClass.my_var_hash[Thread.current.object_id][your_var] = 100
... and in your model
lvalue = YourClass.my_var_hash[Thread.current.object_id][your_var]
Note, if you use this method, you will also want to make one of the hash values a timestamp, and do some housekeeping on getting, by deleting old keys, b/c you'll eventually use up all your system memory if you don't do the housekeeping
with cache:
# in your controller
#var = Rails.cache.fetch("#{Thread.current.object_id}_var_name") do
return 100 # do your work here to create the var value and return it
end
# in your model
lvalue = Rails.cache.fetch(("#{Thread.current.object_id}_var_name")
You can then set the cache expiration to 5 minutes, or you can wildcard clear your cache at the end of your request.

Using devise "rememberable" without cookies

I have a working Rails site that uses devise to manage users. For session management, I am using devise's rememberable strategy, which stores and retrieves encrypted authentication information from a user's cookie.
I'm implementing a multi-photo upload widget that uses flash. Flash does not support sending cookies along with requests. This is a problem with multiple multi-upload flash+javascript libraries, so fixing this shortcoming is probably not feasible.
So my question is: can I successfully authenticate to devise/rememberable without using cookies? And if so, how?
More details
Devise/rememberable depends on the value of remember_token within the cookie. If I could fool Rails into thinking that the value was supplied as a cookie (e.g. request.cookies['remember_token'] = '...'), my problem would be solved. Devise/rememberable would find the correct value there, unpack it, and successfully authenticate. However, the request.cookies hash is apparently read-only. Writing to the hash is silently ignored. Example (debug console from an incoming POST request):
>> request.cookies['remember_token'] = 'a string'
=> "a string"
>> request.cookies['remember_token']
=> nil
>> request.cookies
=> {}
I'm using (or trying to use) the FancyUpload v3 widget.
How about overriding Devise slightly?
Based on Devise 1.2.rc something like this should work:
module Devise
module Strategies
class Rememberable
def remember_cookie
# your code to get the hashed value from the request
end
end
end
end
Alternatively, you could add a new (subclassed) strategy:
module Devise
module Strategies
class RememberableParameter < Rememberable
def remember_cookie
# your code to get the hashed value from the request
end
end
end
end
Warden::Strategies.add(:rememberable_parameter, Devise::Strategies::Rememberable)
Or, look into Token Authenticatable:
Token Authenticatable: signs in a user based on an authentication token (also known as
"single access token"). The token can be given both through query string or
HTTP Basic Authentication
There's more about it here:
https://github.com/plataformatec/devise/blob/master/lib/devise/models/token_authenticatable.rb
Good luck!

Resources