Elixir Phoenix Swagger Security Definitions - jwt

I have integrated phoenix_swagger into my backend. I am autogenerating my swagger doc UI based off my controllers and using it to interactively test my endpoints.
Nonetheless, my routes are secured with Bearer JWTs. I am trying to figure out how to define authorization headers in phoenix_swagger with absolutely no luck.
I really appreciate the help Elixir friends!
For a visual:
swagger_path :create_user do
post "/api/v1/users/create"
description "Create a user."
parameters do
user :body, Schema.ref(:Create), "User to save", required: true
end
response 200, "Success"
end
def create_user(conn, query_params) do
changeset = User.changeset(%User{}, query_params)
with {:ok, user} <- Repo.insert(changeset),
{:ok, token, _claims} <- Guardian.encode_and_sign(user) do
conn
|> Conn.put_status(201)
|> render("jwt.json", jwt: token)
else
{:error, changeset} ->
conn
|> put_status(400)
|> render(ErrorView, "400.json", %{changeset: changeset})
end
end
Standard Swagger 2.0 JSON Reference:
How can I represent 'Authorization: Bearer <token>' in a Swagger Spec (swagger.json)

Okay, I think I got it! Adding security [%{Bearer: []}] to swagger_path passes the authorization token to the call.
Controller:
...
swagger_path :create_user do
post "/api/v1/users/create"
description "Create a user."
parameters do
user :body, Schema.ref(:Create), "User to save", required: true
end
security [%{Bearer: []}]
response 200, "Success"
end
def create_user(conn, query_params) do
changeset = User.changeset(%User{}, query_params)
with {:ok, user} <- Repo.insert(changeset),
{:ok, token, _claims} <- Guardian.encode_and_sign(user) do
conn
|> Conn.put_status(201)
|> render("jwt.json", jwt: token)
else
{:error, changeset} ->
conn
|> put_status(400)
|> render(ErrorView, "400.json", %{changeset: changeset})
end
end
...
Router:
...
def swagger_info do
%{
info: %{
version: "0.0.1",
title: "Server"
},
securityDefinitions: %{
Bearer: %{
type: "apiKey",
name: "Authorization",
in: "header"
}
}
}
end
...

This is something I need to look into myself. Here are a couple links that may help.
https://github.com/xerions/phoenix_swagger/blob/master/docs/getting-started.md#router
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object

Related

i'm unable to get list of all the users who joined channel through presence in elixir, phoenix

this is a quiz channel and after joining I want to get all the users who joined all the quizzes
quiz channel
def join("quiz:" <> id, _params, socket) do
presence = Nuton.Presence.list("quiz:" <> id)
if presence == %{} do
send(self(), {:after_join_quiz, id})
response = %{message: "you can now listen"}
{:ok, response, socket}
else
quiz = "quiz:#{id}"
%{^quiz => %{metas: metas}} = presence
if Enum.count(metas) > 1 do
{:error, %{reason: "Some other user already accepted the invitation"}}
else
send(self(), {:after_join_quiz, id})
response = %{message: "you can now listen"}
:ok = ChannelWatcher.monitor(:quiz, self(), {__MODULE__, :leave, [id]})
{:ok, response, socket}
end
end
end
def handle_info({:after_join_quiz, id}, socket) do
presence = Presence.list(socket)
if presence == %{} do
{:ok, _} =
Presence.track(socket, "quiz:" <> id, %{
user_name: socket.assigns.current_user.username,
user_id: socket.assigns.current_user.id,
quiz_id: id
})
{:noreply, socket}
else
{:ok, _} =
Presence.track(socket, "quiz:" <> id, %{
user_name: socket.assigns.current_user.username,
user_id: socket.assigns.current_user.id,
quiz_id: id
})
Core.Endpoint.broadcast(
"user:all",
"invitation:decline",
%{message: "Some other user already accepted the invitation"}
)
{:noreply, socket}
end
end
with specific quiz_id I can get all the user who joined the channel but with all I cant is there any issue in my code plz check if it is
Controller
quiz_users = Nuton.Presence.list("quiz:all")
You'd need to cycle through all of the channels, somehow, to get that information. I believe there was a PR to get this info, before, actually, so you can cycle through all.
What would be better: if you have the socket join 2 channels, like a "lobby" or "all" channel and then each individual channel. When you track, you can specify the string for the topic instead of only putting in the socket:
track(socket, "quiz:all", socket.assigns.user.id,%{})

Unable to perform successful Paypal webhook validation

I am working to validate Paypal webhook data but I'm running into an issue where it's always returning a FAILURE for the validation status. I'm wondering if it's because this is all happening in a sandbox environment and Paypal doesn't allow verification for sandbox webhook events? I followed this API doc to implement the call: https://developer.paypal.com/docs/api/webhooks/v1/#verify-webhook-signature
Relevant code (from separate elixir modules):
def call(conn, _opts) do
conn
|> extract_webhook_signature(conn.params)
|> webhook_signature_valid?()
|> # handle the result
end
defp extract_webhook_signature(conn, params) do
%{
auth_algo: get_req_header(conn, "paypal-auth-algo") |> Enum.at(0, ""),
cert_url: get_req_header(conn, "paypal-cert-url") |> Enum.at(0, ""),
transmission_id: get_req_header(conn, "paypal-transmission-id") |> Enum.at(0, ""),
transmission_sig: get_req_header(conn, "paypal-transmission-sig") |> Enum.at(0, ""),
transmission_time: get_req_header(conn, "paypal-transmission-time") |> Enum.at(0, ""),
webhook_id: get_webhook_id(),
webhook_event: params
}
end
def webhook_signature_valid?(signature) do
body = Jason.encode!(signature)
case Request.post("/v1/notifications/verify-webhook-signature", body) do
{:ok, %{verification_status: "SUCCESS"}} -> true
_ -> false
end
end
I get back a 200 from Paypal, which means that Paypal got my request and was able to properly parse it and run it though its validation, but it's always returning a FAILURE for the validation status, meaning that the authenticity of the request couldn't be verified. I looked at the data I was posting to their endpoint and it all looks correct, but for some reason it isn't validating. I put the JSON that I posted to the API (from extract_webhook_signature) into a Pastebin here cause it's pretty large: https://pastebin.com/SYBT7muv
If anyone has experience with this and knows why it could be failing, I'd love to hear.
I solved my own problem. Paypal does not canonicalize their webhook validation requests. When you receive the POST from Paypal, do NOT parse the request body before you go to send it back to them in the verification call. If your webhook_event is any different (even if the fields are in a different order), the event will be considered invalid and you will receive back a FAILURE. You must read the raw POST body and post that exact data back to Paypal in your webhook_event.
Example:
if you receive {"a":1,"b":2} and you post back {..., "webhook_event":{"b":2,"a":1}, ...} (notice the difference in order of the json fields from what we recieved and what we posted back) you will recieve a FAILURE. Your post needs to be {..., "webhook_event":{"a":1,"b":2}, ...}
For those who are struggling with this, I'd like to give you my solution which includes the accepted answer.
Before you start, make sure to store the raw_body in your conn, as described in Verifying the webhook - the client side
#verification_url "https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature"
#auth_token_url "https://api-m.sandbox.paypal.com/v1/oauth2/token"
defp get_auth_token do
headers = [
Accept: "application/json",
"Accept-Language": "en_US"
]
client_id = Application.get_env(:my_app, :paypal)[:client_id]
client_secret = Application.get_env(:my_app, :paypal)[:client_secret]
options = [
hackney: [basic_auth: {client_id, client_secret}]
]
body = "grant_type=client_credentials"
case HTTPoison.post(#auth_token_url, body, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
%{"access_token" => access_token} = Jason.decode!(body)
{:ok, access_token}
error ->
Logger.error(inspect(error))
{:error, :no_access_token}
end
end
defp verify_event(conn, auth_token, raw_body) do
headers = [
"Content-Type": "application/json",
Authorization: "Bearer #{auth_token}"
]
body =
%{
transmission_id: get_header(conn, "paypal-transmission-id"),
transmission_time: get_header(conn, "paypal-transmission-time"),
cert_url: get_header(conn, "paypal-cert-url"),
auth_algo: get_header(conn, "paypal-auth-algo"),
transmission_sig: get_header(conn, "paypal-transmission-sig"),
webhook_id: Application.get_env(:papervault, :paypal)[:webhook_id],
webhook_event: "raw_body"
}
|> Jason.encode!()
|> String.replace("\"raw_body\"", raw_body)
with {:ok, %{status_code: 200, body: encoded_body}} <-
HTTPoison.post(#verification_url, body, headers),
{:ok, %{"verification_status" => "SUCCESS"}} <- Jason.decode(encoded_body) do
:ok
else
error ->
Logger.error(inspect(error))
{:error, :not_verified}
end
end
defp get_header(conn, key) do
conn |> get_req_header(key) |> List.first()
end

Authorization graphql subscriptions with elixir and absinthe using cookies

I'm trying make authorization/authentication graphql subscriptions with elixir and absinthe using cookies and I used the follow link:
https://nts.strzibny.name/graphql-subscriptions-with-elixir-and-absinth/
I'm trying authenticate the user for subscribe the right topic but I don't have access to the cookies in the subscription connection. Why?
After I saw the follow link:
https://hexdocs.pm/absinthe_phoenix/Absinthe.Phoenix.Socket.html
And in my user_socket.ex I pass the user_id as query param, this works, but it's not secure at all... I can pass the id that I want ??!!
Can someone help me?
#moduledoc false
use Phoenix.Socket
use Absinthe.Phoenix.Socket,
schema: MyAppGraphQL.Schema
## Channels
# channel "room:*", MyAppWeb.RoomChannel
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error`.
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
def connect(%{"user_id" => user_id}, socket) do
case current_user(user_id) do
nil ->
:error
current_user ->
socket =
Absinthe.Phoenix.Socket.put_options(socket,
context: %{
current_user: current_user
}
)
{:ok, socket}
end
end
def connect(_, _), do: :error
defp current_user(user_id), do: MyApp.Accounts.lookup_user_with_company(user_id)
# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# MyAppWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
def id(_socket), do: nil
end```

getting NoAuthorization Header Missing Exception while using flask-jwt-extended

When I try this example and if the jet token is not provided by header I get error:
{
"msg": "Missing cookie \"access_token_cookie\""
}
example:
from flask import Flask, jsonify, request
from flask_jwt_extended import (
JWTManager, jwt_required, create_access_token,
jwt_refresh_token_required, create_refresh_token,
get_jwt_identity, set_access_cookies,
set_refresh_cookies, unset_jwt_cookies
)
from flask_jwt_extended.config import config
# NOTE: This is just a basic example of how to enable cookies. This is
# vulnerable to CSRF attacks, and should not be used as is. See
# csrf_protection_with_cookies.py for a more complete example!
app = Flask(__name__)
# Configure application to store JWTs in cookies. Whenever you make
# a request to a protected endpoint, you will need to send in the
# access or refresh JWT via a cookie.
app.config['JWT_TOKEN_LOCATION'] = ['cookies']
# Set the cookie paths, so that you are only sending your access token
# cookie to the access endpoints, and only sending your refresh token
# to the refresh endpoint. Technically this is optional, but it is in
# your best interest to not send additional cookies in the request if
# they aren't needed.
app.config['JWT_ACCESS_COOKIE_PATH'] = '/api/'
app.config['JWT_REFRESH_COOKIE_PATH'] = '/token/refresh'
# Disable CSRF protection for this example. In almost every case,
# this is a bad idea. See examples/csrf_protection_with_cookies.py
# for how safely store JWTs in cookies
app.config['JWT_COOKIE_CSRF_PROTECT'] = False
# Set the secret key to sign the JWTs with
app.config['JWT_SECRET_KEY'] = 'super-secret' # Change this!
jwt = JWTManager(app)
# Use the set_access_cookie() and set_refresh_cookie() on a response
# object to set the JWTs in the response cookies. You can configure
# the cookie names and other settings via various app.config options
#app.route('/token/auth', methods=['POST'])
def login():
# username = request.json.get('username', None)
# password = request.json.get('password', None)
# if username != 'test' or password != 'test':
# return jsonify({'login': False}), 401
# print dir(config)
# Create the tokens we will be sending back to the user
access_token = create_access_token(identity="test")
refresh_token = create_refresh_token(identity="test")
# Set the JWT cookies in the response
resp = jsonify({'login': True, "cookie_key": config.access_cookie_name, "cooke_value": access_token})
set_access_cookies(resp, access_token)
set_refresh_cookies(resp, refresh_token)
return resp, 200
# Same thing as login here, except we are only setting a new cookie
# for the access token.
#app.route('/token/refresh', methods=['POST'])
#jwt_refresh_token_required
def refresh():
# Create the new access token
current_user = get_jwt_identity()
access_token = create_access_token(identity=current_user)
# Set the JWT access cookie in the response
resp = jsonify({'refresh': True})
set_access_cookies(resp, access_token)
return resp, 200
# Because the JWTs are stored in an httponly cookie now, we cannot
# log the user out by simply deleting the cookie in the frontend.
# We need the backend to send us a response to delete the cookies
# in order to logout. unset_jwt_cookies is a helper function to
# do just that.
#app.route('/token/remove', methods=['POST'])
def logout():
resp = jsonify({'logout': True})
unset_jwt_cookies(resp)
return resp, 200
# We do not need to make any changes to our protected endpoints. They
# will all still function the exact same as they do when sending the
# JWT in via a header instead of a cookie
#app.route('/api/example', methods=['GET'])
#jwt_required
def protected():
username = get_jwt_identity()
return jsonify({'hello': 'from {}'.format(username)}), 200
if __name__ == '__main__':
app.run(debug=True)
But in my office I have similar setup except I am not calling
username = get_jwt_identity()
I get NoAuthorization exception get raised.
how does this work ...
It's mean you not login and flask-jwt can't find any token on your cookies.
Do you login before call this resource?
check your cookie that returned from app.
In my case it was CORS error, I was using a different api address from the website

ChangeSet Ecto.Model all fields of Struct are nil

I'm working on some Phoenix framework and i have encountered a weird problem (as usual). Whenever I try to create some Users, i get User with all fields set to nil. I'm using Mongo.Ecto/
def post_login(conn, %{"login" => login, "password" => password}) do
# IO.inspect Plug.Conn.read_body(conn)
a = User.changeset(%User{}, %{"login" => "login", "password" => "password"})
IO.inspect a
Repo.insert( a )
redirect conn, to: "/default"
end
And the model:
defmodule HelloWorld.User do
use HelloWorld.Web, :model
#primary_key {:id, :binary_id, autogenerate: true}
schema "users" do
field :login, :string
field :password, :string
end
#required_fields ~w()
#optional_fields ~w()
def changeset(model, params \\ :empty) do
model
|> cast(params, #required_fields, #optional_fields)
end
end
And the screen from console:
As you can see in the picture, both login and password fields are nils which makes me feel I've done something incredibly stupid.
The fields need to exist in the options to the cast/4 function:
#required_fields ~w()
#optional_fields ~w(login password)
def changeset(model, params \\ :empty) do
model
|> cast(params, #required_fields, #optional_fields)
end
Anything that is in required_fields but not in the params will add an error to that field on the changeset. If you want the fields to be required just move them to the required_fields list.