Skip to content

Latest commit

 

History

History
420 lines (298 loc) · 11.1 KB

0.14.to.1.0.md

File metadata and controls

420 lines (298 loc) · 11.1 KB

Upgrade guide 0.14.x to 1.0

The move from 0.14.x to 1.0 is significant behind the scenes. Although we've tried to keep backwards compatibility where we could, in order to move Guardian forward we've had to make some breaking changes. Guardian is no longer constrained to using only JWT. Although it provides all the JWT behaviour out of the box that it used to, you can now add your own types of tokens.

Dependencies on Phoenix and Plug have been made optional so that Guardian can be used in a more stand-alone manner.

Implementation

In 0.14.x Guardian was a single, globally unique implementation. In 1.0 you'll need to define a module where all your authentication related items live. You'll also interact with this module rather than directly with Guardian (unless you're a library developer).

The Implementation module is the serializer and provides a place for you to put Hooks. To create the implementation:

defmodule MyApp.Guardian do
  use Guardian, otp_app: :my_app

  # ...
end

Configuration

Configuration has not changed a lot, but the way that you can define config values has become a lot more useful.

Configuration values can be given using the following types:

  • {MyModule, :func, [:some, :args]} Calls the function on the module with args
  • any other value

The old values (assuming you're using JWT) are still present, but there are a few new options.

  • token_verify_module - The module to verify claims. Default Guardian.Token.Jwt.Verify
  • token_ttl - A map of %{<token_type> => <ttl_value>}

Rather than setting configuration for Guardian directly, use your implementation module.

0.14.x

config :guardian, Guardian,
  # options

1.0

config :my_app, MyApp.Guardian,
  # options

Serializers

In 0.14.x you defined a serializer. The two functions from_token, and for_token have been renamed and moved. In 0.14.x you'd create a serializer with these two functions.

The new functions are subject_for_token and resource_from_claims. These functions are more clearly named and receive more complete arguments.

defmodule MyApp.Guardian do
  use Guardian, otp_app: :my_app

  def subject_for_token(resource, _claims) do
    {:ok, to_string(resource.id)}
  end

  def subject_for_token(_, _) do
    {:error, :reason_for_error}
  end

  def resource_from_claims(claims) do
    {:ok, find_me_a_resource(claims["sub"])}
  end
  def resource_from_claims(_claims) do
    {:error, :reason_for_error}
  end
end

The options for the use call require an :otp_app specification. You can also include your configuration that does not change across environments here if you choose.

Creating tokens

Was:

Guardian.encode_and_sign(resource, [token_type, claims])
Guardian.decode_and_verify(token, [claims_to_check])

Becomes:

MyApp.Guardian.encode_and_sign(resource, [claims, options])
MyApp.Guardian.decode_and_verify(token, [claims_to_check, options])

Where options are specified in the Guardian.Token.Jwt module

Token types

0.14.x

Guardian.encode_and_sign(resource, "other_type")

1.0

MyApp.Guardian.encode_and_sign(resource, %{}, token_type: "other_type")

Setting TTL

0.14.x

Guardian.encode_and_sign(resource, %{ttl: {1, :hour}})

1.0

MyApp.Guardian.encode_and_sign(resource, %{}, token_ttl: {1, :hour})

Custom Secrets

0.14.x

Guardian.encode_and_sign(resource, %{secret: "some_secret"})

1.0

MyApp.Guardian.encode_and_sign(resource, %{}, secret: resolvable_config_value)

Exchange

0.14.x

    {:ok, new_token, new_claims} = Guardian.exchange(old_token, "refresh", "access")

1.0

  {:ok, {old_token, old_claims}, {new_token, new_claims}} =
    MyApp.Guardian.exchange(old_token, ["refresh"], "access")

Refresh

0.14.x

    {:ok, new_token, new_claims} = Guardian.refresh!(old_token, claims_to_check, options)

1.0

  {:ok, {old_token, old_claims}, {new_token, new_claims}} =
    MyApp.Guardian.refresh(old_token, opts)

Revoke

0.14.x

  :ok = Guardian.revoke!(token, claims, opts)

1.0

  {:ok, old_claims} = MyApp.Guardian.revoke(old_token, opts)

Sign In/Sign Out

When working with plugs, sign_in/sign_out are still with us but instead of using the Guardian Module directly use your own implementation

0.14.x

conn = Guardian.Plug.sign_in(conn, resource, [token_type, claims])
conn = Guardian.Plug.sign_out(conn, token, [claims_to_check])

1.0

conn = MyApp.Guardian.Plug.sign_in(conn, resource, [claims, opts])
conn = MyApp.Guardian.Plug.sign_out(conn, resource, [claims_to_check, opts])

Pipelines

To use Guardian with Plugs you'll now need to define a pipeline. A pipeline is a plug that

  1. Sets the implementation module to use
  2. Sets the error handler to use
  3. Adds the plug pipeline to use
defmodule MyApp.Guardian.AuthPipeline do
  @claims %{typ: "access"}

  use Guardian.Plug.Pipeline, otp_app: :my_app,
                              module: MyApp.Guardian,
                              error_handler: MyApp.Guardian.AuthErrorHandler

  plug Guardian.Plug.VerifySession, claims: @claims
  plug Guardian.Plug.VerifyHeader, claims: @claims, realm: "Bearer"
  plug Guardian.Plug.EnsureAuthenticated
  plug Guardian.Plug.LoadResource, ensure: true
end

Use this as a plug to:

  1. Lookup the token in either the session or header
  2. Verify the token, it must be of type "access"
  3. Ensure we found a token
  4. Load the resource for the token and make sure one was found

You'd use this in your Phoenix router.

Error handler

The error handler modules have had a makeover. Previously when you used one of Guardians plugs you would specify a module with a number of callbacks

0.14.x

  • already_authenticated(Plug.Conn.t, map) :: Plug.Conn.t
  • unauthenticated(Plug.Conn.t, map) :: Plug.Conn.t
  • unauthorized(Plug.Conn.t, map) :: Plug.Conn.t
  • no_resource(Plug.Conn.t, map) :: Plug.Conn.t

In 1.0 error handler modules have been simplified. There is now a single function required

auth_error(conn, {failure_type, reason}, opts)

The failure types that come out of the box are:

  • :invalid_token
  • :unauthorized
  • :unauthenticated
  • :already_authenticated
  • :no_resource_found

Custom Claims

0.14.x required any custom claims to be specified when you create the token either with sign_in or encode_and_verify. 1.0 still allows specifying literal claims inline but also provides a hook in your implementation module.

defmodule MyApp.Guardian do
  use Guardian, otp_app: :my_app

  def build_claims(claims, resource, opts) do
    # opts is passed directly from the caller
    new_claims = do_things_to_claims(claims)
    {:ok, claims}
  end
end

Hooks

The hooks module has been removed. Rather than this there are callbacks defined on your implementation. These are noops by default. See the Guardian module documentation for more information.

  • after_encode_and_sign
  • after_sign_in
  • before_sign_out
  • on_verify
  • on_revoke
  • on_refresh
  • on_exchange
  • build_claims
  • build_claims

Sockets/Channels

The macros that were provided for Phoenix sockets and channels have been removed. There is now only a lightweight set of functions to help store resource/claims/token on the socket.

  • Guardian.Phoenix.Socket.authenticated? - check if the socket has been authenticated
  • Guardian.Phoenix.Socket.authenticate - Sign in a resource to a socket. Sets token/claims/resource on the socket.
  • Guardian.Phoenix.Socket.current_claims
  • Guardian.Phoenix.Socket.current_token
  • Guardian.Phoenix.Socket.current_resource

0.14.x

defmodule MyApp.UserSocket do
  use Phoenix.Socket
  use Guardian.Phoenix.Socket
  
  ## Channels
  channel "user:*", MyApp.UserChannel, timeout: 45_000

  ## Transports
  transport :websocket, Phoenix.Transports.WebSocket
  # transport :longpoll, Phoenix.Transports.LongPoll

  def connect(_params, _socket) do
    :error
  end

  def id(socket), do: "users_socket:#{current_resource(socket).id}"
end

1.0

defmodule MyApp.UserSocket do
  use Phoenix.Socket

  ## Channels
  channel "user:*", MyApp.UserChannel, timeout: 45_000

  ## Transports
  transport :websocket, Phoenix.Transports.WebSocket
  # transport :longpoll, Phoenix.Transports.LongPoll

  def connect(%{"guardian_token" => token}, socket) do
    case Guardian.Phoenix.Socket.authenticate(socket, MyApp.Guardian, token) do
      {:ok, authed_socket} ->
        {:ok, authed_socket}

      {:error, _} ->
        :error
    end
  end
  def connect(_params, _socket) do
    :error
  end

  def id(socket), do: "users_socket:#{Guardian.Phoenix.Socket.current_resource(socket).id}"
end

Permissions

Permissions are not always active anymore. These have become optional and renamed to be more clear.

See Guardian.Permissions.Bitwise for more information

To use permissions (Bitwise), in your implementation module use the bitwise module and then utilize the build_claims hook.

Permissions can be defined as a list - using positional bits, or via a Map where the value is the bit location.

  defmodule MyApp.Guardian do

    use Guardian, otp_app: :my_app,
                           permissions: %{
                           user_actions: %{
                             books: 0b1,
                             fitness: 0b100,
                             music: 0b1000,
                           }
                         }

    use Guardian.Permissions.Bitwise

    # snip

    def build_claims(claims, _resource, opts) do
      claims =
        claims
        |> encode_permissions_into_claims!(Keyword.get(opts, :permissions))
      {:ok, claims}
    end
  end
end

In this case, when creating the token the options are passed to build claims as-is. So to call this it would look like.

MyApp.Guardian.encode_and_sign(resource, %{}, permissions: %{user_actions: [:books, :fitness]})

To interrogate permissions:

# Get the encoded permissions from the claims
found_perms = MyApp.Guardian.decode_permissions_from_claims(claims)

# Check if all permissions are present
has_all_these_things? =
  claims
  |> MyApp.Guardian.decode_permissions_from_claims(claims)
  |> MyApp.Guardain.all_permissions?(%{user_actions: [:books]})

# Checks if any permissions are present
show_any_media_things? =
  claims
  |> MyApp.Guardain.decode_permissions_from_claims(claims)
  |> MyApp.Guardian.any_permissions?(%{user_actions: [:books, :fitness, :music]})

Usage with Plug

The Guardian.Plug.EnsurePermissions Plug has been removed. Instead use the Guardian.Plug.Bitwise module. This must be used downstream of a Guardian Pipeline

  plug Guardian.Permissions.Bitwise, ensure: %{user_actions: [:books]}

A one_of feature is supported where you can give a list of possible permission configurations. If one of them matches, the permission will be considered granted.

Allow the request to continue when the token contains any of the permission sets specified

plug Guardian.Permissions.Bitwise, one_of: [
  %{default: [:public_profile], user_actions: [:books]},
  %{default: [:public_profile], user_actions: [:music]},
]