-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
344 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |