magic email authentication for your nextjs app
- ✅ 🙈 No passwords
- ✅ 👋 No third parties (Say goodbye to Facebook, Google, etc.)
- ✅ 🔒 Two-factor authentication (2FA) by default
- ✅ 💪 Secured with JWT tokens
- ✅ 🔥 Real-time login activty details
- ✅ 📧 Email only
Inspired by Magic
This authentication can be added to any NextJS app by following the setup instructions below.
In order for this package to function correctly, four mandatory environmental variables are required.
As you generate these values, copy them into a file named .env.local
and be sure to add .env.local
to your .gitignore
file to ensure it is not accidentally committed to version control. You can find an example of this file in this repository called .env
.
Do not share or expose these values.
For example, if you are using Vercel for your deployments you can store it as a secret in Vercel.
-
HASURA_ADMIN_SECRET
Secret password to access Hasura as an admin, used by backend to make authenticated GraphQL requests in order to create, update and verify authenticated login requests and sessions.
Generate this value using a secure password tool
-
JWT_SECRET
The secret key used to generate secured JSON web tokens. For example, we have a value of
{ "type":"HS512", "key": "zNZmbXfZuv/1cvMuieq9oW2ygtBOdJlu0x25Tyy0q9qkpNK6sa++Y2dj7z6xhTTa4mYuLfetsm6GfbKrkXkc9g==" }
This means we use
HS512
as the hashing algorithm and thekey
is the actual secret value.You can use the commands and explanations below to generate a secure value for your use
# output all system information uname -a # e.g. Linux magic-auth 5.4.0-51-generic #56-Ubuntu SMP Mon Oct 5 14:28:49 UTC 2020 x86_64 x86_64 # NOTE: `x86_64` indicates 64-bit # we will select HS512 which uses SHA-512 since it is both # 1. more secure than SHA-256 and # 2. ~1.5x faster than SHA-256 on 64-bit systems (64-bit arithmetic internally) # for 512 bits of entropy for HS512, encoded as base64 (6 bits per character) # 512 / 6 = 85.33... characters ~= 86 characters # generate a random string with 512-bits (64-bytes) of entropy and encode in base64 # this value will be our JWT_SECRET, used to sign our authentication keys and should be kept secure openssl rand -base64 64
-
SENDGRID_API_KEY
Used to send emails to the login email address to authenticate login requests and sessions.
Signup for SendGrid and generate an API key for use in this field.
-
MAXMIND_LICENSE_KEY
MaxMind GeoLite2 is a free, open source set of IP geolocation databases which can be used to secure your login sessions and requests.
Under your account generate the license key under Services > My License Key
With these three values prepared we can proceed with the rest of the setup.
In your NextJS app you can now install the next-magic-auth
package like you would normally for any dependency. The one important thing to note here is that you will need to specify the MAXMIND_LICENSE_KEY
environmental variable for the yarn install
postinstall
step.
export MAXMIND_LICENSE_KEY="<api_key_above>"
yarn install
In order to support login next-magic-auth
certain API routes are required in your application. By creating these files located under pages/api
in the NextJS project (see NextJS API Routes). For example, the snippet below is used in the demo example for the login API route.
These routes can be located at whichever path you choose. These values will be configured in your client and in some configuration we set below. For the purposes of these instructions we will set them up as follows.
login /api/auth/login
confirm /api/auth/confirm
complete /api/auth/complete
refresh /api/auth/refresh
logout /api/auth/logout
loginSendEmail /api/events/loginSendEmail
Now that we have included the required API routes, we should ensure all the configuration variables are set to the correct values. In general, sane defaults are chosen and you there are only a few which you must change, indicated with the ❗️, e.g. ❗️HOSTNAME.
Config | Default | Description |
---|---|---|
❗️HOSTNAME | 'magicwords.vercel.app' |
Hostname of this application |
Config | Default | Description |
---|---|---|
❗️EMAIL_FROM | '[email protected]' |
Email from address, you must verify this sender in SendGrid |
❗️EMAIL_LOGIN_CONFIRM | '/api/auth/confirm' |
The path to the NextMagicAuth.API.Auth.confirm route, setup in the route setup above |
❗️EMAIL_LOGIN_CONFIRM_REDIRECT | '/auth/confirm' |
The path to the page to show |
The authentication mechanisms rely on a centralized database to store the login metadata and active authenticated sessions. For this we use a Hasura, a GraphQL engine in front of a solid PostgreSQL database backend. To setup that databse backend, follow the steps below.
-
Visit https://cloud.digitalocean.com/ and setup a new $5 droplet
-
Select Dokku as a one-click app install during setup (this will save yourself some time setting it up later on)
-
Setup an
A
DNS record for the ip4v addressFor example, I setup an
A
record formagic
that points to the ip4v address on Cloudflare -
Copy the ipv4 address and replace
127.0.0.1
in the command below -
Once ssh'd into the droplet, run the commands below to setup Hasura and Postgres database
# install dokku if you didn't select it during droplet setup # this will take about 10 minutes wget https://raw.githubusercontent.com/dokku/dokku/v0.22.2/bootstrap.sh; sudo DOKKU_TAG=v0.22.2 bash bootstrap.sh dokku apps:create hasura # install the postgres plugin # plugin installation requires root, hence the user change sudo dokku plugin:install https://github.com/dokku/dokku-postgres.git # setup hostname you should replace with the domain you would like to use dokku domains:add hasura magic-graphql.iamnoah.com # setup postgres database hasura-db dokku postgres:create hasura-db dokku postgres:link hasura-db hasura # after the above command completes you will see a DATABASE_URL that starts with postgres:// # e.g. postgres://postgres:lwm7kumqjklqz6jubirf6u36n9zjo3zz@dokku-postgres-hasura-db:5432/hasura_db # <PG_PASSWORD> password in the postgres link output above, e.g. lwm7kumqjklqz6jubirf6u36n9zjo3zz # <ADMIN_SECRET> password to 1. access hasura admin console and 2. authenticate server side admin graphql queries # use a password tool to generate a strong secure password # <JWT_SECRET> secret used to generate JWT tokens, generated in secrets section above # replace <PASSWORD> <ADMIN_SECRET> and <JWT_SECRET> in the command below dokku config:set hasura \ HASURA_GRAPHQL_DATABASE_URL="postgres://postgres:<PG_PASSWORD>@dokku-postgres-hasura-db:5432/hasura_db" \ HASURA_GRAPHQL_ADMIN_SECRET="<ADMIN_SECRET>" \ HASURA_GRAPHQL_ENABLE_CONSOLE="true" \ HASURA_GRAPHQL_JWT_SECRET='{"type":"HS512", "key": "<JWT_SECRET>"}' \ HASURA_GRAPHQL_UNAUTHORIZED_ROLE="anonymous" \ HASURA_GRAPHQL_ENABLED_APIS="graphql,metadata,pgdump" dokku proxy:ports-set hasura http:80:8080 docker pull hasura/graphql-engine docker tag hasura/graphql-engine dokku/hasura dokku tags:deploy hasura # install letsencrypt plugin sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git # configure letsencrypt with email dokku config:set --no-restart hasura [email protected] # setup letsencrypt with hasura dokku letsencrypt hasura # setup automatic certificate renewal (daily) dokku letsencrypt:cron-job --add # you can setup a MAILTO to ensure errors are emailed to you # use a text editor to open the cron job sudo -u dokku vim /var/lib/dokku/plugins/available/letsencrypt/cron-job # add `[email protected]` on the line above the PATH # # e.g. # MAILTO="[email protected]" # PATH=$PATH:/usr/local/bin # dokku letsencrypt:auto-renew &>> /var/log/dokku/letsencrypt.log
-
You should now be able to visit the Hasura admin console by navigating to the domain you setup above
-
Now that you have confirmed everything is setup properly you can disable the admin console. Don't worry you can still access it locally but making it unavailable publically is a best practice for production.
dokku config:set hasura HASURA_GRAPHQL_ENABLE_CONSOLE="false"
-
Finally we setup the local
hasura
project folder to version control database metadatahttps://hasura.io/docs/1.0/graphql/core/migrations/migrations-setup.html#migrations-setup
# Create a directory with endpoint and admin secret configured hasura init hasura --endpoint https://magic-graphql.iamnoah.com export HASURA_GRAPHQL_ADMIN_SECRET=<ADMIN_SECRET> cd hasura hasura console # this will open a browser tab pointed at a local server running the hasura admin console
This example uses Apollo to provide a graphql client we can use to talk to the authentication backend. This is used to interface with the Hasura GraphQL backend with the next-magic-auth
generated token.
The authentication flow follows a consistent pattern to enforce security
-
Login request is generated by calling
/api/auth/login
withemail
field in the body -
Email is sent via SendGrid with a link to
EMAIL_LOGIN_CONFIRM
-
User clicks
EMAIL_LOGIN_CONFIRM
and is redirected toEMAIL_LOGIN_CONFIRM_REDIRECT
-
Page from Step 1, original login request, calls
auth.actions.complete
-
AuthProvider
calls/api/auth/complete
to finish login -
/api/auth/complete
responds withjwtToken
At this point authentication values are stored in the AuthProvider
React Context and available for use by any components using the useAuth
hook, for example.
This React Context component is responsible for handling the authentication state, including transitioning between various states such as logged out and logged in by calling the authentication endpoints, e.g. /api/auth/refresh
.
Ensures the loginToken
for this session exists, if it is revoked, logs out the session immediately, deleting all cookies.
Watch the login request (loginToken
) and automatically transition once the login is approved (via clicking the link to /api/auth/confirm
in an email).
This hook exposes the authentication state from the AuthProvider
context provider to any component that uses it.
const auth = useAuth();
if (auth.isLoggedIn) {
return <div>...</div>;
}
Use nodejs profiling and flamebearer to profile server code easily
# build a production build of the server that will be deployed
yarn build
# profile and run that build
NODE_ENV=production node --prof ./node_modules/.bin/next start
# this produces many isolate files with all the relevant (unprocessed) profile performance data
# the largest of these will contain the data for the server code
ls -Slh isolate*
# generate flamegraph.html and open it in the browser
node --prof-process --preprocess -j isolate-0x110008000-90734-v8.log | flamebearer
# use the search box to type the function name (e.g. the api route function name, e.g. auth/refresh)
You can now use the admin console to create new tables, view and edit rows, etc. As you make changes they will be saved as migration files and can be applied to remote hasura instances as well allowing you to test and keep changes in sync across environments.
As you make migrations you can squash from a particular migration all the way up to the latest, keeping them concise to avoid many migration folders initially when making many changes to setup the database.
# Setup migration files for the first time by introspecting a server:
hasura migrate create "init" --from-server
# squash all migrations from version 1608459932016 to the latest
hasura migrate squash --from 1608459932016
You can use the migration under hasura/migrations
to jump start your database with the models used by this demo. Be sure to replace https://magic-graphql.iamnoah.com with the domain you setup above.
hasura migrate apply --endpoint https://magic-graphql.iamnoah.com
Analyze disk usage
sudo apt-get install ncdu
sudo ncdu /
View all databases
SELECT datname FROM pg_database
WHERE datistemplate = false;
View size of database in human friendly bytes
SELECT pg_size_pretty(pg_database_size('hasura_db'))