- Authorisation
- Authentication Table
- Create Authentication Table
- Define the Authentication Entity
- Graphweaver Config
- Secondary Authentication Methods
- Password
- Magic Link
- One Time Password
- Web3
- Passkey
As an example on how to use two factor authentication you will need a Graphweaver app. For this page we assume that you have already followed the Adding Password Authentication guide.
This creates a new Graphweaver app that has a connected Sqlite database.
By default the Graphweaver API is deny all. This means that any request to the server will come back with a FORBIDDEN
message.
Let’s switch this to implicit allow so that we can just focus on the authentication.
Authorisation
There is a helper method that will help us switch to implicit allow which looks like this:
import { setImplicitAllow } from "@exogee/graphweaver-auth";
setImplicitAllow(true);
For this how to add the above code to the ./src/backend/index.ts
file. This will give access to every entity for every request.
We will need to add a new table to this datasource so let’s look at this next.
Authentication Table
In order to use the secondary authentication methods we need to save information to a data source. As the password example we are using uses Sqlite we will continue with that.
Whichever secondary authentication method you use you will need to create this table.
Create Authentication Table
Next, let’s add a new table to the database to store authentication data:
sqlite3 ./database.sqlite "CREATE TABLE Authentication (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type VARCHAR(255) NOT NULL,
user_id INTEGER NOT NULL,
data TEXT NOT NULL,
created_at DATE DEFAULT (datetime('now','localtime'))
);"
- This directly interacts with the SQLite database to create a table named 'Authentication'
- This table will store authentication data from all secondary authentication methods.
Define the Authentication Entity
Now that we have added the table in the database we need to tell Graphweaver about it. To do that open the project in your editor and navigate to the ./src/backend/entities/sqlite
directory.
Then create a new file ./src/backend/entities/sqlite/authentication.ts
with this contents:
touch ./src/backend/entities/sqlite/authentication.ts
import {
Entity,
PrimaryKey,
Property,
JsonType,
BigIntType,
} from "@mikro-orm/core";
import type { AuthenticationBaseEntity } from "@exogee/graphweaver-auth";
@Entity({ tableName: "Authentication" })
export class Authentication<T> implements AuthenticationBaseEntity<T> {
@PrimaryKey({ type: new BigIntType("string") })
id!: string;
@Property({ type: String })
type!: string;
@Property({ type: new BigIntType("string") })
userId!: string;
@Property({ type: JsonType })
data!: T;
@Property({ type: Date })
createdAt!: Date;
}
We also need to make sure that the index file (./src/backend/entities/sqlite/index.ts
) is updated to export this file:
import { Album } from "./album";
import { Artist } from "./artist";
import { Authentication } from "./authentication";
import { Credential } from "./credential";
import { Customer } from "./customer";
import { Employee } from "./employee";
import { Genre } from "./genre";
import { Invoice } from "./invoice";
import { InvoiceLine } from "./invoice-line";
import { MediaType } from "./media-type";
import { Playlist } from "./playlist";
import { Track } from "./track";
export * from "./album";
export * from "./artist";
export * from "./authentication";
export * from "./credential";
export * from "./customer";
export * from "./employee";
export * from "./genre";
export * from "./invoice";
export * from "./invoice-line";
export * from "./media-type";
export * from "./playlist";
export * from "./track";
export const entities = [
Album,
Artist,
Authentication,
Credential,
Customer,
Employee,
Genre,
Invoice,
InvoiceLine,
MediaType,
Playlist,
Track,
];
Graphweaver Config
There are some configuration settings that need to be set when using a secondary authentication method. These settings are inside the graphweaver-config.js
file that is in the root of your project.
Here we need to add the secondary method like this:
module.exports = {
adminUI: {
auth: {
primaryMethods: ["PASSWORD"],
secondaryMethods: ["ONE_TIME_PASSWORD"],
},
},
};
In the above example we have enabled OTP as a secondary method. The valid options in this array are:
ONE_TIME_PASSWORD
MAGIC_LINK
ONE_TIME_PASSWORD
WEB3
PASSKEY
Secondary Authentication Methods
There are a few secondary auth methods that can be configured with Graphweaver. They are:
PASSWORD
- Prompt for the users passwordMAGIC_LINK
- Send a link to the user that they click to authenticateONE_TIME_PASSWORD
- Send a 6 digit code to the end user for verificationWEB3
- Use a Web3 wallet to authenticatePASSKEY
- Use a device key to authenticate
Password
To configure Password as a secondary data source we first need to create a new instance of the Password class:
import {
UserProfile,
Password,
} from "@exogee/graphweaver-auth";
import { MikroBackendProvider } from "@exogee/graphweaver-mikroorm";
import { Credential as OrmCredential } from "./entities/sqlite";
import { connection } from "./database";
export const password = new Password({
provider: new MikroBackendProvider(OrmCredential, connection),
acl: {
Everyone: {
all: true,
},
},
getUserProfile: async (id: string): Promise<UserProfile<string>> => {
return new UserProfile({
id: '1',
email: 'test@test.com',
roles: ['LoggedInUser'],
});
},
});
We can then use the ApplyMultifactorAuthentication
decorator to add MFA to an existing entity:
@ApplyMultiFactorAuthentication<Tag>(() => ({
LIGHT_SIDE: {
Write: [{ factorsRequired: 1, providers: [AuthenticationMethod.PASSWORD] }],
},
}))
In the example above any attempt by the light side group to write data will need to present their password.
Magic Link
The magic link implementation does not specify how the link should be sent, it only gives you a sendMagicLink
function where you can integrate your sender.
For example, you could the link via email using AWS SES.
import {
MagicLink,
MagicLinkData,
UserProfile,
} from '@exogee/graphweaver-auth';
import { MikroBackendProvider } from '@exogee/graphweaver-mikroorm';
import { Authentication } from './entities';
import { connection } from './database';
export const magicLink = new MagicLink({
provider: new MikroBackendProvider(Authentication<MagicLinkData>, connection),
getUser: async (): Promise<UserProfile<string>> => {
return new UserProfile({
id: '1',
email: 'test@test.com',
roles: ['LoggedInUser'],
});
},
sendMagicLink: async (url: URL): Promise<boolean> => {
// In a production system this would email / sms the magic link and you would not log to the console!
console.log(`\n\n ######## MagicLink: ${url.toString()} ######## \n\n`);
return true;
},
});
We can then use the ApplyMultifactorAuthentication
decorator to add MFA to an existing entity:
@ApplyMultiFactorAuthentication<Tag>(() => ({
LIGHT_SIDE: {
Write: [{ factorsRequired: 1, providers: [AuthenticationMethod.MAGIC_LINK] }],
},
}))
In the example above any attempt by the light side group to write data will need to click the link sent to their email.
One Time Password
One time password will prompt the user to enter a code that can be sent to their email or phone.
The one time password implementation does not specify how the code should be sent it only gives you a sendOTP
function where you can integrate your sender.
For example, you could SMS using Twilio or send an email using AWS SES.
import { OneTimePassword, OneTimePasswordData } from '@exogee/graphweaver-auth';
import { MikroBackendProvider } from '@exogee/graphweaver-mikroorm';
import { Authentication } from "./entities/sqlite";
import { connection } from './database';
export const oneTimePassword = new OneTimePassword({
provider: new MikroBackendProvider(Authentication<OneTimePasswordData>, connection),
sendOTP: async (otp) => {
// In a production system this would email / sms the OTP and you would not log to the console!
console.log(`\n\n ######## One Time Password Code: ${otp.data.code} ######## \n\n`);
return true;
},
});
Once you have created the OneTimePassword instance you can secure an entity using the applyMultifactorAuthentication
decorator like this:
import { Entity, Field, ID, RelationshipField } from "@exogee/graphweaver";
import { MikroBackendProvider } from "@exogee/graphweaver-mikroorm";
import { Artist } from "./artist";
import { Track } from "./track";
import { Album as OrmAlbum } from "../entities";
import { connection } from "../database";
import {
ApplyMultiFactorAuthentication,
AuthenticationMethod,
} from "@exogee/graphweaver-auth";
@ApplyMultiFactorAuthentication<Album>(() => ({
Everyone: {
Write: [
{
factorsRequired: 1,
providers: [AuthenticationMethod.ONE_TIME_PASSWORD],
},
],
},
}))
@Entity<Album>("Album", {
provider: new MikroBackendProvider(OrmAlbum, connection),
})
export class Album {
@Field(() => ID, { primaryKeyField: true })
albumId!: number;
@Field(() => String, { adminUIOptions: { summaryField: true } })
title!: string;
@RelationshipField<Album>(() => Artist, {
id: (entity) => entity.artist?.artistId,
})
artist!: Artist;
@RelationshipField<Track>(() => [Track], { relatedField: "album" })
tracks!: Track[];
}
This will prompt any user to complete OTP before writing data to the Album entity.
Web3
The Web3 implementation is similar to the above however it does take a new function. This function is called multiFactorAuthentication
.
The purpose of this function is to prompt for a MFA before adding a Web3 wallet.
The response to this function is the same as what you would define using the ApplyMultiFactorAuthentication
decorator.
Here is an example of its usage:
import { Web3, AuthenticationMethod, WalletAddress } from '@exogee/graphweaver-auth';
import { MikroBackendProvider } from '@exogee/graphweaver-mikroorm';
import { Authentication } from "./entities/sqlite";
import { connection } from './database';
export const web3 = new Web3({
provider: new MikroBackendProvider(Authentication<WalletAddress>, connection),
multiFactorAuthentication: async () => {
return {
Everyone: {
// all users must provide a OTP mfa when saving a wallet address
Write: [{ factorsRequired: 1, providers: [AuthenticationMethod.ONE_TIME_PASSWORD] }],
},
};
},
});
We can then use the ApplyMultifactorAuthentication
decorator to add MFA to an existing entity:
@ApplyMultiFactorAuthentication<Tag>(() => ({
LIGHT_SIDE: {
Write: [{ factorsRequired: 1, providers: [AuthenticationMethod.WEB3] }],
},
}))
In the example above any attempt by the light side group to write data will need to verify their wallet access.
Passkey
Finally, we have the passkey integration. This is the most straight forward to configure and
import { Passkey, PasskeyData } from '@exogee/graphweaver-auth';
import { MikroBackendProvider } from '@exogee/graphweaver-mikroorm';
import { Authentication } from "./entities/sqlite";
import { connection } from './database';
export const passkey = new Passkey({
dataProvider: new MikroBackendProvider(Authentication<PasskeyData>, connection),
});
We can then use the ApplyMultifactorAuthentication
decorator to add MFA to an existing entity:
@ApplyMultiFactorAuthentication<Tag>(() => ({
LIGHT_SIDE: {
Write: [{ factorsRequired: 1, providers: [AuthenticationMethod.PASSKEY] }],
},
}))
In the example above any attempt by the light side group to write data will need to verify their device key access.