DEV Community

Cover image for πŸš€ GitGuard – Secure Just-In-Time Repository Access with Biometrics & Permit.io
NIkhil Sahni
NIkhil Sahni

Posted on

18 2 2 2 3

πŸš€ GitGuard – Secure Just-In-Time Repository Access with Biometrics & Permit.io

πŸ” GitGuard – Just-in-Time GitHub Access Control

Submission for the Permit.io Authorization Challenge: Permissions Redefined

What I Built

I built GitGuard - a full-stack, production-grade access control and auditing system for secure, temporary, and role-based GitHub access management that leverages Permit.io for dynamic authorization.

In traditional GitHub environments, access control follows a static model: either you have access to a repository, or you don't. This creates security risks as teams often grant excessive permissions to ensure work isn't blocked. GitGuard solves this by implementing Just-in-Time access - granting temporary, scoped permissions only when needed, verified through biometric authorization.

Think of GitGuard as a "Just-in-Time IAM layer" tailored for GitHub. Perfect for fast-moving teams that need security without sacrificing agility.

Demo Screenshots

πŸ” Login Screen

Login Screen

πŸ“ Register Screen

Register Screen

🏠 Home Page

Home Page

πŸ“ Repository Screen

Repository Screen

πŸ—‚οΈ Access Request Manager

Access Request Manager

βœ… Approval/Reject Filter

Approval/Reject

βœ”οΈ Approve Request Flow

Approve Request

πŸ“₯ Access Request Form

Access Request Form

🧬 Biometric Approval (Simulated)

Biometrics cannot be captured in screenshots; simulated via mobile preview.

Biometric Approval

πŸ”” Push Notification for Approvals

Push Notification

πŸ“‚ Repository Details

Repository Details

πŸ”” Push Notifications List

Push Notifications List

🏒 Organisation List and Create

Organisation List

πŸ“œ Audit Logs

(Local preview only)
Audit logs 1
Audit logs 2

Key Features

Feature Description
πŸ” Biometric Authentication Approve access requests using fingerprint/face ID
⏱️ Just-in-Time Access Time-bound repository access with automatic expiration
πŸ‘₯ Role-Based Access Multiple repository roles with different permission sets
πŸ“Š Audit Logging Comprehensive activity tracking for compliance
πŸ”” Push Notifications Instant alerts for access requests and approvals
πŸ”„ Multi-Approver Flow Quorum requirements for sensitive repositories
🚨 Emergency Access Expedited access for critical situations
πŸŒ™ Auto-Expiration Automatic revocation after defined timeframes
🏦 Organization Grouping Manage access across multiple organizations

Role-Based Capabilities

Feature Viewer Contributor Admin
View Repository βœ… βœ… βœ…
Clone Repository βœ… βœ… βœ…
Push Changes ❌ βœ… βœ…
Approve Access ❌ ❌ βœ…
Repository Settings ❌ ❌ βœ…
Delete Repository ❌ ❌ βœ…
Create Repository ❌ ❌ βœ…
View Audit Logs ❌ ❌ βœ…

Project Repositories

Component Link
🧠 Backend GitGuard Backend
πŸ“± Mobile App GitGuard Mobile

Permissions Redefined with Permit.io

GitGuard implements a true Permissions Redefined model using Permit.io's policy-as-code approach, completely separating the business logic from the authorization layer.

Core Authorization Flow

  1. User initiates an access request for a repository
  2. Admin receives notification and authenticates with biometrics
  3. Backend verifies biometric token and processes approval
  4. Permit.io is used to check, assign, and enforce permissions
  5. Time-bound role is assigned to the user
  6. Access is automatically revoked after expiration
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Mobile  │───▢│ GitGuard API │───▢│ permitUtils│───▢│  Permit.io   β”‚
β”‚   App    │◀───│              │◀───│  middleware│◀───│  Cloud PDP   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚                                                      β–²
     β”‚                                                      β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        Policies defined in Permit.io dashboard
Enter fullscreen mode Exit fullscreen mode

Implementation

GitGuard implements authorization with a modular, clean approach through a dedicated permitUtils.ts layer:

// Backend initialization (src/index.ts)
export const permit = new Permit({
  token: process.env.PERMIT_API_KEY || "",
  pdp: process.env.PERMIT_PDP_URL || "http://localhost:7766",
});
Enter fullscreen mode Exit fullscreen mode
// Permission check implementation (src/utils/permitUtils.ts)
export const checkPermission = async (
  userId: string,
  action: string,
  resource: string,
  resourceInstance?: string
) => {
  let resourceObj: string | { type: string; id: string } = resource;

  // If resource instance is provided, create resource object
  if (resourceInstance) {
    resourceObj = {
      type: resource,
      id: resourceInstance,
    };
  }

  // Try to check permission with Permit.io first
  try {
    const permitted = await permit.check(userId, action, resourceObj);
    if (permitted) return true;
  } catch (permitError) {
    console.warn(
      `Permit check failed for user ${userId} on ${resource}:${resourceInstance} - ${permitError}`
    );
    // Continue to fallback check
  }

  // If Permit.io check fails or returns false, fall back to database check
  if (resource === "repository" && resourceInstance) {
    const { prisma } = await import("../index");

    // Check if user is the repository owner
    const repo = await prisma.repository.findUnique({
      where: { id: resourceInstance },
      select: { ownerId: true },
    });

    if (repo && repo.ownerId === userId) {
      return true; // Repository owners have all permissions
    }

    // Check role assignments
    const roleAssignment = await prisma.roleAssignment.findFirst({
      where: {
        userId,
        repositoryId: resourceInstance,
      },
      include: {
        role: {
          include: {
            permissions: true,
          },
        },
      },
    });

    if (roleAssignment) {
      // Check if the assigned role has the required permission
      const hasPermission = roleAssignment.role.permissions.some(
        (permission) =>
          permission.action === action || permission.action === "admin"
      );

      if (hasPermission) {
        return true;
      }
    }
  }

  return false;
};
Enter fullscreen mode Exit fullscreen mode
// Role assignment (src/utils/permitUtils.ts)
export const assignRoleInPermit = async (
  userId: string,
  roleKey: string,
  resourceType: string,
  resourceInstanceKey: string
) => {
  try {
    // First ensure the user exists in Permit.io
    try {
      await syncUserWithPermit(userId);
    } catch (userError) {
      console.warn(`Could not sync user with Permit.io: ${userError}`);
      // Continue anyway - we'll try to assign the role
    }

    await permit.api.roleAssignments.assign({
      user: userId,
      role: roleKey,
      tenant: "default",
      resource_instance: `${resourceType}:${resourceInstanceKey}`,
    });

    console.log(
      `Successfully assigned role ${roleKey} to user ${userId} for ${resourceType}:${resourceInstanceKey}`
    );
    return true;
  } catch (error: any) {
    // If it's a 409 conflict (role already assigned), treat as success
    if (error.response && error.response.status === 409) {
      console.log(
        `Role ${roleKey} already assigned to user ${userId}, skipping`
      );
      return true;
    }

    console.error("Failed to assign role in Permit.io:", error);
    // Don't throw the error, just log it and continue - this makes the app more resilient
    // We'll fall back to database checks for permissions
    return false;
  }
};
Enter fullscreen mode Exit fullscreen mode
// Usage in API endpoints (src/routes/repository.ts)
router.get("/:id", authenticateJWT, async (req, res, next) => {
  try {
    const { id } = req.params;
    const userId = req.user.id;

    // Check permission with Permit.io
    const hasViewPermission = await checkPermission(
      userId,
      "view",
      "repository",
      id
    );

    if (!hasViewPermission) {
      throw new ApiError(
        403,
        "You don't have permission to view this repository"
      );
    }

    // Proceed with repository retrieval...
  } catch (error) {
    next(error);
  }
});
Enter fullscreen mode Exit fullscreen mode

Dashboard Configuration

For GitGuard to work correctly, you must configure the following in the Permit.io dashboard:

  1. Define Resources:
  • Create repository resource type with actions:
    • view: View repository contents
    • clone: Clone repository
    • push: Push changes to repository
    • admin: Administer repository settings
    • delete: Delete repository
    • create: Create new repository
  1. Define Roles:
  • viewer: Can view and clone repositories
  • contributor: Can view, clone, and push to repositories
  • admin: Has full access to all repository actions
  1. Configure User-to-Role assignments in the Roles tab

  2. Set up Resource Relations for ownership model:

    • Relation: owner between user and repository

Setup Guide

Step 1: Clone the repository

git clone https://github.com/nikhilsahni7/GitGuard.git
cd GitGuard
Enter fullscreen mode Exit fullscreen mode

Step 2: Set up Permit.io

  1. Create a free account at Permit.io
  2. Create a new project
  3. Set up:
    • Resource type: repository
    • Actions: view, clone, push, admin, delete, create
    • Roles: viewer, contributor, admin
    • Configure role permissions as described above
  4. Generate an Environment API key from the dashboard

Step 3: Configure environment variables

Create a .env file in the backend directory:

# Permit.io
PERMIT_API_KEY=your_permit_api_key
PERMIT_PDP_URL=http://localhost:7766 # Or cloud PDP URL

# Database
DATABASE_URL=postgresql://user:password@localhost:5432/gitguard

# JWT Authentication
JWT_SECRET=your_jwt_secret
JWT_EXPIRES_IN=7d

# GitHub Integration
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret

# Push Notifications
EXPO_ACCESS_TOKEN=your_expo_token
Enter fullscreen mode Exit fullscreen mode

Step 4: Install dependencies and run

# Backend setup
cd backend
bun install
bun run db:migrate
bun run permit:setup
bun run dev

# Mobile setup (in a separate terminal)
cd ../mobile
yarn install
yarn start
Enter fullscreen mode Exit fullscreen mode

Step 5: Initialize Permit.io

GitGuard includes a setup script that configures all necessary resources and permissions:

cd backend
bun run permit:setup
Enter fullscreen mode Exit fullscreen mode

This sets up:

  • Repository resource with all actions
  • User resource
  • Standard roles (viewer, contributor, admin)
  • Resource relations for ownership model

Step 6: Verify Permit.io Setup

To verify your Permit.io configuration:

bun run permit:verify
Enter fullscreen mode Exit fullscreen mode

This will:

  • Check if resources and roles exist
  • Create a test user
  • Assign roles and test permissions
  • Create and test resource relationships

Challenges and Solutions

Challenge 1: Resource Instance Permissions

Initially, I struggled with implementing resource-instance level permissions in Permit.io to grant access to specific repositories rather than all repositories.

Solution: I implemented a robust resource relations system using Permit.io's API:

// Setup owner relation (setup-permit.ts)
const relationData = {
  key: "owner",
  name: "Owner",
  subject_resource: "user",
};

await permit.api.resourceRelations.create("repository", relationData);

// Create relationship tuples for specific repositories
await permit.api.relationshipTuples.create({
  subject: `user:${userId}`,
  relation: "owner",
  object: `repository:${repositoryId}`,
  tenant: "default",
});
Enter fullscreen mode Exit fullscreen mode

Challenge 2: Fallback Mechanism

What if Permit.io is temporarily unavailable? GitGuard needed resilience.

Solution: I implemented a dual-check system that falls back to database checks:

export const checkPermission = async (
  userId,
  action,
  resource,
  resourceInstance
) => {
  // Try Permit.io first
  try {
    const permitted = await permit.check(userId, action, resourceObj);
    if (permitted) return true;
  } catch (permitError) {
    // Log and continue to fallback
  }

  // Fallback to database check
  // [Database permission check logic omitted for brevity]
};
Enter fullscreen mode Exit fullscreen mode

Challenge 3: Time-bound Access

Implementing automatic role expiration was critical for the Just-in-Time model.

Solution: Combined Permit.io role assignments with a database TTL mechanism:

// When approving access requests (routes/accessRequest.ts)
await prisma.roleAssignment.create({
  data: {
    userId: requestData.userId,
    repositoryId: requestData.repositoryId,
    roleId: requestData.roleId,
    expiresAt: new Date(Date.now() + duration), // Time-bound access
    approvedBy: adminId,
    approvedAt: new Date(),
  },
});

// Also register in Permit.io
await assignRoleInPermit(
  requestData.userId,
  role.key,
  "repository",
  requestData.repositoryId
);

// Background job runs to revoke expired access
// [Scheduled job implementation omitted for brevity]
Enter fullscreen mode Exit fullscreen mode

Challenge 4: Biometric Verification Flow

Securing the approval process with biometrics while maintaining a smooth user experience was challenging.

Solution: Implemented a secure token-based verification system:

// Mobile app generates a biometric token (mobile code)
const biometricAuth = async () => {
  const compatible = await LocalAuthentication.hasHardwareAsync();

  if (!compatible) {
    throw new Error("Biometric authentication not available");
  }

  const result = await LocalAuthentication.authenticateAsync({
    promptMessage: "Authenticate to approve access request",
    fallbackLabel: "Use passcode",
  });

  if (result.success) {
    // Generate token only after successful biometric auth
    return generateBiometricToken();
  }

  throw new Error("Authentication failed");
};

// Backend verifies token before approving (routes/accessRequest.ts)
router.post("/:id/approve", authenticateJWT, async (req, res, next) => {
  try {
    const { biometricToken } = req.body;
    const adminId = req.user.id;

    // Verify biometric token
    const validToken = await verifyBiometricToken(adminId, biometricToken);

    if (!validToken) {
      throw new ApiError(401, "Invalid biometric verification");
    }

    // Process approval with Permit.io
    // [Approval logic omitted for brevity]
  } catch (error) {
    next(error);
  }
});
Enter fullscreen mode Exit fullscreen mode

What I Learned

Building GitGuard with Permit.io provided several key insights:

Technical Benefits

  • Separation of Concerns: Clean separation between business logic and authorization decisions
  • Flexible Policy Management: Ability to update access policies without code changes
  • Resource-Based Model: Modeling GitHub repositories as protected resources with granular permissions

Business Benefits

  • Enhanced Security: Just-in-Time access model vastly reduces the attack surface
  • Centralized Control: Administrators can manage all permissions from one dashboard
  • Audit Compliance: Comprehensive logging of all access decisions
  • Reduced Overhead: Automating approval workflows saves significant administrative time

Developer Experience

  • Cleaner Codebase: Authorization logic centralized in one place rather than scattered throughout
  • Reduced Boilerplate: Fewer permission checks needed in business logic
  • Easier Testing: Simpler mocking of authorization decisions for unit tests

Why Permit.io Works for Just-in-Time Access

Permit.io is particularly well-suited for Just-in-Time access control because:

  1. External Policy Management: Policies can be updated in real-time without deploying code
  2. Resource Instance Granularity: Permissions can be scoped to specific repositories
  3. Relationship Modeling: Owner/member relationships easily modeled in permissions
  4. Flexible Role System: Easy to create and assign temporary roles for specific durations
  5. Audit Trail: Built-in logging for compliance and security reviews

Future Improvements

With more time, I would enhance GitGuard with:

  1. Local PDP: Set up a local Policy Decision Point for improved performance and reliability
  2. Attribute-Based Policies: Extend beyond role-based to include context like time of day, IP range, etc.
  3. Multi-Tenant Support: Enhanced organization isolation for enterprise environments
  4. Custom Policy Editor: Allow admins to create custom policies beyond predefined roles
  5. Integration with CI/CD: Automated access for deployment pipelines with temporary credentials

Built with ❀️ using:
Bun β€’ Prisma β€’ PostgreSQL β€’ Expo β€’ React Native β€’ TypeScript β€’ Permit.io

Not Sure What Type of Protection is Best for Your Mobile App?

Not Sure What Type of Protection is Best for Your Mobile App?

While mobile app wrappers can be easy to implement security measures, they also present a single point of failure in an attack. Compilers integrate multiple security checks, offering a stronger and more robust security solution than wrappers.

Learn more

Top comments (1)

Collapse
 
kush_gautam_8b4bdeabeb6de profile image
Kush Gautam β€’

Good work πŸ‘

Dev Diairies image

User Feedback & The Pivot That Saved The Project

πŸ”₯ Check out Episode 3 of Dev Diairies, following a successful Hackathon project turned startup.

Watch full video πŸŽ₯

πŸ‘‹ Kindness is contagious

Delve into this thought-provoking piece, celebrated by the DEV Community. Coders from every walk are invited to share their insights and strengthen our collective intelligence.

A heartfelt β€œthank you” can transform someone’s dayβ€”leave yours in the comments!

On DEV, knowledge sharing paves our journey and forges strong connections. Found this helpful? A simple thanks to the author means so much.

Get Started