Speakeasy
August 6, 2021 ยท View on GitHub
Speakeasy is authentication agnostic middleware based authorization for Absinthe GraphQL powered by Bodyguard.
Installation
Speakeasy can be installed
by adding speakeasy to your list of dependencies in mix.exs:
def deps do
[
{:speakeasy, "~> 0.3"}
]
end
Configuration
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.
Usage
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_useris 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
Middleware
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
LoadResourceusing the:idin the Absinthe arguments. -
Speakeasy.LoadResourceBy - A convenience middleware to
LoadResourceusing 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.Plug
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