npm package: @gdnaio/cognito-auth
Description: Reusable AWS Cognito auth library for React and Next.js
Author: g/d/n/a
Latest version: 1.2.1

# @gdnaio/cognito-auth

Reusable AWS Cognito auth library for React and Next.js using Hosted UI + PKCE.

## Core project overview

- Security-first: Authorization Code + PKCE via Cognito Hosted UI
- Token lifecycle: access/id tokens + refresh support, auto-refresh scheduling
- Storage modes: memory, localStorage, and cookie-first (when using Next.js routes)
- Next.js support: Edge middleware, route handlers for login/callback/logout, server session util
- Lightweight: framework-agnostic core, no Amplify; uses `jose` and `cross-fetch`

### Public API

- Provider: `CognitoAuthProvider({ config })`
  - `region`, `userPoolId`, `clientId`, `domain`, `redirectUri`, `signOutRedirectUri`, `id?`
  - `scopes?` (default: `['openid','email','profile']`), `storage?` (`'cookie'|'localStorage'|'memory'`)
  - `cookieNames?`, `clockSkewSec?`, `onAuthStateChange?`
- Hook: `useAuth()` → `{ isAuthenticated, user, id, signIn, signOut, handleRedirectCallback, getAccessToken, getIdToken }`
  - `handleRedirectCallback(authCode?, callbackUrl?)`: If `authCode` is provided, exchanges the code directly without PKCE and skips reading params from the URL. When provided, `callbackUrl` overrides the `redirect_uri` used for token exchange.
- Component: `RequireAuth` – guards children with `fallback`
- Next.js helpers (subpath `@gdnaio/cognito-auth/next`):
  - `createCognitoMiddleware(cfg)`
  - `getServerSession(cfg)`
  - `createLoginRoute(cfg)`, `createAuthCallbackRoute(cfg)`, `createLogoutRoute(cfg)`

Notes:

- `config.id` is captured once on provider mount and exposed as `id` from `useAuth`. Subsequent changes to `config.id` are ignored (a warning is logged).

## Install

```bash
npm i @gdnaio/cognito-auth jose cross-fetch
```

Peer deps:

- react >=18
- next >=13 (optional)

### Configuration notes

- Hosted UI (redirect-based) requires `domain` and `redirectUri`.
- SRP (username/password) does not require `domain` or `redirectUri`; only `region`, `userPoolId`, `clientId` are needed.
- If you use both flows, keep all fields.
- In Next.js, cookie TTLs set in the callback route are configurable via `cookieMaxAgeSeconds` (access/id) and `refreshCookieMaxAgeSeconds` (refresh).
- Client auto-refresh (based on token expiry) is available via `autoRefreshToken: true` (default false). Supported for `storage='localStorage'` (and `memory` if a refresh token is present). For HTTP-only cookie setups, prefer server refresh.

Auto-logout (SPA)

- For `storage='localStorage'` (and `memory`), the provider schedules at access-token expiry. Behavior:
  - When `autoRefreshToken: true`, it tries a refresh at that moment; if refresh succeeds, the session continues; if it fails, it signs out (clears tokens and redirects to `signOutRedirectUri`).
  - When `autoRefreshToken: false`, it signs out immediately at access-token expiry (clears tokens and redirects).
  - Cookie/HTTP-only flows should rely on server/middleware expiry handling instead.

Subscribe to token updates (including auto-refresh) using `useAuth()`:

```tsx
const { onTokensUpdated } = useAuth()
useEffect(() => {
  const unsubscribe = onTokensUpdated((tokens, { reason }) => {
    if (reason === 'refresh') {
      console.log('tokens refreshed', tokens)
    }
  })
  return unsubscribe
}, [onTokensUpdated])
```

## Usage in React (SPA)

1. Wrap your app with the provider

```tsx
import { CognitoAuthProvider } from '@gdnaio/cognito-auth'

export function Root() {
  return (
    <CognitoAuthProvider
      config={{
        region: 'us-east-1',
        userPoolId: 'us-east-1_XXXX',
        clientId: 'xxxxxxxx',
        domain: 'https://your-domain.auth.us-east-1.amazoncognito.com',
        redirectUri: 'http://localhost:5173/auth/callback',
        signOutRedirectUri: 'http://localhost:5173/',
        storage: 'localStorage',
      }}
    >
      <App />
    </CognitoAuthProvider>
  )
}
```

2. Handle the OAuth callback once after redirect

```tsx
import { useEffect } from 'react'
import { useAuth } from '@gdnaio/cognito-auth'

export default function CallbackPage() {
  const { handleRedirectCallback } = useAuth()
  useEffect(() => {
    handleRedirectCallback().finally(() => {
      window.history.replaceState({}, '', '/')
    })
  }, [handleRedirectCallback])
  return null
}
```

Alternate: supply an explicit authorization code and optional callback URL override

```tsx
import { useEffect } from 'react'
import { useAuth } from '@gdnaio/cognito-auth'

export default function CallbackPage() {
  const { handleRedirectCallback } = useAuth()
  useEffect(() => {
    const url = new URL(window.location.href)
    const authCode = url.searchParams.get('code') || undefined
    const callbackUrl = 'https://your-app.example.com/auth/callback' // optional override

    if (authCode) {
      handleRedirectCallback(authCode, callbackUrl).finally(() => {
        window.history.replaceState({}, '', '/')
      })
    } else {
      handleRedirectCallback().finally(() => {
        window.history.replaceState({}, '', '/')
      })
    }
  }, [handleRedirectCallback])
  return null
}
```

Notes:

- When `authCode` is provided, the library exchanges the code without a PKCE verifier and skips URL param parsing.
- `callbackUrl`, when provided, overrides the `redirect_uri` used in the token exchange.
- Exchanging a code without PKCE works only if your Cognito App Client allows it (PKCE not enforced for your flow).

3. Use auth state and actions

```tsx
import { useAuth, RequireAuth } from '@gdnaio/cognito-auth'

export function Page() {
  const { isAuthenticated, user, signIn, signOut } = useAuth()
  return (
    <main>
      {!isAuthenticated ? (
        <button onClick={() => signIn()}>Sign in</button>
      ) : (
        <>
          <pre>{JSON.stringify(user, null, 2)}</pre>
          <button onClick={() => signOut()}>Sign out</button>
        </>
      )}
      <RequireAuth fallback={<p>Please sign in</p>}>
        <p>Protected content</p>
      </RequireAuth>
    </main>
  )
}
```

Tokens auto-refresh automatically just before expiry when using `storage='localStorage'`.

Notes:

- Client auto-refresh runs automatically based on token expiry. For HTTP-only cookie setups, prefer server refresh (Next.js callback/cookie flow).

Optional: attach an Axios interceptor

```ts
import axios from 'axios'
import { createAxiosInterceptor, useAuth } from '@gdnaio/cognito-auth'

const api = axios.create()
const { getAccessToken } = useAuth()
createAxiosInterceptor(api, getAccessToken)
```

### Optional: Embedded username/password (SRP) login

If you prefer an in-app form instead of the Hosted UI, enable SRP by calling `signInWithPassword` provided by the context. Your app must have the Cognito App Client configured to allow SRP (no client secret) and the password-based flow enabled.

```tsx
import { useState } from 'react'
import { useAuth } from '@gdnaio/cognito-auth'

export function PasswordLogin() {
  const { signInWithPassword, isAuthenticated, user, signOut } = useAuth()
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')
  return (
    <div>
      {!isAuthenticated ? (
        <form
          onSubmit={e => {
            e.preventDefault()
            signInWithPassword?.(username, password)
          }}
        >
          <input value={username} onChange={e => setUsername(e.target.value)} placeholder='email or username' />
          <input value={password} onChange={e => setPassword(e.target.value)} type='password' placeholder='password' />
          <button type='submit'>Sign in</button>
        </form>
      ) : (
        <>
          <pre>{JSON.stringify(user, null, 2)}</pre>
          <button onClick={() => signOut()}>Sign out</button>
        </>
      )}
    </div>
  )
}
```

Notes:

- SRP happens fully in the browser via `amazon-cognito-identity-js`.
- If MFA, new password, or custom challenges are required, the method throws codes like `MFA_REQUIRED` or `NEW_PASSWORD_REQUIRED`. Extend the flow to handle those as needed.

## Usage in Next.js (App Router)

1. Add route handlers

```ts
// app/api/auth/login/route.ts
import { createLoginRoute } from '@gdnaio/cognito-auth/next'
export const GET = createLoginRoute({
  domain: process.env.NEXT_PUBLIC_COGNITO_DOMAIN!,
  clientId: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!,
  redirectUri: process.env.NEXT_PUBLIC_COGNITO_REDIRECT_URI!,
  scopes: ['openid', 'email', 'profile'],
})
```

```ts
// app/api/auth/callback/route.ts
import { createAuthCallbackRoute } from '@gdnaio/cognito-auth/next'
export const GET = createAuthCallbackRoute({
  region: process.env.NEXT_PUBLIC_AWS_REGION!,
  userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID!,
  clientId: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!,
  domain: process.env.NEXT_PUBLIC_COGNITO_DOMAIN!,
  redirectUri: process.env.NEXT_PUBLIC_COGNITO_REDIRECT_URI!,
  // Optional cookie TTL configuration (defaults: access/id 1h, refresh 30d)
  cookieMaxAgeSeconds: 60 * 30, // 30 minutes for access/id tokens
  refreshCookieMaxAgeSeconds: 7 * 24 * 3600, // 7 days for refresh token
})
```

```ts
// app/api/auth/logout/route.ts
import { createLogoutRoute } from '@gdnaio/cognito-auth/next'
export const GET = createLogoutRoute({
  domain: process.env.NEXT_PUBLIC_COGNITO_DOMAIN!,
  clientId: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!,
  signOutRedirectUri: process.env.NEXT_PUBLIC_SIGNOUT_REDIRECT_URI!,
})
```

2. Protect routes with middleware

```ts
// middleware.ts
import { createCognitoMiddleware } from '@gdnaio/cognito-auth/next'
export default createCognitoMiddleware({
  region: process.env.NEXT_PUBLIC_AWS_REGION!,
  userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID!,
  audience: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!,
  publicRoutes: ['/api/auth', '/public'],
})
```

3. Wrap application with provider

```tsx
// app/layout.tsx
import { CognitoAuthProvider } from '@gdnaio/cognito-auth'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <CognitoAuthProvider
          config={{
            region: process.env.NEXT_PUBLIC_AWS_REGION!,
            userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID!,
            clientId: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!,
            domain: process.env.NEXT_PUBLIC_COGNITO_DOMAIN!,
            redirectUri: process.env.NEXT_PUBLIC_COGNITO_REDIRECT_URI!,
            signOutRedirectUri: process.env.NEXT_PUBLIC_SIGNOUT_REDIRECT_URI!,
            scopes: ['openid', 'email', 'profile'],
            storage: 'cookie',
          }}
        >
          {children}
        </CognitoAuthProvider>
      </body>
    </html>
  )
}
```

SSR/API usage (optional):

```ts
import { getServerSession } from '@gdnaio/cognito-auth/next'

export async function GET() {
  const session = await getServerSession({
    region: process.env.NEXT_PUBLIC_AWS_REGION!,
    userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID!,
    audience: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!,
  })
  // use session?.claims
}
```

## Examples

- Next.js (App Router): `examples/next-app`
- React SPA (Vite): `examples/react-spa`

Each example has its own README with environment variables and run commands.

## Notes

- Hosted UI + PKCE used by default; SRP is available when you need embedded forms
- For cookie flow in Next.js, use HTTPS in production and set correct redirect/logout URIs in Cognito