Speakeasy is authentication agnostic middleware based authorization for Absinthe GraphQL powered by Bodyguard.
Speakeasy can be installed
by adding speakeasy
to your list of dependencies in mix.exs
:
def deps do
[
{:speakeasy, "~> 0.3"}
]
end
Configuration can be done in each Absinthe middleware call, but you can set global defaults as well.
config :speakeasy,
user_key: :current_user, # the key the current user will be under in the GraphQL context
authn_error_message: :unauthenticated # default authentication failure message
Note: no authz_error_message
is provided because it is set from Bodyguard.
tl;dr: A full example authentication, authorizing, loading, and resolving an Absinthe schema:
This example assumes:
- You are authorizing a standard phoenix context
- You already have a bodyguard policy
- Your
:current_user
is already in the Absinthe context or you are usingSpeakeasy.Plug
defmodule MyApp.Schema.PostTypes do
use Absinthe.Schema.Notation
alias Spectra.Posts
object :post do
field(:id, non_null(:id))
field(:name, non_null(:string))
end
object :post_mutations do
@desc "Create post"
field :create_post, type: :post do
arg(:name, non_null(:string))
middleware(Speakeasy.Authn)
middleware(Speakeasy.Authz, {Posts, :create_post})
middleware(Speakeasy.Resolve, &Posts.create_post/2)
middleware(MyApp.Middleware.ChangesetErrors) # :D
end
@desc "Update post"
field :update_post, type: :post do
arg(:name, non_null(:string))
middleware(Speakeasy.Authn)
middleware(Speakeasy.Authz, {Posts, :update_post})
middleware(Speakeasy.Resolve, &Posts.update_post/3)
middleware(MyApp.Middleware.ChangesetErrors) # :D
end
end
object :post_queries do
@desc "Get posts"
field :posts, list_of(:post) do
middleware(Speakeasy.Authn)
middleware(Speakeasy.Resolve, fn(attrs, user) -> MyApp.Posts.search(attrs, user) end)
end
@desc "Get post"
field :post, type: :post do
arg(:id, non_null(:string))
middleware(Speakeasy.Authn)
middleware(Speakeasy.LoadResourceByID, &Posts.get_post/1)
middleware(Speakeasy.Authz, {Posts, :get_post})
middleware(Speakeasy.Resolve)
end
end
end
And of course you can use Absinthe's resolve function as well:
@desc "Get post"
field :post, type: :post do
arg(:id, non_null(:string))
middleware(Speakeasy.Authn)
middleware(Speakeasy.LoadResourceByID, &Posts.get_post/1)
middleware(Speakeasy.Authz, {Posts, :get_post})
resolve(fn(_parent, _args, ctx) ->
{:ok, ctx[:speakeasy].resource}
end)
end
Speakeasy is a collection of Absinthe middleware:
-
Speakeasy.Authn - Resolution middleware for Absinthe.
-
Speakeasy.LoadResource - Loads a resource into the speakeasy context.
-
Speakeasy.LoadResourceById - A convenience middleware to
LoadResource
using the:id
in the Absinthe arguments. -
Speakeasy.LoadResourceBy - A convenience middleware to
LoadResource
using a value from the attrs with the given key in the Absinthe arguments. -
Speakeasy.AuthZ - Authorization middleware for Absinthe.
-
Speakeasy.Resolve - Resolution middleware for Absinthe.
Speakeasy includes a Plug for loading the current user into the Absinthe context. It isn't required if you already have a method for loading the user into your Absinthe context.
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :graphql do
plug(Speakeasy.Plug, load_user: &MyApp.Users.whoami/1, user_key: :current_user)
end
end