← Back to DevBytes

TypeORM Authentication: JWT, Sessions, and OAuth Integration

Introduction to TypeORM Authentication

Authentication is one of the most critical aspects of any modern web application. When you combine TypeORM — a powerful TypeScript ORM for Node.js — with robust authentication strategies like JWT, sessions, and OAuth, you get a scalable and secure foundation for user management. This tutorial walks you through implementing all three authentication patterns with TypeORM, from basic setup to production-ready best practices.

What Is TypeORM Authentication?

TypeORM authentication refers to the practice of using TypeORM as the data layer to manage user records, credentials, and tokens while integrating authentication mechanisms on top of it. TypeORM itself is not an authentication library — it handles database operations — but it pairs naturally with libraries like bcrypt, jsonwebtoken, express-session, and passport to build complete auth flows.

Why It Matters

Project Setup and User Entity

Before implementing any authentication strategy, we need a TypeORM project with a User entity. Let's start by installing the necessary dependencies.

Installing Dependencies

npm install typeorm reflect-metadata pg bcrypt jsonwebtoken express-session
npm install @types/bcrypt @types/jsonwebtoken @types/express-session --save-dev

Configuring the Data Source

import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "./entities/User";

export const AppDataSource = new DataSource({
  type: "postgres",
  host: "localhost",
  port: 5432,
  username: "postgres",
  password: "password",
  database: "auth_demo",
  synchronize: true,
  entities: [User],
  logging: false,
});

AppDataSource.initialize()
  .then(() => console.log("Database connected"))
  .catch((err) => console.error("Connection error", err));

Defining the User Entity

The User entity is the backbone of any authentication system. We store a hashed password (never plaintext) and include fields for OAuth providers so we can support multiple auth strategies on the same entity.

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  Index,
} from "typeorm";
import { BeforeInsert, BeforeUpdate } from "typeorm";
import * as bcrypt from "bcrypt";

@Entity("users")
export class User {
  @PrimaryGeneratedColumn("uuid")
  id: string;

  @Column({ unique: true })
  @Index()
  email: string;

  @Column({ nullable: true })
  username: string;

  @Column({ nullable: true, select: false })
  password: string;

  @Column({ nullable: true })
  googleId: string;

  @Column({ nullable: true })
  githubId: string;

  @Column({ default: false })
  isOAuthUser: boolean;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @BeforeInsert()
  @BeforeUpdate()
  async hashPassword() {
    if (this.password && !this.password.startsWith("$2b$")) {
      const salt = await bcrypt.genSalt(12);
      this.password = await bcrypt.hash(this.password, salt);
    }
  }

  async comparePassword(candidate: string): Promise<boolean> {
    if (!this.password) return false;
    return bcrypt.compare(candidate, this.password);
  }
}

Notice the select: false option on the password column. This ensures the hashed password is never returned in queries unless explicitly requested, reducing accidental exposure.

JWT Authentication

JSON Web Tokens (JWT) are the most popular stateless authentication mechanism. The server issues a signed token after login, and the client sends it with each request in the Authorization header. The server verifies the signature without needing to look up session state in a database.

Creating the JWT Service

import * as jwt from "jsonwebtoken";
import { User } from "../entities/User";

const JWT_SECRET = process.env.JWT_SECRET || "super-secret-change-me";
const ACCESS_TOKEN_EXPIRY = "15m";
const REFRESH_TOKEN_EXPIRY = "7d";

export class JwtService {
  static generateAccessToken(user: User): string {
    return jwt.sign(
      { sub: user.id, email: user.email },
      JWT_SECRET,
      { expiresIn: ACCESS_TOKEN_EXPIRY }
    );
  }

  static generateRefreshToken(user: User): string {
    return jwt.sign(
      { sub: user.id, type: "refresh" },
      JWT_SECRET,
      { expiresIn: REFRESH_TOKEN_EXPIRY }
    );
  }

  static verifyToken(token: string): any {
    try {
      return jwt.verify(token, JWT_SECRET);
    } catch (err) {
      return null;
    }
  }
}

Implementing Registration and Login

import { Request, Response } from "express";
import { AppDataSource } from "../data-source";
import { User } from "../entities/User";
import { JwtService } from "../services/JwtService";

const userRepo = AppDataSource.getRepository(User);

export async function register(req: Request, res: Response) {
  const { email, password, username } = req.body;

  const existing = await userRepo.findOne({ where: { email } });
  if (existing) {
    return res.status(409).json({ message: "Email already registered" });
  }

  const user = userRepo.create({ email, password, username });
  await userRepo.save(user);

  const accessToken = JwtService.generateAccessToken(user);
  const refreshToken = JwtService.generateRefreshToken(user);

  return res.status(201).json({ accessToken, refreshToken, user: { id: user.id, email: user.email } });
}

export async function login(req: Request, res: Response) {
  const { email, password } = req.body;

  const user = await userRepo
    .createQueryBuilder("user")
    .addSelect("user.password")
    .where("user.email = :email", { email })
    .getOne();

  if (!user) {
    return res.status(401).json({ message: "Invalid credentials" });
  }

  const valid = await user.comparePassword(password);
  if (!valid) {
    return res.status(401).json({ message: "Invalid credentials" });
  }

  const accessToken = JwtService.generateAccessToken(user);
  const refreshToken = JwtService.generateRefreshToken(user);

  return res.json({ accessToken, refreshToken });
}

export async function refreshToken(req: Request, res: Response) {
  const { refreshToken } = req.body;
  const payload = JwtService.verifyToken(refreshToken);

  if (!payload || payload.type !== "refresh") {
    return res.status(401).json({ message: "Invalid refresh token" });
  }

  const user = await userRepo.findOne({ where: { id: payload.sub } });
  if (!user) {
    return res.status(401).json({ message: "User not found" });
  }

  const newAccessToken = JwtService.generateAccessToken(user);
  return res.json({ accessToken: newAccessToken });
}

JWT Middleware for Protected Routes

import { Request, Response, NextFunction } from "express";
import { JwtService } from "../services/JwtService";

export function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return res.status(401).json({ message: "Missing or invalid token" });
  }

  const token = authHeader.split(" ")[1];
  const payload = JwtService.verifyToken(token);

  if (!payload) {
    return res.status(401).json({ message: "Invalid or expired token" });
  }

  req.user = { id: payload.sub, email: payload.email };
  next();
}

With this middleware in place, you can protect any route by simply adding authMiddleware to the route definition. The authenticated user's ID and email will be available on req.user.

Session-Based Authentication

Session-based authentication is a stateful approach where the server stores session data (typically in a database or Redis) and sends a session ID to the client via a cookie. This is a great choice when you need the ability to revoke sessions immediately or when you want to avoid sending tokens in headers.

Setting Up Express Session with TypeORM Store

import session from "express-session";
import { TypeORMStore } from "connect-typeorm";
import { AppDataSource } from "./data-source";
import { Session } from "./entities/Session";

// Session entity for storing sessions in the database
@Entity("sessions")
export class Session {
  @PrimaryGeneratedColumn("uuid")
  id: string;

  @Column({ type: "bigint" })
  expiredAt: number;

  @Column({ type: "text" })
  json: string;
}

export function configureSession(app: Express) {
  const sessionRepo = AppDataSource.getRepository(Session);

  app.use(
    session({
      name: "sid",
      secret: process.env.SESSION_SECRET || "session-secret-change-me",
      resave: false,
      saveUninitialized: false,
      store: new TypeORMStore({
        repository: sessionRepo,
        ttl: 86400, // 24 hours
      }),
      cookie: {
        httpOnly: true,
        secure: process.env.NODE_ENV === "production",
        sameSite: "strict",
        maxAge: 24 * 60 * 60 * 1000,
      },
    })
  );
}

Session Login and Logout Handlers

import { Request, Response } from "express";
import { AppDataSource } from "../data-source";
import { User } from "../entities/User";

const userRepo = AppDataSource.getRepository(User);

export async function sessionLogin(req: Request, res: Response) {
  const { email, password } = req.body;

  const user = await userRepo
    .createQueryBuilder("user")
    .addSelect("user.password")
    .where("user.email = :email", { email })
    .getOne();

  if (!user || !(await user.comparePassword(password))) {
    return res.status(401).json({ message: "Invalid credentials" });
  }

  req.session.userId = user.id;
  req.session.email = user.email;

  return res.json({ message: "Logged in", user: { id: user.id, email: user.email } });
}

export async function sessionLogout(req: Request, res: Response) {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ message: "Logout failed" });
    }
    res.clearCookie("sid");
    return res.json({ message: "Logged out" });
  });
}

export function requireAuth(req: Request, res: Response, next: NextFunction) {
  if (!req.session.userId) {
    return res.status(401).json({ message: "Not authenticated" });
  }
  next();
}

Session-based auth shines when you need immediate logout capability. Because the server controls session validity, destroying the session on the server instantly revokes access — something JWT alone cannot do without a token blacklist.

OAuth Integration

OAuth allows users to authenticate using third-party providers like Google, GitHub, or Facebook. We'll use passport with provider-specific strategies, storing the provider's user ID on our TypeORM User entity.

Installing Passport and Strategies

npm install passport passport-google-oauth20 passport-github2
npm install @types/passport @types/passport-google-oauth20 @types/passport-github2 --save-dev

Configuring Passport Strategies

import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as GitHubStrategy } from "passport-github2";
import { AppDataSource } from "./data-source";
import { User } from "../entities/User";

const userRepo = AppDataSource.getRepository(User);

export function configurePassport() {
  // Serialize only the user ID into the session
  passport.serializeUser((user: any, done) => {
    done(null, user.id);
  });

  passport.deserializeUser(async (id: string, done) => {
    try {
      const user = await userRepo.findOne({ where: { id } });
      done(null, user);
    } catch (err) {
      done(err, null);
    }
  });

  passport.use(
    new GoogleStrategy(
      {
        clientID: process.env.GOOGLE_CLIENT_ID!,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
        callbackURL: "/auth/google/callback",
      },
      async (accessToken, refreshToken, profile, done) => {
        try {
          let user = await userRepo.findOne({
            where: { googleId: profile.id },
          });

          if (!user) {
            // Check if a user with this email already exists
            const email = profile.emails?.[0]?.value;
            if (email) {
              user = await userRepo.findOne({ where: { email } });
              if (user) {
                user.googleId = profile.id;
                await userRepo.save(user);
                return done(null, user);
              }
            }

            // Create a new user
            user = userRepo.create({
              email: profile.emails?.[0]?.value || "",
              username: profile.displayName,
              googleId: profile.id,
              isOAuthUser: true,
            });
            await userRepo.save(user);
          }

          return done(null, user);
        } catch (err) {
          return done(err, undefined);
        }
      }
    )
  );

  passport.use(
    new GitHubStrategy(
      {
        clientID: process.env.GITHUB_CLIENT_ID!,
        clientSecret: process.env.GITHUB_CLIENT_SECRET!,
        callbackURL: "/auth/github/callback",
        scope: ["user:email"],
      },
      async (accessToken, refreshToken, profile, done) => {
        try {
          let user = await userRepo.findOne({
            where: { githubId: profile.id },
          });

          if (!user) {
            const email = profile.emails?.[0]?.value;
            if (email) {
              user = await userRepo.findOne({ where: { email } });
              if (user) {
                user.githubId = profile.id;
                await userRepo.save(user);
                return done(null, user);
              }
            }

            user = userRepo.create({
              email: profile.emails?.[0]?.value || "",
              username: profile.username,
              githubId: profile.id,
              isOAuthUser: true,
            });
            await userRepo.save(user);
          }

          return done(null, user);
        } catch (err) {
          return done(err, undefined);
        }
      }
    )
  );
}

OAuth Routes

import { Router } from "express";
import passport from "passport";

const router = Router();

// Google OAuth
router.get("/google", passport.authenticate("google", { scope: ["profile", "email"] }));

router.get(
  "/google/callback",
  passport.authenticate("google", { failureRedirect: "/login" }),
  (req, res) => {
    // Successful authentication — issue JWT or set session
    res.redirect("/dashboard");
  }
);

// GitHub OAuth
router.get("/github", passport.authenticate("github", { scope: ["user:email"] }));

router.get(
  "/github/callback",
  passport.authenticate("github", { failureRedirect: "/login" }),
  (req, res) => {
    res.redirect("/dashboard");
  }
);

// Logout
router.get("/logout", (req, res) => {
  req.logout((err) => {
    if (err) return res.status(500).json({ message: "Logout failed" });
    res.redirect("/");
  });
});

export default router;

Combining OAuth with JWT

If you prefer stateless JWT auth even with OAuth, you can issue a JWT after the OAuth callback instead of relying on sessions:

router.get(
  "/google/callback",
  passport.authenticate("google", { session: false, failureRedirect: "/login" }),
  (req, res) => {
    const user = req.user as User;
    const accessToken = JwtService.generateAccessToken(user);
    const refreshToken = JwtService.generateRefreshToken(user);
    // Redirect with tokens as query params or set cookies
    res.redirect(`/auth/success?accessToken=${accessToken}&refreshToken=${refreshToken}`);
  }
);

Putting It All Together

Here is how you wire everything into your Express application:

import express from "express";
import "reflect-metadata";
import { AppDataSource } from "./data-source";
import { configureSession } from "./session-config";
import { configurePassport } from "./passport-config";
import passport from "passport";
import { register, login, refreshToken } from "./controllers/jwtController";
import { sessionLogin, sessionLogout, requireAuth } from "./controllers/sessionController";
import { authMiddleware } from "./middleware/authMiddleware";
import authRoutes from "./routes/authRoutes";

const app = express();

app.use(express.json());
configureSession(app);
configurePassport();
app.use(passport.initialize());
app.use(passport.session());

// JWT routes
app.post("/api/register", register);
app.post("/api/login", login);
app.post("/api/refresh", refreshToken);
app.get("/api/protected", authMiddleware, (req, res) => {
  res.json({ message: "Access granted", user: req.user });
});

// Session routes
app.post("/api/session/login", sessionLogin);
app.post("/api/session/logout", sessionLogout);
app.get("/api/session/protected", requireAuth, (req, res) => {
  res.json({ message: "Session valid", userId: req.session.userId });
});

// OAuth routes
app.use("/auth", authRoutes);

AppDataSource.initialize().then(() => {
  app.listen(3000, () => console.log("Server running on port 3000"));
});

Best Practices

Security Best Practices

TypeORM-Specific Best Practices

OAuth Best Practices

Conclusion

TypeORM provides a clean, type-safe foundation for building authentication systems in Node.js. Whether you choose stateless JWT authentication for scalability, session-based authentication for immediate revocation control, or OAuth integration for frictionless third-party logins — or a combination of all three — the key is understanding the trade-offs of each approach. JWT excels in distributed and API-first architectures, sessions offer superior control in traditional web apps, and OAuth dramatically improves user onboarding. By following the patterns and best practices outlined in this tutorial, you can build a secure, maintainable authentication system that grows with your application's needs. Remember that authentication is never "set and forget" — regularly audit your secrets, update dependencies, and stay informed about emerging security threats to keep your users safe.

— Ad —

Google AdSense will appear here after approval

← Back to all articles