Send SMS Directly from the Browser (No Backend Code Required!)
- Share:
Do More With Frontend
With just frontend code, frontend developers today have a great deal more power. It's not just about new browser APIs, but also about backendless tools that let you build complete applications without any backend code. However, there's still one area where backend code is required: backend-oriented APIs that need access control enforcement. The reason for that is that those APIs need to be authenticated with a secret key that cannot be exposed in the frontend. But guess what? There’s a solution for that.
In this article, we'll explore an awesome new way to use backend APIs that are often needed as a feature in frontend applications. We'll learn how to send SMS (or messages on WhatsApp, Viber, and Facebook Messenger) directly from the browser. To authenticate our users in the browser, we'll use Clerk.dev, and for managing their permissions to send messages using Vonage APIs, we'll rely on Permit.io. So let's dive in and see how it's done!
Frontend Only Authorization
The approach we will take in this article to authorize our users to send messages is called Frontend Only Authorization (FoAz), an open standard created by Permit.io. FoAz is based on a proxy service that verifies the user's frontend JWT token, checks if they have proper permissions to perform the API call, and then forwards the call to the backend API.
By using FoAz, we can authenticate our users in the frontend, and then use the same JWT token to authorize them to perform API calls. Let's start the tutorial by setting up our application.
Setup the Application
For building our application, we will use the React framework. You don't need any prior knowledge of react to follow along, as the code is short, and we will explain it as we go.
Note: You'll need a local installation of node.js and npm to continue with the tutorial.
First, let's create a new React app by using Vite, a fast-build tool for modern web apps. Open your terminal and run the following command in your desired projects directory:
npm create vite@latest frontend-messages-demo --template react \
&& cd frontend-messages-demo \
&& npm install
This will create a new react app in the frontend-messages-demo directory.
We can see our app by running the following command:
npm run dev
This will start a local development server and open a browser window with our app.
Setup Authentication in the App
As a first step to access control, we will need a way to authenticate our application users, so we can verify their identity. For this, we will use Clerk.dev, a frontend-first authentication platform that lets you add authentication to your application in minutes. Clerk offers a free tier that is more than enough for our needs, let's go to https://dashboard.clerk.com/sign-up and create an account there. Let's set up our newly created Vite app to use Clerk.dev for authentication.
Go to Clerk.com dashboard
In the Clerk dashboard, click on Add Application
Give your application a name, and click Create Application (I named mine
Frontend Messages Demo
)In the next screen, choose the React option, and copy the key that shows up.
In our React app, create a new file in the root folder called .env and add the following line to it (It is important to add the VITE_ prefix to the variable name, as Vite will use it to inject the variable into our app):
VITE_CLERK_FRONTEND_API_KEY=<YOUR_API_KEY_HERE>
In the same folder, run the following command on your terminal to install the Clerk SDK:
npm install @clerk/clerk-react
Now that we have setup the Clerk account in our application, it is time to test the user authentication.
In the App.tsx file, remove the whole content of the return section of the App function and replace it with the following code:
function App() { return ( <> <ClerkProvider publishableKey={clerkPubKey}> <SignedIn> <SendMessage /> </SignedIn> <SignedOut> <RedirectToSignIn /> </SignedOut> </ClerkProvider> </> ) }
At the top of the file, before the App function, let's paste the following code that will create an empty component to verify our users' login status and add the required imports to the file:
import { ClerkProvider, RedirectToSignIn, SignedIn, SignedOut, useAuth, useClerk } from '@clerk/clerk-react' const clerkPubKey = import.meta.env.VITE_REACT_APP_CLERK_PUBLISHABLE_KEY; function SendMessage() { const { isSignedIn, getToken } = useAuth(); const { signOut } = useClerk(); if (!isSignedIn) { return null; } return ( <div> <h1>Hello</h1> <button onClick={() => signOut()}>Sign Out</button> </div> ) }
Going now back to the browser, we can see that the app is now showing a login screen.
Let's go back to Clerk and create two users in the `Users` screen to mock our permission model with two users. For the example, let's used the following emails:
admin@messaging-foaz.app
anduser@messaging-foaz.app
(choose any password you want).With the users created, let's go back to our newly created app and login with one of the users, and check that we see the correct name in the app.
At this point, we have our users authenticated, and we can start working on the permissions model.
Setting Up Permissions for Sending SMS
Now that we have our users authenticated, we need to setup the permissions for sending messages. Let's say that our application has two kinds of users: admins and regular users. We want to allow admins to send messages and deny regular users from sending messages.
Let's configure the API call to Vonage in Permit.io to allow admins to send messages.
Get API Secret from Vonage
Sending an SMS is something that requires a service provider, and in this tutorial, we will use Vonage. Vonage is a cloud communications platform that offers APIs for SMS, voice, and video.
After signing up, you will need to create a new API key and secret. You can find your Vonage API key and secret in the Vonage dashboard.
Configure the API Call Permissions in Permit.io
To use FoAz, we will need to setup the particular SMS sending API call to Vonage in Permit.io, and let users perform that call only if they have the right permissions. If you are not a Permit.io user yet, go to app.permit.io and create a free account.
After signing in, you'll have to go to FoAz Proxy page and click Add Configuration.
This is how the configuration page should look like:
Let's go over the fields to make sure we understand what we are doing here:
In the URL field, enter
api.nexmo.com/v1/messages
. This is the API endpoint we will use to send messages.In the
HTTP Method
field, selectPOST
. This is the HTTP method we will use to send messages.In the
Select Resource
field, choose to create a new resource and call itMessage
. This is the name of the resource we will use to give permissions.When you create the resource, also assign one action to it. Let's call it
Send
.After creating the resource, Choose the
Send
action in theSelect Action
field.In the
Secret Configuration
field, chooseBasic
and insert the API key and secret you got from Vonage in the relevant fields.
At this point, we configure the FoAz proxy to support our call to Vonage, we can now configure the permissions for our users.
Assigning Permissions to Users
One of the easiest methods to check if a user has permission to perform an action is to use the Role-Based Access Control (RBAC) model. In RBAC, we assign role(s) to users, and then we can define which roles can perform which actions on which resources.
In the Permit.io Policy page, go to the Roles tab and create two roles: Admin and User.
Let's go to the Clerk dashboard, and find the User ID of the admin user we created earlier. You can find the User ID in the User Profile page.
In Permit.io, go to the Users tab and create a new user with the User ID of the admin user we found in the previous step, and assign the Admin role to the user.
Repeat the previous two steps for the regular user we created earlier, but assign the User role to the user.
Now that we have our users and roles, we can assign permissions to them. In Permit.io, go to the Policy Editor. In the Admin check the box next to the Send action we created earlier for the Message resource.
Configure Clerk JWKs in Permit.io
Now, that we have all our permissions configured, we need to configure the way that Permit.io will verify the JWT token that Clerk generates for our users. With our users authenticated, Clerk generates a JWT token for them. Permit.io is using Clerk JWKs to verify the JWT token, so we need to configure the JWKs in Permit.io.
In the Clerk dashboard, go to
JWT Template
page and clickNew Template
. This template will use to configure JWTs that are generated for FoAz.In the popup opened, click
Blank
Give our token a name and leave all the other fields with their default values.
Copy the
JWKS Endpoint
URL and open it in a new tab.In Permit.io, Go to the
Settings
screen, and then in theJWKs Config
tab, clickConfigure
on the Environment you use (you can see the active environment in the left sidebar)In the popup, paste the content from the JWKS Endpoint page we opened in the previous step, and click
Save
.
Now that we are done will the configuration, let's go back to our application code and send our first message!
Sending Messages from the Browser
First, to send a message, let's create the UI components that will use as a form for sending messages. To make our UI look nicer, we will install Material UI, a popular React UI framework for fancy UI components.
In the terminal, run the following command to install Material UI:
npm install @mui/material @emotion/react @emotion/styled --save
Now, let's create the UI components for sending messages. In the SendMessage function in App.tsx file, replace the return statement of the function with the following code:
<>
<Container maxWidth="sm">
<Paper component={'form'} sx={{ p: 1 }} onSubmit={send}>
{error && <Alert severity='error' sx={{ mb: 1 }} onClose={() => (setError(''))}>{error}</Alert>}
{success && <Alert severity='success' sx={{ mb: 1 }} onClose={() => (setSuccess(''))}>{success}</Alert>}
<Box sx={{ display: 'flex' }}>
<FormControl sx={{ mb: 1, flex: 1 }}>
<TextField label='To' id="to" value={to} onChange={(e) => setTo(e.target.value)} />
</FormControl>
</Box>
<Box sx={{ display: 'flex' }}>
<FormControl sx={{ m: 0, minWidth: 200, flex: 1 }}>
<TextField label='message' id="message" multiline rows={4} value={message} onChange={(e) => setMessage(e.target.value)} />
</FormControl>
</Box>
<Button variant="contained" type="submit" fullWidth sx={{ mt: 1 }}>Send</Button>
</Paper>
<Button variant='outlined' fullWidth sx={{ mt: 1 }} onClick={() => (signOut())}>Sign Out</Button>
</Container>
</>
In the top of the function, add the following assignemetns and an empty send function:
const [error, setError] = useState < string | null > ('');
const [success, setSuccess] = useState < string | null > ('');
const [to, setTo] = useState < string > ('');
const [message, setMessage] = useState < string > ('');
const send = async (e: FormEvent<HTMLFormElement>) => {};
Going back to the browser, you should see a message sending form like this:
Now, let's fill the code that will send the message. In the App.tsx file, add the following code to the SendMessage function (you can see the whole file version here):
e.preventDefault();
const token = await getToken();
if (!token) {
setError('No token');
return;
}
try {
const res = await fetch('PASTE_YOUR_URL_FROM_FOAZ_CONFIG', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
from: '14157386102',
to,
text: message,
channel: 'sms',
message_type: 'text',
}),
});
if (res.status !== 202) {
const body = await res.json();
setError(body?.detail || 'Error sending message');
return;
}
setSuccess('Message sent');
} catch (error: any) {
setError(error.message);
return;
}
As you can see, the fetch function we added, is empty. We should grab the fetch config from Permit.io proxy settings. You can get the code easily by clicking the copy button on the FoAz proxy page, and pasting it in the code.
Returning to the browser, let's try to send a message.
And, it works!
To ensure our safety with FoAz permissions, let's log out with the admin user and log in with another user who doesn't have a role in Permit.io (you can use the user@foaz-messaging.app user we created earlier).
Now, when we try to send a message, it fails!
🥳 Congratulations! You just sent your first message from the browser without any backend code!
What Next?
In this article, we touched on just the tip of what FoAz can do. Even if we continue with the SMS-sending example, there are many things we can do to improve our application. Sending messages in different channels and limiting the send only for particular users, limiting roles to send only to particular countries, and more.
But, FoAz is not limited to SMS sending. You can use FoAz to access any API that requires authentication, and you can use it to build a complete application without any backend code. And you can use it with any frontend framework, not just React.
We hope you enjoyed this article and are already thinking about how you can use FoAz in your next project. To stay in touch and contribute to the revolution of frontend development, join our Slack community.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker