Passwordless Auth0 and Netlify functions: backend

Katie - Dec 7 '20 - - Dev Community

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
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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:

{
    "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"
}
Enter fullscreen mode Exit fullscreen mode

(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);
}
Enter fullscreen mode Exit fullscreen mode

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:

{
  "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  ]
}
Enter fullscreen mode Exit fullscreen mode

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']}!`
  };
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .