Graphweaver Docs
  • Home
  • Github
Get Started 🚀
⛓️

Multifactor Authentication

  • 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.

⚠️
You will want to remove this and add your own access control lists to each entity. For more information on how to do this see the 🔑Implementing Authorization page.

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

We also need to make sure that the index file (./src/backend/entities/sqlite/index.ts) is updated to export this file:

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 password
  • MAGIC_LINK - Send a link to the user that they click to authenticate
  • ONE_TIME_PASSWORD - Send a 6 digit code to the end user for verification
  • WEB3 - Use a Web3 wallet to authenticate
  • PASSKEY - 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:

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.

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.

Once you have created the OneTimePassword instance you can secure an entity using the applyMultifactorAuthentication decorator like this:

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:

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.

Made with 💜 in Australia

Exogee

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;
}
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,
];
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'],
	    });
  },
});
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;
    },
});
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;
	},
});
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[];
}
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] }],
			},
		};
	},
});