AE
Ash Elixirโ€ข2y ago
simpers

Authentication.Plug - What to do as an API? I use Phoenix, but not with views. What is provided?

As I'm exploring and trying to setup AshAuthentication I ended up using use AshAuthentication.Plug for a plug, thinking this would provide the same set of routes as the Phoenix view version would, but it doesn't seem like it. I'm not even sure, as the docs aren't clear on this and I'm at this point digging in the code itself. Do I have to implement my own plug and controller(s) for this? I have an Angular frontend, and will maybe consume the same API from native apps later on. And also, does this mean that some features aren't available unless I use Phoenix?
73 Replies
ZachDaniel
ZachDanielโ€ข2y ago
You should be able to use ash_authentication_phoenix even if not building a UI.
simpers
simpersOPโ€ข2y ago
So I'll just not pipe it through a :browser pipeline and instead my :api one, and it should work?
ZachDaniel
ZachDanielโ€ข2y ago
Honestly not too sure but I know people have set it up for APIs plenty of times before Actually, maybe not maybe people have been rolling their own sign in/\sign out endpoints
simpers
simpersOPโ€ข2y ago
Reading the docs for the Phoenix setup it does include the :load_from_bearer function-plug in the example, so I assume it should be possible? I just reacted to the part where the routes were only included under a scope with :browser, which I do not have at all. Struggling to include the ~psigil from Phoenix.VerifiedRoutes that is used in the AuthController example of the code in the docs. I could just delete it, but it bugs me why I can't import it ๐Ÿ˜ฎ
frankdugan3
frankdugan3โ€ข2y ago
Assuming you have the generated function in your MyAppWeb file:
def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: MyAppWeb.Endpoint,
router: MyAppWeb.Router,
statics: MyAppWeb.static_paths()
end
end
def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: MyAppWeb.Endpoint,
router: MyAppWeb.Router,
statics: MyAppWeb.static_paths()
end
end
You would enable verified routes in your module with:
use MyAppWeb, :verified_routes
use MyAppWeb, :verified_routes
Alternatively, if you are already doing this:
use MyAppWeb, :view
use MyAppWeb, :view
you might consider just adding:
unquote(verified_routes())
unquote(verified_routes())
to the view macro.
simpers
simpersOPโ€ข2y ago
This is a controller, as per the example docs of AuthController. I did that, but optionally. Meaning I implemented a way to not get that. And as far as I can see it should have worked as intended, but it still complains unless I inline it in the controller. I can post my version of it later, but now I'm heading toward a train ๐Ÿ™‚
Alan Heywood
Alan Heywoodโ€ข2y ago
I have an API only ash project successfully integrated with ash_authentication_phoenix. Perhaps not 100% relevant to your situation since I have a graphql api via ash_graphql. In the router's api pipeline I use plug(:load_from_bearer), followed by my own plug that takes the actor from the assigns, does some processing and then calls Ash.PlugHelpers.set_actor. Finally I have plug(AshGraphql.Plug) which makes the actor available to ash_graphql. Following that I was able to use some of the built-in ash_authentication actions on my user object, while I had to wrap some of them in my own actions to customise or make more compatible with my API design. Some actions such as sign_out I had to implement myself.
simpers
simpersOPโ€ข2y ago
I also use GraphQL through AshGraphql At the time when I implemented my GraphQL stuff I had to use my own plugs, I think? :thinkies: Or maybe I just missed that those helper functions and plugs existed haha. This was my attempt at getting the option to include verified_routes:
def controller(opts \\ []) do
IO.inspect opts, pretty: true
caller = Keyword.get(opts, :caller, nil)
verified_routes? = Keyword.get(opts, :verified_routes, false)

quote do
use Phoenix.Controller, namespace: LpeWeb

if unquote(verified_routes?) do
IO.puts("Adding verified routes to: #{unquote(caller)}")
unquote(verified_routes(caller: caller))
end

import Plug.Conn
import LpeWeb.Gettext
alias LpeWeb.Router.Helpers, as: Routes
end
end

...

defp verified_routes(caller: caller) do
IO.puts "using verified_routes #{inspect caller, pretty: true}"
quote do
use Phoenix.VerifiedRoutes,
endpoint: LpeWeb.Endpoint,
router: LpeWeb.Router,
statics: ~w()
end
end
def controller(opts \\ []) do
IO.inspect opts, pretty: true
caller = Keyword.get(opts, :caller, nil)
verified_routes? = Keyword.get(opts, :verified_routes, false)

quote do
use Phoenix.Controller, namespace: LpeWeb

if unquote(verified_routes?) do
IO.puts("Adding verified routes to: #{unquote(caller)}")
unquote(verified_routes(caller: caller))
end

import Plug.Conn
import LpeWeb.Gettext
alias LpeWeb.Router.Helpers, as: Routes
end
end

...

defp verified_routes(caller: caller) do
IO.puts "using verified_routes #{inspect caller, pretty: true}"
quote do
use Phoenix.VerifiedRoutes,
endpoint: LpeWeb.Endpoint,
router: LpeWeb.Router,
statics: ~w()
end
end
Ignore any IO calls as they wouldn't be there normally. I just tried to debug and see what was going on as it seemingly didn't work unless I inlined it into the controller itself. My problem is that I can't figure out what I am supposed to POST to the endpoint for password registration:
โ†ช mix phx.routes
Compiling 1 file (.ex)
auth_path GET /a/sign-out LpeWeb.AuthController :sign_out
auth_path * /a/auth/user/password/sign_in_with_token LpeWeb.AuthController {:user, :password, :sign_in_with_token}
auth_path * /a/auth/user/password/register LpeWeb.AuthController {:user, :password, :register}
auth_path * /a/auth/user/password/sign_in LpeWeb.AuthController {:user, :password, :sign_in}
auth_path * /a/auth/user/password/reset_request LpeWeb.AuthController {:user, :password, :reset_request}
auth_path * /a/auth/user/password/reset LpeWeb.AuthController {:user, :password, :reset}
auth_path * /a/auth/user/magic_link/request LpeWeb.AuthController {:user, :magic_link, :request}
auth_path * /a/auth/user/magic_link LpeWeb.AuthController {:user, :magic_link, :sign_in}
* /api/gql Absinthe.Plug [schema: Lpe.Graphql.Schema, document_providers: [LpeWeb.Apq.DocumentProvider, Absinthe.Plug.DocumentProvider.Default]]
* /api/playground Absinthe.Plug.GraphiQL [schema: Lpe.Graphql.Schema, interface: :playground]
...
โ†ช mix phx.routes
Compiling 1 file (.ex)
auth_path GET /a/sign-out LpeWeb.AuthController :sign_out
auth_path * /a/auth/user/password/sign_in_with_token LpeWeb.AuthController {:user, :password, :sign_in_with_token}
auth_path * /a/auth/user/password/register LpeWeb.AuthController {:user, :password, :register}
auth_path * /a/auth/user/password/sign_in LpeWeb.AuthController {:user, :password, :sign_in}
auth_path * /a/auth/user/password/reset_request LpeWeb.AuthController {:user, :password, :reset_request}
auth_path * /a/auth/user/password/reset LpeWeb.AuthController {:user, :password, :reset}
auth_path * /a/auth/user/magic_link/request LpeWeb.AuthController {:user, :magic_link, :request}
auth_path * /a/auth/user/magic_link LpeWeb.AuthController {:user, :magic_link, :sign_in}
* /api/gql Absinthe.Plug [schema: Lpe.Graphql.Schema, document_providers: [LpeWeb.Apq.DocumentProvider, Absinthe.Plug.DocumentProvider.Default]]
* /api/playground Absinthe.Plug.GraphiQL [schema: Lpe.Graphql.Schema, interface: :playground]
...
I'm getting a 401 when trying to register, as it if wants me to be authenticated to begin with before even registering. Seems odd and incorrect. My pipelines in the router for these endpoints doesn't require it and it is the AuthController's failure/3 callback that reports this.
Alan Heywood
Alan Heywoodโ€ข2y ago
I chose a different design, where I use GQL for all of my auth. I have mutations for sign in, register and sign out etc so I never had to work out how to set up routes, other then my single /gql api endpoint, which was already working.
simpers
simpersOPโ€ข2y ago
Yeah, that's what I was doing before I found out about AshAuthentication. Had hoped to just set this up and then not think about auth for a while ๐Ÿฅฒ I'm so early in my MVP that I don't want to think so much about the boring stuff
jart
jartโ€ข2y ago
Hi @simpers if you're just doing password authentication you can submit a JSON payload that looks like
%{
resource_subject_name => %{
identity_field => "my identity",
password_field => "my password",
password_confirmation_field => "my password}
}
%{
resource_subject_name => %{
identity_field => "my identity",
password_field => "my password",
password_confirmation_field => "my password}
}
straight to the generated sign-in route (you don't need to send the confirmation field if confirmation is not enabled on your strategy). You will likely need to modify your auth controller/plug to return a JSON response rather than HTML. The other alternative which @Alan Heywood suggested (and what I chose in another project recently) was to expose the sign in action to graphql and have the clients access every resource via the me graph node (ie me { posts { name } } gives me a list of all my related posts but returns null if there is no current user).
actions do
read :current_user do
get? true
manual fn
%{resource: resource}, _, %{actor: actor} when is_struct(actor, resource) ->
{:ok, [actor]}
_, _, _ -> {:ok, []}
end
end

graphql do
type :user

queries do
get :me, :current_user do
identity false
end

get :sign_in, :sign_in_with_password do
type_name :user_with_token
identity false
end
end
end
actions do
read :current_user do
get? true
manual fn
%{resource: resource}, _, %{actor: actor} when is_struct(actor, resource) ->
{:ok, [actor]}
_, _, _ -> {:ok, []}
end
end

graphql do
type :user

queries do
get :me, :current_user do
identity false
end

get :sign_in, :sign_in_with_password do
type_name :user_with_token
identity false
end
end
end
And have the GraphQL client take the token out of the returned payload and use it for auth. My router is set up as so:
pipeline :api do
plug :accepts, ["json"]
plug :load_from_bearer
plug :set_actor, :user
end

scope "/" do
pipe_through [:api, AshGraphql.Plug]

forward "/gql", Absinthe.Plug, schema: MyApp.Schema
forward "/playground", Absinthe.Plug.GraphiQL, schema: MyApp.Schema, interface: :playground
end
pipeline :api do
plug :accepts, ["json"]
plug :load_from_bearer
plug :set_actor, :user
end

scope "/" do
pipe_through [:api, AshGraphql.Plug]

forward "/gql", Absinthe.Plug, schema: MyApp.Schema
forward "/playground", Absinthe.Plug.GraphiQL, schema: MyApp.Schema, interface: :playground
end
Things get more complicated if you want to do OAuth - you will need a browser based flow for that.
simpers
simpersOPโ€ข2y ago
I want OAuth and other things, such as magic links and so on. And the confirmation was to confirm the email upon registration. I'm not at home at the moment so I'll get back to you about the details tomorrow ๐Ÿ™‚
franckstifler
franckstiflerโ€ข2y ago
Hi, What did you fianlly go with @simpers. I have the same issue. I can't figure out how to add registration/sign-in/resets for api's endpoints
simpers
simpersOPโ€ข2y ago
I'm still sitting here looking at the router. It was a national holiday in Sweden yesterday and today I've been off work so I haven't been by the computer much for two days. The problem to me seems to be that this is meant for an OAuth2 explicit flow (not sure of the exact definition of implicit vs explicit right now tbh, it's been a while since I did this sort of thing). Though there are things in AshAuthentication to finish the remaining pieces on your own, I feel like there are some gaps in either the docs or the features to make this an explicit flow, which is what I needed. Not out of the box anyway. My frontend is Angular and not LiveView. I am not sure how I will solve it yet as I wanted to get the other features too, so I could hook up GitHub, Google, Apple ID, e.t.c..
jart
jartโ€ข2y ago
Happy to advise. If you implement a custom strategy you get very fine grained control over the whole process.
simpers
simpersOPโ€ข2y ago
Is custom necessary here because the existing ones don't have what I suspected and described above? I think I will be putting advanced auth on hold until my MVP takes a bit of shape first, and then come back to it. But it would be nice to contribute upstream if it would be possible/desired
jart
jartโ€ข2y ago
Im not sure because Iโ€™ve never tried to do oauth without a browser-based flow and I donโ€™t know how that works. Re custom strategy - if you canโ€™t get what you need from the built in strategies then this is the go to method to add your own.
ZachDaniel
ZachDanielโ€ข2y ago
Doesnโ€™t oauth make no sense without a browser? It requires the redirect to consent screens. Pretty sure itโ€™s literally(or at least practically) impossible.
simpers
simpersOPโ€ข2y ago
The flow does not require a browser, as far as I'm aware. There is a difference in the flow, though. I've had to deal with Angular for a while and since it often relies on JWT rather than cookies, the flow is similar to that any native app. It's just that the flow of credentials is a little different. Thus explicit vs implicit flow. Implicit is what this is, I think. Though I have always struggled to grasp the whole notion of this, and it was back in 2018 (gosh, time flies) when I researched this to implement some SSO stuff for some frontend(s). For example, I think when I fiddled with Angular + Facebook Auth/data I had to get the token through some pop-up, and then pass it along to my server so the server could store it and ack to Facebook that it received it, or something like that? It's like it goes in a circle rather than the server always being in the middle.
ZachDaniel
ZachDanielโ€ข2y ago
lets back up. You have an api. You want people to be able to authenticate to use it, right?
simpers
simpersOPโ€ข2y ago
Yup!
ZachDaniel
ZachDanielโ€ข2y ago
sorry, got alot going on today, ๐Ÿ˜† So when people authenticate to use your api, who are they? Okay, nvm I think I see what you're talking about is this a way for people to give you some kind of token to access their information in some specific service? what information are you expecting they would put in to your api to authenticate, for example
simpers
simpersOPโ€ข2y ago
It is for for both auth and accessing others data (calendars in this case) So sign-up can be with either pass/email as per the usual, but you should with time be able to sign-up with GitHub, Twitter, Google, AppleID, Discord whatever I might decide to add with time. And link calendars from say Google and Apple
ZachDaniel
ZachDanielโ€ข2y ago
but when people sign up that way, they'll sign up in a browser, right?
simpers
simpersOPโ€ข2y ago
It should not be dependent on that
ZachDaniel
ZachDanielโ€ข2y ago
why not?
simpers
simpersOPโ€ข2y ago
If it's a native app, it'll use native APIs to accomplish this
ZachDaniel
ZachDanielโ€ข2y ago
native apps use oauth all the time, but typically they pop up a little browser window
simpers
simpersOPโ€ข2y ago
Yes, but the UI isn't necessarily mine in these cases For Android the UI is Google's
ZachDaniel
ZachDanielโ€ข2y ago
๐Ÿค” what does your app do?
simpers
simpersOPโ€ข2y ago
Not much yet haha. Still in MVP. But the idea is to gather a user's calendars (work, private, whatever, e.t.c....) and use those as inputs. Then you can set a default output which your events through our service will be saved, with whatever settings are appropriate for that service (Google's settings might differ from Apple's). Then the service will allow groups of people to find free slots for meeting up
ZachDaniel
ZachDanielโ€ข2y ago
and when will it be google's UI and not your own?
simpers
simpersOPโ€ข2y ago
People vote on those, it is saved and it gets pushed to each person's calendar Sign-in och registration On say Android.
ZachDaniel
ZachDanielโ€ข2y ago
like...what app will be open when it happens ๐Ÿ˜†
simpers
simpersOPโ€ข2y ago
On Apple it'll be a different thing, though I haven't personally implemented this. My app will open, but the system's UI will pop up over it. Usually this is done by calling some SDK from within the app and setting up some hooks ๐Ÿ™‚
ZachDaniel
ZachDanielโ€ข2y ago
why will the system's UI pop up over it?
simpers
simpersOPโ€ข2y ago
And that is also how I've done it in Angular previously. Used Firebase's SDK for this Because that's how it works when integrating with them haha. What do you mean? Like, my view will show "Sign up with any of these" and list some buttons. And depending on the click, a different SDK will be called Then that'll take over
ZachDaniel
ZachDanielโ€ข2y ago
and that forces you to implement the implicit oauth flow on your service? because they send you a token or something?
simpers
simpersOPโ€ข2y ago
Yes
ZachDaniel
ZachDanielโ€ข2y ago
that sounds unideal. AFAIK implicit flows are significantly less secure than regular flows, and it means your api will have to support those
simpers
simpersOPโ€ข2y ago
Yes
ZachDaniel
ZachDanielโ€ข2y ago
you're sure there is no way for those SDKs you're talking about to use the standard oauth flow? And open a browser window, like everyone else does everywhere else ๐Ÿ˜† ?
simpers
simpersOPโ€ข2y ago
Everyone else on which platforms? haha
ZachDaniel
ZachDanielโ€ข2y ago
I mean, in this case I'm talking about native apps/ios, I don't use android can you like...do it yourself? instead of using a native SDK?
simpers
simpersOPโ€ข2y ago
The thing is, it may or may not open a UI in a browser if it wants to (the SDK), I don't care. But how would browser cookies help in my native iOS app? I need a JWT for the API service in that app. Hahah well, I don't know. I'm trying to minimise the complexity of what I have to do to get my MVP up. Since auth isn't that interesting of a problem to solve. It's been solved a million (or a billion?) times already haha
ZachDaniel
ZachDanielโ€ข2y ago
okay, so you're using the android SDK for oauth2, this thing I assume: https://developer.android.com/training/id-auth/authenticate
Android Developers
Authenticate to OAuth2 services  |  Android Developers
In order to securely access an online service, users need to authenticate to the serviceโ€”they need to provide proof of their identity. For an application that accesses a third-party service, the security problem is even more complicated. Not only doesโ€ฆ
simpers
simpersOPโ€ข2y ago
No, I'm not I haven't done anything yet on the android side. I'm only building the frontend in Angular for now, and the server in Elixir
ZachDaniel
ZachDanielโ€ข2y ago
either way, just an example. What you end up with on one end is a token valid for the target service right?
simpers
simpersOPโ€ข2y ago
The target service meaning my service? If so, yes Also, I think what I want is actually OpenID Connect, which is built on top of OAuth 2.0 If I'm not mistaken. If I'm not mistaken, what I had to do on the Facebook integration was to start the flow on the server, wait for the client to send me a nonce to pass back to Facebook to confirm the client approved the whole ordeal. And as such the actual credential didn't go in a circle and was only visible to the server. And they had filters and whatnot setup so that the DNS had to match with the registered IP and so on.
ZachDaniel
ZachDanielโ€ข2y ago
I'm wondering if you can just authenticate w/ google (for example) and then exchange that token w/ a regular token from your server lol, this is @jart's wheelhouse. At the end of the day I think the unfortunate answer is that implicit oauth flows (which it seems like might be what you need) are not implemented by ash_authentication. You can likely use other libraries like assent and friends to authenticate for those cases, so ideally you don't have to be entirely on your own
simpers
simpersOPโ€ข2y ago
I think you mean explicit, since implicit means the flow of the browser, as the browser implicitly just deals with this. Explicit is when you have to be explicit about sending the auth headers yourself. I think assent is used by ash_authentication? :thinkies:
โ†ช mix deps.tree | grep -C 5 assent
โ”‚ โ”‚ โ””โ”€โ”€ sourceror ~> 0.1 (Hex package)
โ”‚ โ”œโ”€โ”€ stream_data ~> 0.5.0 (Hex package)
โ”‚ โ””โ”€โ”€ telemetry ~> 1.1 (Hex package)
โ”œโ”€โ”€ ash_authentication ~> 3.11 (Hex package)
โ”‚ โ”œโ”€โ”€ ash >= 2.5.11 and < 3.0.0-0 (Hex package)
โ”‚ โ”œโ”€โ”€ assent ~> 0.2 (Hex package) <--------------------
โ”‚ โ”‚ โ”œโ”€โ”€ certifi >= 0.0.0 (Hex package)
โ”‚ โ”‚ โ”œโ”€โ”€ jose ~> 1.8 (Hex package)
โ”‚ โ”‚ โ”œโ”€โ”€ mint ~> 1.0 (Hex package)
โ”‚ โ”‚ โ””โ”€โ”€ ssl_verify_fun >= 0.0.0 (Hex package
โ†ช mix deps.tree | grep -C 5 assent
โ”‚ โ”‚ โ””โ”€โ”€ sourceror ~> 0.1 (Hex package)
โ”‚ โ”œโ”€โ”€ stream_data ~> 0.5.0 (Hex package)
โ”‚ โ””โ”€โ”€ telemetry ~> 1.1 (Hex package)
โ”œโ”€โ”€ ash_authentication ~> 3.11 (Hex package)
โ”‚ โ”œโ”€โ”€ ash >= 2.5.11 and < 3.0.0-0 (Hex package)
โ”‚ โ”œโ”€โ”€ assent ~> 0.2 (Hex package) <--------------------
โ”‚ โ”‚ โ”œโ”€โ”€ certifi >= 0.0.0 (Hex package)
โ”‚ โ”‚ โ”œโ”€โ”€ jose ~> 1.8 (Hex package)
โ”‚ โ”‚ โ”œโ”€โ”€ mint ~> 1.0 (Hex package)
โ”‚ โ”‚ โ””โ”€โ”€ ssl_verify_fun >= 0.0.0 (Hex package
ZachDaniel
ZachDanielโ€ข2y ago
I'm not so sure Yeah, it is but you might need to use it directly instead of through ash_authentication I don't think the standard browser based flow is the implicit flow isn't that the "authorization code flow"?
simpers
simpersOPโ€ข2y ago
I think a new strategy is in place. AshAuthentication.Strategy.OIDC or something? haha
ZachDaniel
ZachDanielโ€ข2y ago
Didn't someone get oidc working here at some point?
simpers
simpersOPโ€ข2y ago
This is where I read it, and though it is from 2015 it is the top result haha https://leastprivilege.com/2015/04/01/implicit-vs-explicit-authentication-in-browser-based-applications/
Dominick Baier
leastprivilege.com
Implicit vs Explicit Authentication in Browser-based Applications
I got the idea for this post from my good friend Pedro Felix โ€“ I hope I donโ€™t steal his thunder (I am sure I wonโ€™t โ€“ since he is much more elaborate than I am) โ€“ but when I saw his tweet this morniโ€ฆ
ZachDaniel
ZachDanielโ€ข2y ago
what the heck
ZachDaniel
ZachDanielโ€ข2y ago
Ash HQ
Ash.Resource
View the documentation for Ash.Resource on Ash HQ.
simpers
simpersOPโ€ข2y ago
I'm just as confused as you are. I don't know anything anymore ๐Ÿ˜…
ZachDaniel
ZachDanielโ€ข2y ago
there is an oidc strategy in ash_authentication maybe that does what you want ๐Ÿ˜†
simpers
simpersOPโ€ข2y ago
AshAuthentication.UserIdentity is part of this, I can see. It will declare a table for what I like to call connected identities. I can't find the OIDC in the docs though, but I can see there is an issue here on discord about it
jart
jartโ€ข2y ago
I thought I had merged OIDC support But maybe it got preempted by client work.
simpers
simpersOPโ€ข2y ago
That could be a reason I can't find it ๐Ÿ˜…
jart
jartโ€ข2y ago
GitHub
ash_authentication/oidc.ex at main ยท team-alembic/ash_authentication
The Ash Authentication framework. Contribute to team-alembic/ash_authentication development by creating an account on GitHub.
simpers
simpersOPโ€ข2y ago
Well, it doesn't show, but I can imagine it is because the AshAuthentication framework is hidden on the left. And I added it and tried to refresh the same link but it just reset the selection to exclude AshAuthentication
simpers
simpersOPโ€ข2y ago
No description
simpers
simpersOPโ€ข2y ago
So it is there! It might need a guide then? ๐Ÿ˜„ Clarification: once I add the AshAuthentication to the browsable frameworks, I can manually search for OIDC again. But there seem to be a parameter in the URL missing to allow the linking to work @Zach Daniel
jart
jartโ€ข2y ago
There are at least two issues open on the assent repo about mobile auth. Seems like itโ€™s not natively supported but can be bodged.
simpers
simpersOPโ€ข2y ago
Okay, sure! This is not the urgent part though haha. I think we got a little side-tracked tbh The issue I am having currently is figuring out what the built-in plugs/router stuff provides for me I'd like to just set them up and them working as expected for all strategies I add with time, but when I was trying to copy my GraphQL registration test and adapt it to AshAuthentication, I couldn't get it working :thinkies: Which is a simple email & pass registration thing Deleted message were jart was tagged - found the solution to that particular problem.
ZachDaniel
ZachDanielโ€ข2y ago
He is on vacation this week, FYI may or may not get back to you until next week
simpers
simpersOPโ€ข2y ago
Ah, thanks for letting me know! ๐Ÿ™‚ He will not be allowed to respond until he's back haha Vacation should be vacation

Did you find this page helpful?