How would you implement this as a custom Ueberauth strategy?

I’m trying to implement a passwordless email login strategy. Essentially, someone types in their email address and are then redirected to a form where they enter a 6-digit code they were emailed. They’re also emailed a magic link should they wish to click that instead.

I roughly understand the mechanics of how to implement this–create or update an email address->code mapping when the form is submitted, email the code, accept the code via form/link and delete it, and expire it after a short timeout. I’m just not clear how to implement it as an Ueberauth strategy. In the above, is the request phase:

  • Someone typing in an email address, triggering the code to be generated and emailed (I.e. requesting the code.)
  • Someone typing in a code or clicking a magic link, requesting a successful or failed authentication.

Either way, I gather the response phase is accepting the code from the user either via form or link, and either succeeding or failing to authenticate.

What I’m unclear about is that I’d seem to have two request phases here–a request for the email address, and a second request for the code. Or is it actually a single request disguised as two? (I.e. the ultimate request flow checks both the email address and code?)

Thanks for any guidance. I’ll likely have more questions once I get a bit more clarity on these. :slight_smile:

So this is the request part of ueberauth.

And these are the callback parts, if the code is in the GET params (and the email of course) then it uses it, otherwise it displays a page asking for the 6-digit code (and email if not in the session), which then gets submitted as a param and the login happens.

To create a mapping of email address to code you’ll need a datastore, like a database. Honestly I wouldn’t bother with 6-digit codes, I’d use a Phoenix.Token and embed that in the URL that is sent via email, and also include it as easily copy/pasted text. You can have it be useless after, say, 5 minutes or whatever as well, though it does mean it can be used multiple times within that 5 minutes. Adding a database is better overall, and you can do simple 6 digit mappings then, but honestly I’d make it a 12 digit mappings or so, harder to brute-force where 6 digits can be pretty trivially done on even moderately fast websites, or just use a Phoenix.Token regardless that they have to copy/paste.

Nah, just 1 callback on ‘your’ side, it is the user that implements the front-ends job to show a page if it is missing the necessary information that it needs to pass to your strategy (or you can just error out if it’s missing and they can handle it on the error step). Ueberauth just concerns itself with the backend work, not the frontend.

Ah, got it, the one approach I didn’t consider was correct. :slight_smile: Thanks.

I lifted my AuthController from the ueberauth-example, and my callback
action currently looks like:

   def request(conn, _params) do
     render(conn, "request.html", callback_url: Helpers.callback_url(conn))

   def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
     >> put_flash(:error, "Failed to authenticate.")
     >> redirect(to: "/")

   def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
     case UserFromAuth.find_or_create(auth) do
       {:ok, user} ->
         >> put_flash(:info, "Successfully authenticated.")
         >> put_session(:current_user, user)
         >> configure_session(renew: true)
         >> redirect(to: "/")

       {:error, reason} ->
         >> put_flash(:error, reason)
         >> redirect(to: "/")

Some of that code is just stub and isn’t written yet (I.e. I don’t have
any users, and UserFromAuth will likely be replaced with an Auth
context as soon as I’ve loaded my current schema and gotten plsm sorted.)

Seems like this code should correctly handle the request phase–display
my HTML form, and other strategies that need to redirect won’t hit it
because they’ll redirect first. How should I inject a form into the
callback phase? I’m guessing the individual strategies don’t redirect,
because that seems like an application-level detail, so they’d probably
fall back through to the action. So how do I distinguish between a
callback where no UI is needed, or one where UI is required? If I pushed
the entire process to the request phase then I could just use
request.html and show a different form based on what values are in the
params, then callback just redirects/flashes regardless of what values
are given or what strategy is used, because by that point you’ve either
typed in the correct details, or we bounce you back to request with your
email filled in and you try again.

Thanks for your advice. I’ve been combing through the examples and
trying to shoehorn what I learn into our legacy requirements, and it’s
been challenging.

Just remember, ueberauth doesn’t even really know about phoenix or web serving or anythinig, ueberauth works with pure API’s, console apps, all kind of things, it just gets fed data and tries to authenticate to various sources based on the data you gave it. Your strategy shouldn’t know anything about how to redirect or forms or anything of the sort, it just tells the user of the strategy what they should do. :slight_smile:

Understood, though looking at the strategy implementation, it looks like
it has the concept of a request and callback URL. Is that only for
remote services like Oauth? So in other words, my local-only magic link
strategy won’t care about these at all?

I assumed the callback URL was the Phoenix action the strategy should
redirect to, but now I’m guessing not. Should I not be collecting the
email and magic code in the AuthController I pasted above? Is that
only for calling behind the scenes by a more traditional action at
/login, with my form-based methods alongside buttons to authenticate
with Microsoft/Google/etc.?

Thanks again.

No, it’s for anything that has both a request and callback cycle.

Actually it fits it well, request is where it ‘creates’ the token and/or mails it or so, whatever it needs to do, then callback is where it is called back to via a link in email or whatever. :slight_smile:

Got it. Sorry to be dense, but how does this square with your earlier
statements that Ueberauth isn’t aware of Phoenix at all and can work
from any context? (Sorry, using email for replies so I can’t easily quote.)

Specifically, you wrote “Your strategy shouldn’t know anything about how
to redirect or forms or anything”.

I.e. it seems like either Ueberauth does know and care that this
particular strategy has a request and callback URL set, and is thus tied
to a web-related context, or it doesn’t care, and the fact that I’m
collecting email addresses on one screen and a magic code on another is
an implementation detail that shouldn’t leak into the strategy’s
request/callback URLs. That’s why I wondered if the request and callback
URLs were for external strategies like, say, Twitter that do
explicitly have those features.

Thanks again. If I seem obsessed with getting this right, it’s because I
spent the last week fighting with Auth0, an SPA, and wrangling half a
dozen different technologies to agree on who was authenticated, their
role, etc. I really want to get this right, forget about it, and not
return to it until my client wants to add AD or other external services,
at which point I want to just plug in another strategy and continue not
worrying about it. :slight_smile:

Precisely the same. You via phoenix feed it the inputs, it does what it needs and gives information back on what needs to be done.

Correct, say like sending an email would be an aspect of the strategy (perhaps even a callback if you want it fully generic) configureable either via call or globally.

It doesn’t care what is ‘using’ the request/callback, but in general request is used to ‘setup’ an authentication request and ‘callback’ is used to actually perform it.

Hear hear! Using the above patterns I have generic request/callback handlers in my system (I don’t have local auth, but rather a few external auths) and it handles all of them, whatever may be configured, and I just read from the configuration what’s configured to be used to display buttons for in my web UI.