Fine-grained authorization for APIs with NestJS and OpenFGA

  1. Introduction
    1. What is Fine-Grained Authorization (FGA)?
    2. Understanding NestJS, OpenFGA, and Auth0
    3. Diving deeper into OpenFGA
  1. Implemeantation
    1. Setting Up a Basic NestJS Project
    2. Integrating Auth0 for Authentication
    3. Implementing OpenFGA for Authorization
  1. Conclusion
    1. Key Takeaways
    2. Benefits of This Approach
    3. Final Thoughts

Introduction

As modern applications grow in complexity, managing user access and permissions becomes increasingly challenging. Traditional role-based access control (RBAC) often falls short when dealing with intricate scenarios, such as multi-tenant platforms, collaborative tools, or resource-specific permissions. This is where Fine-Grained Authorization (FGA) comes into play, offering a powerful and flexible way to manage who can do what within your application.

What is Fine-Grained Authorization (FGA)?

Fine-Grained Authorization goes beyond simple roles like “admin” or “user.” It enables you to define and enforce detailed access rules based on relationships between users, resources, and actions. For example:

  • “Alice can edit Document A because she is a member of Team X.”
  • “Bob has admin access to Project Y because he owns it.”

FGA systems allow for:

  • Relationship-Based Access Control (ReBAC): Defining permissions based on dynamic relationships (e.g., ownership, team membership, or project assignments).
  • Scalability: Handling thousands of users, roles, and permissions without performance degradation.
  • Flexibility: Supporting complex, domain-specific rules that adapt to your application’s needs.

This article guides you through setting up an FGA authorization for API’s using NestJS, Auth0, and OpenFGA.

By the end of this guide, you will clearly understand how these tools work together to build a secure and efficient permissions system.

We will build an example application for managing users’ access to projects. Projects will have three levels of permissions:

  • Owner: Full access to the project.
  • Admin: Can add members and view the project.
  • Member: Can only view the project.

Understanding NestJS, OpenFGA, and Auth0

Before diving into the implementation, it’s essential to understand the roles each tool plays:

  • NestJS: A versatile and scalable framework for building server-side applications. It leverages TypeScript and incorporates features like dependency injection, making it a favorite among developers for building robust APIs.
  • OpenFGA: An open-source authorization system that provides fine-grained access control. It allows you to define complex permission models and manage them efficiently.
  • Auth0: A cloud-based authentication and authorization service. It simplifies user authentication, offering features like social login, single sign-on, and multifactor authentication.

When combined, these tools allow you to create a secure backend where Auth0 handles user authentication, OpenFGA manages authorization, and NestJS serves as the backbone of your application.

This article assumes that you have some understanding of NestJS and Auth0 (and OAuth in general) but for OpenFGA I will give a basic intro.

Diving deeper into OpenFGA

OpenFGA is a modern authorization system built for handling complex access control with simplicity and flexibility. Inspired by Google Zanzibar, it provides fine-grained, relationship-based access control, letting you define “who can do what and why.” This makes it ideal for applications with intricate permission hierarchies, like multi-tenant platforms or collaborative tools.

At its core are two key concepts: authorization models and stores. Models define relationships between users, roles, and resources—like “John is an admin of Project X” or “Alice can edit Document Y because she’s in Team Z.” Stores serve as isolated containers for these models and their data, keeping systems clean and scalable.

Core Concepts of OpenFGA

Authorization rules are configured in OpenFGA using an authorization model which is a combination of one or more type definitions. Type is a category of objects in the system and type definition defines all possible relations a user or another object can have in relation to this type. Relations are defined by relation definitions, which list the conditions or requirements under which a relationship is possible.

An object represents an instance of a type. While a user represents an actor in the system. The notion of a user is not limited to the common meaning of “user”. It could be

  • any identifier: e.g. user:gganebnyi
  • any object: e.g. project:nestjs-openfga or organization:FusionWorks
  • a group or a set of users (also called a userset): e.g. organization:FusionWorks#members, which represents the set of users related to the object organization:FusionWorks as member
  • everyone, using the special syntax: *

Authorization data is stored in OpenFGA as relationship tuples. They specify a specific user-object-relation combination. Combined with the authorization model they allow checking user relationships to certain objects, which is then used in application authorization flow. In OpenFGA relationships could be direct (defined by tuples) or implied (computed from combining tuples and model).

Let’s illustrate this with a model we will use in our sample project.

model
  schema 1.1
  
type user

type project
  relations
    define admin: [user] or owner
    define member: [user] or admin
    define owner: [user]
JSON
[
  {
    "user": "user:johndoe",
    "relation": "member",
    "object": "project:FusionAI"
  },
  {
    "user": "user:gganebnyi",
    "relation": "owner",
    "object": "project:FusionAI"
  }
]

Based on this user “user:johndoe“ has direct relationship “member“ with object “project:FusionAI“, while for user “user:gganebnyi“ this relationship is implied based on his direct relationship “owner“ with this object.

At runtime, both the authorization model and relationship tuples are stored in the OpenFGA store. OpenFGA provides API for managing this data and performing authorization checks against it.

While this quick intro gives us enough information to proceed with further implementation, I strongly recommend checking Authorization Concepts | OpenFGA and Concepts | OpenFGA.

Implementation

The full project source code is located in our GitHub repo. You can check it out and use it for reference while reading the article. If you are familiar with NestJS and Auth0 setup, please skip right to the OpenFGA part.

Setting Up a Basic NestJS Project

Let’s start by setting up a basic NestJS project. Ensure you have Node.js and npm installed, then proceed with the following commands:

Bash
# Install NestJS CLI globally
npm install -g @nestjs/cli

# Create a new NestJS project
nest new nestjs-auth0-openfga

# Getting inside project folder and install dependencies we have so far
cd nestjs-auth0-openfga
npm install

# We will use MongoDB to store our data
npm install @nestjs/mongoose mongoose

# For easier use of environment variables
npm install @nestjs/config

# Adding Swagger for API documentation and testing
npm install @nestjs/swagger swagger-ui-express

This gives us all the NestJS components we need so far installed. The next step is to create our Projects Rest API.

Bash
# Generate Projects Module
nest generate module projects
nest generate service projects
nest generate controller projects

This will scaffold NestJS artifacts for Projects API and update app.module.ts file.
The next step is to create the Project’s Mongoose schema and implement projects Service and ProjectsController.

projects/schemas/project.schema.ts

TypeScript
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Document, HydratedDocument } from "mongoose";
export type ProjectDocument = HydratedDocument<Project>;

@Schema({
  toObject: {
    virtuals: true,
  },
})

export class Project {
  @Prop({ required: true })
  name: string;
}

export const ProjectSchema = SchemaFactory.createForClass(Project);

projects/projects.service.ts

TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Project, ProjectDocument } from './schemas/project.schema';
import { CreateProjectDto } from './dto/create-project.dto';

@Injectable()
export class ProjectsService {
  constructor(
    @InjectModel(Project.name) private projectModel: Model<ProjectDocument>,
  ) { }

  async findOne(id: string): Promise<ProjectDocument> {
    const project = await this.projectModel.findById(id).exec();
    if (!project) {
      throw new NotFoundException(`Project with ID ${id} not found`);
    }
    return project;
  }

  async create(createProjectDto: CreateProjectDto): Promise<ProjectDocument> {
    const createdProject = new this.projectModel(createProjectDto);
    return createdProject.save();
  }

  async findAll(): Promise<ProjectDocument[]> {
    return this.projectModel.findAll().exec();
  }
}

projects/projects.controller.ts

TypeScript
import {
  Controller,
  Get,
  Post,
  Body,
  Param,
  UseGuards,
  Delete,
  UseInterceptors,
  ClassSerializerInterceptor,
} from '@nestjs/common';
import { ProjectsService } from './projects.service';
import { CreateProjectDto } from './dto/create-project.dto';
import { ApiCreatedResponse, ApiOkResponse } from '@nestjs/swagger';
import { ProjectDto } from './dto/project.dto';

@Controller('projects')
@UseInterceptors(ClassSerializerInterceptor)
export class ProjectsController {
  constructor(
    private readonly projectsService: ProjectsService,
  ) { }

  /**
   * Get all projects.
   */
  @Get()
  @ApiOkResponse({ type: ProjectDto, isArray: true })
  async getProjects(): Promise<ProjectDto[]> {
    const projects = await this.projectsService.findAll();
    return projects.map((project) => ProjectDto.fromDocument(project));
  }

  /**
   * Get a project by ID.
   */
  @Get(':id')
  @ApiOkResponse({ type: ProjectDto })
  async getProject(@Param('id') id: string): Promise<ProjectDto> {
    const project = await this.projectsService.findOne(id);
    return ProjectDto.fromDocument(project);
  }

  /**
   * Create a new project.
   */
  @Post()
  async createProject(@Body() createProjectDto: CreateProjectDto) {
    const project = await this.projectsService.create(createProjectDto);
    return ProjectDto.fromDocument(project);
  }
}

projects/projects.module.ts

TypeScript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ProjectsController } from './projects.controller';
import { ProjectsService } from './projects.service';
import { Project, ProjectSchema } from './schemas/project.schema';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    MongooseModule.forFeature([{ name: Project.name, schema: ProjectSchema }]),
    ConfigModule,
  ],
  controllers: [ProjectsController],
  providers: [ProjectsService],
})
export class ProjectsModule { }

app.module.ts

TypeScript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ProjectsModule } from './projects/projects.module';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    ProjectsModule,
    MongooseModule.forRoot(process.env.MONGODB_URI),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

As you may notice, the controller uses DTOs, ClassValidator, and ClassTransformer to control its input/output. DTOs are also important for proper Swagger documentation. I am committing them here for brevity but you can see them here: nestjs-openfga-example/src/projects/dto at master · FusionWorks/nestjs-openfga-example.

The last thing is to activate Swagger and set environment variables.

main.ts

TypeScript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
async function bootstrap() {

  const app = await NestFactory.create(AppModule);
  const configService = new ConfigService();

  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

  const options = new DocumentBuilder()
    .setTitle('API')
    .setDescription('The API description')
    .setVersion('1.0')
    .addTag('API')
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);

  const port = process.env.PORT || 3000;
  await app.listen(port);

  console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

.env

Bash
# MongoDB Configuration
MONGODB_URI=mongodb://YOUR_USER:PASSWORD@HOST:PORT

# Application Port
PORT=3000

Our basic app is ready. You can launch it with npm run start:dev and access Swagger UI via http://localhost:3000/api/ to try the API.

Integrating Auth0 for Authentication

Authentication is the first step in securing your application. Auth0 simplifies this process by handling user authentication, allowing you to focus on building your application logic. Auth0 is a SaaS solution and you need to register at https://auth0.com to use it. To integrate Auth0 we will install PassportJS and configure it. Here are the steps.

Install required packages

Bash
npm install @nestjs/passport passport passport-jwt jwks-rsa

Create an auth module to manage authentication (this will also add it to app.module.ts)

Bash
nest generate module auth

auth/auth.module.ts

TypeScript
import { Module } from '@nestjs/common';
import { JwtStrategy } from './jwt.strategy';

@Module({
  providers: [JwtStrategy],
  exports: [],
})
export class AuthModule {}

Implement JWT strategy

auth/jwt.strategy.ts

TypeScript
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import * as jwksRsa from 'jwks-rsa';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKeyProvider: jwksRsa.passportJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 10,
        jwksUri: `https://${configService.get<string>('AUTH0_DOMAIN')}/.well-known/jwks.json`,
      }),
      audience: configService.get<string>('AUTH0_AUDIENCE'),
      issuer: `https://${configService.get<string>('AUTH0_DOMAIN')}/`,
      algorithms: ['RS256'],
    });
  }

  async validate(payload: any) {
    return payload;
  }
}

Create JwtAuthGuard

auth/jwt-auth.guard.ts

TypeScript
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

And use it to protect controller routes

projects/projects.controller.ts

TypeScript
...

import { JwtAuthGuard } from '../auth/jwt-auth.guard';
...
import { ApiBearerAuth, ApiCreatedResponse, ApiOkResponse } from '@nestjs/swagger';
...

@ApiBearerAuth('Auth0')
@Controller('projects')
@UseGuards(JwtAuthGuard)
@UseInterceptors(ClassSerializerInterceptor)
export class ProjectsController {
...
}

As a final step, we need to update the app configuration

.env

Bash
...

# Auth0 Configuration
AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN.eu.auth0.com
AUTH0_AUDIENCE=YOUR_AUTH0_API_AUDIENCE
AUTH0_OAUTH_CLIENT_ID=YOUR_AUTH0_CLEINTID

...

Now Project API will require you to pass valid authentication information to invoke its methods. This is done by setting Authorization: Bearer YOUR_TOKEN header, where YOUR_TOKEN is obtained during the Auth0 authentication flow.

To make our testing of API easier let’s add Auth0 authentication support to Swagger UI

main.ts

TypeScript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = new ConfigService();

  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

  const options = new DocumentBuilder()
    .setTitle('API')
    .setDescription('The API description')
    .setVersion('1.0')
    .addTag('API')
    .addOAuth2(
      {
        type: 'oauth2',
        flows: {
          implicit: {
            authorizationUrl: `https://${configService.get(
              'AUTH0_DOMAIN',
            )}/authorize?audience=${configService.get('AUTH0_AUDIENCE')}`,
            tokenUrl: configService.get('AUTH0_AUDIENCE'),
            scopes: {
              openid: 'Open Id',
              profile: 'Profile',
              email: 'E-mail',
            },
          },
        },
        scheme: 'bearer',
        bearerFormat: 'JWT',
        in: 'header',
      },
      'Auth0',
    )
    .build();

  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document, {
    swaggerOptions: {
      initOAuth: {
        clientId: configService.get('AUTH0_OAUTH_CLIENT_ID'),
      },
    },
  });

  const port = process.env.PORT || 3000;
  await app.listen(port);

  console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

As a result, the Authorize option will appear in Swagger UI and the Authorization header will be attached to all requests.

Implementing OpenFGA for Authorization

With authentication in place, the next step is managing authorization using OpenFGA. We’ll design our authorization model, integrate OpenFGA into NestJS, and build permission guards to enforce access control.

Since OpenFGA is a service you either need to install it locally (Docker Setup Guide | OpenFGA ) or use a hosted analog like Okta FGA. For this tutorial, I recommend using Okta FGA since it has a UI for designing and testing models and managing relationship tuples.

As the first step to implementing authorization, we will define our authorization model and save it in Okta FGA

Bash
model
  schema 1.1

type user

type project
  relations
    define admin: [user] or owner
    define member: [user] or admin
    define owner: [user]

The next step is to create Okta FGA API client

And update our .env with its parameters

Bash
...

FGA_API_URL='https://api.eu1.fga.dev' # depends on your account jurisdiction
FGA_STORE_ID=
FGA_MODEL_ID=
FGA_API_TOKEN_ISSUER="auth.fga.dev"
FGA_API_AUDIENCE='https://api.eu1.fga.dev/' # depends on your account jurisdiction
FGA_CLIENT_ID=
FGA_CLIENT_SECRET=

... 

Now we install OpenFGA SDK and create an authorization module in our app

npm install @openfga/sdk
nest generate module authorization
nest generate service authorization

Next, we implement AuthorizationService around OpenFGA API. It provides methods to check user relationships and manipulate them. Below is the code for the service itself, for small artifacts referenced in the service class please check nestjs-openfga-example/src/authorization at master · FusionWorks/nestjs-openfga-example.

authorization/authorization.service.ts

TypeScript
import { Injectable, ForbiddenException, Logger } from '@nestjs/common';
import { OpenFgaClient, CredentialsMethod, TupleKey } from '@openfga/sdk';
import { ConfigService } from '@nestjs/config';
import { AuthorizationPartyTypes } from './authorization-types.enum';
import { AuthorizationRelations } from './authorization-relations.enum';
import { AuthorizationParty } from './authorization-party';

@Injectable()
export class AuthorizationService {

  private readonly logger = new Logger(AuthorizationService.name);
  private client: OpenFgaClient;

  constructor(private readonly configService: ConfigService) {
    this.client = new OpenFgaClient({
      apiUrl: this.configService.get<string>('FGA_API_URL'),
      storeId: this.configService.get<string>('FGA_STORE_ID'),
      credentials: {
        method: CredentialsMethod.ClientCredentials,
        config: {
          clientId: this.configService.get<string>('FGA_CLIENT_ID'),
          clientSecret: this.configService.get<string>('FGA_CLIENT_SECRET'),
          apiTokenIssuer: this.configService.get<string>('FGA_API_TOKEN_ISSUER'),
          apiAudience: this.configService.get<string>('FGA_API_AUDIENCE'),
        },
      },
    });
  }

  async checkPermission(
    userId: string,
    permission: string,
    objectType: string,
    objectId: string,
  ): Promise<boolean> {
    try {
      const response = await this.client.check({
        user: `${AuthorizationPartyTypes.USER}:${userId}`,
        relation: permission,
        object: `${objectType}:${objectId}`,
      });
      return response.allowed;
    } catch (error) {
      return false;
    }
  }

  async listObjectIds(user: AuthorizationParty, objectType: AuthorizationPartyTypes, relation: AuthorizationRelations): Promise<string[]> {
    const request = {
      user: user.toOpenFgaString(),
      relation,
      type: objectType
    };
    const response = await this.client.listObjects(request);
    return response.objects.map((object) => object.split(':')[1]);
  }

  async listUsers(object: AuthorizationParty, relations: AuthorizationRelations[]): Promise<{ userId: string; relations: string[] }[]> {
    const responses = await Promise.all(
      relations.map(async (relation) => {
        const response = await this.client.listUsers({
          object: object.toOpenFgaObject(),
          relation,
          user_filters: [
            {
              type: AuthorizationPartyTypes.USER,
            },
          ],
        });
        return { relation, users: response.users };
      })
    );

    const userRolesMap = responses.reduce((acc, { relation, users }) => {
      users.forEach(user => {
        const userId = user.object.id;
        if (!acc[userId]) {
          acc[userId] = new Set<string>();
        }
        acc[userId].add(relation);
      });
      return acc;
    }, {} as Record<string, Set<string>>);

    const aggregatedUsers = Object.entries(userRolesMap).map(([userId, rolesSet]) => ({
      userId,
      relations: Array.from(rolesSet),
    }));

    return aggregatedUsers;
  }

  async addRelationship(user: AuthorizationParty, object: AuthorizationParty, relationship: AuthorizationRelations): Promise<void> {
    const tupleKey: TupleKey = {
      user: user.toOpenFgaString(),
      relation: relationship,
      object: object.toOpenFgaString(),
    };
    await this.client.write({
      writes: [tupleKey],
    });
  }

  async removeRelationship(user: AuthorizationParty, object: AuthorizationParty, relationship: AuthorizationRelations): Promise<void> {
    const tupleKey: TupleKey = {
      user: user.toOpenFgaString(),
      relation: relationship,
      object: object.toOpenFgaString(),
    };
    await this.client.write({
      deletes: [tupleKey],
    });
  }
}

Now we can implement PermissionsGuard and PermissionsDecorator to be used on our controllers. PermissionsGuard will extract the object ID from the request URL, body, or query parameters and based on object type and required relation from the decorator and user ID from authentication data perform a relationship check in OpenFGA.

authorization/permissions.decorator.ts

TypeScript
import { SetMetadata } from '@nestjs/common';

export const PERMISSIONS_KEY = 'permissions';

export type Permission = {
  permission: string;
  objectType: string;
  objectIdParam: string; // The name of the route parameter containing the object ID
};

export const Permissions = (...permissions: Permission[]) =>
  SetMetadata(PERMISSIONS_KEY, permissions);

authorization/permissions.guard.ts

TypeScript
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PERMISSIONS_KEY, Permission } from './permissions.decorator';
import { AuthorizationService } from './authorization.service';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private authzService: AuthorizationService,
    private configService: ConfigService,
  ) { }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const permissions = this.reflector.get<Permission[]>(
      PERMISSIONS_KEY,
      context.getHandler(),
    );
    if (!permissions) {
      return true; // No permissions required
    }

    const request = context.switchToHttp().getRequest();
    const user = request.user;
    if (!user) {
      throw new ForbiddenException('No user found in request');
    }

    for (const permission of permissions) {
      let objectId: string;
      // Check where the objectIdParam is located: params, query, or body
      if (request.params && request.params[permission.objectIdParam]) {
        objectId = request.params[permission.objectIdParam];
      } else if (request.query && request.query[permission.objectIdParam]) {
        objectId = request.query[permission.objectIdParam];
      } else if (request.body && request.body[permission.objectIdParam]) {
        objectId = request.body[permission.objectIdParam];
      } else {
        throw new ForbiddenException(
          `Cannot find parameter '${permission.objectIdParam}' in request`,
        );
      }

      const hasPermission = await this.authzService.checkPermission(
        user.sub,
        permission.permission,
        permission.objectType,
        objectId,
      );
      if (!hasPermission) {
        throw new ForbiddenException(
          `Insufficient permissions for ${permission.permission} on ${permission.objectType}`,
        );
      }
    }
    return true;
  }
}

Now let’s see how this integrates with ProjectsController. Besides permissions checking, we will also add the project creator as the Owner and give him and the project admins the possibility to manipulate project members. For easier user extraction from the authentication context, we added a User decorator.

auth/user.decorator.ts

TypeScript
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

projects/projects.controller.ts

TypeScript
import {
  Controller,
  Get,
  Post,
  Body,
  Param,
  UseGuards,
  Delete,
  UseInterceptors,
  ClassSerializerInterceptor,
} from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { PermissionsGuard } from '../authorization/permissions.guard';
import { Permissions } from '../authorization/permissions.decorator';
import { ProjectsService } from './projects.service';
import { AuthorizationService } from '../authorization/authorization.service';
import { AuthorizationParty } from '../authorization/authorization-party';
import { AuthorizationPartyTypes } from '../authorization/authorization-types.enum';
import { AuthorizationRelations } from '../authorization/authorization-relations.enum';
import { CreateProjectDto } from './dto/create-project.dto';
import { User } from 'src/auth/user.decorator';
import { AddProjectMemberDto } from './dto/add-project-member.dto';
import { DeleteProjectMemberDto } from './dto/delete-project-member.dto';
import { ApiBearerAuth, ApiCreatedResponse, ApiOkResponse } from '@nestjs/swagger';
import { ProjectDto } from './dto/project.dto';
import { MemberDto } from './dto/member.dto';

@ApiBearerAuth('Auth0')
@Controller('projects')
@UseGuards(JwtAuthGuard, PermissionsGuard)
@UseInterceptors(ClassSerializerInterceptor)
export class ProjectsController {
  constructor(
    private readonly projectsService: ProjectsService,
    private readonly authorizationService: AuthorizationService,
  ) { }

  /**
   * Get all projects.
   */
  @Get()
  @ApiOkResponse({ type: ProjectDto, isArray: true })
  async getProjects(@User() currentUser): Promise<ProjectDto[]> {
    const user = new AuthorizationParty(AuthorizationPartyTypes.USER, currentUser.sub);
    const projectIds = await this.authorizationService.listObjectIds(
      user,
      AuthorizationPartyTypes.PROJECT,
      AuthorizationRelations.MEMBER,
    );
    const projects = await this.projectsService.findAll(projectIds);
    return projects.map((project) => ProjectDto.fromDocument(project));
  }

  /**
   * Get a project by ID.
   */
  @Get(':id')
  @ApiOkResponse({ type: ProjectDto })
  @Permissions({
    permission: AuthorizationRelations.MEMBER,
    objectType: 'project',
    objectIdParam: 'id',
  })
  async getProject(@Param('id') id: string): Promise<ProjectDto> {
    const project = await this.projectsService.findOne(id);
    return ProjectDto.fromDocument(project);
  }

  /**
   * Create a new project.
   */
  @Post()
  @ApiCreatedResponse({ type: ProjectDto })
  async createProject(@Body() createProjectDto: CreateProjectDto, @User() currentUser) {
    const project = await this.projectsService.create(createProjectDto);
    const userId = currentUser.sub;
    const user = new AuthorizationParty(AuthorizationPartyTypes.USER, userId);
    const object = new AuthorizationParty(AuthorizationPartyTypes.PROJECT, project.id);
    await this.authorizationService.addRelationship(
      user,
      object,
      AuthorizationRelations.OWNER,
    );
    return ProjectDto.fromDocument(project);
  }

  /**
   * List all members of a project.
   */
  @Get(':id/members')
  @ApiOkResponse({ type: String, isArray: true })
  @Permissions({
    permission: AuthorizationRelations.MEMBER,
    objectType: AuthorizationPartyTypes.PROJECT,
    objectIdParam: 'id',
  })
  async listMembers(@Param('id') projectId: string): Promise<MemberDto[]> {
    const project = new AuthorizationParty(
      AuthorizationPartyTypes.PROJECT,
      projectId,
    );
    const relations = [
      AuthorizationRelations.MEMBER,
      AuthorizationRelations.OWNER,
      AuthorizationRelations.ADMIN
    ];
    return (await this.authorizationService.listUsers(project, relations)).map(
      (user) => {
        return {
          userId: user.userId,
          roles: user.relations.map((role) => role.toString()),
        }
      }
    );
  }

  /**
   * Assign a member to a project.
   * Requires 'admin' permission on the project.
   */
  @Post(':id/members')
  @Permissions({
    permission: AuthorizationRelations.ADMIN,
    objectType: AuthorizationPartyTypes.PROJECT,
    objectIdParam: 'id',
  })
  async addMember(
    @Param('id') projectId: string,
    @Body() addMemberDto: AddProjectMemberDto,
  ) {
    const { userId, role } = addMemberDto;
    const user = new AuthorizationParty(AuthorizationPartyTypes.USER, userId);
    const project = new AuthorizationParty(
      AuthorizationPartyTypes.PROJECT,
      projectId,
    );
    await this.authorizationService.addRelationship(user, project, role);
    return { message: 'Ok' };
  }

  /**
   * Remove a member from a project.
   * Requires 'admin' permission on the project.
   */
  @Delete(':id/members')
  @Permissions({
    permission: AuthorizationRelations.ADMIN,
    objectType: AuthorizationPartyTypes.PROJECT,
    objectIdParam: 'id',
  })
  async removeMember(
    @Param('id') projectId: string,
    @Body() deleteMemberDto: DeleteProjectMemberDto,
  ) {
    const { userId } = deleteMemberDto;
    const user = new AuthorizationParty(AuthorizationPartyTypes.USER, userId);
    const project = new AuthorizationParty(
      AuthorizationPartyTypes.PROJECT,
      projectId,
    );
    await this.authorizationService.removeRelationship(user, project, deleteMemberDto.role);
    return { message: 'Ok' };
  }
}

Now if we try to access a project, that we are not a part of we will get an HTTP 403 exception:

Conclusion

In today’s fast-paced web development landscape, establishing a reliable permissions management system is essential for both security and functionality. This article demonstrated how to build such a system by integrating NestJS, OpenFGA, and Auth0.

Key Takeaways

  • NestJS provided a scalable and structured framework for developing the backend, ensuring maintainable and robust API development.
  • Auth0 streamlined the authentication process, offering features like social login and multifactor authentication without the complexity of building them from scratch.
  • OpenFGA enabled fine-grained access control, allowing precise management of user roles and permissions, ensuring that each user—whether Owner, Admin, or Member—has appropriate access levels.

Benefits of This Approach

  1. Enhanced Security: Clearly defined roles reduce the risk of unauthorized access, protecting sensitive project data.
  2. Scalability: The combination of NestJS, OpenFGA, and Auth0 ensures the system can grow with your application.
  3. Maintainability: Using industry-standard tools with clear separations of concern makes the system easier to manage and extend.
  4. Flexibility: OpenFGA’s detailed access control accommodates complex permission requirements and evolving business needs.

Final Thoughts

Building a secure and efficient permissions management system is crucial for modern web applications. By leveraging NestJS, OpenFGA, and Auth0, developers can create a robust backend that meets current security standards and adapts to future challenges. Implementing these tools will help ensure your applications are both secure and scalable, providing a solid foundation for growth in an ever-evolving digital environment.

Finding the Right Audience and Understanding Your Offering: Lessons from Two Musicians

Is having a great product enough to succeed?

When we talk about product development and business, we often think of market research, customer feedback, and strategic planning. But the world of art can teach us valuable lessons about these same principles. And these principles could be applied to everything we do in our life. Consider the stories of two musicians, each with a unique perspective on finding the right audience and value of their performance.

Story 1

In the middle of a busy city’s metro station, a famous violinist Joshua Bell played beautiful classical music. Despite his status and the usual high price of his concert tickets, he was largely ignored by passersby. His hat, left out for tips, collected only a few dollars. This experiment highlighted a curious paradox: the same music that commanded $100 per ticket in a concert hall went unnoticed and underappreciated in the busy metro.

Story 2

Contrast this with a personal story about my 10-year-old daughter. She loves playing her recorder and often performs on the streets, earning pocket money from generous listeners. One day, we were strolling around town, and she had her recorder with her, looking for a spot to play. We passed a museum with a long queue of people waiting to get in. My daughter, with her keen sense of opportunity, said, “Look at these people. They’re here for art. If I play near them, they’ll definitely listen and maybe give me some money.”

She set up her spot near the museum queue and started playing. True to her intuition, the people waiting for the museum, already inclined towards art, appreciated her performance. She received smiles, applause, and even some money.

These two stories underscore a critical lesson in finding the right market for your product. The world-renowned violinist had immense talent, but in the wrong setting, it went unnoticed. Meanwhile, my daughter found a receptive audience by positioning herself where people were already inclined to appreciate her art.

Just test this in your mind: what if Joshua Bell would put his classics aside and try something like the Star Wars intro theme? Not that sophisticated? But the people at the metro station would definitely appreciate it more since this music matches more what they are inclined to hear when hurrying to work.

Key Takeaways:

  1. Know your audience: Even the best products can fail if they aren’t presented to the right audience. Understanding who will value your product is crucial.
  2. Context matters: The environment in which you present your product can greatly influence its reception. Aligning your offering with the right context can make all the difference.
  3. Understand the full value proposition: When people buy a ticket to see a renowned violinist, it’s not just about the music; it’s also about the atmosphere, the prestige, and the social experience of attending a high-profile concert. This holistic experience was absent in the metro station, which contributed to the lack of appreciation. Similarly, understanding all aspects of why people value your product is essential for success.
  4. Adapt and test: My daughter’s success came from her willingness to adapt and test her hypothesis about where her music would be appreciated. Similarly, businesses should be ready to experiment and pivot based on feedback and observation.

In conclusion, product-market fit is not just about having a great product; it’s about finding the right audience, the right context, and understanding the complete value your product offers. By learning from these two musicians, we can better understand how to position our own offerings for success.


Are you looking to develop a product that truly fits your market? At FusionWorks, we specialize in product and software development, ensuring that your vision aligns perfectly with your audience’s needs. Let’s bring your ideas to life together. Contact us today to get started on your journey to success.

Using WebView with Flutter

Flutter comes with many packages that grant the possibility to run web pages in mobile apps. After research, the leader has been found: webview_flutter.

Why webview_flutter

First of all, it is currently maintained by the official Google team. When the development started, there were no official recommendations by the Flutter team on which package should be used.

The second reason why we started using this package — it gives us the possibility to share data between the web page and the flutter application in two directions. This feature was crucial when we investigated this case.

On iOS the WebView widget is backed by a WKWebView; On Android, the WebView widget is backed by a WebView.

How to set up webview_flutter

The setup is pretty easy. Install the library using the description on the official page.

How do we get data from a web page

JavascriptChannel gives us possibility to get data from web page. We set it up in the javascriptChannels list:

Firstly we choose the name for the channel in name parameter. This name will used to access channel from inside the web page. When we call the method channel from the page, onMessageReceived will be fired transporting the message.

Now let’s see how to send messages from the page. Firstly, webview_flutter mutates window object. If a web page has been loaded using this package it will have a property that we have defined in JavascriptChannel. In our case we can access the channel by calling:

We can use postMessage method to pass data to WebView and trigger onMessageReceived callback.

If you should use TypeScript in your project, you will need to override Window interface using following syntax:

How to pass data to a web page

In this particular case, we can not just use a JavascriptChannel, we should somehow inject some JavaScript code that will fire messages inside the web page. In this case, the web page will have a subscriber that will process the data received from the app.

The packagewebview_flutter comes with a solution. We can use WebViewController class that has runJavascript(String script) method:

Once script is executed and message is fired, a callback from inside the page is triggered:

Summary

In this article we have successfully transported data from a web page into Flutter application using WebView and vice versa.

Web page:

Send data: window.WebViewUserLocation.postMessage('');

Receive data: window.addEventListener('message', onMessageReceived);

Flutter app:

Send data:

Receive data: use JavascriptChannel

How to prevent sensitive data leakages through code repositories

Version control software (VCS) is essential for most modern software development practices. Among other benefits, software like Git, Mercurial, Bazaar, Perforce, CVS, and Subversion allows developers to save snapshots of their project history to enable better collaboration, revert to previous states, recover from unintended code changes, and manage multiple versions of the same codebase. These tools allow multiple developers to safely work on the same project and provide significant benefits even if you do not plan to share your work with others.

Although it is important to save your code in source control, it is equally important for some project assets to be kept out of your repository. Certain data like binary blobs and configuration files are best left out of source control for performance and usability reasons. But more importantly, sensitive data like passwords, secrets, and private keys should never be checked into a repository unprotected for security reasons.

Checking your Git Repository for Sensitive Data

First of all, once you started managing your secret security you need to check the repository for certain data. If you know an exact string that you want to search for, you can try using your VCS tool’s native search function to check whether the provided value is present in any commits. For example, with git, a command like this can search for a specific password:

git grep my_secret $(git rev-list --all)

Setting the security

Once you have removed sensitive data from the repository you should consider setting some internal tools to ensure you did not commit those files.

Ignoring Sensitive Files

The most basic way to keep files with sensitive data out of your repository is to leverage your VCS’s ignore functionality from the very beginning. VCS “ignore” files (like .gitignore) define patterns, directories, or files that should be excluded from the repository. These are a good first line of defense against accidentally exposing data. This strategy is useful because it does not rely on external tooling, the list of excluded items is automatically configured for collaborators, and it is easy to set up.

While VCS ignore functionality is useful as a baseline, it relies on keeping the ignore definitions up-to-date. It is easy to commit sensitive data accidentally prior to updating or implementing the ignore file. Ignore patterns that only have file-level granularity, so you may have to refactor some parts of your project if secrets are mixed in with code or other data that should be committed.

Using VCS Hooks to Check Files Prior to Committing

Most modern VCS implementations include a system called “hooks” for executing scripts before or after certain actions are taken within the repository. This functionality can be used to execute a script to check the contents of pending changes for sensitive material. The previously mentioned git-secrets tool has the ability to install pre-commit hooks that implement automatic checking for the type of content it evaluates. You can add your own custom scripts to check for whatever patterns you’d like to guard against.

Repository hooks provide a much more flexible mechanism for searching for and guarding against the addition of sensitive data at the time of commit. This increased flexibility comes at the cost of having to script all of the behavior you’d like to implement, which can potentially be a difficult process depending on the type of data you want to check. An additional consideration is that hooks are not shared as easily as ignore files, as they are not part of the repository that other developers copy. Each contributor will need to set up the hooks on their own machine, which makes enforcement a more difficult problem.

Adding Files to the Staging Area Explicitly

While more localized in scope, one simple strategy that may help you to be more mindful of your commits is to only add items to the VCS staging area explicitly by name. While adding files by wildcard or expansion can save some time, being intentional about each file you want to add can help prevent accidental additions that might otherwise be included. A beneficial side effect of this is that it generally allows you to create more focused and consistent commits, which helps with many other aspects of collaborative work.

Rules that you need to consider:

  • Never store unencrypted secrets in .git repositories. 
    A secret in a private repo is like a password written on a $20 bill, you might trust the person you gave it to, but that bill can end up in hundreds of peoples hands as a part of multiple transactions and within multiple cash registers.
  • Avoid git add * commands on git. 
    Using wildcard commands like git add *or git add . can easily capture files that should not enter a git repository, this includes generated files, config files and temporary source code. Add each file by name when making a commit and use git status to list tracked and untracked files.
  • Don’t rely on code reviews to discover secrets.
    It is extremely important to understand that code reviews will not always detect secrets, especially if they are hidden in previous versions of code. The reason code reviews are not adequate protection is because reviewers are only concerned with the difference between current and proposed states of the code, they do not consider the entire history of the project.
  • Use local environment variables, when feasible.
    An environment variable is a dynamic object whose value is set outside of the application. This makes them easier to rotate without having to make changes within the application itself. It also removes the need to have these written within source code, making them more appropriate to handle sensitive data.
  • Use automated secrets scanning on repositories.
    Implement real time alerting on repositories and gain visibility over where your secrets are with tools like GitGuardian