Part 3
Up up and away to Auth0 we go!
I’d just like to do a quick detour and say the following:
- HashIDs are not a security tool, it’s a visual makeup tool if you need / want to obfuscate your IDs for UX or PR purposes. Like I said in my initial comment and someone else repeated above. Going to uuids because you dont want your first big customer to see that their Company PK field is 4 is the wrong reason to use an UUID.
- UUIDv4 is practically un-enumerable, but a very determined attacker with a large botnet can still conceivably hit a correct UUID, so you still need to follow the same best practices around row level security protecting against integer based enumeration attacks. To me it’s a dangerous crutch with the potential of being a foot gun because you remove an attack vector that is important to always protect against. For example a user accidentally pasting a sensitive URL somewhere. ACL is always needed regardless. This means the “upside” shrinks enough that it does not in any way outweigh the multiple downsides of uuids as PKs. Postgres is not Oracle, Postgres uses the operating system to handle all the disk stuff through pages, and random uuids are detrimental to such page loading. It will be interesting to see some numbers once UUIDv7 with the time-bits starts to see some use though.
- If you really want to have uuids to be external facing, the idiomatic solution is to keep the int as a PK and then add .marketing_uid or whatever if you need a uuid to pump into google for a ecommerce product or whatever.
- Going to uuids because they are a good shard key is a very strange argument. I so happened to work on selling CitusDB for said petabyte databases when I worked at Microsoft (both as an architect and later at pre-sales, better comp earlier in the pipe :D) and I have never ever recommended to use an uuid PK as a distribution column. Sure you want even distribution, but you also want to plan for co-location of shards for any complex joining you do and if you want to shard by tenant id for example (in a multi tenancy scenario) then it doesn’t really matter if you use uuids or not because it all gets overriden by tenant affinity.
If any interested parties want to read more, the people over at Citus has an excellent readme on data sharding here: Choosing Distribution Column — Citus 12.1 documentation
Beware of the lure of Citus though. Only Azure has it as a managed option after it was aqui-hired by Microsoft and you can go extremely far with some judicious usage of partitions and materialized views to simulate the realtime aggregates before you should reach for Citus. Even the managed version can feel like a chore to manage, with loads of foot guns
I was never involved in anything under 500TB when Citus was discussed, just to give a baseline on what kind of database size you should have before you should look towards that solution Typically the competing tech was BigTable or Databricks engine (Python on Spark and an SQL translator on top of Azure datalake/S3)
I did say in my intro post that I would talk about not only Ash, but also other topics that could be relevant for people who want to learn how to architect something and not just “code it”, but I think we have covered uuids enough now. I think it is the wrong decision to default to them in Ash but I don’t think it strongly enough to not recommend using Ash over it. Especially if “hey you can also use integers in this easy way” makes its way into the documentation PostgreSQL itself heavily recommends sticking with bigints for the PK after all.
Now, Auth0!
This is an interesting one, because I have architected (and implemented) a ton of Openid and Auth0 usage over the years and from the elixir perspective my firm belief these days is that no matter if you are on liveview, exposing a graphql endpoint or other REST the auth0 flowchart should always go directly from Openid → Elixir. As in skip any Auth0-js stuff for the SPA and all such stuff. I have made a comprehensive OpenID module that I bring along from project to project which has convenience functions to handle a full PKCE-flow (tamper proof, and encoded state for easy post-login or post-logout redirects) without any external deps other than Joken and JWKS for JWT verification.
Auth0 has a ton of functionality to create password reset tickets you can use in invite mails to facilitate an invite-only system and you can simply disable open registration in the login flow. For the purpose of this app I will just assume we allow open login and that a user goes through the regular auth0 verification loop.
Essentially you are looking at a flow more or less going:
- Use internal code to generate a login-url with whatever state and/or redirect info you want
- Go to auth0 login either by the SPA sending you there, or more convenient in liveview, simply
redirect/2
there.
- Login as usual
- Callback is set to /auth/callback_handler (or something like that)
- Elixir parses IdToken and syncs the user to the db. Check that email_verified is true and reject the callback, redirecting to a page saying why if the user is not email verified.
- If IDToken parses fine, pass off the auth and refresh token to the UserSession service that verifies the auth token. Auth token will also hold information about if the user is an admin or not, if you prefer to manage that in auth0.
- UserSessionService writes the auth and refresh token to the UserSession table and the usersession ID to Plug.Session
- Subsequent requests loads the usersession from db based on the ID, verifies the auth token (for time validity). If expired (or within a minute or two), use refresh token to grab new set of auth and refresh token from the OpenID token endpoint. This will let auth0 control token validity etc without you having to think about invalidating things for other than application logic reasons. It is also a checkbox to tick off if you need to SOC2 I believe
Important note: If you don’t create an “api” in auth0 and ask for that “api” (I just call it literally “api”) in the claims request when redirecting in, auth0 will simply give you an opaque string as the auth token and you lose the ability to JWT.verify it for time claims etc.
Important note2: Don’t fall for the temptation to just pass the IDToken around, that big boy is heavy and should only be used at initial login
I have created a tenant, extracted the various config points and I am now curious what Ash actually does under the hood. I suspect / fear that it is not what I outlined above, but hopefully I will be pleasantly surprised! After reading the auth0 guide that is placed at the top I however see that no concept of session is mentioned and I also realize that even though the Auth0 is at the top, I should probably start with “Getting started with auhtentication”.
Meaning I go from https://ash-hq.org/docs/guides/ash_authentication/latest/tutorials/auth0-quickstart to https://ash-hq.org/docs/guides/ash_authentication/latest/tutorials/getting-started-with-authentication
Bingo! One of the first things it shows is a Token
resource, this makes me happy. …Oh it’s if you need to encode stuff that “doesn’t fit in your Token resource”. As in wont be easily encoded in Phoenix.Token maybe? I don’t know. I read on in the docs that start to feel a bit like a rabbit hole in Alice in Wonderland now and move on to the Ash Authentication + Phoenix guide. I think my experience so far is more a problem with the jumbled way Ash displays all these different guides in the sidebar more than “bad documentation”, just so that is said.
Landing safely at https://ash-hq.org/docs/guides/ash_authentication_phoenix/latest/tutorials/getting-started-with-ash-authentication-phoenix I follow the guide there now, adding all the stuff they ask me to:
- deps
- helpers in the web macro module (would really have liked a why here :D)
- tailwind stuff “if you plan on using our default tailwind components” (would also have liked a link to some explanation here, because I have no idea what the ash components referenced are, but it sounds cool and I am going FULL KOOL AID in this toy project, so I’m adding it!)
- Repo stuff is a repeat of part 2 (I still don’t have any extensions even after following the guide, just so thats said @zachdaniel :D)
- Supervisor for the authentication system. Small nitpick here, the docs should probably show the
Endpoint
thing last in the list so that you stick to the good practice of not starting the Endpoint (inherently starting to serve requests) before everything else is done.
Then we start digging into the Accounts
stuff and define the ash APIs needed.
First hiccup is when adding the user.ex resource as described. It has a hashed_password
attribute, has a authentication
section that defines a password strategy. This does not feel like what I want to do and I hop back to the Auth0 guide to read a bit more.
Bingo, the section that shows the User even says “assuming you have everything else setup” shows me to add:
authentication do
strategies do
auth0 do
client_id MyApp.Secrets
redirect_uri MyApp.Secrets
client_secret MyApp.Secrets
site MyApp.Secrets
end
end
end
I grab this snippet and hop back to the ash auth phoenix guide.
However, I now have a token section that I have no idea WTF does.
tokens do
enabled? true
token_resource Example.Accounts.Token
signing_secret Example.Accounts.Secrets
end
Why do I want this?
The auth0 guide talks about AshAuthentication.GenerateTokenChange
.
I google this and end up at the following page:
I am thoroughly impressed that an Elixir framework managed to send me back to 2008 and coding Enterprise Java Beans
I am obviously knowledgeable enough about the various moving pieces here to start to unravel this and look under the hood tomorrow, but this is going to be very confusing for a noob who just wants to login and figure out how to do this with Auth0 without Growth as a sample app to look at because you need to hop guides back and forth so much.
Todays questions and suggestions:
- Auth0 guide needs to stand a lot more on its own legs. I would just duplicate the everliving hell out of everything. It means you need to work harder to keep stuff up to date, but I hope that at 3.X Ash is mature enough that there won’t be overly large changes needed to something as core as authentication?
- What is it that actually goes on when using Oauth2 / OpenID in Ash? The strategy page doesn’t really say other than explaining what the various configuration options do.
- I do like how easy it is to do an openid provided upsert allowing for registration, while at the same time also letting you easily deny non-invited users coming in from auth0.
- What is it
change AshAuthentication.GenerateTokenChange
in AshAuthentication.Strategy.OAuth2 — ash_authentication v3.12.0 actually does? I have tried to dig for like 10min now and have no idea whats going on
I hoped I’d have gotten auth done in my two hours post-baby time today, but it seems my Auth0 ventures will continue tomorrow.
Edit: And just so its said, there is a substantial non-zero likelyhood that I will simply keep authentication outside Ash and just plug into AshAuthentication Policies (which do look very nice), but with all the authorization and verification ceremony happening outside Ash. It will be a nice excersize in utilizing the fabled simple to use escape hatches
Edit2: I also realized that ash_authentication replaces mix phx.gen.auth and I just want all the nice policy stuff, so yes, tomorrow will be good old OpenID on Phoenix before I circle back to Ash