Skip to main content

Overview

Diffy API uses a two-step authentication process:
  1. GitHub OAuth - Users authenticate via GitHub to link their account
  2. JWT Tokens - API requests are authenticated using JWT bearer tokens

GitHub OAuth Flow

1

Initiate OAuth Flow

Redirect users to the GitHub OAuth endpoint to start authentication:
window.location.href = 'https://api.diffy.dev/auth/github';
This redirects to GitHub’s authorization page where users grant access to your application.
2

Handle OAuth Callback

After successful authentication, GitHub redirects back to your callback URL with a JWT token:
// Parse token from URL
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');

// Store token for API requests
localStorage.setItem('diffy_token', token);
3

Use Token for API Requests

Include the JWT token in the Authorization header for all API requests:
const response = await fetch('https://api.diffy.dev/pull-requests', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

Authentication Implementation

Backend OAuth Controller

The authentication flow is handled by the auth controller:
import { Controller, Get, Redirect, UseGuards, Req, Res } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport';
import type { Request, Response } from 'express';
import { ConfigService } from '@nestjs/config';

@Controller('auth')
export class AuthController {
  constructor(
    private readonly authService: AuthService,
    private readonly configService: ConfigService,
  ) {}

  @Get('github')
  @UseGuards(AuthGuard('github2'))
  @Redirect(
    `https://github.com/login/oauth/authorize?client_id=${process.env.GITHUB_CLIENT_ID}`,
    302,
  )
  async GitHub() {}

  @Get('callback')
  @UseGuards(AuthGuard('github2'))
  GitHubCallback(@Res() res: Response, @Req() req: Request) {
    const token = this.authService.generateToken(req.user as JwtPayload);
    return res.redirect(
      `${this.configService.get<string>('GITHUB_REDIRECT_URL')}?token=${token}`,
    );
  }
}

GitHub Strategy

The GitHub OAuth strategy validates users and creates accounts:
github.strategy.ts
import { Profile, Strategy } from 'passport-github2';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { AuthService } from '../auth.service';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy, 'github2') {
  constructor(
    private authService: AuthService,
    private configService: ConfigService,
  ) {
    super({
      clientID: configService.get<string>('GITHUB_APP_CLIENT_ID')!,
      clientSecret: configService.get<string>('GITHUB_APP_CLIENT_SECRET')!,
      callbackURL: configService.get<string>('GITHUB_APP_CALLBACK_URL')!,
      scope: ['user:email'],
    });
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: Profile,
    done: (err: Error | null, user?: any) => void,
  ): Promise<any> {
    const { id, displayName, username, emails, photos } = profile;
    const userDetails = {
      githubId: id,
      username: username,
      email: emails ? emails[0].value : undefined,
      avatarUrl: photos ? photos[0].value : undefined,
      name: displayName,
      githubAccessToken: accessToken,
      githubRefreshToken: refreshToken,
    };

    try {
      const user = await this.authService.validateOrCreateUser(userDetails);
      done(null, user);
    } catch (err) {
      done(err as Error, null);
    }
  }
}

JWT Strategy

The JWT strategy validates tokens on protected routes:
jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtPayload } from '../interfaces/jwt-payload.interface';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get<string>('JWT_SECRET')!,
    });
  }

  validate(payload: JwtPayload) {
    return { id: payload.id, githubId: payload.githubId, name: payload.name };
  }
}

Protecting Routes

Use the JwtAuthGuard to protect API endpoints:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt.guard';

@Controller('pull-requests')
export class PullRequestsController {
  @Get()
  @UseGuards(JwtAuthGuard)
  async getPullRequests() {
    // Only authenticated users can access this
    return [];
  }
}

Environment Variables

Configure these environment variables for authentication:
.env
# GitHub OAuth App Credentials
GITHUB_APP_CLIENT_ID=your_github_client_id
GITHUB_APP_CLIENT_SECRET=your_github_client_secret
GITHUB_APP_CALLBACK_URL=http://localhost:3000/auth/callback

# Redirect URL after authentication
GITHUB_REDIRECT_URL=http://localhost:5173

# JWT Configuration
JWT_SECRET=your_jwt_secret_key_here
Keep your JWT_SECRET and GITHUB_APP_CLIENT_SECRET secure. Never commit these to version control.

JWT Token Payload

The JWT token contains the following user information:
interface JwtPayload {
  id: number;          // User's database ID
  githubId: string;    // GitHub user ID
  name: string;        // User's display name
}

Token Expiration

JWT tokens expire after 1 hour. Clients should handle token expiration gracefully:
const response = await fetch('https://api.diffy.dev/pull-requests', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

if (response.status === 401) {
  // Token expired - redirect to login
  window.location.href = '/login';
}
Tokens are configured with a 1-hour expiration in the JwtModule configuration (see auth.module.ts:19-21).

Frontend Integration Example

Here’s a complete example of integrating authentication in a React application:
useAuth.ts
import { useState, useEffect } from 'react';

interface User {
  id: number;
  githubId: string;
  name: string;
}

export function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [token, setToken] = useState<string | null>(null);

  useEffect(() => {
    // Check for token in URL (from OAuth callback)
    const urlParams = new URLSearchParams(window.location.search);
    const tokenFromUrl = urlParams.get('token');
    
    if (tokenFromUrl) {
      localStorage.setItem('diffy_token', tokenFromUrl);
      setToken(tokenFromUrl);
      // Clean up URL
      window.history.replaceState({}, '', window.location.pathname);
    } else {
      // Check localStorage
      const storedToken = localStorage.getItem('diffy_token');
      if (storedToken) {
        setToken(storedToken);
      }
    }
  }, []);

  const login = () => {
    window.location.href = 'https://api.diffy.dev/auth/github';
  };

  const logout = () => {
    localStorage.removeItem('diffy_token');
    setToken(null);
    setUser(null);
  };

  return { user, token, login, logout };
}

Testing Authentication

Test the authentication flow using curl:
# 1. Get a token (replace with actual token from OAuth flow)
TOKEN="your_jwt_token_here"

# 2. Test authenticated endpoint
curl -H "Authorization: Bearer $TOKEN" \
  https://api.diffy.dev/github/pull-request/owner/repo/1

Build docs developers (and LLMs) love