@tidecloak/verify
Version:
A lightweight utility for server-side verification of TideCloak-issued JSON Web Tokens (JWTs).
207 lines (156 loc) • 8.2 kB
Markdown
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';
```
---
| 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'));
````
```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>;
}
```
```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 });
}
```
```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}` });
}
```
---
```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;
[]: unknown;
}
export declare function verifyTideCloakToken(
config: TidecloakConfig,
token: string,
allowedRoles?: string[]
): Promise<Record<string, any> | null>;
```
---