Skip to content

Commit

Permalink
Merge pull request #11 from yannvery/dynamic-configuration
Browse files Browse the repository at this point in the history
Dynamic configuration
  • Loading branch information
niknetniko authored Nov 22, 2020
2 parents 1eb6b9f + e521316 commit 67b26cd
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 87 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ Central Authentication Service strategy for Überauth.
config :ueberauth, Ueberauth,
providers: [cas: {Ueberauth.Strategy.CAS, [
base_url: "http://cas.example.com",
callback: "http://your-app.example.com/auth/cas/callback",
callback_url: "http://your-app.example.com/auth/cas/callback",
]}]
```

4. Include the Überauth plug in your controller:

```elixir
defmodule MyApp.AuthController do
use MyApp.Web, :controller
Expand Down
5 changes: 0 additions & 5 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
use Mix.Config

config :ueberauth, Ueberauth,
providers: [
cas: {Ueberauth.Strategy.CAS, [base_url: "http://cas.example.com", service: "http://svc.example.com"]}
]
66 changes: 43 additions & 23 deletions lib/ueberauth/strategy/cas.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ defmodule Ueberauth.Strategy.CAS do
providers: [cas: {Ueberauth.Strategy.CAS, [
base_url: "http://cas.example.com",
validation_path: "/serviceValidate",
callback: "http://your-app.example.com/auth/cas/callback",
callback_url: "http://your-app.example.com/auth/cas/callback",
attributes: %{
last_name: "surname"
},
Expand All @@ -99,8 +99,7 @@ defmodule Ueberauth.Strategy.CAS do
Ueberauth `request` handler. Redirects to the CAS server's login page.
"""
def handle_request!(conn) do
conn
|> redirect!(redirect_url(conn))
redirect!(conn, redirect_url(conn))
end

@doc """
Expand Down Expand Up @@ -148,17 +147,18 @@ defmodule Ueberauth.Strategy.CAS do
"""
def info(conn) do
user = conn.private.cas_user
attributes = user.attributes
user_attributes = user.attributes
attribute_mapping = attributes(conn)

%Info{
name: user.name,
email: get_attribute(attributes, :email),
birthday: get_attribute(attributes, :birthday),
description: get_attribute(attributes, :description),
first_name: get_attribute(attributes, :first_name),
last_name: get_attribute(attributes, :last_name),
nickname: get_attribute(attributes, :nickname),
phone: get_attribute(attributes, :phone)
email: get_attribute(attribute_mapping, user_attributes, :email),
birthday: get_attribute(attribute_mapping, user_attributes, :birthday),
description: get_attribute(attribute_mapping, user_attributes, :description),
first_name: get_attribute(attribute_mapping, user_attributes, :first_name),
last_name: get_attribute(attribute_mapping, user_attributes, :last_name),
nickname: get_attribute(attribute_mapping, user_attributes, :nickname),
phone: get_attribute(attribute_mapping, user_attributes, :phone)
}
end

Expand All @@ -174,10 +174,6 @@ defmodule Ueberauth.Strategy.CAS do
}
end

defp redirect_url(conn) do
CAS.API.login_url() <> "?service=#{callback_url(conn)}"
end

defp handle_ticket(conn, ticket) do
conn
|> put_private(:cas_ticket, ticket)
Expand All @@ -186,7 +182,7 @@ defmodule Ueberauth.Strategy.CAS do

defp fetch_user(conn, ticket) do
ticket
|> CAS.API.validate_ticket(conn)
|> CAS.API.validate_ticket(validate_url(conn), callback_url(conn))
|> handle_validate_ticket_response(conn)
end

Expand All @@ -205,19 +201,43 @@ defmodule Ueberauth.Strategy.CAS do
|> put_private(:cas_user, user)
end

defp get_attribute(attributes, key) do
{_, settings} = Application.get_env(:ueberauth, Ueberauth)[:providers][:cas]

name =
Keyword.get(settings, :attributes, %{})
|> Map.get(key, Atom.to_string(key))
defp get_attribute(attribute_mapping, user_attributes, key) do
name = Map.get(attribute_mapping, key, Atom.to_string(key))

value = Map.get(attributes, name)
value = Map.get(user_attributes, name)

if is_list(value) do
Enum.at(value, 0)
else
value
end
end

defp redirect_url(conn) do
login_url(conn) <> "?service=" <> callback_url(conn)
end

defp validate_url(conn) do
base_url(conn) <> validation_path(conn)
end

defp login_url(conn) do
base_url(conn) <> "/login"
end

defp base_url(conn) do
Keyword.get(settings(conn), :base_url)
end

defp validation_path(conn) do
Keyword.get(settings(conn), :validation_path, "/serviceValidate")
end

defp attributes(conn) do
Keyword.get(settings(conn), :attributes, %{})
end

defp settings(conn) do
Ueberauth.Strategy.Helpers.options(conn) || []
end
end
24 changes: 3 additions & 21 deletions lib/ueberauth/strategy/cas/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,14 @@ defmodule Ueberauth.Strategy.CAS.API do
CAS server API implementation.
"""

use Ueberauth.Strategy
alias Ueberauth.Strategy.CAS

import SweetXml

@doc "Returns the URL to this CAS server's login page."
def login_url do
settings(:base_url) <> "/login"
end

@doc "Validate a CAS Service Ticket with the CAS server."
def validate_ticket(ticket, conn) do
HTTPoison.get(validate_url(), [], params: %{ticket: ticket, service: callback_url(conn)})
def validate_ticket(ticket, validate_url, service) do
validate_url
|> HTTPoison.get([], params: %{ticket: ticket, service: service})
|> handle_validate_ticket_response()
end

Expand Down Expand Up @@ -52,17 +47,4 @@ defmodule Ueberauth.Strategy.CAS.API do

{error_code || "unknown_error", message || "Unknown error"}
end

def validate_url do
settings(:base_url) <> validate_path()
end

defp validate_path do
settings(:validation_path) || "/serviceValidate"
end

defp settings(key) do
{_, settings} = Application.get_env(:ueberauth, Ueberauth)[:providers][:cas]
settings[key]
end
end
21 changes: 11 additions & 10 deletions test/ueberauth/strategy/cas/api_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ defmodule Ueberauth.Strategy.CAS.API.Test do

alias Ueberauth.Strategy.CAS.API

test "generates a cas login url" do
assert API.login_url() == "http://cas.example.com/login"
end

test "validates a valid ticket response" do
ok_xml = """
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
Expand All @@ -33,7 +29,7 @@ defmodule Ueberauth.Strategy.CAS.API.Test do
{:ok, %HTTPoison.Response{status_code: 200, body: ok_xml, headers: []}}
end do
{:ok, %Ueberauth.Strategy.CAS.User{name: name}} =
API.validate_ticket("ST-XXXXX", %Plug.Conn{})
API.validate_ticket("ST-XXXXX", "http://cas.example.com/serviceValidate", "service_name")

assert name == "mail@marceldegraaf.net"
end
Expand All @@ -50,7 +46,8 @@ defmodule Ueberauth.Strategy.CAS.API.Test do
get: fn _url, _opts, _params ->
{:ok, %HTTPoison.Response{status_code: 200, body: error_xml, headers: []}}
end do
{:error, {code, message}} = API.validate_ticket("ST-XXXXX", %Plug.Conn{})
{:error, {code, message}} =
API.validate_ticket("ST-XXXXX", "http://cas.example.com/serviceValidate", "service_name")

assert code == "INVALID_TICKET"
assert message == "Ticket 'ST-XXXXX' already consumed"
Expand All @@ -68,7 +65,8 @@ defmodule Ueberauth.Strategy.CAS.API.Test do
get: fn _url, _opts, _params ->
{:ok, %HTTPoison.Response{status_code: 200, body: unknown_error_xml, headers: []}}
end do
{:error, {code, message}} = API.validate_ticket("ST-XXXXX", %Plug.Conn{})
{:error, {code, message}} =
API.validate_ticket("ST-XXXXX", "http://cas.example.com/serviceValidate", "service_name")

assert code == "unknown_error"
assert message == "An unknown error occurred"
Expand All @@ -86,7 +84,8 @@ defmodule Ueberauth.Strategy.CAS.API.Test do
get: fn _url, _opts, _params ->
{:ok, %HTTPoison.Response{status_code: 200, body: unknown_error_xml, headers: []}}
end do
{:error, {code, message}} = API.validate_ticket("ST-XXXXX", %Plug.Conn{})
{:error, {code, message}} =
API.validate_ticket("ST-XXXXX", "http://cas.example.com/serviceValidate", "service_name")

assert code == "CONNECTION_ERROR"
assert message == "Unknown error"
Expand All @@ -104,7 +103,8 @@ defmodule Ueberauth.Strategy.CAS.API.Test do
get: fn _url, _opts, _params ->
{:ok, %HTTPoison.Response{status_code: 200, body: unknown_error_xml, headers: []}}
end do
{:error, {code, message}} = API.validate_ticket("ST-XXXXX", %Plug.Conn{})
{:error, {code, message}} =
API.validate_ticket("ST-XXXXX", "http://cas.example.com/serviceValidate", "service_name")

assert code == "unknown_error"
assert message == "Unknown error"
Expand All @@ -116,7 +116,8 @@ defmodule Ueberauth.Strategy.CAS.API.Test do
get: fn _url, _opts, _params ->
{:ok, %HTTPoison.Response{status_code: 200, body: "blip blob", headers: []}}
end do
{:error, {code, _}} = API.validate_ticket("ST-XXXXX", %Plug.Conn{})
{:error, {code, _}} =
API.validate_ticket("ST-XXXXX", "http://cas.example.com/serviceValidate", "service_name")

assert code == "malformed_xml"
end
Expand Down
95 changes: 69 additions & 26 deletions test/ueberauth/strategy/cas_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,26 @@ defmodule Ueberauth.Strategy.CAS.Test do
alias Ueberauth.Strategy.CAS

setup do
ueberauth_request_options = %{
callback_url: "http://service.com/auth/provider/callback",
options: [
base_url: "http://cas.example.com",
validate_path: "serviceValidate"
]
}

conn = %Plug.Conn{
private: %{
cas_user: %CAS.User{name: "Marcel de Graaf", attributes: %{"email" => "mail@marceldegraaf.net", "roles" => ["developer"], "first_name" => ["Joe", "Example"]}},
cas_ticket: "ST-XXXXX",
ueberauth_request_options: ueberauth_request_options,
cas_user: %CAS.User{
name: "Marcel de Graaf",
attributes: %{
"email" => "mail@marceldegraaf.net",
"roles" => ["developer"],
"first_name" => ["Joe", "Example"]
}
},
cas_ticket: "ST-XXXXX"
}
}

Expand Down Expand Up @@ -39,57 +55,84 @@ defmodule Ueberauth.Strategy.CAS.Test do
conn: conn,
ok_xml: ok_xml,
error_xml: error_xml,
ueberauth_request_options: ueberauth_request_options
}
end

test "redirect callback redirects to login url" do
conn = conn(:get, "/login") |> CAS.handle_request!
test "redirect callback redirects to login url", %{
ueberauth_request_options: ueberauth_request_options
} do
conn =
conn(:get, "/login")
|> Plug.Conn.put_private(:ueberauth_request_options, ueberauth_request_options)
|> CAS.handle_request!()

assert conn.status == 302

assert Plug.Conn.get_resp_header(conn, "location") ==
[
"http://cas.example.com/login?service=http://service.com/auth/provider/callback"
]
end

test "login callback without service ticket shows an error" do
conn = CAS.handle_callback!(%Plug.Conn{params: %{}})
assert Map.has_key?(conn.assigns, :ueberauth_failure)
end

test "successful login callback validates the ticket", %{ok_xml: xml} do
with_mock HTTPoison, [
get: fn(_url, _opts, _params) ->
{:ok, %HTTPoison.Response{status_code: 200, body: xml, headers: []}
} end
] do
conn = CAS.handle_callback!(%Plug.Conn{params: %{"ticket" => "ST-XXXXX"}})
test "successful login callback validates the ticket", %{
ok_xml: xml,
ueberauth_request_options: ueberauth_request_options
} do
with_mock HTTPoison,
get: fn _url, _opts, _params ->
{:ok, %HTTPoison.Response{status_code: 200, body: xml, headers: []}}
end do
conn =
%Plug.Conn{params: %{"ticket" => "ST-XXXXX"}}
|> Plug.Conn.put_private(:ueberauth_request_options, ueberauth_request_options)
|> CAS.handle_callback!()

assert conn.private.cas_ticket == "ST-XXXXX"
assert conn.private.cas_user.name == "mail@marceldegraaf.net"
refute conn.private.cas_user.attributes["isFromNewLogin"]
end
end

test "invalid login callback returns an error", %{error_xml: xml} do
with_mock HTTPoison, [
get: fn(_url, _opts, _params) ->
{:ok, %HTTPoison.Response{status_code: 200, body: xml, headers: []}
} end
] do
conn = CAS.handle_callback!(%Plug.Conn{params: %{"ticket" => "ST-XXXXX"}})
test "invalid login callback returns an error", %{
error_xml: xml,
ueberauth_request_options: ueberauth_request_options
} do
with_mock HTTPoison,
get: fn _url, _opts, _params ->
{:ok, %HTTPoison.Response{status_code: 200, body: xml, headers: []}}
end do
conn =
%Plug.Conn{params: %{"ticket" => "ST-XXXXX"}}
|> Plug.Conn.put_private(:ueberauth_request_options, ueberauth_request_options)
|> CAS.handle_callback!()

assert List.first(conn.assigns.ueberauth_failure.errors).message_key == "INVALID_TICKET"
assert List.first(conn.assigns.ueberauth_failure.errors).message == "Ticket 'ST-XXXXX' already consumed"

assert List.first(conn.assigns.ueberauth_failure.errors).message ==
"Ticket 'ST-XXXXX' already consumed"
end
end

test "network error propagates" do
with_mock HTTPoison, [
get: fn(_url, _opts, _params) ->
test "network error propagates", %{ueberauth_request_options: ueberauth_request_options} do
with_mock HTTPoison,
get: fn _url, _opts, _params ->
{:error, %HTTPoison.Error{reason: :timeout, id: nil}}
end
] do
conn = CAS.handle_callback!(%Plug.Conn{params: %{"ticket" => "ST-XXXXX"}})
end do
conn =
%Plug.Conn{params: %{"ticket" => "ST-XXXXX"}}
|> Plug.Conn.put_private(:ueberauth_request_options, ueberauth_request_options)
|> CAS.handle_callback!()

assert List.first(conn.assigns.ueberauth_failure.errors).message_key == "NETWORK_ERROR"
assert List.first(conn.assigns.ueberauth_failure.errors).message == "An error occurred: timeout"

assert List.first(conn.assigns.ueberauth_failure.errors).message ==
"An error occurred: timeout"
end
end

Expand Down

0 comments on commit 67b26cd

Please sign in to comment.