Passwordless Auth0 and Netlify functions:  backend
Tags: serverless, webdev, auth0, netlify
Published: 2020-12-07 23:26:06

---
title: "Passwordless Auth0 and Netlify functions:  backend"
description:  "Hello World -- but only if I like you"
published: true
date: 2020-12-07 10:00:00 UTC
tags: serverless,webdev,auth0,netlify
series: "Holiday letter authentication"
canonical_url: https://katiekodes.com/auth0-netlify-backend/
---

Obviously the way to send a holiday letter to a limited audience is to make a PDF of it and attach it to a BCC email.  But [what would be the fun in that?](https://xkcd.com/1319/).  With immeasurable thanks to the ever-patient Sandrino Di Mattia from Auth0, who held my hand teaching me all of this, I now have passwordless Auth0 and Netlify Functions working together on the backend.

{% link https://dev.to/sandrinodimattia/securing-netlify-functions-with-serverless-jwt-and-auth0-5hfd %}

## Create a user

In Postman, I performed an authenticated POST HTTP request against Auth0's Management API at `https://lftbs.us.auth0.com/api/v2/users` with a `Content-Type` header of `application/json` and a body of:

```json
{
  "email": "listed_example@mydomain.com",
  "email_verified": true,
  "app_metadata": {},
  "given_name": "Katie",
  "family_name": "Kodes",
  "name": "Katie Kodes",
  "nickname": "the Python lady",
  "connection": "email",
  "verify_email": false
}
```

At first, I received an HTTP response with the `Bad Request` status code `400`, and a response body of:

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "connection is disabled (client_id: my_management_client_id - connection: email)",
  "errorCode": "auth0_idp_error"
}
```

I realized I'd turned off almost the app/API connections in `https://manage.auth0.com/dashboard/us/my-username/connections/passwordless` on a "principle of least security" _(if I can't remember why an authorization is enabled, disable it & see what breaks)_.  I flipped the appropriate application back on and tried again.

This time, I received an HTTP response with the `Created` status code `201`, and a response body of:

```json
{
    "created_at": "2020-12-07T22:29:40.755Z",
    "email": "listed_example@mydomain.com",
    "email_verified": true,
    "family_name": "Kodes",
    "given_name": "Katie",
    "identities": [
        {
            "connection": "email",
            "user_id": "876545678",
            "provider": "email",
            "isSocial": false
        }
    ],
    "name": "Katie Kodes",
    "nickname": "the Python lady",
    "picture": "https://s.gravatar.com/avatar/543212345?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fkk.png",
    "updated_at": "2020-12-07T22:29:40.755Z",
    "user_id": "email|876545678"
}
```

_(Note:  in running it again, I got the same response, only now the `created_at` and `updated_at` timestamps were different.  Indeed, there were not redundant records at `https://manage.auth0.com/dashboard/us/my-username/users`.)_

---

## Create a rule

To get `listed_example@mydomain.com` to be embedded in the access token used later in this process, I had to create a "rule" at `https://manage.auth0.com/dashboard/us/my-username/rules` and fill it with the following code:

```javascript
function (user, context, callback) {
  if (user.email) {
    context.accessToken['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress']=user.email;
  }
  return callback(null, user, context);
}
```

The actual URL of the "schemas" name isn't important, other than making sure I type the same thing later in my Netlify Function -- but it does seem to only work if it looks like a URL.  I tried simpler values like `user-email` and the email address failed to become embedded in my access token.

---

## Request a magic link

To fake being prompted to log in, in Postman I performed an unauthenticated GET HTTP request against `https://my-username.us.auth0.com/passwordless/start` with a `Content-Type` header of `application/json` and a body of:

```json
{
  "client_id": "my_app_client_id",
  "connection": "email",
  "email": "not_a_user@mydomain.com",
  "send": "link",
  "authParams": {
    "scope": "openid profile email read:letters",
    "audience": "my_api_audience"
  }
}
```

All of the space-delimited words in `authParams.scope` do separate things but are important _(well, TBD if `read:letters` will be important, but the other words ensure proper data comes back encoded in the access token I'll obtain later by clicking a magic link)_.

Including all of `client_id`, `connection`, and `authParams.audience` was also really important -- thanks, Sandrino.

At first, I received an HTTP response with the `Bad Request` status code `400`, and a response body of `{"error": "bad.connection", "error_description": "Public signup is disabled"}`.

That's a good thing -- I don't want strangers asking to get in.

I changed the email address in the body from `not_a_user@mydomain.com` to `listed_example@mydomain.com` and tried again.  This time, I received an HTTP response with the `OK` status code `200`, and a response body of:

```json
{
  "_id": "876545678",
  "email": "listed_example@mydomain.com",
  "email_verified": false
}
```

---

## Fetch a token from the magic link

I checked my e-mail and saw:

```
From: Katie Kodes <root@auth0.com>
To: listed_example@mydomain.com
Subject: Welcome to Letter From Katie
Date: Monday, December 07, 2020 6:37 PM
Size: 29 KB

Welcome to Letter From Katie!

Click and confirm that you want to sign in to Letter From Katie. This link will expire in five minutes:

https://my-username.us.auth0.com/passwordless/verify_redirect?scope=openid%20profile%20email%20read%3Aletters&response_type=token&redirect_uri=https%3A%2F%2Fmy-username.us.auth0.com%2Fauth0%2Fcallback&audience=my_api_audience&verification_code=987987&connection=email&client_id=my_app_client_id&email=listed_example%40mydomain.com

If you are having any issues with your account, please contact us through our Support Center .
Thanks!
Letter From Katie
```

At some point I'll have to figure out how to customize the wording of the email so as not to confuse tech-savvy people _(I mean, I don't exactly have a "support center")_ -- plus Auth0 wants me to use someone else's SMTP for production, nottheirs.

Nevertheless, visiting this magic link from my email inbox, I'm redirected to `https://my-username.us.auth0.com/auth0/callback#access_token=REALLY-LONG-TOKEN&scope=openid%20profile%20email%20read%3Aletters&expires_in=7200&token_type=Bearer`.  Unless I try to visit the magic link a 2nd time, that is.  In that case, I'm redirected to `https://my-username.us.auth0.com/auth0/callback#error=unauthorized&error_description=Wrong%20email%20or%20verification%20code.`

I won't expect real users to do this -- I still have to write front-end code to handle it for them -- but this works for testing purposes.

---

## Inspect the token

Grabbing `REALLY-LONG-TOKEN` out of that URL and pasting it into [https://jwt.io/](https://jwt.io/), I can see that its data payload is:

```json
{
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "listed_example@mydomain.com",
  "iss": "https://my-username.us.auth0.com/",
  "sub": "email|876545678",
  "aud": [
    "my_api_audience",
    "https://my-username.us.auth0.com/userinfo"
  ],
  "iat": 1607379133,
  "exp": 1607386333,
  "azp": "my_app_client_id",
  "scope": "openid profile email read:letters",
  "permissions": [
    "read:letters"
  ]
}
```

That's great -- I see `listed_example@mydomain.com` _(Sandrino and I had to work through adding "email" to the initial link-sending API call and adding a Rule to Auth0 to get this working)_.

---

## Summon a Netlify Function

Finally, I was ready to make a `GET`-typed HTTP request to `http://my-site.netlify.com/.netlify/functions/hiAuth` with an `Authorization` header of `Bearer REALLY-LONG-TOKEN`.

The JavaScript behind this function is straight from Sandrino's tutorial and is:

```javascript
// /functions/hiAuth.js

const { NetlifyJwtVerifier } = require('@serverless-jwt/netlify');

const verifyJwt = NetlifyJwtVerifier({
  issuer: process.env.AUTH0_JWT_ISSUER,
  audience: process.env.AUTH0_JWT_AUDIENCE,
});

exports.handler = verifyJwt(async (event, context) => {
  const { claims } = context.identityContext;
  return {
    statusCode: 200,
    body: `Hi there ${claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress']}!`
  };
});
```

[GitHub repostory here](https://github.com/kkgthb/auth0-netlify/blob/main/functions/hiAuth.js)

I received an HTTP response with the `OK` status code `200` and a response body of `Hi there listed_example@mydomain.com!`.

**Perfect**.

Purposely deforming the token, I received an HTTP response with the `Unauthorized` status code `401`, a `Content-Type` header of `application/json`, and a response body of `{"error":"jwt_invalid","error_description":"Invalid token provided"}`.

Also good.  I don't want people getting secret content without permission out of my Netlify Function.  That said, it could probably use a nicer exception handler.

Purposely omitting the token altogether, I received an HTTP response with the `Unauthorized` status code `401`, a `Content-Type` header of `application/json`, and a response body of `{"error":"invalid_header","error_description":"The Authorization header is missing or empty"}`.

Also good -- with the caveat of needing to improve exception handling, more along the lines of this JavaScript that is meant to serve a similar function using Netlify Identity authentication instead of generic JWT authentication, based on [Thor and Jason's tutorial on the Netlify blog](https://www.netlify.com/blog/2020/07/13/manage-subscriptions-and-protect-content-with-stripe/):

```javascript
// /functions/helloNetlify.js

// Begin HTTP-GET handler
exports.handler = async (event, context) => {
  
  // "clientContext" is the magic of turning on "Identity" in Netlify -- all function calls from Netlify-hosted pages w/ the "widget" in them have it
  const { user } = context.clientContext;
  const roles = user ? user.app_metadata.roles : false;
  
  // Begin bad-login short-circuit
  if ( !roles || !roles.some((role) => ['fammy'].includes(role)) ) { // PRODUCTION LINE
  //if (roles) { // DEBUG LINE ONLY
    return {
      statusCode: 402,
      body: JSON.stringify({
        message: `This content requires authentication.`,
      }),
    };
  } // End bad-login short-circuit
  
  // Begin returning secret content
  return {
    statusCode: 200,
    body: JSON.stringify({
      message: `HELLO, FRIEND OR FAMILY`,
    }),
  }; // End returning secret content
  
}; // End HTTP-GET handler
```

---

I'm quite happy with how everything turned out once Sandrino got involved.

I feel ready to move on to the front end and build a "callback" URL filled with JavaScript that can take care of transforming access tokens into cookies for me.