npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dewabuanam-oauth

v0.1.5

Published

OAuth 2.0 client library for Dewabuanam authentication with PKCE support.

Readme

dewabuanam-oauth

OAuth 2.0 client library for Dewabuanam authentication with PKCE support.

Installation

npm install dewabuanam-oauth

Features

  • OAuth 2.0 Authorization Code flow with PKCE
  • Client-side helpers to generate PKCE + redirect to authorize
  • Server-side helpers to exchange code for tokens, verify JWTs, and fetch userinfo
  • TypeScript support

Environment Variables

Required (client + callback):

NEXT_PUBLIC_OAUTH_CLIENT_ID=71c43497-63db-4c56-b6be-f8645a73cc1e
NEXT_PUBLIC_OAUTH_REDIRECT_URI=https://life.dewabuanam.com/api/oauth/callback

Optional (server overrides):

# Local development (override endpoints)
OAUTH_TOKEN_ENDPOINT=http://localhost:3000/api/oauth/token
OAUTH_ISSUER=http://localhost:3000
OAUTH_JWKS_URL=http://localhost:3000/.well-known/jwks.json
OAUTH_USERINFO_ENDPOINT=http://localhost:3000/api/oauth/userinfo

# Client-side (optional helper for your app code)
NEXT_PUBLIC_OAUTH_AUTHORIZE_ENDPOINT=http://localhost:3000/api/oauth/authorize

Provider Endpoints (Local)

  • Authorize (GET): http://localhost:3000/api/oauth/authorize
  • Token (POST): http://localhost:3000/api/oauth/token
  • Userinfo (GET): http://localhost:3000/api/oauth/userinfo
  • JWKS (GET): http://localhost:3000/.well-known/jwks.json

Usage

Client-Side (PKCE + Redirect)

For more control, use the client utilities directly:

import { createPkce, redirectToAuthorize } from "dewabuanam-oauth/client";

async function handleLogin() {
  // Generate PKCE challenge
  const pkce = await createPkce();
  
  // Store verifier somewhere the callback route can read it (cookie/session/etc).
  // Note: SameSite=Lax is required so the cookie is sent on the OAuth provider -> callback redirect.
  const isSecureContext = typeof window !== "undefined" && window.location.protocol === "https:";
  const secureAttr = isSecureContext ? "; Secure" : "";
  document.cookie = `pkce_verifier=${encodeURIComponent(pkce.verifier)}; Path=/; Max-Age=600; SameSite=Lax${secureAttr}`;
  
  // Redirect to authorization endpoint
  redirectToAuthorize({
    clientId: process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID!,
    redirectUri: process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI!,
    codeChallenge: pkce.challenge,
    authorizeEndpoint: process.env.NEXT_PUBLIC_OAUTH_AUTHORIZE_ENDPOINT, // optional override
  });
}

Tip: store the PKCE verifier somewhere the callback route can read it (cookie, session, etc.).

Server-Side (Token Exchange)

Handle the callback and exchange the authorization code for tokens:

import { exchangeCodeForToken } from "dewabuanam-oauth/server";

export async function handleCallback(code: string, codeVerifier: string) {
  const tokens = await exchangeCodeForToken({
    code,
    codeVerifier,
    clientId: process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID!,
    redirectUri: process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI!,
    // Optional override; otherwise uses OAUTH_TOKEN_ENDPOINT or the default cloud endpoint.
    tokenEndpoint: process.env.OAUTH_TOKEN_ENDPOINT,
  });
  
  // tokens contains: { access_token, id_token?, token_type?, expires_in?, refresh_token? }
  return tokens;
}

Server-Side (Refresh Token)

When access tokens expire, you can exchange the refresh token for a new access token.

Important: refresh tokens are rotated — each refresh returns a new refresh_token, and the old one becomes invalid.

import { refreshAccessToken } from "dewabuanam-oauth/server";

export async function handleRefresh(refreshToken: string) {
  const tokens = await refreshAccessToken({
    refreshToken,
    clientId: process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID!,
    tokenEndpoint: process.env.OAUTH_TOKEN_ENDPOINT,
  });

  // If tokens.refresh_token is present, replace your stored refresh token with it.
  return tokens;
}

Token Verification

Verify and decode JWT tokens:

import { verifyToken } from "dewabuanam-oauth/server";

export async function getUserFromToken(token: string) {
  try {
    const payload = await verifyToken(token);
    console.log("User ID:", payload.sub);
    console.log("Email:", payload.email);
    return payload;
  } catch (error) {
    console.error("Invalid token:", error);
    throw error;
  }
}

API Reference

Client API

createPkce()

Generates a PKCE code verifier and challenge.

Returns:

{
  verifier: string;
  challenge: string;
  method: "S256";
}

redirectToAuthorize(options)

Redirects the user to the Dewabuanam authorization page.

Parameters:

  • clientId: string - Your OAuth client ID
  • redirectUri: string - Callback URL after authorization
  • codeChallenge: string - PKCE challenge from createPkce()
  • authorizeEndpoint?: string - Override authorize endpoint (optional)
  • state?: string - Provide your own state (optional)

Server API

exchangeCodeForToken(options)

Exchanges an authorization code for access and ID tokens.

Parameters:

  • code: string - Authorization code from callback
  • codeVerifier: string - PKCE verifier stored during authorization
  • clientId: string - Your OAuth client ID
  • redirectUri: string - Must match the redirect URI used in authorization
  • tokenEndpoint?: string - Custom token endpoint (optional)

Returns:

{
  access_token: string;
  id_token?: string;
  token_type?: string;
  expires_in?: number;
  refresh_token?: string;
}

refreshAccessToken(options)

Exchanges a refresh token for a new access token (refresh token rotation supported).

Parameters:

  • refreshToken: string - Refresh token you previously stored
  • clientId: string - Your OAuth client ID
  • tokenEndpoint?: string - Custom token endpoint (optional)

verifyToken(token)

Verifies and decodes a JWT token.

Parameters:

  • token: string - The JWT token to verify

Returns: JWT payload containing user information

fetchUserInfo(accessToken)

Fetches profile information from the Dewabuanam userinfo endpoint.

Options:

  • endpoint?: string - Custom userinfo endpoint (optional)

OAuth Flow (Authorization Code + PKCE)

  1. Generate a PKCE code_verifier and code_challenge (S256) in your web app.
  2. Redirect the user to the authorize endpoint.
  3. Handle the callback on your redirect URL and read the returned code.
  4. Exchange the code for tokens via the token endpoint.
  5. Use the access token as a Bearer token when calling userinfo.
  6. When the access token expires, exchange the refresh token for a new access token (and rotated refresh token).

Authorize URL example:

http://localhost:3000/api/oauth/authorize?response_type=code&client_id=71c43497-63db-4c56-b6be-f8645a73cc1e&redirect_uri=https%3A%2F%2Flife.dewabuanam.com%2Fapi%2Foauth%2Fcallback&code_challenge=...&code_challenge_method=S256&state=...

Token request example (x-www-form-urlencoded):

POST http://localhost:3000/api/oauth/token

Body:

grant_type=authorization_code&client_id=71c43497-63db-4c56-b6be-f8645a73cc1e&redirect_uri=https%3A%2F%2Flife.dewabuanam.com%2Fapi%2Foauth%2Fcallback&code=...&code_verifier=...

Userinfo example:

GET http://localhost:3000/api/oauth/userinfo with Authorization: Bearer <access_token>

Refresh token request example (x-www-form-urlencoded):

POST http://localhost:3000/api/oauth/token

Body:

grant_type=refresh_token&client_id=71c43497-63db-4c56-b6be-f8645a73cc1e&refresh_token=...

Signature validation (JWKS):

Access tokens are JWTs signed with RS256. Validate the signature using the public keys from the JWKS endpoint.

GET http://localhost:3000/.well-known/jwks.json

Required Validation (Provider / Server)

Authorize (GET):

  • client_id must exist
  • redirect_uri must exactly match the registered redirect URL
  • PKCE code_challenge + code_challenge_method=S256

Token (POST, authorization code):

  • code must be valid, not expired, not used
  • client_id must match the authorization code
  • PKCE code_verifier must match stored code_challenge

Token (POST, refresh):

  • grant_type=refresh_token
  • client_id must exist
  • refresh_token must be valid, not expired, not revoked
  • Refresh tokens are rotated: each refresh returns a new refresh_token, and the old one becomes invalid

Full Example (Next.js App Router)

1. Login Page (app/login/page.tsx)

"use client";

import { createPkce, redirectToAuthorize } from "dewabuanam-oauth/client";

export default function LoginPage() {
  return (
    <button
      onClick={async () => {
        const clientId = process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID;
        const redirectUri = process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI;

        if (!clientId || !redirectUri) {
          console.error("Missing NEXT_PUBLIC_OAUTH_CLIENT_ID or NEXT_PUBLIC_OAUTH_REDIRECT_URI");
          return;
        }

        const pkce = await createPkce();

        // SameSite=Lax is required so the cookie is sent on the OAuth provider -> callback redirect.
        const isSecureContext = typeof window !== "undefined" && window.location.protocol === "https:";
        const secureAttr = isSecureContext ? "; Secure" : "";
        document.cookie = `pkce_verifier=${encodeURIComponent(pkce.verifier)}; Path=/; Max-Age=600; SameSite=Lax${secureAttr}`;

        redirectToAuthorize({
          clientId,
          redirectUri,
          codeChallenge: pkce.challenge,
          authorizeEndpoint: process.env.NEXT_PUBLIC_OAUTH_AUTHORIZE_ENDPOINT, // optional override
        });
      }}
    >
      Sign in
    </button>
  );
}

2. Callback Handler (app/auth/callback/route.ts)

import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { exchangeCodeForToken, fetchUserInfo, verifyToken } from "dewabuanam-oauth/server";

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const code = searchParams.get("code");
  const cookieStore = await cookies();
  const rawCodeVerifier = cookieStore.get("pkce_verifier")?.value;
  const codeVerifier = (() => {
    if (!rawCodeVerifier) return undefined;
    try {
      return decodeURIComponent(rawCodeVerifier);
    } catch {
      return rawCodeVerifier;
    }
  })();

  if (!code || !codeVerifier) {
    return NextResponse.redirect("/login?error=missing_params");
  }

  try {
    const tokens = await exchangeCodeForToken({
      code,
      codeVerifier,
      clientId: process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID!,
      redirectUri: process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI!,
      tokenEndpoint: process.env.OAUTH_TOKEN_ENDPOINT, // optional override
    });

    await verifyToken(tokens.id_token ?? tokens.access_token, {
      issuer: process.env.OAUTH_ISSUER,
      jwksUrl: process.env.OAUTH_JWKS_URL,
    });
    const userInfo = await fetchUserInfo(tokens.access_token, {
      endpoint: process.env.OAUTH_USERINFO_ENDPOINT,
    });
    console.info("userinfo", userInfo);

    // Store tokens securely (e.g., in HTTP-only cookies or session)
    cookieStore.delete("pkce_verifier");
    cookieStore.set("access_token", tokens.access_token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "strict",
      path: "/",
    });

    if (typeof tokens.refresh_token === "string" && tokens.refresh_token.length > 0) {
      cookieStore.set("refresh_token", tokens.refresh_token, {
        httpOnly: true,
        secure: process.env.NODE_ENV === "production",
        sameSite: "strict",
        path: "/",
      });
    }

    return NextResponse.redirect(new URL("/", request.url));
  } catch (error) {
    console.error("OAuth error:", error);
    return NextResponse.redirect("/login?error=auth_failed");
  }
}

3. Refresh Route (app/api/oauth/refresh/route.ts)

This endpoint uses your refresh token cookie to refresh the session. If the provider returns a new refresh_token, it overwrites the old cookie (rotation).

import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { refreshAccessToken } from "dewabuanam-oauth/server";

function computeSessionExpiresAtMs(tokens: { expires_in?: number }) {
  if (typeof tokens.expires_in === "number" && Number.isFinite(tokens.expires_in) && tokens.expires_in > 0) {
    return Date.now() + tokens.expires_in * 1000;
  }
  return undefined;
}

export async function POST(request: NextRequest) {
  const cookieStore = await cookies();
  const refreshToken = cookieStore.get("refresh_token")?.value;

  if (!refreshToken) {
    return NextResponse.json({ ok: false, error: "missing_refresh_token" }, { status: 401, headers: { "Cache-Control": "no-store" } });
  }

  try {
    const tokens = await refreshAccessToken({
      refreshToken,
      clientId: process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID!,
      tokenEndpoint: process.env.OAUTH_TOKEN_ENDPOINT,
    });

    const secure = request.nextUrl.protocol === "https:" || process.env.NODE_ENV === "production";
    const sessionExpiresAtMs = computeSessionExpiresAtMs(tokens);

    const response = NextResponse.json(
      {
        ok: true,
        sessionExpiresAt: sessionExpiresAtMs ?? null,
        sessionExpiresAtIso: sessionExpiresAtMs ? new Date(sessionExpiresAtMs).toISOString() : null,
      },
      { headers: { "Cache-Control": "no-store" } },
    );

    const accessTokenCookie: Parameters<typeof response.cookies.set>[2] = {
      httpOnly: true,
      secure,
      sameSite: "lax",
      path: "/",
    };
    if (sessionExpiresAtMs) {
      accessTokenCookie.expires = new Date(sessionExpiresAtMs);
    }

    response.cookies.set("access_token", tokens.access_token, accessTokenCookie);

    if (typeof tokens.refresh_token === "string" && tokens.refresh_token.length > 0) {
      response.cookies.set("refresh_token", tokens.refresh_token, {
        httpOnly: true,
        secure,
        sameSite: "lax",
        path: "/",
      });
    }

    if (sessionExpiresAtMs) {
      response.cookies.set("session_expires_at", String(sessionExpiresAtMs), {
        httpOnly: false,
        secure,
        sameSite: "lax",
        path: "/",
        expires: new Date(sessionExpiresAtMs),
      });
    }

    return response;
  } catch (error) {
    console.error("OAuth refresh error:", error);
    return NextResponse.json({ ok: false, error: "refresh_failed" }, { status: 500, headers: { "Cache-Control": "no-store" } });
  }
}

4. Client Session Refresher (example)

Add a small client component that watches session_expires_at and triggers /api/oauth/refresh shortly before expiry.

"use client";

import { useEffect, useRef } from "react";

const REFRESH_EARLY_MS = 5 * 60 * 1000;
const FALLBACK_RETRY_MS = 30 * 1000;

function getCookieValue(name: string): string | null {
  if (typeof document === "undefined") return null;
  const cookies = document.cookie ? document.cookie.split(";") : [];
  for (const cookie of cookies) {
    const [rawKey, ...rest] = cookie.trim().split("=");
    if (rawKey === name) return decodeURIComponent(rest.join("="));
  }
  return null;
}

export function OAuthSessionRefresher() {
  const timerRef = useRef<number | null>(null);

  useEffect(() => {
    const clearTimer = () => {
      if (timerRef.current !== null) {
        window.clearTimeout(timerRef.current);
        timerRef.current = null;
      }
    };

    const schedule = () => {
      clearTimer();

      const raw = getCookieValue("session_expires_at");
      if (!raw) return;

      const expiresAtMs = Number(raw);
      if (!Number.isFinite(expiresAtMs) || expiresAtMs <= 0) return;

      const now = Date.now();
      const computedDelayMs = expiresAtMs - now - REFRESH_EARLY_MS;
      const delayMs = computedDelayMs > 0 ? computedDelayMs : 10_000;

      const run = async () => {
        try {
          const res = await fetch("/api/oauth/refresh", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: "{}",
          });

          if (!res.ok) {
            timerRef.current = window.setTimeout(schedule, FALLBACK_RETRY_MS);
            return;
          }

          // Cookies get updated by the response; just reschedule.
          schedule();
        } catch {
          timerRef.current = window.setTimeout(schedule, FALLBACK_RETRY_MS);
        }
      };

      if (delayMs <= 0) {
        void run();
        return;
      }

      // setTimeout max is ~24.8 days; clamp to be safe.
      const maxDelay = 2_000_000_000;
      timerRef.current = window.setTimeout(run, Math.min(delayMs, maxDelay));
    };

    const onVisibility = () => {
      if (document.visibilityState === "visible") schedule();
    };

    schedule();
    document.addEventListener("visibilitychange", onVisibility);

    return () => {
      document.removeEventListener("visibilitychange", onVisibility);
      clearTimer();
    };
  }, []);

  return null;
}

Security Considerations

  • Always use HTTPS in production
  • Store the PKCE verifier securely (HTTP-only cookies recommended)
  • Validate the state parameter to prevent CSRF attacks
  • Keep your client ID confidential
  • Use HTTP-only cookies for storing tokens
  • Implement proper token refresh logic

License

See LICENSE file for details.

Support

For issues and questions, please visit the GitHub repository.