Merge branch 'fix/oauth-http-basic' into 'develop'

Make OAuth token endpoint work with HTTP Basic auth

See merge request pleroma/pleroma!191
This commit is contained in:
lambda 2018-06-06 08:27:08 +00:00
commit b5d8213e70
3 changed files with 156 additions and 14 deletions

View File

@ -56,12 +56,7 @@ def create_authorization(conn, %{
# TODO # TODO
# - proper scope handling # - proper scope handling
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
with %App{} = app <- with %App{} = app <- get_app_from_request(conn, params),
Repo.get_by(
App,
client_id: params["client_id"],
client_secret: params["client_secret"]
),
fixed_token = fix_padding(params["code"]), fixed_token = fix_padding(params["code"]),
%Authorization{} = auth <- %Authorization{} = auth <-
Repo.get_by(Authorization, token: fixed_token, app_id: app.id), Repo.get_by(Authorization, token: fixed_token, app_id: app.id),
@ -76,7 +71,9 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
json(conn, response) json(conn, response)
else else
_error -> json(conn, %{error: "Invalid credentials"}) _error ->
put_status(conn, 400)
|> json(%{error: "Invalid credentials"})
end end
end end
@ -86,12 +83,7 @@ def token_exchange(
conn, conn,
%{"grant_type" => "password", "name" => name, "password" => password} = params %{"grant_type" => "password", "name" => name, "password" => password} = params
) do ) do
with %App{} = app <- with %App{} = app <- get_app_from_request(conn, params),
Repo.get_by(
App,
client_id: params["client_id"],
client_secret: params["client_secret"]
),
%User{} = user <- User.get_cached_by_nickname(name), %User{} = user <- User.get_cached_by_nickname(name),
true <- Pbkdf2.checkpw(password, user.password_hash), true <- Pbkdf2.checkpw(password, user.password_hash),
{:ok, auth} <- Authorization.create_authorization(app, user), {:ok, auth} <- Authorization.create_authorization(app, user),
@ -106,7 +98,9 @@ def token_exchange(
json(conn, response) json(conn, response)
else else
_error -> json(conn, %{error: "Invalid credentials"}) _error ->
put_status(conn, 400)
|> json(%{error: "Invalid credentials"})
end end
end end
@ -115,4 +109,28 @@ defp fix_padding(token) do
|> Base.url_decode64!(padding: false) |> Base.url_decode64!(padding: false)
|> Base.url_encode64() |> Base.url_encode64()
end end
defp get_app_from_request(conn, params) do
# Per RFC 6749, HTTP Basic is preferred to body params
{client_id, client_secret} =
with ["Basic " <> encoded] <- get_req_header(conn, "authorization"),
{:ok, decoded} <- Base.decode64(encoded),
[id, secret] <-
String.split(decoded, ":")
|> Enum.map(fn s -> URI.decode_www_form(s) end) do
{id, secret}
else
_ -> {params["client_id"], params["client_secret"]}
end
if client_id && client_secret do
Repo.get_by(
App,
client_id: client_id,
client_secret: client_secret
)
else
nil
end
end
end end

View File

@ -146,4 +146,15 @@ def websub_client_subscription_factory do
subscribers: [] subscribers: []
} }
end end
def oauth_app_factory do
%Pleroma.Web.OAuth.App{
client_name: "Some client",
redirect_uris: "https://example.com/callback",
scopes: "read",
website: "https://example.com",
client_id: "aaabbb==",
client_secret: "aaa;/&bbb"
}
end
end end

View File

@ -0,0 +1,113 @@
defmodule Pleroma.Web.OAuth.OAuthControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
alias Pleroma.Repo
alias Pleroma.Web.OAuth.{Authorization, Token}
test "redirects with oauth authorization" do
user = insert(:user)
app = insert(:oauth_app)
conn =
build_conn()
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => app.redirect_uris,
"state" => "statepassed"
}
})
target = redirected_to(conn)
assert target =~ app.redirect_uris
query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
assert %{"state" => "statepassed", "code" => code} = query
assert Repo.get_by(Authorization, token: code)
end
test "issues a token for an all-body request" do
user = insert(:user)
app = insert(:oauth_app)
{:ok, auth} = Authorization.create_authorization(app, user)
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => app.redirect_uris,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token} = json_response(conn, 200)
assert Repo.get_by(Token, token: token)
end
test "issues a token for request with HTTP basic auth client credentials" do
user = insert(:user)
app = insert(:oauth_app)
{:ok, auth} = Authorization.create_authorization(app, user)
app_encoded =
(URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret))
|> Base.encode64()
conn =
build_conn()
|> put_req_header("authorization", "Basic " <> app_encoded)
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => app.redirect_uris
})
assert %{"access_token" => token} = json_response(conn, 200)
assert Repo.get_by(Token, token: token)
end
test "rejects token exchange with invalid client credentials" do
user = insert(:user)
app = insert(:oauth_app)
{:ok, auth} = Authorization.create_authorization(app, user)
conn =
build_conn()
|> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=")
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => app.redirect_uris
})
assert resp = json_response(conn, 400)
assert %{"error" => _} = resp
refute Map.has_key?(resp, "access_token")
end
test "rejects an invalid authorization code" do
app = insert(:oauth_app)
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => "Imobviouslyinvalid",
"redirect_uri" => app.redirect_uris,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 400)
assert %{"error" => _} = json_response(conn, 400)
refute Map.has_key?(resp, "access_token")
end
end