Dumping the netlify identity widget - using gotrue auth with email verification

I wanted my own auth UI mostly so that I could get a little more information off the user when signing up (I wanted a username and to add a newsletter checkbox).

and doing so I need to code up the sign-in UI, as well as password recovery (I should probably add email update but haven’t yet), Non-UI things that were added are dealing with incoming confirmation_tokens that come from the emails gotrue sends.
There is a cook book for gotrue Auth, though it’s for auth without email verification, which is great if you can get away with it makes the implementation much simpler.

While not the cleanest PR ever, I did put some effort into commenting much of what’s happening. So if you’re looking to implement something similar you should be able skip some of the struggling I did.

For the casual reader some highlights are

You can still use the login and signup functions from useAuth though they now need params. Confusingly enough they both take a remember param but they do very different things, on login it has to do with persisting the user’s login, and for signup remember is extra data that ends up on the user_metadata property for the identity-signup netlify function. This is redwood specific as params have different names in the gotrue auth, I discuss it more in the PR.

As far as I know confirm, recover, requestPasswordRecovery and update functions need to be done using the client directly. These are all related to dealing with verifying emails and changing passwords.

There is a useEffect that looks for specific hash params in the URL to take verification actions

      const hash = window.location.hash
      useEffect(() => {
        const [key, token] = hash.slice(1).split('=')
        if (key === 'confirmation_token') {
          // send token to go true to verify email
        } else if (key === 'recovery_token') {
          // send recovery token to gotrue to temporarily log the user in
          // then redirect to the update password page
        }
      }, [hash, client])

Hope that helps someone at some point.

4 Likes

Wanted to post a quick thanks! Was planning on replacing the Netlify Identity Widget with GoTrue this weekend and this was a great resource and timesaver.

Especially the intel on the behavior of the “remember” object.

Neal

2 Likes

@eastofwestern Oh fantastic, perfect timing.

For those who want use the GoTrue authentication interface and still want “verifying emails and changing passwords” but do not want the Netlify Widget, you can always:

  • Implement your own sign/up forms. Just use the client from the Auth Provider.
  • Use Supabase https://supabase.io/docs/guides/auth and implement pretty much the same forms (they are both GoTrue based).

They have a nice User admin management dashboard, email verification, change password features, etc.

They also recently “completed a security audit by DigitalXRAID”

2 Likes

Hi! Nice job! I’m trying to do nearly the same on my project, but i need only sign up by invitation. So far I implemented verification of invitation token and big question for me: How can i implement sending invitation email from my app? Docs not really helped me, so maybe You have some idea on this? I would really appreciate any help

1 Like

You’ll have to write a service that integrates with an email service like Postmark, Mailgun, SendGrid, Mandrill, etc.

You may want to send via Netlify background job in the case that it takes longer than 10 seconds.

You will also want to back it by some retry service or capability to ensure delivery.

This is why using an auth provider like Netlify, supabase, or auth0 is nice.

You may also need to setup mail DNS records for your domain.

1 Like

This example shows the basic features

I’d recommend using the templates offered my Postmark or Mailgun to simplify generating the mail content body.

@dthyresson Hi. Thanks for interesting article, but actually i asked about a little bit different stuff: on the Netlify website on identity sesction you can invite user by email, and he/she will receive email with invitation token. GoTrue provides API to send invitation:

POST /invite

Invites a new user with an email.

{
“email”: “email@example.com
}
Returns:

{
“id”: “11111111-2222-3333-4444-5555555555555”,
“email”: “email@example.com”,
“confirmation_sent_at”: “2016-05-15T20:49:40.882805774-07:00”,
“created_at”: “2016-05-15T19:53:12.368652374-07:00”,
“updated_at”: “2016-05-15T19:53:12.368652374-07:00”,
“invited_at”: “2016-05-15T19:53:12.368652374-07:00”
}

Invite a user
To invite a user using the admin token, do a POST request to /invite endpoint. It’s not possible to set user_metadata or app_metadata until a user has been created.

Example usage:

import fetch from 'node-fetch';

exports.handler = async (event, context) => {
  const { identity } = context.clientContext;
  const inviteUrl = `${identity.url}/invite`;
  const adminAuthHeader = "Bearer " + identity.token;

    try {
      return fetch(inviteUrl, {
        method: "POST",
        headers: { Authorization: adminAuthHeader },
        body: JSON.stringify({ email: "example@example.com" })
      })
      .then(response => {
        return response.json();
      })
      .then(data => {
        console.log("Invited a user! 204!");
        console.log(JSON.stringify({ data }));
        return { statusCode: 204 };
      })
      .catch(e => return {...});
  } catch (e) { return e; };
};

But I`m not sure how to use it with redwood

1 Like

Oh, I misunderstood

How can i implement sending invitation email from my app?

thinking you wondered how to send an email from a service.

Remember, GoTrue is an API – but you still have to implement the functionality that the API exposes.

So, while, yes, the API has an invite, by rolling your own auth, you still need to implement sending the mail, creating the link, and then handling the link back in the app that registers the user.

For example, Supabase also uses the GoTrue API because it establishes those endpoints and a standard interface, but they still implement the emails, the invites, the change password, the forgot password, the verification and generating tokens.

It’s a non-trivial amount of work.

1 Like

Thanks for posting this @Irev-Dev! The screenshot has me excited :star_struck:

How’s your project coming along?

Good thanks @thedavid.

Atm I’m pretty happy with the functionality for an MVP (a number of rough edges but that’s kinda the point) and so I’m close to trying to get a few users onto the website for some early feedback, there are just a couple of things I want to do first.

  • I want to make a few more example posts (there’s mostly dumby data there atm and it’s obviously the case).
  • Do a bit of research about if I need and what kind of community policy to have on the website (I’ve not stood up a public app before, I assume there are templates available :man_shrugging:).
  • Figure out what needs to be done for GDPR.

I’ll definitely put up a show and tell at some point soonish for sure.

It’s up at https://cadhub.xyz/

1 Like

When it comes to community policy you want a step-by-step guide to contributing if it’s open source, but otherwise just an FAQ about how stuff works is probably good. The other piece is then a Code of Conduct which will require a little more care. Definitely check out both of those resources from Redwood, the core team did a really great job with those.

The contributing guide will be more specific to whatever your project is, but if you’re looking for other CoC’s then some other communities that are known for having really good resources on that would include Rust and Vue.

1 Like

Awesome, thanks for the resources @ajcwebdev.

Looking great, @Irev-Dev :star_struck:

@rob I think these might be your people --> https://cadhub.xyz/

1 Like

You dabble in CAD modelling @rob :star_struck:?

What’s your go-to software?

Can anyone point to an example of how to implement the following as a service in Redwood?

In particular, how to import and call that service in the front-end.

*Oops hit send too soon. Looks I need to read this :slight_smile:

Hi @0x1a4f7d58

const { error, data } = await auth.api
  .inviteUserByEmail('example@email.com')

Is the Supabase code example.

Here the auth is the auth client of their sdk.

So, in Redwood, you can access your auth client that you’ve declared

<AuthProvider client={supabase} type="supabase">

using the useAuth() hook like:

import { useAuth } from '@redwoodjs/auth'

....

// in some React component, like a Page or Layout etc

const SomePage = () => {
  const { client } = useAuth()

and then you can access anything on the Supabase client as seen here:

`client.auth.api.inviteUserByEmail`

Or, you could do this in your api in a service by simply importing the SupabaseClient in your api and creating it like you might using the anon token and using that to supabaseClient.auth.api.inviteUserByEmail

1 Like

Hi @dthyresson Thanks. I was close to your first suggestion, but had excluded auth from client.auth.api.inviteUserByEmail

I have since moved to creating it as a service as the SERVICE_KEY is needed, and have implemented as follows

import { createClient } from '@supabase/supabase-js'
...
export const inviteUserByEmail = async ({ input }) => {
  const supabase = createClient(
    process.env.SUPABASE_URL,
    process.env.SERVICE_KEY
  )

  return await supabase.auth.api.inviteUserByEmail({
    email: input.email,
  })
}

But no email is being sent and the response is null :confused:

image

image

This is how I’m calling the service

const INVITE_USER_BY_EMAIL_MUTATION = gql`
  mutation InviteUserByEmailMutation($input: InviteUserInput!) {
    inviteUserByEmail(input: $input) {
      id
      email
    }
  }
`
...
const InviteUser = () => {
  const [inviteUser, { loading, error }] = useMutation(
    INVITE_USER_BY_EMAIL_MUTATION,
    {
      onCompleted: () => {
        toast.success('User invited')
        navigate(routes.currentUsers())
      },
    }
  )

  const onSave = async (input) => {
    inviteUser({ variables: { input: input } })
  }
...

And this is from the sdl

  input InviteUserInput {
    email: String!
    name: String
  }

  type InvitedUser {
    id: Int
    email: String
    confirmation_sent_at: DateTime
    created_at: DateTime
    updated_at: DateTime
    invited_at: DateTime
  }
  #https://supabase.io/docs/gotrue/server/about#post-invite

  type Mutation {
    inviteUserByEmail(input: InviteUserInput!): InvitedUser!
  }

Is there anything obvious that looks wrong? Thanks in advance.

Supabase would send the email. I’d look at the Auth logs in your Supabase dashboard auth section to see if there is any error messages – and then ask Supabase support here: supabase · Discussions · GitHub

Also, here are the docs:

https://supabase.io/docs/gotrue/client/api-inviteuserbyemail

https://supabase.io/docs/gotrue/client/api-sendmagiclinkemail

You might want to get the response, but not return the exact response back to web client.

Also, make sure you have sign up via email enabled in Suapabase.

But, it looks like maybe there’s no email on the input? I’d make sure you are sending an email address to their api.

1 Like

@dthyresson Thanks. Yes, expecting Supabase to send the email. Plus the input is good, I just tested the api call on the front-end and it works as expected…

//web side, this works but I want to create it as a service
  const onSave = async (input) => {
    await supabaseAdmin.auth.api.inviteUserByEmail(input.email)
  ...

The issue I a have is how to call the following function/service and invoke the api call via the front-end. Can I import the function? Or should I use fetch, POST, etc. I’m experimenting with the latter atm (see below)*

//api side, how can I call this function in the front-end?
export const inviteUserByEmail = async ({ input }) => {
  const { data, error } = await supabaseAdmin.auth.api.inviteUserByEmail(
    input.email
  )
}

*This is what I currently have (I need to read-up on this topic)

   const INVITE_USER_BY_EMAIL_MUTATION = gql`
     mutation InviteUserByEmailMutation($input: InviteUserInput!) {
       inviteUserByEmail(input: $input)
     }
   `

  const onSave = async (input) => {

    await global.fetch(`${global.__REDWOOD__API_PROXY_PATH}/graphql`, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
      },
      body: {
        query: INVITE_USER_BY_EMAIL_MUTATION,
        variables: { input: input },
      },
    })
  }