forked from AkkomaGang/akkoma
Session token setting on token exchange. Auth-related refactoring.
This commit is contained in:
parent
489b12cde4
commit
12a5981cc3
11 changed files with 56 additions and 31 deletions
|
@ -4,9 +4,12 @@
|
||||||
|
|
||||||
defmodule Pleroma.Helpers.AuthHelper do
|
defmodule Pleroma.Helpers.AuthHelper do
|
||||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||||
|
alias Plug.Conn
|
||||||
|
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
|
||||||
|
@oauth_token_session_key :oauth_token
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Skips OAuth permissions (scopes) checks, assigns nil `:token`.
|
Skips OAuth permissions (scopes) checks, assigns nil `:token`.
|
||||||
Intended to be used with explicit authentication and only when OAuth token cannot be determined.
|
Intended to be used with explicit authentication and only when OAuth token cannot be determined.
|
||||||
|
@ -22,4 +25,16 @@ def drop_auth_info(conn) do
|
||||||
|> assign(:user, nil)
|
|> assign(:user, nil)
|
||||||
|> assign(:token, nil)
|
|> assign(:token, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_session_token(%Conn{} = conn) do
|
||||||
|
get_session(conn, @oauth_token_session_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def put_session_token(%Conn{} = conn, token) when is_binary(token) do
|
||||||
|
put_session(conn, @oauth_token_session_key, token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_session_token(%Conn{} = conn) do
|
||||||
|
delete_session(conn, @oauth_token_session_key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,7 +25,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
||||||
alias Pleroma.Web.MastodonAPI.MastodonAPIController
|
alias Pleroma.Web.MastodonAPI.MastodonAPIController
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
alias Pleroma.Web.OAuth.OAuthController
|
alias Pleroma.Web.OAuth.OAuthController
|
||||||
alias Pleroma.Web.OAuth.OAuthView
|
|
||||||
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
|
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||||
alias Pleroma.Web.Plugs.RateLimiter
|
alias Pleroma.Web.Plugs.RateLimiter
|
||||||
|
@ -103,7 +102,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
|
||||||
{:ok, user} <- TwitterAPI.register_user(params),
|
{:ok, user} <- TwitterAPI.register_user(params),
|
||||||
{_, {:ok, token}} <-
|
{_, {:ok, token}} <-
|
||||||
{:login, OAuthController.login(user, app, app.scopes)} do
|
{:login, OAuthController.login(user, app, app.scopes)} do
|
||||||
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
|
OAuthController.after_token_exchange(conn, %{user: user, token: token})
|
||||||
else
|
else
|
||||||
{:login, {:account_status, :confirmation_pending}} ->
|
{:login, {:account_status, :confirmation_pending}} ->
|
||||||
json_response(conn, :ok, %{
|
json_response(conn, :ok, %{
|
||||||
|
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
|
||||||
|
|
||||||
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
|
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
|
||||||
|
|
||||||
|
alias Pleroma.Helpers.AuthHelper
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.OAuth.App
|
alias Pleroma.Web.OAuth.App
|
||||||
alias Pleroma.Web.OAuth.Authorization
|
alias Pleroma.Web.OAuth.Authorization
|
||||||
|
@ -30,7 +31,7 @@ def login(conn, %{"code" => auth_token}) do
|
||||||
{:ok, auth} <- Authorization.get_by_token(app, auth_token),
|
{:ok, auth} <- Authorization.get_by_token(app, auth_token),
|
||||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||||
conn
|
conn
|
||||||
|> put_session(:oauth_token, token.token)
|
|> AuthHelper.put_session_token(token.token)
|
||||||
|> redirect(to: local_mastodon_root_path(conn))
|
|> redirect(to: local_mastodon_root_path(conn))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -53,7 +54,7 @@ def login(conn, _) do
|
||||||
@doc "DELETE /auth/sign_out"
|
@doc "DELETE /auth/sign_out"
|
||||||
def logout(conn, _) do
|
def logout(conn, _) do
|
||||||
conn
|
conn
|
||||||
|> clear_session
|
|> clear_session()
|
||||||
|> redirect(to: "/")
|
|> redirect(to: "/")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ defmodule Pleroma.Web.OAuth.MFAController do
|
||||||
alias Pleroma.Web.Auth.TOTPAuthenticator
|
alias Pleroma.Web.Auth.TOTPAuthenticator
|
||||||
alias Pleroma.Web.OAuth.MFAView, as: View
|
alias Pleroma.Web.OAuth.MFAView, as: View
|
||||||
alias Pleroma.Web.OAuth.OAuthController
|
alias Pleroma.Web.OAuth.OAuthController
|
||||||
alias Pleroma.Web.OAuth.OAuthView
|
|
||||||
alias Pleroma.Web.OAuth.Token
|
alias Pleroma.Web.OAuth.Token
|
||||||
|
|
||||||
plug(:fetch_session when action in [:show, :verify])
|
plug(:fetch_session when action in [:show, :verify])
|
||||||
|
@ -75,7 +74,7 @@ def challenge(conn, %{"mfa_token" => mfa_token} = params) do
|
||||||
{:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
|
{:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
|
||||||
{:ok, _} <- validates_challenge(user, params),
|
{:ok, _} <- validates_challenge(user, params),
|
||||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||||
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
|
OAuthController.after_token_exchange(conn, %{user: user, token: token})
|
||||||
else
|
else
|
||||||
_error ->
|
_error ->
|
||||||
conn
|
conn
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
defmodule Pleroma.Web.OAuth.OAuthController do
|
defmodule Pleroma.Web.OAuth.OAuthController do
|
||||||
use Pleroma.Web, :controller
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.Helpers.AuthHelper
|
||||||
alias Pleroma.Helpers.UriHelper
|
alias Pleroma.Helpers.UriHelper
|
||||||
alias Pleroma.Maps
|
alias Pleroma.Maps
|
||||||
alias Pleroma.MFA
|
alias Pleroma.MFA
|
||||||
|
@ -248,7 +249,7 @@ def token_exchange(
|
||||||
with {:ok, app} <- Token.Utils.fetch_app(conn),
|
with {:ok, app} <- Token.Utils.fetch_app(conn),
|
||||||
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
|
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
|
||||||
{:ok, token} <- RefreshToken.grant(token) do
|
{:ok, token} <- RefreshToken.grant(token) do
|
||||||
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
|
after_token_exchange(conn, %{user: user, token: token})
|
||||||
else
|
else
|
||||||
_error -> render_invalid_credentials_error(conn)
|
_error -> render_invalid_credentials_error(conn)
|
||||||
end
|
end
|
||||||
|
@ -260,7 +261,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"}
|
||||||
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
|
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
|
||||||
%User{} = user <- User.get_cached_by_id(auth.user_id),
|
%User{} = user <- User.get_cached_by_id(auth.user_id),
|
||||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||||
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
|
after_token_exchange(conn, %{user: user, token: token})
|
||||||
else
|
else
|
||||||
error ->
|
error ->
|
||||||
handle_token_exchange_error(conn, error)
|
handle_token_exchange_error(conn, error)
|
||||||
|
@ -275,7 +276,7 @@ def token_exchange(
|
||||||
{:ok, app} <- Token.Utils.fetch_app(conn),
|
{:ok, app} <- Token.Utils.fetch_app(conn),
|
||||||
requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
|
requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
|
||||||
{:ok, token} <- login(user, app, requested_scopes) do
|
{:ok, token} <- login(user, app, requested_scopes) do
|
||||||
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
|
after_token_exchange(conn, %{user: user, token: token})
|
||||||
else
|
else
|
||||||
error ->
|
error ->
|
||||||
handle_token_exchange_error(conn, error)
|
handle_token_exchange_error(conn, error)
|
||||||
|
@ -298,7 +299,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
|
||||||
with {:ok, app} <- Token.Utils.fetch_app(conn),
|
with {:ok, app} <- Token.Utils.fetch_app(conn),
|
||||||
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
|
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
|
||||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||||
json(conn, OAuthView.render("token.json", %{token: token}))
|
after_token_exchange(conn, %{token: token})
|
||||||
else
|
else
|
||||||
_error ->
|
_error ->
|
||||||
handle_token_exchange_error(conn, :invalid_credentails)
|
handle_token_exchange_error(conn, :invalid_credentails)
|
||||||
|
@ -308,6 +309,12 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
|
||||||
# Bad request
|
# Bad request
|
||||||
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
|
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
|
||||||
|
|
||||||
|
def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
|
||||||
|
conn
|
||||||
|
|> AuthHelper.put_session_token(token.token)
|
||||||
|
|> json(OAuthView.render("token.json", view_params))
|
||||||
|
end
|
||||||
|
|
||||||
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
|
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
|
||||||
conn
|
conn
|
||||||
|> put_status(:forbidden)
|
|> put_status(:forbidden)
|
||||||
|
@ -365,9 +372,9 @@ def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
|
||||||
with {:ok, app} <- Token.Utils.fetch_app(conn),
|
with {:ok, app} <- Token.Utils.fetch_app(conn),
|
||||||
{:ok, %Token{} = oauth_token} <- RevokeToken.revoke(app, params) do
|
{:ok, %Token{} = oauth_token} <- RevokeToken.revoke(app, params) do
|
||||||
conn =
|
conn =
|
||||||
with session_token = get_session(conn, :oauth_token),
|
with session_token = AuthHelper.get_session_token(conn),
|
||||||
%Token{token: ^session_token} <- oauth_token do
|
%Token{token: ^session_token} <- oauth_token do
|
||||||
delete_session(conn, :oauth_token)
|
AuthHelper.delete_session_token(conn)
|
||||||
else
|
else
|
||||||
_ -> conn
|
_ -> conn
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias Pleroma.Helpers.AuthHelper
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.OAuth.App
|
alias Pleroma.Web.OAuth.App
|
||||||
|
@ -98,7 +99,7 @@ defp fetch_token_str([]), do: :no_token_found
|
||||||
|
|
||||||
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
|
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
|
||||||
defp fetch_token_from_session(conn) do
|
defp fetch_token_from_session(conn) do
|
||||||
case get_session(conn, :oauth_token) do
|
case AuthHelper.get_session_token(conn) do
|
||||||
nil -> :no_token_found
|
nil -> :no_token_found
|
||||||
token -> {:ok, token}
|
token -> {:ok, token}
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
|
defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
|
||||||
import Plug.Conn
|
alias Pleroma.Helpers.AuthHelper
|
||||||
|
|
||||||
alias Pleroma.Web.OAuth.Token
|
alias Pleroma.Web.OAuth.Token
|
||||||
|
|
||||||
def init(opts) do
|
def init(opts) do
|
||||||
|
@ -12,7 +11,7 @@ def init(opts) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def call(%{assigns: %{token: %Token{} = oauth_token}} = conn, _) do
|
def call(%{assigns: %{token: %Token{} = oauth_token}} = conn, _) do
|
||||||
put_session(conn, :oauth_token, oauth_token.token)
|
AuthHelper.put_session_token(conn, oauth_token.token)
|
||||||
end
|
end
|
||||||
|
|
||||||
def call(conn, _), do: conn
|
def call(conn, _), do: conn
|
||||||
|
|
|
@ -320,6 +320,11 @@ defmodule Pleroma.Web.Router do
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/oauth", Pleroma.Web.OAuth do
|
scope "/oauth", Pleroma.Web.OAuth do
|
||||||
|
get("/registration_details", OAuthController, :registration_details)
|
||||||
|
|
||||||
|
post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
|
||||||
|
get("/mfa", MFAController, :show)
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
pipe_through(:oauth)
|
pipe_through(:oauth)
|
||||||
|
|
||||||
|
@ -327,17 +332,12 @@ defmodule Pleroma.Web.Router do
|
||||||
post("/authorize", OAuthController, :create_authorization)
|
post("/authorize", OAuthController, :create_authorization)
|
||||||
end
|
end
|
||||||
|
|
||||||
post("/token", OAuthController, :token_exchange)
|
|
||||||
get("/registration_details", OAuthController, :registration_details)
|
|
||||||
|
|
||||||
post("/mfa/challenge", MFAController, :challenge)
|
|
||||||
post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
|
|
||||||
get("/mfa", MFAController, :show)
|
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
pipe_through(:fetch_session)
|
pipe_through(:fetch_session)
|
||||||
|
|
||||||
|
post("/token", OAuthController, :token_exchange)
|
||||||
post("/revoke", OAuthController, :token_revoke)
|
post("/revoke", OAuthController, :token_revoke)
|
||||||
|
post("/mfa/challenge", MFAController, :challenge)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
|
|
||||||
defmodule Pleroma.Web.OAuth.OAuthControllerTest do
|
defmodule Pleroma.Web.OAuth.OAuthControllerTest do
|
||||||
use Pleroma.Web.ConnCase
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
alias Pleroma.Helpers.AuthHelper
|
||||||
alias Pleroma.MFA
|
alias Pleroma.MFA
|
||||||
alias Pleroma.MFA.TOTP
|
alias Pleroma.MFA.TOTP
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
|
@ -454,7 +456,7 @@ test "renders authentication page if user is already authenticated but `force_lo
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> put_session(:oauth_token, token.token)
|
|> AuthHelper.put_session_token(token.token)
|
||||||
|> get(
|
|> get(
|
||||||
"/oauth/authorize",
|
"/oauth/authorize",
|
||||||
%{
|
%{
|
||||||
|
@ -478,7 +480,7 @@ test "renders authentication page if user is already authenticated but user requ
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> put_session(:oauth_token, token.token)
|
|> AuthHelper.put_session_token(token.token)
|
||||||
|> get(
|
|> get(
|
||||||
"/oauth/authorize",
|
"/oauth/authorize",
|
||||||
%{
|
%{
|
||||||
|
@ -501,7 +503,7 @@ test "with existing authentication and non-OOB `redirect_uri`, redirects to app
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> put_session(:oauth_token, token.token)
|
|> AuthHelper.put_session_token(token.token)
|
||||||
|> get(
|
|> get(
|
||||||
"/oauth/authorize",
|
"/oauth/authorize",
|
||||||
%{
|
%{
|
||||||
|
@ -527,7 +529,7 @@ test "with existing authentication and unlisted non-OOB `redirect_uri`, redirect
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> put_session(:oauth_token, token.token)
|
|> AuthHelper.put_session_token(token.token)
|
||||||
|> get(
|
|> get(
|
||||||
"/oauth/authorize",
|
"/oauth/authorize",
|
||||||
%{
|
%{
|
||||||
|
@ -551,7 +553,7 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> put_session(:oauth_token, token.token)
|
|> AuthHelper.put_session_token(token.token)
|
||||||
|> get(
|
|> get(
|
||||||
"/oauth/authorize",
|
"/oauth/authorize",
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
defmodule Pleroma.Web.Plugs.OAuthPlugTest do
|
defmodule Pleroma.Web.Plugs.OAuthPlugTest do
|
||||||
use Pleroma.Web.ConnCase, async: true
|
use Pleroma.Web.ConnCase, async: true
|
||||||
|
|
||||||
|
alias Pleroma.Helpers.AuthHelper
|
||||||
alias Pleroma.Web.OAuth.Token
|
alias Pleroma.Web.OAuth.Token
|
||||||
alias Pleroma.Web.OAuth.Token.Strategy.Revoke
|
alias Pleroma.Web.OAuth.Token.Strategy.Revoke
|
||||||
alias Pleroma.Web.Plugs.OAuthPlug
|
alias Pleroma.Web.Plugs.OAuthPlug
|
||||||
|
@ -84,7 +85,7 @@ test "with invalid token, it does not assign the user", %{conn: conn} do
|
||||||
conn
|
conn
|
||||||
|> Session.call(Session.init(session_opts))
|
|> Session.call(Session.init(session_opts))
|
||||||
|> fetch_session()
|
|> fetch_session()
|
||||||
|> put_session(:oauth_token, oauth_token.token)
|
|> AuthHelper.put_session_token(oauth_token.token)
|
||||||
|
|
||||||
%{conn: conn}
|
%{conn: conn}
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do
|
defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do
|
||||||
use Pleroma.Web.ConnCase, async: true
|
use Pleroma.Web.ConnCase, async: true
|
||||||
|
|
||||||
|
alias Pleroma.Helpers.AuthHelper
|
||||||
alias Pleroma.Web.Plugs.SetUserSessionIdPlug
|
alias Pleroma.Web.Plugs.SetUserSessionIdPlug
|
||||||
|
|
||||||
setup %{conn: conn} do
|
setup %{conn: conn} do
|
||||||
|
@ -28,7 +29,7 @@ test "doesn't do anything if the user isn't set", %{conn: conn} do
|
||||||
assert ret_conn == conn
|
assert ret_conn == conn
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sets :oauth_token in session to :token assign", %{conn: conn} do
|
test "sets session token basing on :token assign", %{conn: conn} do
|
||||||
%{user: user, token: oauth_token} = oauth_access(["read"])
|
%{user: user, token: oauth_token} = oauth_access(["read"])
|
||||||
|
|
||||||
ret_conn =
|
ret_conn =
|
||||||
|
@ -37,6 +38,6 @@ test "sets :oauth_token in session to :token assign", %{conn: conn} do
|
||||||
|> assign(:token, oauth_token)
|
|> assign(:token, oauth_token)
|
||||||
|> SetUserSessionIdPlug.call(%{})
|
|> SetUserSessionIdPlug.call(%{})
|
||||||
|
|
||||||
assert get_session(ret_conn, :oauth_token) == oauth_token.token
|
assert AuthHelper.get_session_token(ret_conn) == oauth_token.token
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue