@@ -397,9 +397,7 @@ config :ueberauth, | |||
base_path: "/oauth", | |||
providers: ueberauth_providers | |||
config :pleroma, :auth, | |||
oauth_consumer_strategies: oauth_consumer_strategies, | |||
oauth_consumer_enabled: oauth_consumer_strategies != [] | |||
config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies | |||
config :pleroma, Pleroma.Mailer, adapter: Swoosh.Adapters.Sendmail | |||
@@ -412,3 +412,58 @@ Pleroma account will be created with the same name as the LDAP user name. | |||
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator | |||
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication | |||
## :auth | |||
Authentication / authorization settings. | |||
* `oauth_consumer_strategies`: lists enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable. | |||
OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). | |||
Implementation is based on Ueberauth; see the list of [available strategies](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies). | |||
Note: each strategy is shipped as a separate dependency; in order to get the strategies, run `OAUTH_CONSUMER_STRATEGIES="..." mix deps.get`, | |||
e.g. `OAUTH_CONSUMER_STRATEGIES="twitter facebook google microsoft" mix deps.get`. | |||
The server should also be started with `OAUTH_CONSUMER_STRATEGIES="..." mix phx.server` in case you enable any strategies. | |||
Note: each strategy requires separate setup (on external provider side and Pleroma side). Below are the guidelines on setting up most popular strategies. | |||
* For Twitter, [register an app](https://developer.twitter.com/en/apps), configure callback URL to https://<your_host>/oauth/twitter/callback | |||
* For Facebook, [register an app](https://developers.facebook.com/apps), configure callback URL to https://<your_host>/oauth/facebook/callback, enable Facebook Login service at https://developers.facebook.com/apps/<app_id>/fb-login/settings/ | |||
* For Google, [register an app](https://console.developers.google.com), configure callback URL to https://<your_host>/oauth/google/callback | |||
* For Microsoft, [register an app](https://portal.azure.com), configure callback URL to https://<your_host>/oauth/microsoft/callback | |||
Once the app is configured on external OAuth provider side, add app's credentials and strategy-specific settings (if any — e.g. see Microsoft below) to `config/prod.secret.exs`, | |||
per strategy's documentation (e.g. [ueberauth_twitter](https://github.com/ueberauth/ueberauth_twitter)). Example config basing on environment variables: | |||
``` | |||
config :ueberauth, Ueberauth.Strategy.Twitter.OAuth, | |||
consumer_key: System.get_env("TWITTER_CONSUMER_KEY"), | |||
consumer_secret: System.get_env("TWITTER_CONSUMER_SECRET") | |||
config :ueberauth, Ueberauth.Strategy.Facebook.OAuth, | |||
client_id: System.get_env("FACEBOOK_APP_ID"), | |||
client_secret: System.get_env("FACEBOOK_APP_SECRET"), | |||
redirect_uri: System.get_env("FACEBOOK_REDIRECT_URI") | |||
config :ueberauth, Ueberauth.Strategy.Google.OAuth, | |||
client_id: System.get_env("GOOGLE_CLIENT_ID"), | |||
client_secret: System.get_env("GOOGLE_CLIENT_SECRET"), | |||
redirect_uri: System.get_env("GOOGLE_REDIRECT_URI") | |||
# Microsoft | |||
config :ueberauth, Ueberauth.Strategy.Microsoft.OAuth, | |||
client_id: System.get_env("MICROSOFT_CLIENT_ID"), | |||
client_secret: System.get_env("MICROSOFT_CLIENT_SECRET") | |||
config :ueberauth, Ueberauth, | |||
providers: [ | |||
microsoft: {Ueberauth.Strategy.Microsoft, [callback_params: []]} | |||
] | |||
``` |
@@ -57,4 +57,8 @@ defmodule Pleroma.Config do | |||
def delete(key) do | |||
Application.delete_env(:pleroma, key) | |||
end | |||
def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], []) | |||
def oauth_consumer_enabled?, do: oauth_consumer_strategies() != [] | |||
end |
@@ -59,7 +59,7 @@ defmodule Pleroma.Web.Endpoint do | |||
else: "pleroma_key" | |||
same_site = | |||
if Pleroma.Config.get([:auth, :oauth_consumer_enabled]) do | |||
if Pleroma.Config.oauth_consumer_enabled?() do | |||
# Note: "SameSite=Strict" prevents sign in with external OAuth provider | |||
# (there would be no cookies during callback request from OAuth provider) | |||
"SameSite=Lax" | |||
@@ -6,8 +6,21 @@ defmodule Pleroma.Web.OAuth.FallbackController do | |||
use Pleroma.Web, :controller | |||
alias Pleroma.Web.OAuth.OAuthController | |||
# No user/password | |||
def call(conn, _) do | |||
def call(conn, {:register, :generic_error}) do | |||
conn | |||
|> put_status(:internal_server_error) | |||
|> put_flash(:error, "Unknown error, please check the details and try again.") | |||
|> OAuthController.registration_details(conn.params) | |||
end | |||
def call(conn, {:register, _error}) do | |||
conn | |||
|> put_status(:unauthorized) | |||
|> put_flash(:error, "Invalid Username/Password") | |||
|> OAuthController.registration_details(conn.params) | |||
end | |||
def call(conn, _error) do | |||
conn | |||
|> put_status(:unauthorized) | |||
|> put_flash(:error, "Invalid Username/Password") | |||
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] | |||
if Pleroma.Config.get([:auth, :oauth_consumer_enabled]), do: plug(Ueberauth) | |||
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) | |||
plug(:fetch_session) | |||
plug(:fetch_flash) | |||
@@ -62,60 +62,65 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
def create_authorization( | |||
conn, | |||
%{ | |||
"authorization" => %{"redirect_uri" => redirect_uri} = auth_params | |||
} = params, | |||
%{"authorization" => auth_params} = params, | |||
opts \\ [] | |||
) do | |||
with {:ok, auth} <- | |||
(opts[:auth] && {:ok, opts[:auth]}) || | |||
do_create_authorization(conn, params, opts[:user]) do | |||
redirect_uri = redirect_uri(conn, redirect_uri) | |||
cond do | |||
redirect_uri == "urn:ietf:wg:oauth:2.0:oob" -> | |||
render(conn, "results.html", %{ | |||
auth: auth | |||
}) | |||
true -> | |||
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" | |||
url = "#{redirect_uri}#{connector}" | |||
url_params = %{:code => auth.token} | |||
url_params = | |||
if auth_params["state"] do | |||
Map.put(url_params, :state, auth_params["state"]) | |||
else | |||
url_params | |||
end | |||
with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do | |||
after_create_authorization(conn, auth, auth_params) | |||
else | |||
error -> | |||
handle_create_authorization_error(conn, error, auth_params) | |||
end | |||
end | |||
url = "#{url}#{Plug.Conn.Query.encode(url_params)}" | |||
def after_create_authorization(conn, auth, %{"redirect_uri" => redirect_uri} = auth_params) do | |||
redirect_uri = redirect_uri(conn, redirect_uri) | |||
redirect(conn, external: url) | |||
end | |||
if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do | |||
render(conn, "results.html", %{ | |||
auth: auth | |||
}) | |||
else | |||
{scopes_issue, _} when scopes_issue in [:unsupported_scopes, :missing_scopes] -> | |||
# Per https://github.com/tootsuite/mastodon/blob/ | |||
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 | |||
conn | |||
|> put_flash(:error, "This action is outside the authorized scopes") | |||
|> put_status(:unauthorized) | |||
|> authorize(auth_params) | |||
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" | |||
url = "#{redirect_uri}#{connector}" | |||
url_params = %{:code => auth.token} | |||
{:auth_active, false} -> | |||
# Per https://github.com/tootsuite/mastodon/blob/ | |||
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 | |||
conn | |||
|> put_flash(:error, "Your login is missing a confirmed e-mail address") | |||
|> put_status(:forbidden) | |||
|> authorize(auth_params) | |||
url_params = | |||
if auth_params["state"] do | |||
Map.put(url_params, :state, auth_params["state"]) | |||
else | |||
url_params | |||
end | |||
error -> | |||
Authenticator.handle_error(conn, error) | |||
url = "#{url}#{Plug.Conn.Query.encode(url_params)}" | |||
redirect(conn, external: url) | |||
end | |||
end | |||
defp handle_create_authorization_error(conn, {scopes_issue, _}, auth_params) | |||
when scopes_issue in [:unsupported_scopes, :missing_scopes] do | |||
# Per https://github.com/tootsuite/mastodon/blob/ | |||
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 | |||
conn | |||
|> put_flash(:error, "This action is outside the authorized scopes") | |||
|> put_status(:unauthorized) | |||
|> authorize(auth_params) | |||
end | |||
defp handle_create_authorization_error(conn, {:auth_active, false}, auth_params) do | |||
# Per https://github.com/tootsuite/mastodon/blob/ | |||
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 | |||
conn | |||
|> put_flash(:error, "Your login is missing a confirmed e-mail address") | |||
|> put_status(:forbidden) | |||
|> authorize(auth_params) | |||
end | |||
defp handle_create_authorization_error(conn, error, _auth_params) do | |||
Authenticator.handle_error(conn, error) | |||
end | |||
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do | |||
with %App{} = app <- get_app_from_request(conn, params), | |||
fixed_token = fix_padding(params["code"]), | |||
@@ -202,6 +207,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
end | |||
end | |||
@doc "Prepares OAuth request to provider for Ueberauth" | |||
def prepare_request(conn, %{"provider" => provider} = params) do | |||
scope = | |||
oauth_scopes(params, []) | |||
@@ -218,6 +224,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
|> Map.drop(~w(scope scopes client_id redirect_uri)) | |||
|> Map.put("state", state) | |||
# Handing the request to Ueberauth | |||
redirect(conn, to: o_auth_path(conn, :request, provider, params)) | |||
end | |||
@@ -266,7 +273,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
conn | |||
|> put_session(:registration_id, registration.id) | |||
|> redirect(to: o_auth_path(conn, :registration_details, registration_params)) | |||
|> registration_details(registration_params) | |||
end | |||
else | |||
_ -> | |||
@@ -292,32 +299,28 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
end | |||
def register(conn, %{"op" => "connect"} = params) do | |||
create_authorization_params = %{ | |||
"authorization" => Map.merge(params, %{"name" => params["auth_name"]}) | |||
} | |||
authorization_params = Map.put(params, "name", params["auth_name"]) | |||
create_authorization_params = %{"authorization" => authorization_params} | |||
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), | |||
%Registration{} = registration <- Repo.get(Registration, registration_id), | |||
{:ok, auth} <- do_create_authorization(conn, create_authorization_params), | |||
{_, {:ok, auth}} <- | |||
{:create_authorization, do_create_authorization(conn, create_authorization_params)}, | |||
%User{} = user <- Repo.preload(auth, :user).user, | |||
{:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do | |||
conn | |||
|> put_session_registration_id(nil) | |||
|> create_authorization( | |||
create_authorization_params, | |||
auth: auth | |||
) | |||
|> after_create_authorization(auth, authorization_params) | |||
else | |||
_ -> | |||
params = Map.delete(params, "password") | |||
{:create_authorization, error} -> | |||
{:register, handle_create_authorization_error(conn, error, create_authorization_params)} | |||
conn | |||
|> put_flash(:error, "Unknown error, please try again.") | |||
|> redirect(to: o_auth_path(conn, :registration_details, params)) | |||
_ -> | |||
{:register, :generic_error} | |||
end | |||
end | |||
def register(conn, params) do | |||
def register(conn, %{"op" => "register"} = params) do | |||
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), | |||
%Registration{} = registration <- Repo.get(Registration, registration_id), | |||
{:ok, user} <- Authenticator.create_from_registration(conn, params, registration) do | |||
@@ -349,13 +352,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
) | |||
conn | |||
|> put_status(:forbidden) | |||
|> put_flash(:error, "Error: #{message}.") | |||
|> redirect(to: o_auth_path(conn, :registration_details, params)) | |||
|> registration_details(params) | |||
_ -> | |||
conn | |||
|> put_flash(:error, "Unknown error, please try again.") | |||
|> redirect(to: o_auth_path(conn, :registration_details, params)) | |||
{:register, :generic_error} | |||
end | |||
end | |||
@@ -9,7 +9,7 @@ | |||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %> | |||
<%= hidden_input f, :state, value: @state %> | |||
<%= for strategy <- Pleroma.Config.get([:auth, :oauth_consumer_strategies], []) do %> | |||
<%= for strategy <- Pleroma.Config.oauth_consumer_strategies() do %> | |||
<%= submit "Sign in with #{String.capitalize(strategy)}", name: "provider", value: strategy %> | |||
<% end %> | |||
<% end %> |
@@ -26,6 +26,6 @@ | |||
<%= submit "Authorize" %> | |||
<% end %> | |||
<%= if Pleroma.Config.get([:auth, :oauth_consumer_enabled]) do %> | |||
<%= if Pleroma.Config.oauth_consumer_enabled?() do %> | |||
<%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %> | |||
<% end %> |
@@ -0,0 +1,59 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.RegistrationTest do | |||
use Pleroma.DataCase | |||
import Pleroma.Factory | |||
alias Pleroma.Registration | |||
alias Pleroma.Repo | |||
describe "generic changeset" do | |||
test "requires :provider, :uid" do | |||
registration = build(:registration, provider: nil, uid: nil) | |||
cs = Registration.changeset(registration, %{}) | |||
refute cs.valid? | |||
assert [ | |||
provider: {"can't be blank", [validation: :required]}, | |||
uid: {"can't be blank", [validation: :required]} | |||
] == cs.errors | |||
end | |||
test "ensures uniqueness of [:provider, :uid]" do | |||
registration = insert(:registration) | |||
registration2 = build(:registration, provider: registration.provider, uid: registration.uid) | |||
cs = Registration.changeset(registration2, %{}) | |||
assert cs.valid? | |||
assert {:error, | |||
%Ecto.Changeset{ | |||
errors: [ | |||
uid: | |||
{"has already been taken", | |||
[constraint: :unique, constraint_name: "registrations_provider_uid_index"]} | |||
] | |||
}} = Repo.insert(cs) | |||
# Note: multiple :uid values per [:user_id, :provider] are intentionally allowed | |||
cs2 = Registration.changeset(registration2, %{uid: "available.uid"}) | |||
assert cs2.valid? | |||
assert {:ok, _} = Repo.insert(cs2) | |||
cs3 = Registration.changeset(registration2, %{provider: "provider2"}) | |||
assert cs3.valid? | |||
assert {:ok, _} = Repo.insert(cs3) | |||
end | |||
test "allows `nil` :user_id (user-unbound registration)" do | |||
registration = build(:registration, user_id: nil) | |||
cs = Registration.changeset(registration, %{}) | |||
assert cs.valid? | |||
assert {:ok, _} = Repo.insert(cs) | |||
end | |||
end | |||
end |
@@ -20,16 +20,11 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do | |||
describe "in OAuth consumer mode, " do | |||
setup do | |||
oauth_consumer_enabled_path = [:auth, :oauth_consumer_enabled] | |||
oauth_consumer_strategies_path = [:auth, :oauth_consumer_strategies] | |||
oauth_consumer_enabled = Pleroma.Config.get(oauth_consumer_enabled_path) | |||
oauth_consumer_strategies = Pleroma.Config.get(oauth_consumer_strategies_path) | |||
Pleroma.Config.put(oauth_consumer_enabled_path, true) | |||
Pleroma.Config.put(oauth_consumer_strategies_path, ~w(twitter facebook)) | |||
on_exit(fn -> | |||
Pleroma.Config.put(oauth_consumer_enabled_path, oauth_consumer_enabled) | |||
Pleroma.Config.put(oauth_consumer_strategies_path, oauth_consumer_strategies) | |||
end) | |||
@@ -42,7 +37,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do | |||
] | |||
end | |||
test "GET /oauth/authorize also renders OAuth consumer form", %{ | |||
test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{ | |||
app: app, | |||
conn: conn | |||
} do | |||
@@ -97,31 +92,6 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do | |||
} = state_components | |||
end | |||
test "on authentication error, redirects to `redirect_uri`", %{app: app, conn: conn} do | |||
state_params = %{ | |||
"scope" => Enum.join(app.scopes, " "), | |||
"client_id" => app.client_id, | |||
"redirect_uri" => app.redirect_uris, | |||
"state" => "" | |||
} | |||
conn = | |||
conn | |||
|> assign(:ueberauth_failure, %{errors: [%{message: "unknown error"}]}) | |||
|> get( | |||
"/oauth/twitter/callback", | |||
%{ | |||
"oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", | |||
"oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", | |||
"provider" => "twitter", | |||
"state" => Poison.encode!(state_params) | |||
} | |||
) | |||
assert response = html_response(conn, 302) | |||
assert redirected_to(conn) == app.redirect_uris | |||
end | |||
test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`", | |||
%{app: app, conn: conn} do | |||
registration = insert(:registration) | |||
@@ -152,7 +122,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do | |||
end | |||
end | |||
test "with user-unbound registration, GET /oauth/<provider>/callback redirects to registration_details page", | |||
test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page", | |||
%{app: app, conn: conn} do | |||
registration = insert(:registration, user: nil) | |||
@@ -177,20 +147,41 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do | |||
} | |||
) | |||
expected_redirect_params = | |||
state_params | |||
|> Map.delete("scope") | |||
|> Map.merge(%{ | |||
"scope" => "read write", | |||
"email" => Registration.email(registration), | |||
"nickname" => Registration.nickname(registration) | |||
}) | |||
assert response = html_response(conn, 200) | |||
assert response =~ ~r/name="op" type="submit" value="register"/ | |||
assert response =~ ~r/name="op" type="submit" value="connect"/ | |||
assert response =~ Registration.email(registration) | |||
assert response =~ Registration.nickname(registration) | |||
end | |||
end | |||
assert response = html_response(conn, 302) | |||
test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{ | |||
app: app, | |||
conn: conn | |||
} do | |||
state_params = %{ | |||
"scope" => Enum.join(app.scopes, " "), | |||
"client_id" => app.client_id, | |||
"redirect_uri" => app.redirect_uris, | |||
"state" => "" | |||
} | |||
assert redirected_to(conn) == | |||
o_auth_path(conn, :registration_details, expected_redirect_params) | |||
end | |||
conn = | |||
conn | |||
|> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]}) | |||
|> get( | |||
"/oauth/twitter/callback", | |||
%{ | |||
"oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", | |||
"oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", | |||
"provider" => "twitter", | |||
"state" => Poison.encode!(state_params) | |||
} | |||
) | |||
assert response = html_response(conn, 302) | |||
assert redirected_to(conn) == app.redirect_uris | |||
assert get_flash(conn, :error) == "Failed to authenticate: (error description)." | |||
end | |||
test "GET /oauth/registration_details renders registration details form", %{ | |||
@@ -243,7 +234,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do | |||
assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/ | |||
end | |||
test "with invalid params, POST /oauth/register?op=register redirects to registration_details page", | |||
test "with invalid params, POST /oauth/register?op=register renders registration_details page", | |||
%{ | |||
app: app, | |||
conn: conn | |||
@@ -257,19 +248,22 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do | |||
"client_id" => app.client_id, | |||
"redirect_uri" => app.redirect_uris, | |||
"state" => "a_state", | |||
"nickname" => another_user.nickname, | |||
"email" => another_user.email | |||
"nickname" => "availablenickname", | |||
"email" => "available@email.com" | |||
} | |||
conn = | |||
conn | |||
|> put_session(:registration_id, registration.id) | |||
|> post("/oauth/register", params) | |||
for {bad_param, bad_param_value} <- | |||
[{"nickname", another_user.nickname}, {"email", another_user.email}] do | |||
bad_params = Map.put(params, bad_param, bad_param_value) | |||
assert response = html_response(conn, 302) | |||
conn = | |||
conn | |||
|> put_session(:registration_id, registration.id) | |||
|> post("/oauth/register", bad_params) | |||
assert redirected_to(conn) == | |||
o_auth_path(conn, :registration_details, params) | |||
assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/ | |||
assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken." | |||
end | |||
end | |||
test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`", | |||
@@ -300,7 +294,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do | |||
assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/ | |||
end | |||
test "with invalid params, POST /oauth/register?op=connect redirects to registration_details page", | |||
test "with invalid params, POST /oauth/register?op=connect renders registration_details page", | |||
%{ | |||
app: app, | |||
conn: conn | |||
@@ -323,10 +317,8 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do | |||
|> put_session(:registration_id, registration.id) | |||
|> post("/oauth/register", params) | |||
assert response = html_response(conn, 302) | |||
assert redirected_to(conn) == | |||
o_auth_path(conn, :registration_details, Map.delete(params, "password")) | |||
assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ | |||
assert get_flash(conn, :error) == "Invalid Username/Password" | |||
end | |||
end | |||