Absinthe - How to put_session in resolver function? - graphql

I'm using Absinthe and have a sign in mutation. When users send over valid credentials, I'd like to set a session cookie in the response via put_session.
The problem I'm facing is that I'm not able to access the conn from within a resolver function. That tells me that I'm not supposed to update the connection's properties from within a resolver.
Is it possible to do this with Absinthe? What are some alternative solutions?

It looks like one solution is:
In the resolver, resolve either an {:ok, _} or an {:error, _} as normal
Add middleware after the resolver to pattern match that resolution.value returned from step 1 and update the GraphQL context
Use the before_send feature of Absinthe (which has access to both the GraphQL context and the connection to put_session before sending a response
Code Example
Mutation:
mutation do
#desc "Authenticate a user."
field :login, :user do
arg(:email, non_null(:string))
arg(:password, non_null(:string))
resolve(&Resolvers.Accounts.signin/3)
middleware(fn resolution, _ ->
case resolution.value do
%{user: user, auth_token: auth_token} ->
Map.update!(
resolution,
:context,
&Map.merge(&1, %{auth_token: auth_token, user: user})
)
_ ->
resolution
end
end)
end
end
Resolver:
defmodule AppWeb.Resolvers.Accounts do
alias App.Accounts
def signin(_, %{email: email, password: password}, _) do
if user = Accounts.get_user_by_email_and_password(email, password) do
auth_token = Accounts.generate_user_session_token(user)
{:ok, %{user: user, auth_token: auth_token}}
else
{:error, "Invalid credentials."}
end
end
end
Router:
defmodule AppWeb.Router do
use AppWeb, :router
pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
end
scope "/" do
pipe_through(:api)
forward("/api", Absinthe.Plug,
schema: AppWeb.Schema,
before_send: {__MODULE__, :absinthe_before_send}
)
forward("/graphiql", Absinthe.Plug.GraphiQL,
schema: AppWeb.Schema,
before_send: {__MODULE__, :absinthe_before_send}
)
end
def absinthe_before_send(conn, %Absinthe.Blueprint{} = blueprint) do
if auth_token = blueprint.execution.context[:auth_token] do
put_session(conn, :auth_token, auth_token)
else
conn
end
end
def absinthe_before_send(conn, _) do
conn
end
end

Not sure why you want to use a session, can't this be solved using a bearer?
Please disregard the interfaces. :-)
Mutation.
object :user_token_payload do
field(:user, :user)
field(:token, :string)
end
object :login_user_mutation_response, is_type_of: :login_user do
interface(:straw_hat_mutation_response)
field(:errors, list_of(:straw_hat_error))
field(:successful, non_null(:boolean))
field(:payload, :user_token_payload)
end
Resolver.
def authenticate_user(args, _) do
case Accounts.authenticate_user(args) do
{:ok, user, token} -> MutationResponse.succeeded(%{user: user, token: token})
{:error, message} -> MutationResponse.failed(StrawHat.Error.new(message))
end
end
Now the client can pass along that token with the Authorization header, and pick it up with a plug.
defmodule MyAppWeb.Plugs.Context do
import Plug.Conn
alias MyApp.Admission
def init(opts), do: opts
def call(conn, _) do
case build_context(conn) do
{:ok, context} -> put_private(conn, :absinthe, %{context: context})
_ -> put_private(conn, :absinthe, %{context: %{}})
end
end
#doc """
Return the current user context based on the authorization header
"""
def build_context(conn) do
auth_header =
get_req_header(conn, "authorization")
|> List.first()
if auth_header do
"Bearer " <> token = auth_header
case Admission.get_token_by_hash(token) do
nil -> :error
token -> {:ok, %{current_user: token.user}}
end
else
:error
end
end
end
Then add the plug to your pipeline
plug(MyApp.Plugs.Context)
Then you can pick up the current user in your resolvers like so.
def create_note(%{input: input}, %{context: %{current_user: user}}) do
end

Related

SSO Auth flow w/ DocuSign_eSign gem: "The specified Integrator Key was not found or is disabled."

I am using the DocuSign_eSign ruby gem version 1.0.2 because version 2.0.0 does not handle JWT auth. The eg-01-ruby-jwt example provided by DocuSign uses v1.0.2 as well, so I just pinned this version in my Gemfile.
I am getting the following error when I attempt to make a call using the DocuSign_eSign::EnvelopesApi.new#create_envelope method:
docusign_esign-1.0.2/lib/docusign_esign/api_client.rb:66:in `call_api': Bad Request (DocuSign_eSign::ApiError)
For good measure, I walked through the code line by line to get a better idea of what was happening, and I ended up with this error:
irb(main):101:0> response.code
=> 401
irb(main):103:0> response.body
=> "{\r\n \"errorCode\": \"PARTNER_AUTHENTICATION_FAILED\",\r\n \"message\": \"The specified Integrator Key was not found or is disabled. An Integrator key was not specified.\"\r\n}"
irb(main):104:0> response.status_message
=> "Unauthorized"
Here is the ruby code (minus logging/error handling) from my DocuSignWebClient where I trigger the call (send_envelope). This is where the auth happens:
TOKEN_REPLACEMENT_IN_SECONDS = 10.minutes.seconds
TOKEN_EXPIRATION_IN_SECONDS = 1.hour.seconds
def initialize(options = {})
#docusign_config = Padrino.config.docusign
#api_client = DocuSignWebClient.setup_api_client
#s3_client = options[:s3_client] || Aws::S3::Client.new(region: Padrino.config.aws.region)
#token_expire_in = options[:token_expire_in] || 0
#account_id = options[:account_id]
end
def self.setup_api_client
configuration = DocuSign_eSign::Configuration.new
DocuSign_eSign::ApiClient.new(configuration)
end
def send_envelope(details)
authorize
envelope = DocuSign::EnvelopeBuilder.new(details, #s3_client).build_envelope
sender = DocuSign::EnvelopeSender.new(#api_client, #account_id)
response = sender.send_envelope(envelope)
update_document_status(details, response)
end
def authorize
check_token
#account_id ||= update_account_id
end
def check_token
if no_token? || token_near_expiration?
update_token
end
end
def no_token?
!#api_client.default_headers['Authorization']
end
def token_near_expiration?
now = Time.now.to_f
(now + TOKEN_REPLACEMENT_IN_SECONDS) > #token_expire_in
end
def update_token
configure_jwt_authorization_flow
#token_expire_in = Time.now.to_f + TOKEN_EXPIRATION_IN_SECONDS
end
def configure_jwt_authorization_flow
#api_client.configure_jwt_authorization_flow(#docusign_config[:private_key_file],
#docusign_config[:auth_server],
#docusign_config[:integrator_key],
#docusign_config[:user_id],
TOKEN_EXPIRATION_IN_SECONDS)
end
def update_account_id
account = fetch_account_info
#api_client.config.host = account[:base_uri]
account[:account_id]
end
def fetch_account_info
response = DocuSignResponse.new(#api_client.call_api('GET', "https://#{#docusign_config[:auth_server]}/oauth/userinfo", return_type: 'Object'))
if response.ok?
response.data[:accounts].detect { |acct| acct[:is_default] }
end
end
I am getting a token back from DocuSign after I go through the authorization flow, but it says that the token is not valid once I try to use it.
irb(main):033:0> account_id = client.authorize
=> "386...a24"
irb(main):036:0> client.api_client
=> #<DocuSign_eSign::ApiClient:0x00007ff6d243c8c8 #config=#<DocuSign_eSign::Configuration:0x00007ff6d243d2f0 #scheme="https", #host="demo.docusign.net", #base_path="/restapi", #api_key={}, #api_key_prefix={}, #timeout=0, #verify_ssl=true, #verify_ssl_host=true, #params_encoding=nil, #cert_file=nil, #key_file=nil, #debugging=false, #inject_format=false, #force_ending_format=false, #logger=#<Logger:0x00007ff6d243d110 #level=0, #progname=nil, #default_formatter=#<Logger::Formatter:0x00007ff6d243d098 #datetime_format=nil>, #formatter=nil, #logdev=#<Logger::LogDevice:0x00007ff6d243cfa8 #shift_period_suffix=nil, #shift_size=nil, #shift_age=nil, #filename=nil, #dev=#<IO:<STDOUT>>, #mon_mutex=#<Thread::Mutex:0x00007ff6d243c8f0>, #mon_mutex_owner_object_id=70349033170900, #mon_owner=nil, #mon_count=0>>>, #user_agent="Swagger-Codegen/1.0.2/ruby", #default_headers={"Content-Type"=>"application/json", "User-Agent"=>"Swagger-Codegen/1.0.2/ruby", "Authorization"=>"Bearer eyJ0eXAiOiJNVCIsImFsZyI6IlJTMjU2Iiwia2lkIjoiNjgxODVmZjEtNGU1MS00Y2U5LWFmMWMtNjg5ODEyMjAzMzE3In0.AQkAAAABAAUABwAACzCudSXXSAgAAHP0D34l10gCABIJ3tlGgoJMvWi9_zzeFocVAAEAAAAYAAEAAAAFAAAADQAkAAAAZTVjOTIwMTItMWI0ZC00ZTgzLTgzNjYtNjgzNDQ0NjQyNjc0IwAkAAAAZTVjOTIwMTItMWI0ZC00ZTgzLTgzNjYtNjgzNDQ0NjQyNjc0EgABAAAABgAAAGp3dF9icg.yt_0QtjwAcL1dosfVuaNoKoM3Yzq9DK4MUf6lx3Sp5EYy0OdeSaKt6TgsAujUNQQoQH4e_IZFGtVXxBjFXzP2hh9EB2nsdwKAzi5EZJcOIp1wEfzKjEllUnOXOkEoUwcmHHCSN1j4LfNF8olRTDJnaSDB9A5TbumLURC_-FkttxHitFVpt39Fvl85VtSlIsQxU544SRjeJGJMl_BDwRmu0JrUSawc5LSUF9ET9SVTBGEjS_vZMz92hdiFM2x4qZqupeSXLrQ92bhzjEXHOH7kmKbE-iKDTH_TOln0rhhqLXq25yOTBJ_yUWqhvYaxpct9GRuPo6IIZCDDv0Of7k-xQ"}>
Does anything in that API instance look wrong? I can't figure out why my token is not working.
UPDATE: I re-attempted the same flow with a token using the OAuth Token Generator and that token also fails with the same error. Additionaly, I am able to send a token using the eg-01-ruby-jwt example code with the SAME EXACT inputs when calling DocuSign_eSign::ApiClient#configure_jwt_authorization_flow. The Envelopes API takes the API client and the account ID, which are identical upon inspection between the example code and my code (besides the auth token).
Try to use the token generator (https://developers.docusign.com/oauth-token-generator) and see if that token works for you. That would isolate the issue to obtaining the token.
Make sure in the sandbox admin, you configured your IK correctly, and that you use the exact same one.
You need an RSA key generated and make sure to use it for JWT.
Make sure you point to account-d.docusign.com and not account.docusign.com (so you use our developer sandbox and not production)

How to link OmniAuth identification (working) and Google Api Calls

After a day trying, testing, googling ... I ask for some help
I try to use Omniauth and Google Calendar. OmniAuth is working like a charm but I just can't link it with Google API
I think I read almost everything, I still get this error message :
dailyLimitExceededUnreg: Daily Limit for Unauthenticated Use Exceeded.
Continued use requires sign up.
It means that my calls are not properly 'connected' to my auth, which seems to be valid. My tokens are in databases but I this point I would like to login / identify / call and have something else that an error message.
client_id = Google::Auth::ClientId.from_file('.....googleusercontent.com.json')
scopes = ['userinfo.email,calendar']
token_store = Google::Auth::MyTokenStore.new()
authorizer = Google::Auth::WebUserAuthorizer.new(
client_id,
scopes,
token_store,
'http://localhost:3000'
)
# credentials = Google::Auth::UserAuthorizer.new( . # Anotheir test
# client_id,
# scopes,
# token_store,
# 'http://localhost:3000'
# )
#
# authorizer = credentials.get_credentials_from_code(
# GoogleUser.find(session[:user_id]) # I tested token ... notking worked
# )
calendar = Google::Apis::CalendarV3::CalendarService.new
calendar.authorization = authorizer
calendar_id = 'primary'
#result = calendar.list_events(calendar_id,
max_results: 10,
single_events: true,
order_by: 'startTime',
time_min: Time.now.iso8601)
and my token storage , I don't understand why but never called
class MyTokenStore
class << self
attr_accessor :default
end
def load(_id)
puts "********** load ************"
return GoogleUser.find(session[:user_id]).access_token
end
def store(_id, _token)
puts "********** store ************"
#user.access_token = _token
end
def delete(_id)
puts "********** delete ************"
#user.access_token = nil
end
end
end
end
For future readers : I took a different technic, after reading an excellent article here : http://gmile.me/simple-google-auth/
I followed it and use signet, it is working like a charm

How can I test a plug that calls an action_fallback controller?

Hello I am currently trying to get this test pass
test "authentication plug should return 401 when not authenticated", %{conn: conn} do
conn = Map.put(conn, :params, %{})
conn = ChaacServerWeb.Plugs.Authentication.call(conn, nil)
assert json_response(conn, 401)["errors"] != %{}
end
Here is my plug
def call(conn, _) do
token = List.first(get_req_header(conn, "authorization"))
case Accounts.validate_token(conn.params["user_id"], token) do
{:ok, valid_token} -> conn
err ->
conn
|> halt()
|> ChaacServerWeb.FallbackController.call(err)
end
end
I get this error
1) test authentication plug should return 401 when not authenticated (ChaacServerWeb.AuthenticationTest) test/chaac_server_web/plugs/authentication_test.exs:26
** (RuntimeError) cannot render template :"401" because conn.params["_format"] is not set.
Please set `plug :accepts, ~w(html json ...)` in your pipeline.
code: conn = ChaacServerWeb.Plugs.Authentication.call(conn, nil)
stacktrace:
(phoenix) lib/phoenix/controller.ex:689: Phoenix.Controller.render/3
test/chaac_server_web/plugs/authentication_test.exs:28: (test)
I understand I need to call plug :accepts, [:json] somehow in my test setup but how do I do that? Thanks
(EDIT) My router has plug :accepts, [:json] in my pipeline
I just did
test "authentication plug should return 401 when not authenticated", %{conn: conn} do
conn = Map.put(conn, :params, %{"_format" => "json"})
conn = ChaacServerWeb.Plugs.Authentication.call(conn, nil)
assert json_response(conn, 401)["errors"] != %{}
end
Feels abit hacky but it works for me.
You can use put_req_header:
setup %{conn: conn} do
{:ok, conn: put_req_header(conn, "accept", "application/json")}
end

Elixir - Check if string is empty

I am playing with Elixir and Phoenix Framework for the first time after following this Tutorial..
I have a simple client/server app.
chat/lib/chat_web/room_channel.ex:
defmodule ChatWeb.RoomChannel do
use Phoenix.Channel
def join("room:lobby", _message, socket) do
{:ok, socket}
end
def join("room:" <> _private_room_id, _params, _socket) do
{:error, %{reason: "unauthorized"}}
end
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast! socket, "new_msg", %{body: body}
{:noreply, socket}
end
end
I want to block empty incoming messages (body is empty string)
def handle_in("new_msg", %{"body" => body}, socket) do
# I guess the code should be here..
broadcast! socket, "new_msg", %{body: body}
{:noreply, socket}
end
How can I do that?
I want to block empty incoming messages (body is empty string)
You can add a guard clause for this. Either when body != "" or when byte_size(body) > 0
def handle_in("new_msg", %{"body" => body}, socket) when body != "" do
...
end
Now this function will only match if body is not "".
If you also want to handle empty body case, you can add two clauses like this (no need for the guard clause anymore since the second clause will never match if body is empty):
def handle_in("new_msg", %{"body" => ""}, socket) do
# broadcast error here
end
def handle_in("new_msg", %{"body" => body}, socket) do
# broadcast normal here
end
You can use answer proposed by #Dogbert, but to be 100% sure that string is not empty you can use wrap the broadcast! in the helper private function or just wrap into if or unless (negative if) expression.
unless String.trim(body) == "" do
broadcast! socket, "new_msg", %{body: body}
end
If you want to return an error message you try to use something more complex eg.:
if String.trim(body) != "" do
broadcast! socket, "new_msg", %{body: body}
else
broadcast! socket, "error_msg", %{body: "Body is empty"}
end

Ruby Gmail API with OAUTH2 responds with Invalid credentials (Failure) when logging in

I am trying to connect to the Gmail api using the Gmail for Ruby gem. I'm following this google oauth2 guide for installed applications.
I have set my app up in the Google Developer's Console, I am able to send a request with my client_id and client_secret to obtain an authorization code. I am then able to send a request with my authorization code to obtain an access token and a refresh token. I am able to successfully send a request to refresh my access token, and it returns a new access token.
The problem arises when I try to connect to Gmail. First I set an instance variable #gmail = Gmail.connect(:xoauth2, #email, #client.access_token). Then, I attempt to login with #gmail.login(true). However, when I try that, I get the following error:
Couldn't login to given Gmail account: caiden.robinson35#gmail.com (Invalid credentials (Failure)) (Gmail::Client::AuthorizationError)
I am at a loss here, everything suggests I'm executing the oauth2 flow correctly, except the fact that when it comes time to login, I get invalid credentials. When generating my authorization code, I specifically click my email and allow my app to have access. The API is also enabled in my developers console. Here is the full code:
class GmailClient
def initialize
load_client_info
#email = "caiden.robinson35#gmail.com"
load_and_set_oauth2_tokens
sign_in_gmail
binding.pry
end
private
def sign_in_gmail
binding.pry
#gmail = Gmail.connect(:xoauth2, #email, #client.access_token)
######################
# RIGHT HERE IS WHERE IT FAIL
######################
#gmail.login true
binding.pry
end
def load_client_info
gmail_credentials = YAML.load_file('config/gmail.yml')
#client_id = gmail_credentials["client_id"]
#client_secret = gmail_credentials["client_secret"]
#redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
end
def load_and_set_oauth2_tokens use_cached_tokens = true
if use_cached_tokens && File.exist?("config/tokens.yml")
token_hash = YAML.load_file('config/tokens.yml')
#authorization_code = { code: token_hash["authorization_code"],
is_cached: true }
#client = signet_client(token_hash)
#token_hash = #client.refresh!
else
if !(instance_variable_defined?("#authorization_code") && #authorization_code[:is_cached] == false)
retrieve_and_set_authorization_code
end
#token_hash = set_client_and_retrieve_oauth2_tokens
end
write_tokens_to_file
end
def retrieve_and_set_authorization_code
puts "Go to the following url to enable the gmail cli app:"
puts "https://accounts.google.com/o/oauth2/auth?scope=email&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&client_id=#{#client_id}"
print "Paste your authorization code here: "
#authorization_code = { code: gets.chomp,
is_cached: false }
end
def set_client_and_retrieve_oauth2_tokens
#client = signet_client
#client.fetch_access_token!
end
def signet_client token_hash = nil
client = Signet::OAuth2::Client.new(
client_id: #client_id,
client_secret: #client_secret,
redirect_uri: #redirect_uri,
scope: 'email',
token_credential_uri: 'https://www.googleapis.com/oauth2/v4/token'
)
if token_hash.present?
client.refresh_token = token_hash["refresh_token"]
else
client.authorization_uri = 'https://accounts.google.com/o/oauth2/auth'
client.code = #authorization_code[:code]
end
client
end
def write_tokens_to_file
if File.exist?("config/tokens.yml")
data = YAML.load_file "config/tokens.yml"
#token_hash.each { |k, v| data[k] = v }
File.open('config/tokens.yml', 'w') do |file|
YAML.dump(data, file)
end
else
File.open('config/tokens.yml', 'w') do |file|
#token_hash.each { |k, v| file.write("#{k}: #{v}\n") }
file.write("authorization_code: #{#authorization_code[:code]}\n")
end
end
end
end
If my question is lacking any info, please just ask, I am eager to solve this.
Scopes matter. Here are right ones:
scope: ['https://mail.google.com/', 'https://www.googleapis.com/auth/userinfo.email' #,'https://www.googleapis.com/auth/gmail.send' - if you'd like to send emails as well]

Resources