Salesforce, Python, SQL, & other ways to put your data where you need it

Need event music? 🎸

Live and recorded jazz, pop, and meditative music for your virtual conference / Zoom wedding / yoga class / private party with quality sound and a smooth technical experience

Passwordless Auth0 and Netlify functions: backend

07 Dec 2020 🔖 jamstack web development
💬 EN

Table of Contents

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?. 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.

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:

{
  "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:

{
  "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 security principle of “least privilege(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:

{
    "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:

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.


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:

{
  "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:

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

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/, I can see that its data payload is:

{
  "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:

// /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

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:

// /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.


Posts In This Series

--- ---