← Back to DevBytes

Yup Authentication: JWT, Sessions, and OAuth Integration

Yup Authentication: JWT, Sessions, and OAuth Integration

Authentication is one of the most security-critical parts of any application, and validating the data that flows through your auth pipeline is just as important as the cryptographic checks themselves. Yup is a lightweight, schema-based JavaScript object validator that pairs beautifully with authentication flows. In this tutorial, you'll learn how to use Yup to validate JWT payloads, session objects, and OAuth provider responses, ensuring that malformed or malicious data never reaches your business logic.

What Is Yup?

Yup is a schema builder for runtime value parsing and validation. It lets you define an expected shape for an object — types, required fields, constraints, and custom rules — and then validate incoming data against that schema. Unlike TypeScript types, which are erased at compile time, Yup schemas run in production and catch bad data before it causes damage.

In the context of authentication, Yup shines because auth data comes from untrusted sources: user input on login forms, decoded JWT tokens, third-party OAuth providers, and session stores. Validating each of these with a schema gives you a single, declarative source of truth for "what a valid auth object looks like."

Why Validation Matters in Authentication

Setting Up

Install Yup in your project:

npm install yup
# or
yarn add yup

Yup works in both Node.js and the browser, and it's framework-agnostic, so you can use it with Express, Fastify, Next.js, NestJS, or any other stack.

Validating Login Input

Before issuing any token or session, you must validate the credentials the user submitted. Here's a schema for a login request:

import * as yup from 'yup';

const loginSchema = yup.object({
  email: yup
    .string()
    .email('Must be a valid email address')
    .required('Email is required'),
  password: yup
    .string()
    .min(8, 'Password must be at least 8 characters')
    .max(128, 'Password is too long')
    .required('Password is required'),
});

async function validateLogin(body) {
  try {
    const valid = await loginSchema.validate(body, { abortEarly: false });
    return { ok: true, data: valid };
  } catch (err) {
    return { ok: false, errors: err.errors };
  }
}

// Usage
const result = await validateLogin({
  email: 'user@example.com',
  password: 'supersecret',
});

The abortEarly: false option tells Yup to collect all validation errors instead of stopping at the first one, which is useful for form feedback.

Validating JWT Payloads

After you verify a JWT's signature and expiration using a library like jsonwebtoken or jose, you should still validate the decoded payload's shape. A valid signature only proves the token wasn't tampered with — it doesn't guarantee the claims match what your application expects.

import * as yup from 'yup';
import jwt from 'jsonwebtoken';

const jwtPayloadSchema = yup.object({
  sub: yup.string().required('Subject (sub) claim is required'),
  email: yup.string().email().required(),
  role: yup
    .string()
    .oneOf(['admin', 'user', 'service'])
    .default('user'),
  iat: yup.number().integer().positive().required(),
  exp: yup.number().integer().positive().required(),
  iss: yup.string().required('Issuer (iss) claim is required'),
  aud: yup
    .string()
    .oneOf(['my-app-api'])
    .required('Audience (aud) must match this service'),
});

async function decodeAndValidate(token, publicKey) {
  // Step 1: Verify signature and expiration
  const decoded = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],
  });

  // Step 2: Validate the payload shape with Yup
  const payload = await jwtPayloadSchema.validate(decoded, {
    abortEarly: false,
    stripUnknown: true,
  });

  return payload;
}

The stripUnknown: true option removes any claims not defined in the schema. This is a security best practice — it prevents unexpected fields from leaking into your application logic.

Validating Session Objects

When using server-side sessions (with Redis, a database, or in-memory stores), you deserialize session data on every request. Validating that data with Yup protects against corruption and schema drift.

import * as yup from 'yup';

const sessionSchema = yup.object({
  sessionId: yup.string().uuid().required(),
  userId: yup.string().required(),
  email: yup.string().email().required(),
  role: yup.string().oneOf(['admin', 'user']).required(),
  createdAt: yup.number().integer().positive().required(),
  expiresAt: yup.number().integer().positive().required(),
  ip: yup.string().matches(
    /^(\d{1,3}\.){3}\d{1,3}$/,
    'Invalid IP address'
  ).required(),
  userAgent: yup.string().required(),
});

async function loadSession(redisClient, sessionId) {
  const raw = await redisClient.get(`session:${sessionId}`);
  if (!raw) return null;

  try {
    const parsed = JSON.parse(raw);
    const session = await sessionSchema.validate(parsed, {
      stripUnknown: true,
    });

    // Check expiration
    if (Date.now() > session.expiresAt) {
      await redisClient.del(`session:${sessionId}`);
      return null;
    }

    return session;
  } catch (err) {
    console.error('Invalid session data:', err.errors);
    return null;
  }
}

Validating OAuth Provider Responses

OAuth providers like Google, GitHub, and Microsoft return user profile data after you exchange an authorization code for an access token. These responses are external and can change, so validating them is essential.

import * as yup from 'yup';

// Google OAuth user profile schema
const googleProfileSchema = yup.object({
  sub: yup.string().required('Google user ID (sub) is required'),
  email: yup.string().email().required(),
  email_verified: yup.boolean().required(),
  name: yup.string().required(),
  given_name: yup.string().optional(),
  family_name: yup.string().optional(),
  picture: yup.string().url().optional(),
  locale: yup.string().optional(),
});

// GitHub OAuth user schema
const githubProfileSchema = yup.object({
  id: yup.number().integer().positive().required(),
  login: yup.string().required(),
  email: yup.string().email().nullable().default(null),
  name: yup.string().nullable().default(null),
  avatar_url: yup.string().url().optional(),
});

async function handleGoogleCallback(accessToken) {
  const res = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const raw = await res.json();

  const profile = await googleProfileSchema.validate(raw, {
    abortEarly: false,
    stripUnknown: true,
  });

  // Critical security check: email must be verified
  if (!profile.email_verified) {
    throw new Error('Email not verified by Google');
  }

  return profile;
}

Notice the explicit email_verified check after validation. This is a security-critical field — accepting an unverified email from an OAuth provider can allow account takeover.

Integrating Yup with Express Middleware

You can wrap Yup validation into reusable middleware for your auth routes:

import * as yup from 'yup';
import express from 'express';

const app = express();
app.use(express.json());

function validateBody(schema) {
  return async (req, res, next) => {
    try {
      req.body = await schema.validate(req.body, {
        abortEarly: false,
        stripUnknown: true,
      });
      next();
    } catch (err) {
      res.status(400).json({
        error: 'Validation failed',
        details: err.errors,
      });
    }
  };
}

const registerSchema = yup.object({
  email: yup.string().email().required(),
  password: yup
    .string()
    .min(8)
    .matches(/[A-Z]/, 'Must contain an uppercase letter')
    .matches(/[a-z]/, 'Must contain a lowercase letter')
    .matches(/[0-9]/, 'Must contain a number')
    .required(),
  name: yup.string().min(1).max(100).required(),
});

app.post('/auth/register', validateBody(registerSchema), async (req, res) => {
  // req.body is guaranteed valid here
  const { email, password, name } = req.body;
  // ... create user, hash password, issue token ...
  res.status(201).json({ message: 'User registered', email });
});

app.post('/auth/login', validateBody(loginSchema), async (req, res) => {
  const { email, password } = req.body;
  // ... verify credentials, issue JWT or create session ...
  res.json({ message: 'Logged in' });
});

app.listen(3000, () => console.log('Server running on port 3000'));

Custom Validation for Auth Logic

Yup supports custom test functions for rules that go beyond built-in validators. For example, you can validate that a password is not in a known breach list, or that a refresh token belongs to the requesting user:

const passwordSchema = yup
  .string()
  .min(8)
  .test('not-common', 'Password is too common', async (value) => {
    if (!value) return true;
    const commonPasswords = ['password123', 'letmein', 'qwerty123'];
    return !commonPasswords.includes(value.toLowerCase());
  })
  .test('not-breached', 'Password has been found in data breaches',
    async (value) => {
      if (!value) return true;
      // Integrate with HaveIBeenPwned API here
      // const count = await checkBreach(value);
      // return count === 0;
      return true;
    }
  )
  .required();

const refreshTokenSchema = yup.object({
  token: yup.string().required(),
  userId: yup.string().required(),
}).test('token-ownership', 'Token does not belong to user',
  async (value) => {
    if (!value) return false;
    // Query your refresh token store
    // const stored = await getRefreshToken(value.token);
    // return stored?.userId === value.userId;
    return true;
  }
);

Best Practices

Conclusion

Yup is a powerful ally in building secure authentication systems. By defining explicit schemas for login input, JWT payloads, session objects, and OAuth responses, you create a robust validation layer that catches malformed data before it reaches your application logic. Combined with proper cryptographic verification, session management, and OAuth security checks, Yup schemas give you confidence that the data flowing through your auth pipeline is exactly the shape your code expects. Start by validating your most critical auth endpoint today, and gradually extend schema coverage to every trust boundary in your system.

— Ad —

Google AdSense will appear here after approval

← Back to all articles