Skip to content

Commit

Permalink
https://github.com/bonfire-networks/bonfire-app/issues/918
Browse files Browse the repository at this point in the history
  • Loading branch information
mayel committed Apr 27, 2024
1 parent f3ea5a8 commit 3123ddb
Show file tree
Hide file tree
Showing 29 changed files with 420 additions and 44 deletions.
10 changes: 10 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,17 @@ config :bonfire_common, :otp_app, :bonfire_open_id
config :bonfire_open_id, :repo_module, Bonfire.Common.Repo
config :bonfire_open_id, ecto_repos: [Bonfire.Common.Repo]
config :bonfire_open_id, :localisation_path, "priv/localisation"
config :bonfire, localisation_path: "priv/localisation"
config :bonfire, :endpoint_module, Bonfire.Web.Endpoint

config :bonfire_data_identity, Bonfire.Data.Identity.Credential, hasher_module: Argon2

# import_config "#{Mix.env()}.exs"

config :ex_cldr,
default_locale: "en",
default_backend: Bonfire.Common.Localise.Cldr,
json_library: Jason

config :ecto_sparkles, :otp_app, :bonfire_open_id
config :ecto_sparkles, :env, config_env()
4 changes: 3 additions & 1 deletion deps.git
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
bonfire_common = "https://github.com/bonfire-networks/bonfire_common"
bonfire_ui_common = "https://github.com/bonfire-networks/bonfire_ui_common"
bonfire_me = "https://github.com/bonfire-networks/bonfire_me"
openid_connect = "https://github.com/DockYard/openid_connect"
openid_connect = "https://github.com/DockYard/openid_connect"

# ex_cldr_territories = "https://github.com/elixir-cldr/cldr_territories"
8 changes: 5 additions & 3 deletions deps.hex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
faker = "~> 0.14" # fake data generation
jason = "~> 1.0"
boruta = "~> 2.3.2" # openid / oauth provider
boruta = "~> 2.3.3" # openid / oauth provider
openid_connect = "~> 0.2.2" # openid client
surface = "~> 0.11.2"
untangle = "~> 0.3"
surface = "~> 0.11.4"
untangle = "~> 0.3"
plug_crypto = "~> 2.0"
# ex_cldr = "~> 2.30" # internationalisation
File renamed without changes.
16 changes: 9 additions & 7 deletions lib/integration.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
defmodule Bonfire.OpenID.Integration do
import Untangle
alias Bonfire.Common.Config
alias Bonfire.Common.Utils
use Bonfire.Common.Utils
alias Bonfire.Me.Users
alias Bonfire.Me.Accounts
alias Boruta.Oauth.ResourceOwner
Expand All @@ -18,7 +16,7 @@ defmodule Bonfire.OpenID.Integration do
end

defp get_user(id_or_username) do
with {:ok, user} <- Users.get_current(id_or_username) do
with %{} = user <- Users.get_current(id_or_username) do
{:ok,
%ResourceOwner{
sub: to_string(user.id),
Expand All @@ -27,7 +25,8 @@ defmodule Bonfire.OpenID.Integration do
last_login_at: nil
}}
else
_ -> {:error, "User not found."}
_ ->
error(id_or_username, l("User not found."))
end
end

Expand All @@ -37,8 +36,11 @@ defmodule Bonfire.OpenID.Integration do
email_or_username: resource_owner.username,
password: password
}) do
{:ok, account, user} -> :ok
_ -> {:error, "Invalid email or password."}
{:ok, account, user} ->
:ok

_ ->
error(resource_owner, l("Invalid email or password."))
end
end

Expand Down
52 changes: 39 additions & 13 deletions lib/clients.ex → lib/server/clients.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,26 @@ defmodule Bonfire.OpenID.Clients do
defdelegate list_scopes, to: Boruta.Ecto.Admin
defdelegate list_active_tokens, to: Boruta.Ecto.Admin

@doc "Define an OAuth client app, providing a name and redirect URI(s)"
def new(name, redirect_uris)
when is_binary(name) and is_list(redirect_uris) and
length(redirect_uris) > 0 do
new(%{name: name, redirect_uris: redirect_uris})
end

def new(name, redirect_uri)
when is_binary(name) and is_binary(redirect_uri) do
new(name, [redirect_uri])
end

def new(params) when is_map(params) do
%{
# OAuth client_id
id: SecureRandom.uuid(),
# OAuth client_secret
secret: SecureRandom.hex(64),
# Display name
name: "A client",
name: "Client app",
# one day
access_token_ttl: 60 * 60 * 24,
# one minute
Expand All @@ -19,11 +31,23 @@ defmodule Bonfire.OpenID.Clients do
refresh_token_ttl: 60 * 60 * 24 * 30,
# one day
id_token_ttl: 60 * 60 * 24,
# redirect_uris: ["http://redirect.uri"], # OAuth client redirect_uris
# ID token signature algorithm, defaults to "RS512"
id_token_signature_alg: "RS256",
# userinfo signature algorithm, defaults to nil (no signature)
userinfo_signed_response_alg: "RS256",
# OAuth client redirect_uris
redirect_uris: ["#{Bonfire.Common.URIs.base_url()}/oauth/ready"],
# take following authorized_scopes into account (skip public scopes)
authorize_scope: true,
# scopes that are authorized using this client
authorized_scopes: [%{name: "identity"}, %{name: "data:public"}],
authorized_scopes: [
%{name: "identity"},
%{name: "data:public"},
%{name: "read"},
%{name: "write"},
%{name: "follow"},
%{name: "push"}
],
# client supported grant types
supported_grant_types: [
"client_credentials",
Expand All @@ -39,20 +63,22 @@ defmodule Bonfire.OpenID.Clients do
# do not require client_secret for refreshing tokens
public_refresh_token: false,
# do not require client_secret for revoking tokens
public_revoke: false
public_revoke: false,
# activate-able client authentication methods
token_endpont_auth_methods: [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
]
# token_endpoint_jwt_auth_alg: nil, # associated to authentication methods, the algorithm to use along
# jwt_public_key: nil # pem public key to be used with `private_key_jwt` authentication method
}
|> Map.merge(params)
|> Boruta.Ecto.Admin.create_client()
end

def new(name, redirect_uris)
when is_binary(name) and is_list(redirect_uris) and
length(redirect_uris) > 0 do
new(%{name: name, redirect_uris: redirect_uris})
end

def new(name, redirect_uri)
when is_binary(name) and is_binary(redirect_uri) do
new(name, [redirect_uri])
def init_test_client_app do
new(%{id: "b0f15e02-b0f1-b0f1-b0f1-b0f15eb0f15e", name: "Test client app"})
end
end
42 changes: 42 additions & 0 deletions lib/web/controllers/server/app_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule Bonfire.API.MastoCompatible.AppController do
use Bonfire.UI.Common.Web, :controller
alias Bonfire.Common.Config
alias Bonfire.OpenID.Web.OauthView

def create(conn, params) do
# curl -X POST \
# -F 'client_name=Test Application' \
# -F 'redirect_uris=urn:ietf:wg:oauth:2.0:oob' \
# -F 'scopes=read write push' \
# -F 'website=https://myapp.example' \
# https://instance.example/api/v1/apps

with {:ok, client} <-
Bonfire.OpenID.Clients.new(%{
name: String.trim("#{params["client_name"]} #{params["website"]}"),
redirect_uris: List.wrap(params["redirect_uris"])
# _: params["scopes"], # TODO
}) do
json(conn, %{
"id" => client.id,
"name" => client.name,
"website" => nil,
"redirect_uri" => List.first(client.redirect_uris || []),
"client_id" => client.id,
"client_secret" => client.secret
# "vapid_key"=> client.vapid_key # TODO?
})
else
other ->
error(other)

conn
|> put_status(500)
|> put_view(OauthView)
|> render("error.json",
error: "Could not create a client",
error_description: inspect(other)
)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,31 @@ defmodule Bonfire.OpenID.Web.Oauth.AuthorizeController do
do: Application.get_env(:bonfire_open_id, :oauth_module, Boruta.Oauth)

def authorize(%Plug.Conn{} = conn, _params) do
current_user = conn.assigns[:current_user]
agent = current_user(conn) || current_account(conn)
conn = store_go(conn)

authorize_response(
conn,
current_user
agent
)
end

defp authorize_response(conn, %_{} = current_user) do
oauth_module().authorize(
defp authorize_response(conn, %_{} = agent) do
%ResourceOwner{
sub: to_string(agent.id),
username: e(agent, :character, :username, nil) || e(agent, :email, :email_address, nil)
}
|> debug()
|> oauth_module().authorize(
conn,
%ResourceOwner{
sub: to_string(current_user.id),
username: e(current_user, :character, :username, e(current_user, :email, nil))
},
...,
__MODULE__
)
|> debug()
end

defp authorize_response(conn, _params) do
defp authorize_response(conn, other) do
warn(other, "no agent in conn")
redirect_to_login(conn)
end

Expand All @@ -41,14 +45,25 @@ defmodule Bonfire.OpenID.Web.Oauth.AuthorizeController do
conn,
%AuthorizeResponse{} = response
) do
redirect(conn, external: AuthorizeResponse.redirect_to_url(response))
redirect(conn, external: AuthorizeResponse.redirect_to_url(response) |> debug())
end

@impl Boruta.Oauth.AuthorizeApplication
def authorize_error(
conn,
%Error{status: :unauthorized, error: :invalid_client} = error
) do
authorize_error(
conn,
%{error | status: :bad_request}
)
end

def authorize_error(
%Plug.Conn{} = conn,
%Error{status: :unauthorized}
%Error{status: :unauthorized} = error
) do
error(error)
redirect_to_login(conn)
end

Expand All @@ -57,6 +72,8 @@ defmodule Bonfire.OpenID.Web.Oauth.AuthorizeController do
%Error{format: format} = error
)
when not is_nil(format) do
error(error)

redirect(conn,
external: Error.redirect_to_url(error)
)
Expand All @@ -66,14 +83,16 @@ defmodule Bonfire.OpenID.Web.Oauth.AuthorizeController do
conn,
%Error{
status: status,
error: error,
error: error_detail,
error_description: error_description
}
} = error
) do
error(error)

conn
|> put_status(status)
|> put_view(OauthView)
|> render("error.html", error: error, error_description: error_description)
|> render("error.html", error: error_detail, error_description: error_description)
end

@impl Boruta.Oauth.AuthorizeApplication
Expand Down
13 changes: 13 additions & 0 deletions lib/web/controllers/server/oauth/ready_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule Bonfire.OpenID.Web.Oauth.ReadyController do
use Bonfire.UI.Common.Web, :controller
alias Bonfire.OpenID.Web.OauthView

def ready(%Plug.Conn{} = conn, %{"code" => code}) do
conn
|> put_view(OauthView)
|> render("error.html",
error: "Ready",
error_description: "Copy/paste your authentication code into the app: #{code}"
)
end
end
10 changes: 10 additions & 0 deletions lib/web/plugs/auth_required.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Bonfire.OpenID.Plugs.AuthRequired do
import Plug

use Bonfire.UI.Common.Web, :controller

def require_auth(conn, _opts) do
Bonfire.OpenID.Plugs.Authorize.maybe_load_authorization(conn) ||
raise Bonfire.Fail, :needs_login
end
end
40 changes: 40 additions & 0 deletions lib/web/plugs/authorize.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule Bonfire.OpenID.Plugs.Authorize do
import Plug

use Bonfire.UI.Common.Web, :controller

alias Boruta.Oauth.Authorization.AccessToken
alias Boruta.Oauth.Scope

def load_authorization(conn, _opts) do
maybe_load_authorization(conn) || conn
end

def maybe_load_authorization(conn, _opts \\ []) do
with [authorization_header] <- get_req_header(conn, "authorization"),
[_authorization_header, bearer] <- Regex.run(~r/[B|b]earer (.+)/, authorization_header),
{:ok, token} <- AccessToken.authorize(value: bearer),
%{} = user <- Bonfire.Me.Users.get_current(token.sub) do
conn
# |> assign(:current_bearer_token, bearer)
|> assign(:current_token, token)
|> assign(:current_user, user)
else
other ->
info(other, "Could not load authorization")
nil
end
end

def authorize(conn, [_h | _t] = required_scopes) do
current_scopes = Scope.split(conn.assigns[:current_token].scope)

case Enum.empty?(required_scopes -- current_scopes) do
true ->
conn

false ->
raise Bonfire.Fail, :unauthorized
end
end
end
Loading

0 comments on commit 3123ddb

Please sign in to comment.