How to Set Up Passwordless Authentication on Airtable with Cotter
This Autocode app is a completely self-contained reading list app with
passwordless login. It allows users to store books they enjoy, which are then
stored in Airtable. The __main__.js
endpoint acts as a simple single page app
that shows the correct books for the logged in user, or a login screen if the
user is logged out. You can install your own version in just a few clicks!
If the user is not logged in or has not signed up, they will be prompted to do
so passwordlessly by entering and verifying their email address using
Cotter. Once the user has authenticated, their information
is also stored in Airtable, and the user can add
books to their personal, private reading list.
This example app combines the awesome user interface and administrative capabilities
of Airtable with lightweight, scalable authentication and API layers. It's great
for projects where non-technical people might need to look at or administer the
database, for proving out MVPs, and more. Let's dig in!
TL;DR (30s)
First, clone this Airtable base to your Airtable account. Next, log into or
sign up for Cotter (passwordlessly!) and grab your
API Key ID from your Cotter dashboard. Finally, press Install Free
above, then link your Airtable base and enter your Cotter API key when prompted.
And that's it!
Navigate to the __main__
endpoint of your deployed project in your browser (it should be
something like https://USERNAME.api.stdlib.com/cotter-airtable-example
)
and you should see global book recommendations. After logging in, the page will
refresh and show a form where you can add to your own personal reading list.
Try it out, and you should see the new books appear in the table and
corresponding records appear in your Airtable base!
How It Works
When you link your cloned Airtable base to your app and install it to your
account, Autocode automatically handles authentication between your app and your
Airtable account using your app's library token
(see the const lib = require('lib')({token: process.env.STDLIB_SECRET_TOKEN})
line at the top of all the endpoints). This allows your app to retrieve and
create Airtable user and book records.
This app contains six endpoints:
functions/main.js
The __main__
endpoint uses the ejs package to serve the static template file found under
/templates/index.ejs
as a frontend. The frontend makes the necessary calls to other endpoints using
fetch
and sets up the Cotter login form and SDK.
<script
src="https://unpkg.com/cotter@0.3.22/dist/cotter.min.js"
type="text/javascript"
></script>
Authentication works by using the Cotter SDK to check for an access token like this:
let cotter = new Cotter("<%= COTTER_API_KEY_ID %>");
cotter.tokenHandler.getAccessToken().then(tokenObject => {
let accessToken = tokenObject.token;
});
If there's no access token set, the app shows a Cotter login screen. Otherwise,
it passes that token into API calls via the x-cotter-access-token
to Autocode
endpoints. We use this header instead of the authorization
header because the
authorization
header is reserved for Autocode token authorization.
Note: Autocode endpoints only respond to GET
and POST
requests, and
respond the same to both methods. Parameters are parsed from the querystring for
GET
requests and the request body for POST
requests, so make sure you use
POST
requests if you're passing sensitive data.
There's also some logic in script
tags in the template for displaying books retrieved
from endpoints, as well as logic that hides and shows elements based on whether
the user is logged in or out.
functions/login.js
This endpoint is responsible for initial authentication. It takes access tokens
created by Cotter on the frontend, verifies them, and, if the user is new,
creates a new record in the Users
table of the linked Airtable base. Because
auth tokens from the frontend are granted passwordlessly via Cotter, that's the
only difference between signing up and logging in!
let userQueryResult = await lib.airtable.query['@0.5.4'].select({
table: 'Users',
where: [{
'Email': email.toLowerCase()
}]
});
// Signup vs. Login
if (!userQueryResult.rows.length) {
let insertQueryResult = await lib.airtable.query['@0.5.4'].insert({
table: `Users`,
fieldsets: [{
'Cotter User ID': cotterUserID,
'Email': email.toLowerCase()
}]
});
return insertQueryResult.rows[0].fields;
} else {
return userQueryResult.rows[0].fields;
}
You might also notice a middleware/cotter_auth.js
file that several endpoints
in the app import. The code in this middleware contains the actual logic that
verifies access tokens with Cotter, and is used in all of the endpoints that
require authentication:
let valid;
try {
valid = await cotter.CotterValidateJWT(token);
} catch (e) {
return false;
}
if (!valid) {
return false;
}
let userInfo = new cotterToken.CotterAccessToken(token);
if (userInfo.payload.aud !== process.env.COTTER_API_KEY_ID) {
return false;
}
return userInfo;
Check out Cotter's docs
for more information on how this flow should work.
functions/select/books/public/recommended.js
This endpoint does not require any authentication, and returns books in the
Airtable base that do not have an associated ID that you can think of as public
recommendations. Like several of the other "backend" APIs here, it uses the
airtable.query API to retrieve data:
let bookQueryResult = await lib.airtable.query['@0.5.4'].select({
table: 'Books',
where: [{
'Cotter User ID__is_null': true
}]
});
These API calls use the KeyQL query language. If you're interested in a deep
dive, you can check out the spec for
more examples.
Because this API does not require any authentication, you can call it directly
with cURL
, fetch
, or whatever HTTP client you prefer.
$ curl --request GET --url \
'https://YOUR_USERNAME.api.stdlib.com/cotter-airtable-example/select/books/public/recommended/'
functions/select/books/main.js
This endpoint works similarly to it's public counterpart, but only returns books
with a user id matching the currently logged in user:
let bookQueryResult = await lib.airtable.query['@0.5.4'].select({
table: 'Books',
where: [{
'Cotter User ID__contains': userInfo.getID()
}]
});
The __main__
at the end of the endpoint name here signifies that this handler
should respond to requests to the path /select/books/
. We opt for this style
instead of functions/select/books.js
because of the conflictingly named
functions/select/books/public/recommended.js
endpoint.
Note: All linked record fields and lookup fields in Airtable, even ones that are one-to-one,
are stored as arrays. Therefore, we must use the contains
KeyQL
comparator.
functions/insert/books.js
This endpoint inserts new books into the Airtable base for the authenticated
user. It takes parameters that define a book, and creates a record with the
insert method of the airtable.query API API:
let insertQueryResult = await lib.airtable.query['@0.5.4'].insert({
table: `Books`,
fieldsets: [{
'Title': title,
'Genre': genre,
'Published On': publishedOn,
'Author': author,
'User': [userQueryResult.rows[0].id]
}]
});
As previously mentioned, because User
is a linked record field and all linked
record fields in Airtable are arrays, we must use an array when creating the record.
functions/delete/books.js
This endpoint deletes a book by title for the authenticated user. It uses the
delete method of the airtable.query API.
Thank You!
If you have any questions or feedback, please join the Autocode community Slack
channel. You can get an invite from the Community tab in the top bar.
For updates, follow Cotter on Twitter @CotterApp,
Airtable @Airtable, and
Autocode @AutocodeHQ. Happy hacking!