Skip to content

Instantly share code, notes, and snippets.

@andrewhao
Last active June 29, 2016 22:47
Show Gist options
  • Save andrewhao/bb20b9a714e71ceac4880fea0628781a to your computer and use it in GitHub Desktop.
Save andrewhao/bb20b9a714e71ceac4880fea0628781a to your computer and use it in GitHub Desktop.
Rails and Phoenix Session Sharing
# elixir_buildpack.config
config_vars_to_export=(SECRET_KEY_BASE SESSION_ENCRYPTED_COOKIE_SALT SESSION_ENCRYPTED_SIGNED_COOKIE_SALT DOMAIN)
%{"_csrf_token" => "ELeSt4MBUINKi0STEBpslw3UevGZuVLUx5zGVP5NlQU=",
"session_id" => "17ec9b696fe76ba4a777d625e57f3521",
"warden.user.user.key" => [[2], "$2a$10$R/3NKl9KQViQxY8eoMCIp."]}
# lib/phoenix_app/endpoint.ex
defmodule PhoenixApp.Endpoint do
plug Plug.Session,
# Remove the original cookie store that comes with Phoenix, out of the box.
# store: :cookie,
# key: "_phoenix_app_key",
# signing_salt: "M8emDP0h"
store: PlugRailsCookieSessionStore,
# Decide on a shared key for your cookie. Oftentimes, this should
# mirror your Rails app session key
key: "_rails_app_session",
secure: true,
encrypt: true,
# Specifies the matching rules on the hostname that this cookie will be valid for
domain: ".#{System.get_env("DOMAIN")}",
signing_salt: System.get_env("SESSION_ENCRYPTED_SIGNED_COOKIE_SALT"),
encryption_salt: System.get_env("SESSION_ENCRYPTED_COOKIE_SALT"),
key_iterations: 1000,
key_length: 64,
key_digest: :sha,
# Specify a JSON serializer to use on the session
serializer: Poison
end
# mix.exs
defmodule PhoenixApp
defp deps do
# snip
{:plug_rails_cookie_session_store, "~> 0.1"},
# snip
end
end
# web/models/session.ex
defmodule PhoenixApp.Session do
use PhoenixApp.Web, :controller
def current_user(conn) do
# Our app's concept of a User is merely whatever is stored in the
# Session key. In the future, we could then use this as the delegation
# point to fetch more details about the user from a backend store.
case load_user(conn) do
{:ok, user_id} -> user_id
{:error, :not_found} -> nil
end
end
def logged_in?(conn) do
!!current_user(conn)
end
def load_user(conn) do
# => The Warden user storage scheme: [user_id, password_hash_truncated]
# [[1], "$2a$10$vnx35UTTJQURfqbM6srv3e"]
warden_key = conn |> get_session("warden.user.user.key")
case warden_key do
[[user_id], _] -> {:ok, user_id}
_ -> {:error, :not_found}
end
end
end
# web/models/session.ex
defmodule PhoenixApp.Session do
use PhoenixApp.Web, :controller
alias PhoenixApp.User
def current_user(conn) do
case load_user(conn) do
# Changed current_user/1 to now return a User or a nil.
{:ok, user_id} -> user_id |> User.fetch
{:error, :not_found} -> nil
end
end
# snip
end
# config/initializer/session_store.rb
# Use cookie session storage in JSON format. Here, we scope the cookie to the root domain.
Rails.application.config.session_store :cookie_store, key: '_rails_app_session', domain: ".#{ENV['DOMAIN']}"
Rails.application.config.action_dispatch.cookies_serializer = :json
# These salts are optional, but it doesn't hurt to explicitly configure them the same between the two apps.
Rails.application.config.action_dispatch.encrypted_cookie_salt = ENV['SESSION_ENCRYPTED_COOKIE_SALT']
Rails.application.config.action_dispatch.encrypted_signed_cookie_salt = ENV['SESSION_ENCRYPTED_SIGNED_COOKIE_SALT']
# web/controllers/some_api_resource_controller.ex
defmodule PhoenixApp.SomeApiResourceController do
use PhoenixApp.Web, :controller
def index(conn, _params) do
{:ok, user_id} = load_user(conn)
conn
|> assign(:user_id, user_id)
|> render("index.html")
end
plug :verify_session
# If we've found a user, then allow the request to continue.
# Otherwise, halt the request and return a 401
defp verify_session(conn, _) do
case load_user(conn) do
{:ok, user_id} -> conn
{:error, _} -> conn |> send_resp(401, "Unauthorized") |> halt
end
end
defp load_user(conn) do
# => The Warden user storage scheme: [user_id, password_hash_truncated]
# [[1], "$2a$10$vnx35UTTJQURfqbM6srv3e"]
warden_key = conn |> get_session("warden.user.user.key")
case warden_key do
[[user_id], _] -> {:ok, user_id}
_ -> {:error, :not_found}
end
end
end
# web/controllers/some_api_resource_controller.ex
defmodule PhoenixApp.SomeApiResourceController do
use PhoenixApp.Web, :controller
alias PhoenixApp.Session
def index(conn, _params) do
IO.inspect conn.private.plug_session
user_id = Session.current_user(conn)
conn
|> assign(:user_id, user_id)
|> render("index.html")
end
plug :verify_session
# Future refinements could extract this into its own Plug file.
defp verify_session(conn, _) do
case Session.logged_in?(conn) do
false -> conn |> send_resp(401, "Unauthorized") |> halt
_ -> conn
end
end
end
# web/models/user.ex
defmodule PhoenixApp.User do
# Gets some user identity information like email, avatar image.
# For this example, we'll use a random user generator.
#
# This example hits an API, but this could just as easily be something that hits
# the database, or Redis, or some cache.
def fetch(user_id) do
%{ body: body } = HTTPotion.get("https://randomuser.me/api?seed=#{user_id}")
[result | _ ] = body |> Poison.decode! |> Map.get("results")
result
end
end
# web/web.ex
defmodule PhoenixApp.Web do
def view do
quote do
# snip
import PhoenixApp.Session
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment