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.

Securing Netlify Functions with serverless-jwt and Auth0
Sandrino Di Mattia ・ Jul 28 ・ 6 min read
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 "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" }
(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.
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" } }
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 }
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/, 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']}!` }; });
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.
Top comments (0)