UNPKG

@tidecloak/verify

Version:

A lightweight utility for server-side verification of TideCloak-issued JSON Web Tokens (JWTs).

207 lines (156 loc) 8.2 kB
# TideCloak Verify SDK A lightweight utility for server‑side verification of TideCloak‑issued JSON Web Tokens (JWTs). This package exports a single function, `verifyTideCloakToken`, which you can use in your Next.js API routes, Node.js servers, or any backend to verify the signature, issuer, authorized party (`azp`), and roles of a JWT issued by your TideCloak realm. --- ## Installation ```bash npm install @tidecloak/verify # or yarn add @tidecloak/verify ``` --- ## Import ```ts import { verifyTideCloakToken } from '@tidecloak/verify'; ``` --- ## API ### `verifyTideCloakToken(config, token, allowedRoles?)` | Parameter | Type | Description | | -------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------- | | `config` | `object` | Your TideCloak adapter JSON (the Tidecloak client configuration you download from your realm settings). | | `token` | `string` | The raw JWT (access token) to verify. | | `allowedRoles` | `string[]` (optional) | Array of Tidecloak realm or client roles. If provided, the user must have at least one of these roles in their token. | **Returns:** `Promise<object | null>` * **Success:** Decoded token payload when all checks pass. * **Failure:** `null` if verification fails or the user lacks the required role(s). #### Under the hood Internally, `verifyTideCloakToken` uses the [jose](https://github.com/panva/jose) library to: 1. Ensure a token is present. 2. Construct the correct issuer URL from `config['auth-server-url']` and `config.realm`. 3. Choose between a local JWK Set (`config.jwk.keys`) or fetch the JWK Set remotely from Tidecloak. 4. Verify the token's signature against a **pinned algorithm allowlist** (`ES256`, `ES384`, `ES512`, `EdDSA` by default), the `issuer`, and the standard time claims (`exp`/`nbf`) with a small `clockTolerance`. 5. Verify the `azp` (authorized party) against `config.resource` — **only when `resource` is configured**. 6. Extract realm (`payload.realm_access.roles`) and client (`payload.resource_access[resource].roles`) roles. 7. Check for at least one matching role if `allowedRoles` is specified. On any failure, it logs the error message to the console and returns `null`. > **Note:** the algorithm allowlist closes algorithm-confusion attacks — without it, `jose` would accept any algorithm a key in the set can validate. A `null` result collapses both invalid tokens and infrastructure failures (e.g. an unreachable remote JWKS endpoint), so treat `null` as "not authorized" and monitor your JWKS reachability separately. ### Optional config fields You can tune verification by adding these optional fields to the `config` object: | Field | Type | Default | Description | | --------------------------- | ---------------------- | ---------------------------------------- | --------------------------------------------------------------------------- | | `tokenSignatureAlgorithms` | `string[]` | `['ES256','ES384','ES512','EdDSA']` | Allowed JWS signature algorithms. Override if your realm signs with others (e.g. `['RS256']`). | | `clockTolerance` | `number` \| `string` | `'5s'` | Allowed clock skew between issuer and verifier (seconds, or a jose duration string). | --- ## Examples ### 1. Plain JavaScript (Express) > **ESM / `import` syntax** (add `{ "type": "module" }` in your `package.json`): ```js // server.js import express from 'express'; import cookieParser from 'cookie-parser'; import { verifyTideCloakToken } from '@tidecloak/verify'; import config from './tidecloakAdapter.json'; const app = express(); app.use(cookieParser()); app.get('/secure', async (req, res) => { const token = req.cookies.kcToken || req.headers.authorization?.split(' ')[1] || ''; const payload = await verifyTideCloakToken(config, token, ['user']); if (!payload) { return res.status(401).json({ error: 'Unauthorized' }); } res.json({ message: `Hello, ${payload.preferred_username}` }); }); app.listen(3000, () => console.log('Server running on port 3000')); ``` > **CommonJS / `require` syntax** (default Node.js): ````js // server.js const express = require('express'); const cookieParser = require('cookie-parser'); const { verifyTideCloakToken } = require('@tidecloak/verify'); const config = require('./tidecloakAdapter.json'); const app = express(); app.use(cookieParser()); app.get('/secure', async (req, res) => { const token = req.cookies.kcToken || req.headers.authorization?.split(' ')[1] || ''; const payload = await verifyTideCloakToken(config, token, ['user']); if (!payload) { return res.status(401).json({ error: 'Unauthorized' }); } res.json({ message: `Hello, ${payload.preferred_username}` }); }); app.listen(3000, () => console.log('Server running on port 3000')); ```` ### 2. React with Server-Side Rendering ```jsx // pages/secure.js (Next.js Pages Router) import React from 'react'; import { verifyTideCloakToken } from '@tidecloak/verify'; import config from '../tidecloakAdapter.json'; export async function getServerSideProps({ req }) { const token = req.cookies.kcToken || req.headers.authorization?.split(' ')[1] || ''; const payload = await verifyTideCloakToken(config, token, ['user']); if (!payload) { return { redirect: { destination: '/login', permanent: false } }; } return { props: { user: payload.preferred_username } }; } export default function SecurePage({ user }) { return <div>Welcome, {user}</div>; } ``` ### 3. Next.js Pages Router (API Route) ```ts // pages/api/secure.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { verifyTideCloakToken } from '@tidecloak/verify'; import config from '../../tidecloakAdapter.json'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const token = req.cookies.kcToken || req.headers.authorization?.split(' ')[1] || ''; const payload = await verifyTideCloakToken(config, token, ['user', 'admin']); if (!payload) { return res.status(401).json({ error: 'Unauthorized' }); } res.status(200).json({ message: 'Hello, ' + payload.preferred_username }); } ``` ### 4. Next.js App Router (API Route) ```ts // app/api/secure/route.ts import { NextRequest, NextResponse } from 'next/server'; import { verifyTideCloakToken } from '@tidecloak/verify'; import config from '../../../tidecloakAdapter.json'; export async function GET(req: NextRequest) { const token = req.cookies.get('kcToken')?.value || ''; const payload = await verifyTideCloakToken(config, token, ['user']); if (!payload) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } return NextResponse.json({ message: `Welcome, ${payload.preferred_username}` }); } ``` --- ## TypeScript Definitions ```ts interface TidecloakConfig { realm: string; 'auth-server-url': string; resource?: string; publicClient?: boolean; confidentialPort?: number; jwk?: { keys: Array<{ kid: string; kty: string; alg?: string; use?: string; x?: string; crv?: string; n?: string; e?: string }> }; /** Allowed JWS signature algorithms. Default: ['ES256','ES384','ES512','EdDSA']. */ tokenSignatureAlgorithms?: string[]; /** Allowed clock skew (seconds or a jose duration string). Default: '5s'. */ clockTolerance?: number | string; [key: string]: unknown; } export declare function verifyTideCloakToken( config: TidecloakConfig, token: string, allowedRoles?: string[] ): Promise<Record<string, any> | null>; ``` ---