Skip to content

Commit

Permalink
Add Steam Strategy.
Browse files Browse the repository at this point in the history
  • Loading branch information
IanLuites committed May 10, 2017
1 parent a5d8eea commit f9167d6
Show file tree
Hide file tree
Showing 2 changed files with 344 additions and 0 deletions.
157 changes: 157 additions & 0 deletions lib/ueberauth/strategy/steam.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
defmodule Ueberauth.Strategy.Steam do
@moduledoc ~S"""
Steam OpenID for Überauth.
"""

use Ueberauth.Strategy

alias Ueberauth.Auth.Info
alias Ueberauth.Auth.Extra

@doc ~S"""
Handles initial request for Steam authentication.
Redirects the given `conn` to the Steam login page.
"""
@spec handle_request!(Plug.Conn.t) :: Plug.Conn.t
def handle_request!(conn) do
query =
%{
"openid.mode" => "checkid_setup",
"openid.realm" => callback_url(conn),
"openid.return_to" => callback_url(conn),
"openid.ns" => "http://specs.openid.net/auth/2.0",
"openid.claimed_id" => "http://specs.openid.net/auth/2.0/identifier_select",
"openid.identity" => "http://specs.openid.net/auth/2.0/identifier_select",
}
|> URI.encode_query

redirect!(conn, "https://steamcommunity.com/openid/login?" <> query)
end

@doc ~S"""
Handles the callback from Steam.
"""
@spec handle_callback!(Plug.Conn.t) :: Plug.Conn.t
def handle_callback!(conn = %Plug.Conn{params: %{"openid.mode" => "id_res"}}) do
params = conn.params

[valid, user] =
[ # Validate and retrieve the steam user at the same time.
fn -> validate_user(params) end,
fn -> retrieve_user(params) end,
]
|> Enum.map(&Task.async/1)
|> Enum.map(&Task.await/1)

case valid && !is_nil(user) do
true ->
conn
|> put_private(:steam_user, user)
false ->
set_errors!(conn, [error("invalid_user", "Invalid steam user")])
end
end

@doc false
def handle_callback!(conn) do
set_errors!(conn, [error("invalid_openid", "Invalid openid response received")])
end

@doc false
@spec handle_cleanup!(Plug.Conn.t) :: Plug.Conn.t
def handle_cleanup!(conn) do
conn
|> put_private(:steam_user, nil)
end

@doc ~S"""
Fetches the uid field from the response.
Takes the `steamid` from the `steamuser` saved in the `conn`.
"""
@spec uid(Plug.Conn.t) :: pos_integer
def uid(conn) do
conn.private.steam_user.steamid |> String.to_integer
end

@doc ~S"""
Fetches the fields to populate the info section of the `Ueberauth.Auth` struct.
Takes the information from the `steamuser` saved in the `conn`.
"""
@spec info(Plug.Conn.t) :: Info.t
def info(conn) do
user = conn.private.steam_user

%Info{
image: user.avatar,
name: user.realname,
location: user.loccountrycode,
urls: %{
Steam: user.profileurl,
}
}
end

@doc ~S"""
Stores the raw information obtained from the Steam callback.
Returns the `steamuser` saved in the `conn` as `raw_info`.
"""
@spec extra(Plug.Conn.t) :: Extra.t
def extra(conn) do
%Extra{
raw_info: %{
user: conn.private.steam_user
}
}
end

@spec retrieve_user(map) :: map | nil
defp retrieve_user(%{"openid.claimed_id" => "http://steamcommunity.com/openid/id/" <> id}) do
key =
:ueberauth
|> Application.fetch_env!(Ueberauth.Strategy.Steam)
|> Keyword.get(:api_key)
url = "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=" <> key <> "&steamids=" <> id

with {:ok, %HTTPoison.Response{body: body}} <- HTTPoison.get(url),
{:ok, user} <- Poison.decode(body, keys: :atoms)
do
List.first(user.response.players)
else
_ -> nil
end
end

@spec validate_user(map) :: boolean
defp validate_user(params) do
query =
params
|> Enum.filter(fn {key, _value} -> String.starts_with?(key, "openid.") end)
|> Enum.into(%{})
|> Map.put("openid.mode", "check_authentication")
|> URI.encode_query

case HTTPoison.get("https://steamcommunity.com/openid/login?" <> query) do
{:ok, %HTTPoison.Response{body: body, status_code: 200}} ->
String.contains?(body, "is_valid:true\n")
_ ->
false
end
end

# Block undocumented function
@doc false
@spec default_options :: []
def default_options

@doc false
@spec credentials(Plug.Conn.t) :: Ueberauth.Auth.Credentials.t
def credentials(_conn), do: %Ueberauth.Auth.Credentials{}

@doc false
@spec auth(Plug.Conn.t) :: Ueberauth.Auth.t
def auth(conn)
end
187 changes: 187 additions & 0 deletions test/ueberauth/strategy/steam_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
defmodule Ueberauth.Strategy.SteamTest do
use ExUnit.Case, async: false
use Plug.Test

alias Ueberauth.Strategy.Steam

@sample_user %{avatar: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/f3/f3dsf34324eawdasdas3rwe.jpg",
avatarfull: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/f3/f3dsf34324eawdasdas3rwe_full.jpg",
avatarmedium: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/f3/f3dsf34324eawdasdas3rwe_medium.jpg",
communityvisibilitystate: 1, lastlogoff: 234234234, loccityid: 36148,
loccountrycode: "NL", locstatecode: "03", personaname: "Sample",
personastate: 0, personastateflags: 0,
primaryclanid: "435345345", profilestate: 1,
profileurl: "http://steamcommunity.com/id/sample/",
realname: "Sample Sample", steamid: "765309403423",
timecreated: 452342342}
@sample_response %{response: %{players: [@sample_user]}}

describe "handle_request!" do
test "redirects" do
conn = Steam.handle_request! conn(:get, "http://example.com/path")

assert conn.state == :sent
assert conn.status == 302
end

test "redirects to the right url" do
conn = Steam.handle_request! conn(:get, "http://example.com/path")

{"location", location} = List.keyfind(conn.resp_headers, "location", 0)

assert location == "https://steamcommunity.com/openid/login?openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.realm=http%3A%2F%2Fexample.com&openid.return_to=http%3A%2F%2Fexample.com"
end
end

describe "handle_callback!" do
setup do
:meck.new Application, [:passthrough]
:meck.expect Application, :fetch_env!, fn _, _ -> [api_key: "API_KEY"] end

on_exit(fn -> :meck.unload end)

:ok
end

defp callback(params \\ %{}) do
conn = %{conn(:get, "http://example.com/path/callback") | params: params}

Steam.handle_callback! conn
end

test "error for invalid callback parameters" do
conn = callback()

assert conn.assigns == %{
ueberauth_failure: %Ueberauth.Failure{errors: [
%Ueberauth.Failure.Error{message: "Invalid openid response received", message_key: "invalid_openid"}
], provider: nil, strategy: nil}
}
end

test "error for missing user valid information" do
:meck.new HTTPoison, [:passthrough]
:meck.expect HTTPoison, :get, fn
"https://steamcommunity.com/openid/login?openid.claimed_id=http%3A%2F%2Fsteamcommunity.com%2Fopenid%2Fid%2F12345&openid.mode=check_authentication" ->
{:ok, %HTTPoison.Response{body: "", status_code: 200}}
"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=API_KEY&steamids=12345" ->
{:ok, %HTTPoison.Response{body: Poison.encode!(@sample_response), status_code: 200}}
end

conn =
callback(%{
"openid.mode" => "id_res",
"openid.claimed_id" => "http://steamcommunity.com/openid/id/12345"
})

assert conn.assigns == %{
ueberauth_failure: %Ueberauth.Failure{errors: [
%Ueberauth.Failure.Error{message: "Invalid steam user", message_key: "invalid_user"}
], provider: nil, strategy: nil}
}
end

test "error for invalid user callback" do
:meck.new HTTPoison, [:passthrough]
:meck.expect HTTPoison, :get, fn
"https://steamcommunity.com/openid/login?openid.claimed_id=http%3A%2F%2Fsteamcommunity.com%2Fopenid%2Fid%2F12345&openid.mode=check_authentication" ->
{:ok, %HTTPoison.Response{body: "is_valid:false\n", status_code: 200}}
"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=API_KEY&steamids=12345" ->
{:ok, %HTTPoison.Response{body: Poison.encode!(@sample_response), status_code: 200}}
end

conn =
callback(%{
"openid.mode" => "id_res",
"openid.claimed_id" => "http://steamcommunity.com/openid/id/12345"
})

assert conn.assigns == %{
ueberauth_failure: %Ueberauth.Failure{errors: [
%Ueberauth.Failure.Error{message: "Invalid steam user", message_key: "invalid_user"}
], provider: nil, strategy: nil}
}
end

test "error for invalid user data" do
:meck.new HTTPoison, [:passthrough]
:meck.expect HTTPoison, :get, fn
"https://steamcommunity.com/openid/login?openid.claimed_id=http%3A%2F%2Fsteamcommunity.com%2Fopenid%2Fid%2F12345&openid.mode=check_authentication" ->
{:ok, %HTTPoison.Response{body: "is_valid:true\n", status_code: 200}}
"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=API_KEY&steamids=12345" ->
{:ok, %HTTPoison.Response{body: "{{{{{{{", status_code: 200}}
end

conn =
callback(%{
"openid.mode" => "id_res",
"openid.claimed_id" => "http://steamcommunity.com/openid/id/12345"
})

assert conn.assigns == %{
ueberauth_failure: %Ueberauth.Failure{errors: [
%Ueberauth.Failure.Error{message: "Invalid steam user", message_key: "invalid_user"}
], provider: nil, strategy: nil}
}
end

test "success for valid user and valid user data" do
:meck.new HTTPoison, [:passthrough]
:meck.expect HTTPoison, :get, fn
"https://steamcommunity.com/openid/login?openid.claimed_id=http%3A%2F%2Fsteamcommunity.com%2Fopenid%2Fid%2F12345&openid.mode=check_authentication" ->
{:ok, %HTTPoison.Response{body: "is_valid:true\n", status_code: 200}}
"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=API_KEY&steamids=12345" ->
{:ok, %HTTPoison.Response{body: Poison.encode!(@sample_response), status_code: 200}}
end

conn =
callback(%{
"openid.mode" => "id_res",
"openid.claimed_id" => "http://steamcommunity.com/openid/id/12345"
})

assert conn.assigns == %{}
assert conn.private == %{steam_user: @sample_user}
end
end

describe "info retrievers fetch" do
setup do
conn = %{conn(:get, "http://example.com/path/callback") | private: %{steam_user: @sample_user}}

conn = Steam.handle_callback! conn

[conn: conn]
end

test "uid", %{conn: conn} do
assert Steam.uid(conn) == 765309403423
end

test "info", %{conn: conn} do
assert Steam.info(conn) == %Ueberauth.Auth.Info{
image: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/f3/f3dsf34324eawdasdas3rwe.jpg",
location: "NL", name: "Sample Sample",
urls: %{Steam: "http://steamcommunity.com/id/sample/"}}
end

test "extra", %{conn: conn} do
assert Steam.extra(conn) == %Ueberauth.Auth.Extra{raw_info: %{user: @sample_user}}
end

test "credentials", %{conn: conn} do
assert Steam.credentials(conn) == %Ueberauth.Auth.Credentials{}
end
end

test "connection is cleaned up" do
conn = %{conn(:get, "http://example.com/path/callback") | private: %{steam_user: @sample_user}}

conn =
conn
|> Steam.handle_callback!
|> Steam.handle_cleanup!

assert conn.private == %{steam_user: nil}
end
end

0 comments on commit f9167d6

Please sign in to comment.