Basic Web App Authentication with Harper
The Harper platform has many great features to improve your web applications from caching, auto generated endpoints from schemas, low-latency roundtrips, and more. Today the focus is setting up basic authentication with harper.
Overview
In this tutorial we will clone an existing application and implement authentication by:
- Configure our Harper server for authentication
- Creating our User table schema for our web app users
- Use the Resource API for creating our sign up, sign in, sign out, and auth check endpoints
- Setup protected routes in our application
Harper Server Configuration
Before getting started in the application, you need to configure Harper for authentication for the app. 1. Open your file explorer and navigate to your hdb directory and open harperdb-config.yaml. (By default this file will be located in ~/hdb/harperdb-config.yaml.)
- Navigate to authentication: and
- Change authorizeLocal to false
- Change enableSessions to true
authentication:
authorizeLocal: false
enableSessions: true
Why?
authorizeLocal will automatically authorize any requests from the loopback IP address as the superuser. This is being disabled(set to false) for the Harper server that may be accessed by untrusted users from the same instance. Since we are leveraging Harper’s authentication, we are using this to prevent users from accessing the Harper server as a super user.
enableSessions being set to false will enable cookie-based sessions to maintain an authenticated session. This is generally the preferred mechanism for maintaining authentication in web browsers as it allows cookies to hold an authentication token securely without giving JavaScript code access to token/credentials that may open up XSS vulnerabilities.
- Save the file and close it.
You have now successfully configured your Harper server for web authentication! Next you will clone the example application and setup the User table schema for the application.
Setting up our App
- Open your terminal and clone the application
git clone https://github.com/HarperFast/harper-auth-example.git
- Navigate to harper-auth-example/and open the start/ directory in a code editor.
- Open a terminal type npm install to install all the required dependencies for the application.
- Next run the application via npm run dev
We can open and view the application visiting http://localhost:9926/. However before we focus on implementing authentication the application you will need to navigate to http://localhost:9925/#/config/roles
- Add a new role with the name least_privileged. super_user and structure_user should remain unchecked. This will give
Now that the application has been successfully cloned and dependencies installed we can now focus on creating your User table schema for the users of the application. For the User table the attributes we will need to include are:
- id: Our primary key for each record. (Harper will handle generating it during account creation)
- username: The name of the individual.
- password: User’s password, encrypted and hidden
- createdAt: Gives us the UTC date time of when the account was created
Open the schema.graphql file and create the User table schema.
type User @table {
id: ID @primaryKey
username: String @indexed
createdAt: Date
}
We have now successfully created our table and next we need to create and connect this table to our Resource APIs for authentication.
Authentication endpoints via the Resource API
The Harper platform streamlines many parts of development. The part you will focus on today is highlighting how the Harper platform can use the Resource API to provide a unified modeling experience when working with different data resources within Harper and how to integrate them into an application. For more information read here.
Open the resources.ts file and within this file you will need to create several resource classes:
- SignUpResource - Will be used to create a Harper user and allows us to leverage Harper’s internal authentication for that user.
- SignInResource - Using the users username/pass to call Harper's login(). Will receive a cookie in the response header.
- SignOutResource - Will remove the user’s cookie via Harper’s session deletion.
- CheckUserResource - Uses the valid cookie to get the current user’s information.
Now that theres a brief overview of the resource classes needing to be created you can now create the base for each resource.
import { tables, server, Resource, type Context } from 'harperdb';
interface UserData {
username: string;
password: string;
}
const UserTable = tables.User;
export class SignUpResource extends UserTable {
// Sign up code....
}
export class SignInResource extends UserTable {
// Sign in code....
}
export class SignOutResource extends UserTable {
// Sign out code....
}
export class CheckUserResource extends UserTable {
// Check current user code....
}
Sign Up Resource
For the SignUpResource several actions need to happen when a user submits their username and password such as:
- Getting the context of the application.
- Create a createUser() function that:
- Calls Harper’s add_user backend/server operation and assign the name, role, etc.
- Create the user inside the User table.
Let’s do exactly this!
export class SignUpResource extends UserTable {
static loadAsInstance = false; // enable the updated API
async post(target, data: UserData) {
const context = this.getContext() // The context contains information about the current transaction, the user that initiated this action, and other metadata that should be retained through the life of an action.
await createUser(crypto.randomUUID(), data, context);
}
}
async function createUser(userId: string, userData: { username: string; password: string }, context: Context) {
const { username, password } = userData;
// Create Harper system user with the least possible priviledge to prevent data access
await server.operation({
operation: 'add_user',
username,
password,
role: 'least_privileged',
active: true,
}, context);
// Create a user record within the User table.
await tables.User.create(
{
id: userId,
username,
password, // In production, ensure to hash passwords before storing
createdAt: new Date().toISOString(),
},
context
);
}
Now when we call the /SignUpResource in our application we’ll be able to successfully create an account. Let’s do exactly that!
Open context/authContext.tsx and navigate to the signUp() function inside the AuthProvider.
Whenever a new user signs up it triggers this function which makes a POST fetch call to /SignUpResource/ with contents of the call being stringified before being sent.
const signUp = async (signUpCredentials: UserCredentials) => {
const response = await fetch('/SignUpResource/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...signUpCredentials,
}),
});
return response;
}
Restart the application and navigate to http://localhost:9926/signup, and create an account. You will have successfully created a new user for your web application. Now the user has been created giving the user the ability to sign in is next.
Sign In Resource
Open the resources.ts and go to the SignInResource class. The UI will call this resource/endpoint via a post request. In other words the method that’ll will need to be modified is the post() method. In this method you will get the current context and from the context make a call to the context.login() function passing the username and password as two separate parameters. Upon success it will return a cookie in the header.
export class SignInResource extends UserTable {
static loadAsInstance = false; // enable the updated API
async post(target: string, data: UserData) {
const context = this.getContext();
await context.login(data.username, data.password);
}
}
Now that the SignInResource will retrieve a cookie navigate to the authContext.tsx file and go to the signIn() function. In this function the sign in credentials will be passed to the body inside the fetch request calling the /SignInResource/ endpoint. When the response comes back successful the isAuthenticated react state hook should be set to true.
const signIn = async (signInCredentials: UserCredentials) => {
const response = await fetch('/SignInResource/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...signInCredentials,
}),
});
if (response.ok) {
setIsAuthenticated(true)
}
return response;
}
Restart the application and navigate to http://localhost:9926. Sign with the credentials used to create the account in the previous section and if the request was successful there will be a toast in the bottom right displaying the successful request. You can also open the network tab in the dev tools, look at the response header from the /SignInResource/ request, and it will contain a cookie.
Sign Out Resource
After signing into the application you are directed to the /dashboard. This great but currently there is no way for an user to sign out and proceed to log into a different account. To sign out you will need to trigger a session deletion via the current context in the SignOutResource.
export class SignOutResource extends Resource {
static loadAsInstance = false;
// This is called to determine if the user has permission to read from the current resource
allowRead() {
return true; // public: returns data only if session exists
}
async post() {
const user = this.getCurrentUser();
if (!user?.username) {
return new Response('Not signed in.', { status: 401 });
}
const context = this.getContext() as Context as any;
await context.session?.delete?.(context.session.id);
return new Response('Signed out successfully.', { status: 200 });
}
}
Next, navigate back to authContext.tsx and to the signOut() function. When you call /SignOutResource/ api endpoint via a fetch request you will use the POST method and leave the empty body. In this case the most important part of this request is the header containing the cookie.
const signOut = async () => {
const response = await fetch('/SignOutResource/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
setIsAuthenticated(false)
} else {
throw new Error('Sign out failed')
}
}
Lastly, while users of the app can sign in and sign out, there is no current way to check if a user is authenticated or not.
Check User Resource
The purpose of the CheckUserResource is to validate whether a user is authenticated or not. If the user is not authenticated and attempts to navigate to the /dashboard page, the user will be redirected to the sign in page.
Navigate to resources.ts and CheckUserResource. Inside this you will will set the allowRead() to return true. This will restrict the endpoint to return data if no session exists. Next, the user will send a get request to this endpoint to check if the session is valid via the cookie and if the session is valid, you will need to check if there is a user. If a user exists return active and username.
export class CheckUserResource extends Resource {
static loadAsInstance = false;
allowRead() {
return true; // public: returns data only if session exists
}
async get() {
const user = this.getCurrentUser?.();
if (user) {
return {
active: user.active,
username: user.username,
};
}
}
}
Next, navigate to the authContext.tsx and go to the checkIsUserAuthenticated() function. Inside here you will make a fetch request to the /CheckUserResource/ endpoint and if the response is okay, change isAuthenticated to true and return the successful response.
const checkIsUserAuthenticated = async () => {
const response = await fetch('/CheckUserResource/')
setIsAuthenticated(response.ok)
return response.ok;
}
Now that the checkIsUserAuthenticated() can call the /CheckUserResource/ to see if the current session is valid you will need to turn the /dashboard page into a protected route. Open routeTree.ts and navigate to the dashboardLayoutRoute and the beforeLoad() function you will want to:
- Get the auth provider from the router context
- call auth.checkIsUserAuthenticated() to check if the user is authenticated
- If the user is not authenticated redirect the user to the sign in page.
const dashboardLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: '_dashboardLayout',
component: DashboardLayout,
beforeLoad: async ({ context}: { context: RouterContext }) => {
const { auth } = context;
const isAuthenticated = await auth.checkIsUserAuthenticated();
if (!isAuthenticated) {
throw redirect({
to: '/',
});
}
},
});
You have now successfully protected any route inside the dashboard layout(i.e. any child routes of /dashboard). Next if a user is authenticated and navigates to public routes such as sign in and sign up, you want to redirect the user to the /dashboard route. Go to authLayoutRoute and the beforeLoad() function inside it. The authentication check is the same as the one above but in contrast if the user IS authenticated redirect the user to the /dashboard page.
const authLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: '_authLayout',
component: AuthLayout,
beforeLoad: async ({ context}: { context: RouterContext }) => {
const { auth } = context;
const isAuthenticated = await auth.checkIsUserAuthenticated();
if (isAuthenticated) {
throw redirect({
to: '/dashboard',
});
}
},
});
Restart the app and test the authentication:
- Signing out and navigating to /dashboard
- Signing in and navigating to /
The redirects should work and you will have successfully implemented basic authentication with Harper into the app.
Conclusion
The Harper platform has features to not only improve the performance of your app but also allows for you to leverage other wonderful features of Harper such as authentication to simplify the complexity of building application. To read more into way to leverage Harper to streamline your application development process visit the the documentation and our other resources.






