A toy project in a non-toy framework. Observe my journey as a Phoenix veteran trying out Ash for the first time

Hello!

I am a guy who really don’t like declarative stuff and I don’t think there is enough plumbing in a regular Phoenix project that I think Ash has a void that I need to fill. But I watched the elixirconf video and realized I was maybe being a bit close minded. It seems it is indeed trying to solve not just plumbing/boiler plate, but also how you go about architecting a project. If I go from my negative stance of this way of “magic included” framework coding to either “oh cool less boilerplate” or “oh cool, a new way to think about how to build software” I will consider this a success.

The premise:
I am following a fairly advanced weightlifting program and the current plateau barrier breaking segments of the program are getting very hard to model sensibly with excel macros. I have been using excel macros as a crutch for logging weights and reps on my phone.

Essentially the program is a “endless” program meant for somewhat advanced lifters who just wanna hit the gym X times a week and get the most ROI. It works in the following way:

  • Test week for two weeks, find your 8-12rep range (if you are beginner, if not you prolly know it already)
  • Progression weeks are 5-8 to reps, if you make 8 reps you bump the weight
  • Since this is a “I have a job, three kids and just need to minmax my gym effort” program, you bump it the minimal amount. This program prefers you to use machines because they are often much easier to scale in this way and there is way less ceremony in messing around with tiny plates of 0.25kg etc.
  • If you plateau, you can regress down to 2 top sets instead of 1 top set. Then you only progress if you manage 5 + 5 reps. As in if you don’t manage to break 180kg leg press x 8, you regress down to trying to do 5 + 5 reps instead of the single top set.

After this? Rince and repeat. Unless you are an ex powerlifter who decided to come back after a year or two of break, this program should have you running for at least a couple of years until you stop seeing meaningful progress.

The tech / architecture I need to achieve this:

  • Logging in (will use auth0 as free tier is generous and Ash seems to have Auth0 stuff)
  • Set up your program
    • How many days (I run 3-split on mine now)
    • Which exercise per day
    • Repeating exercises must be supported (as in I do leg press both day 1 and day 3) and weight increase should be weekly, not per day. So if I make 8 reps day 1 but only 6 reps day 3, it does not bump the weight until I make 8 reps both days.
    • Exercise needs to be able to set rep-bump-weight (leg press machine has different intervals than shoulder press machine etc)
    • Manual overrides are probably needed because of things I have foreseen. Also maybe to reset a weight if you have an injury or if you were busy moving houses over the summer while having a baby and you havent been able to see the inside of a gym for two months. This may or may not have been me over the summer.
  • Stats (main reason I log in the first place, gotta have those sweet gains graphs)
  • Mobile first (mobile only) liveview with tailwind.

This is a non-exhaustive list and I will update it (with notes and timestamps) as I go deeper in my ventures here.

I am going to be doing this over the course of a lot of nights as I am in paternity leave and I have very limited coding time.
So today was spent mainly to write this spec and note down what I need to do get going.

I hope this “blog” series on the forum can be two things:

  1. A nice intro to Ash framework for other people who wonder what the buzz is.
  2. A nice intro to how a 15 year veteran architect / CTO who has coded elixir for almost 10 years goes about modeling this toy project. I will be modeling it as if someone paid me to make some solid software, but with a small budget. I am not going to Enterprise this little baby of mine.

I got as far today as:

  • Making @kip and @zachdaniel get together on slack to make ash_double_entry default (or at least more fully support) using ex_money so that the framework does not expose such a big foot gun to people doing something else than dollars. Any money involving project should always default to using ex_money and the underlying ex_cldr :wink:
  • Realizing that it feels super weird to do mix new with a supervisor instead of mix phx.new which has been my default more or less since a few days after it was first released.
33 Likes

Looking forward to seeing how this goes! It will be a win win for Ash no matter what your results end up being, since we’ll have the context and areas to improve!

5 Likes

To be fair, @zachdaniel has always been amenable to collaboration and we’ve discussed it before. I took an action to drive the next step and then real life got in the way. I hope now the timing is better for all concerned.

9 Likes

I actually do like declarative stuff and I’m big on DDD and Ash has always looked good to me (I’ve read through a lot of the docs) but I still haven’t tried it. I watched the talk last night and it got me a bit more excited again, especially when I saw code that looked my code that I’m generally really happy with and was told it could still be better :sweat_smile:

With some reflection there are few reasons I still haven’t tried it:

  • I started a greenfield project at new job and it’s a bit stressful so I didn’t want to dive into anything new.
  • Sunk Cost Fallacy: I really started hitting my stride with Phoenix last year developing a style I’m really happy with and barely had any time to revel in that sweet feeling of confidence when I learned about Ash :sweat_smile:
  • A possible/probable unfounded fear of being too far removed from the concepts I’ve grown to love, mainly learning another query abstraction on top of Ecto.

In any event, I look forward to this, and the workout focus of this could do me some good too :grimacing:

Btw, I love your presentation style @zachdaniel! I can’t put my finger on it but you held my attention the entire talk.

1 Like

This could be double interesting if OP makes sure to have some sort of milestones – e.g. GitHub releases – with traceable steps and not a huge number of commits / diffs in-between, so any onlookers can retrace his steps and see the meaningful changes step by step.

(Or, as a good CTO already knows – :wink: – to make sure commits/PRs are more or less atomic.)

I have been tempted by Ash for at least a year but health and real life are constantly in the way so this would be a very welcome opportunity to observe passively every now and then.

So – good luck! Interested in the journey.

2 Likes

Part 2!

I rounded off with how weird it was to start with mix new and a supervisor tree. Apparently that feeling of weirdness was correct, because the guides for AshPhoenix says to start with a mix phx.new indeed :smiley: I thought it was a bit weird that Ash hid and abstracted that much. This guide: https://ash-hq.org/docs/guides/ash_phoenix/latest/tutorials/getting-started-with-ash-and-phoenix for those who are curious.

So: I have now done more due dilligence and read more than just the core Ash and Auth0 guides. The guide for AshPhoenix feels a lot more at home to me since I know Phoenix very well. I do wonder why Ash has such a hangup on using uuids. I am not a big fan of using uuids. If you need something opaque for the end user that isn’t a digit (like you don’t want to show your fancy new customer you just sold your software to that his company has a PK of 3) I have had great success with using ints internally and just displaying them externally with a HashedInt (https://hex.pm/packages/hashids). Uuids has their place, like if you have needs where PKs needs to be generated outside your app and still come in with somewhat collision free guarantees etc but the downsides of uuids in postgres are pretty big imo. Clunky to work with when writing queries and it takes away some benefits of stable sorting etc. See running list of questions at the end.

The liveview usage segment intrigues me a bit, I do find typing out all the changeset blabla boilerplate in my service modules (context modules for you who like that nomenclature) to be a bit tedious, so I like that that is all taken care of. Including all the form helper stuff from lv 0.18+.

I am ready and I upgrade phoenix installer to the newest, do a mix phx.new growth (perfect name for a muscle building app) and immediately go in to sort the deps alphabetically before adding the ash ones. Gotta have the house in order, right???

I wondered why ash wants me to use their create repo command but realize now that it is so the extension installer can run. Not sure I am a big fan of that. It should just be emitted by the migration generators. It’s not like I’ll be running ash create against a prod instance anyways, so now I have to manually remember to fix that before I deploy this.

Also, the formatter documentation is out of date (told Zach on slack) as a default mix phx.new includes the html formatter plugin now and the example at https://ash-hq.org/docs/guides/ash_phoenix/latest/tutorials/getting-started-with-ash-and-phoenix#add-dependencies does not show it. Some newbs might be missing out on the awesomeness due to this, so beware :smiley: (Ok before I had time to mix deps.get and set up my Repo file, zach has already pushed a fix for this that will be include in next release, not bad!)

Repo is now set up and I am ready to start specifying my domain. One of the very few things I dislike (a lot) about the phoenix generators is that I think the Context design they produce is promoting sloppy modules. I think it is many orders of magnitude better than when noobs put all their business logic in the controller, but if you generate a couple of schemas you end up with a 800 loc context that is hard to navigate. I have been mandating a pattern of FooSchema (preferably anemic, no code that is not around validation, and no external module calls) coupled with a FooService. FooService deals only with Foo, or in some special cases maybe FooCloseRelative. Think User and UserRole. Any cross-domain communication should be done with a service talking to a service.

Ash resources are anything but anemic, but each section of the resource promotes a clean and anemic-style definition set. It also means that a Registry stays very terse and you never end up with the gazillion-LOC contexts that a generator based phoenix app gives you.

I set the default config required and map up a Growth.Workout registry. I start to map my first entity and I hit the first uuid-snag. I don’t want an uuid, I want an integer. I think the docs here should either showcase both side-by-side or at least have a documentation link directly to the attributes. I had to go hunting in the docs to find how to specify an integer. The doc search shows no results on “primary_key” (I would expect it to at least show the already existing “uuid_primary_key”, but no). I did find “integer_primary_key” by searching for the uuid one and clicking through. Ash is an elixir library after all, so as expected the docs are clear and informative when I just find the right spot :wink:

It is time for me to run mix ash_postgres.create to see what all the fuzz is about! … aaand drumroll wut?
image
Where are my extensions?
https://github.com/omt-tech/growth/blob/main/lib/growth/repo.ex#L5-L7
I was expecting the uuid-ossp and citext extensions to be active?

Anyways, at this point I realize that I should probably put in place the user system before I start mapping up too much of the programs and stuff because almost everything I want to model up is tied to the user. So I have now pushed up an initial commit at https://github.com/omt-tech/growth including my first api + resource and I plan to swap over to the User work to be able to log in with Auth0 tomorrow or the day after.

If my text seems jumbled it’s because I have been writing it on and off throughout the day, my lil six month old girl Ellinor does not find coding as exciting as I do apparently.

Questions (for @zachdaniel or whomever else wants to chip in):

  1. Why so much uuid defaulting?
  2. Are my assumptions about why to use ash create wrong? Did I find a bug around the extensions? :smiley:
6 Likes

Great stuff!

UUIDs

UUID is mostly just a sane starting point, but you can use your own primary keys as well:

attribute :foo, YourType do
  primary_key? true
end

EDIT: one thing to keep in mind is that generated belongs_to attributes default to :uuid. You will get a warning at compile time if you try to do something like have a belongs_to relationship from a :uuid type to an :integer type, but basically for compilation-related reasons, we can’t magically assume the type of a belongs_to relationships source attribute to be the type of the field it refers to. So we just default to :uuid. You can configure the default here:

config :ash, :default_belongs_to_type, :integer

and read more about it here: Relationships — ash v2.14.21

ash_postgres.create

mix ash_postgres.create is a thin wrapper around mix ecto.create that finds the repos to use by looking at all the repos in use by your app. The migration generation happens when you do mix ash_postgres.generate_migrations.

With that said, we’ve got the work done and will be pushing some new mix tasks across the documentation, that automatically pick up what extensions you have and run any corresponding tasks. They are like built-in aliases with extra features.

In the context of ash_postgres (the only one who has implemented the hooks for these new tasks) you have:
mix ash.setup - creates your db
mix ash.codegen - generates migrations
mix ash.migrate - runs migrations
mix ash.tear_down - drops your db

5 Likes

Nice, I subscribed to the repo’s PRs and releases. Also left you a comment on the 2nd commit.

1 Like

this is great stuff - have had a keen eye on Ash for ages - but pragmatism/lack of time/risk aversion always end up postponing the dive in…

2 Likes

You cant introspect the primary key of the remote schema at compile time?

We can, but not in such a way that we can modify the current module (without inducing compile time dependencies that could result in deadlocks). So instead we check at compile time and validate using a compiler step that happens after modules have been compiled (after_verify).

Even if we could, if you were do do something like this (don’t):

belongs_to :foo, Foo
belongs_to :bar, Bar do
  source_attribute :foo_id
  define_attribute? false
end

where the attribute on Foo is a string and the attribute on Bar is a uuid. This is valid in some cases (pretty sure not with postgres, though).

Mostly security, as they don’t expose guessable identifiers, which is especially important in APIs, but also they can give away things like order of magnitude or even close to exact number of users (just signup and you get the next ID and that’s roughly the count).

As you point out uuids can be generated on the client also (think of a complex mesh of objects that you want to ideompotently update or create, e.g. an upsert).

Another consideration for your database is they don’t require as much sequence coordination across nodes if running a cluster, eg a citus postgresql cluster supports uuid (previously citus only supported ints with clever handling of sequences in the citus coordinator).

Sidenote, you can scale to petabytes with Postgres using the Citus extension which makes Postgres a most compelling solution initially, and well beyond unicorn status.

I normally don’t reach for integer IDs, so I personally saw this a positive sensible default in Ash as I stopped using integer IDs around 2012.

I do accept that if you prefer to map integer IDs that is also quite a valid approach to addressing security provided it is actually done securely and efficiently. Sadly that library does not use established cryptographic primitives, a better approach would be to use a well known encryption algorithm (eg AES) to encrypt and decrypt IDs and map the encrypted bits to an alphabet such as the nano id alphabet (A-Za-z0-9_-) on the server.

Excitingly it seems there is a new mix task for generating liveviews on main…

Awesome. That was something I noticed also with the out of the box Ash types in general, no built in support for money (currency and amount), but it was something I planned to implement as a custom type when I implement billing.

5 Likes

Or shamefully low ids.

If these are the only reasons why you reach for uuid (speaking to an audience now), then I have an alternative:

Use this ecto type:

defmodule HashedIdType do
  use Ecto.Type

  @type t() :: String.t()
  @hasher Hashids.new(salt: "123", min_len: 13, alphabet: "123456789abcdefghijklmnopqrstuvwxyz")

  @impl true
  def type, do: :id

  @impl true
  def cast(id) when is_integer(id) do
    {:ok, encode_id(id)}
  end

  def cast(hashed_id) when is_binary(hashed_id) do
    {:ok, hashed_id}
  end

  def cast(_), do: :error

  @impl true
  def load(id) when is_integer(id) do
    hashed_id = encode_id(id)
    {:ok, hashed_id}
  end

  @impl true
  def dump(hashed_id) when is_binary(hashed_id) do
    id = decode_id(hashed_id)
    {:ok, id}
  end

  def dump(_), do: :error

  def encode_id(id) when is_integer(id) do
    Hashids.encode(@hasher, id)
  end

  def decode_id(hashed_id) when is_binary(hashed_id) do
    case Hashids.decode(@hasher, hashed_id) do
      {:ok, [id]} -> id
      _ -> nil
    end
  end
end

put this on every schema of yours (you can do this by replacing use Ecto.Schema with use MyApp.Schema

defmodule MyApp.Schema do
  defmacro __using__(_) do
    quote do
      use Ecto.Schema

      @primary_key {:id, HashedIdType, autogenerate: true}
      @foreign_key_type HashedIdType
    end
  end
end

and with this in place, you’ll never have to think about this again, and your users will never know they are user #42.

3 Likes

Everything is fine, but the hashid implementation is not cryptographiclly secure and possibly slower than AES.

Replace the hashids with AES:

Example encrypt/decrypt from here

Setup a key for your application ONCE (don’t change it once in production).

{:ok, aes_256_key} = ExCrypto.generate_aes_key(:aes_256, :bytes)

Encrypt the db id:

{:ok, {init_vec, cipher_text}} = ExCrypto.encrypt(aes_256_key,  <<db_id::integer-size(64)>>)
param_id = encode_id_string(cipher_text)

Decrypt the param id to get the original db id:

cipher_text = decode_id_string(param_id)
{:ok, val} = ExCrypto.decrypt(aes_256_key, init_vec, cipher_text)
db_id = <<val::integer-size(64)>>

The functions encode_id_string(cipher_text) and decode_id_string(param_id) convert the binary cipher text to the url safe alphabet as discussed in my previous message.

Again, I prefer uuids in the database vs mapping them everywhere as they are also uniformly distributed and shard friendly.

1 Like

I’d also suggest that this is a purely representational concern and should probably not affect the type of your id.

uuid_primary_key :id do
  private? true
end

calculations do
  # add a public_id calculation for display
  calculate :public_id, :string, MyApp.Calculations.PublicId

  calculate :matches_public_id, :boolean, MyApp.Calculations.MatchesPublicId do
    argument :public_id, :string, allow_nil?: false
  end
end

defmodule MyApp.Calculations.PublicId do
  use Ash.Calculation

  # this kind of calculation can't be automatically filtered on
  def calculate(records, _, _) do
    Enum.map(records, fn record -> &hash_id/1)
  end
end

defmodule MyApp.Calculations.PublicIdMatches do
  use Ash.Calculation
  require Ash.Expr

  # this kind can, because it happens in the data layer if necessary
  def expression(_, args) do
    id = unhash(args.public_id)
    Ash.Expr.expr(id == ^id)
  end
end

You could write a small HashedId extension that would add these calculations and the attribute to any resource.

EDIT: it would make hooking up certain get_by_id actions a bit more tedious as you won’t be able to use the default behavior of “primary key equals X”. You’ll end up with stuff like

read :by_public_id do
  argument :id, :string, allow_nil?: false
  filter expr(public_id_matches(public_id: arg(:id)))
end

and then to hook that up to something like ash_graphql

mutations do
  update :update_action,
    read_action: :by_public_id, # look the record up using the by public id action
    identity: false # don't automatically add an input for id & filter by it
end
2 Likes

Can you substantiate this claim? I’d think nothing is cryptically secure for small integers, since collision attack can be done easily. If someone broke my hashids and recover the integer ids, what is the problem other than shameful low ids?

I think maybe we should find a way to branch off of this discussion. It will turn into the kitchen sink as OP continues his writing on Ash and we respond with anything interesting :laughing: Just a thought.

7 Likes

I don’t really want to have this side thread in here either, but I suggest you read the code… :slight_smile: it doesn’t use a cryptographically strong cipher, its merely a weak obfuscation mechanism. The output from strong ciphers can’t be reversed without the secret key and are effectively indistinguishable from random noise. You cannot learn anything about the plain text (db id) from the ciphertext (the id in the url) or guess the private key.

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 :slight_smile:

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 :smiley: 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 :slight_smile: 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:

  1. Use internal code to generate a login-url with whatever state and/or redirect info you want
  2. Go to auth0 login either by the SPA sending you there, or more convenient in liveview, simply redirect/2 there.
  3. Login as usual
  4. Callback is set to /auth/callback_handler (or something like that)
  5. 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.
  6. 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.
  7. UserSessionService writes the auth and refresh token to the UserSession table and the usersession ID to Plug.Session
  8. 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 :smiley:

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 :slight_smile:

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 :joy:

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:

  1. 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?
  2. 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.
  3. 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.
  4. 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 :smiley:

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 :slight_smile:

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 :slight_smile:

12 Likes

Its absolutely the right idea to start with users instead of pushing the problem til later, but FWIW auth0 is a lesser used path for AshAuthentication, and isn’t nearly as plug and play as something like Oauth. This is obviously something we need to fix, especially how difficult it is to follow the guide. I just want to point out that you’ve probably started out with one of the most complicated pieces you could find :laughing: I’ll reach out to users on the discord and see if anyone who has set up auth0 or if @jimsynz has some input.

  1. Yes, agreed :+1: We’ll work on that. As part of our 3.0 plans in the next few months, we will be revisiting and reworking every piece of documentation in the framework to make it cohesive, with a focus not on “information density” but on the experience of setting things up, and the ability to discover things.
  2. I’ll leave this up to @jimsynz or whoever else has set up auth0 with Ash.
  3. noice.

For the GenerateTokenChange, it’s first important to understand what a change is. It’s a plug but for Ash.Changeset. They are meant to be a way to share some modification of an action/build your application. So that change GenerateTokenChange points to a module that has a change/3 function, that takes a changeset and returns a changeset.

The meat of that implementation is this:

    Changeset.after_action(changeset, fn changeset, result ->
      {:ok, strategy} = Info.find_strategy(changeset, context, options)

      if Info.authentication_tokens_enabled?(result.__struct__) do
        {:ok, generate_token(changeset.context[:token_type] || :user, result, strategy)}
      else
        {:ok, result}
      end
    end)

So that change adds an after action hook that will generate a token and set it as metadata on the resource. That token would be the token that would get set in the session. AshAuthentication stores this in the session in the AuthController

  def success(conn, _activity, user, _token) do
    return_to = get_session(conn, :return_to) || ~p"/"

    conn
    |> delete_session(:return_to)
    |> store_in_session(user)
    |> assign(:current_user, user)
    |> redirect(to: return_to)
  end
6 Likes