npm package: @nitrotool/oauth-server
Description: ## Installation
Latest version: 1.0.5

# @nitrotool/oauth-server

## Installation

```bash
pnpm add @nitrotool/oauth-server
```

---

## Usage

### Authorize Endpoint

```ts
// server/api/oauth/authorize.ts
import {
  defineOAuthAuthorizeHandler,
  isValidRedirectUrl,
  oauthError,
} from '@nitrotool/oauth-server';
import { sendRedirect } from 'h3';

export default defineOAuthAuthorizeHandler(async (event, payload) => {
  //This will always pass, since we return the id we give
  const client = await getOAuthClientById(payload.client_id);

  if (!client) {
    return oauthError(event, 'unauthorized_client', 'Invalid client ID.');
  }

  if (!isValidRedirectUrl(payload.redirect_uri, client.redirectUris)) {
    return oauthError(event, 'invalid_request', 'Invalid redirect URI.');
  }

  //TODO: Store & fetch this somewhere safe.
  const userId = '1234567890';
  const code = 'SOME-RANDOM-CODE';

  await saveAuthCode(code, {
    client_id: payload.client_id,
    redirect_uri: payload.redirect_uri,
    user_id: userId,
    scope: payload.scope,
    code_challenge: payload.code_challenge,
  });

  const url = new URL(payload.redirect_uri);
  url.searchParams.set('code', code);
  if (payload.state) {
    url.searchParams.set('state', payload.state);
  }

  return sendRedirect(event, url.toString());
});

```


### OAuth Token Endpoint
```ts
// server/api/oauth/token.post.ts
import { defineOAuthTokenHandler, oauthError } from '@nitrotool/oauth-server';

export default defineOAuthTokenHandler(async (event, payload) => {
  if (payload.grant_type === 'authorization_code') {
    const data = await consumeAuthCode(payload.code);
    if (!data)
      return oauthError(event, 'invalid_grant', 'Invalid authorization code.');

    // Use shared validation
    const error = validateTokenRequest(event, payload, data);
    if (error) return error;

    return issueTokens(data, payload.client_id);
  }

  if (payload.grant_type === 'refresh_token') {
    const data = await getRefreshToken(payload.refresh_token);
    if (!data)
      return oauthError(event, 'invalid_grant', 'Invalid refresh token.');

    // Just issue a new access token for refresh flow
    const tokens = await issueTokens(data, payload.client_id);
    return {
      access_token: tokens.access_token,
      token_type: tokens.token_type,
      expires_in: tokens.expires_in,
    };
  }

  return oauthError(event, 'unsupported_grant_type');
});
```

### OIDC Authorize endpoint
```ts
// server/api/oidc/authorize.ts
import {
  defineOAuthServerHandler,
  oidcAuthorizeSchema, // Use the OIDC schema with nonce/scope requirements
} from '@nitrotool/oauth-server';
import { sendRedirect, getQuery } from 'h3';

export default defineOAuthServerHandler({
  schema: oidcAuthorizeSchema,
  read: getQuery,
  async handler(event, payload) {
    // 1. Shared Validation
    const { client, error } = await validateAuthorizeRequest(event, payload);
    if (error) return error;

    // 2. Identity Logic (Typically behind a session check/login)
    const userId = '1234567890';
    const code = 'OIDC-' + Math.random().toString(36).substring(2);

    // 3. Store OIDC Context
    // Crucially, we store the 'nonce' and 'scope' provided in the request
    await saveAuthCode(code, {
      client_id: payload.client_id,
      redirect_uri: payload.redirect_uri,
      user_id: userId,
      scope: payload.scope, // Will include 'openid'
      nonce: payload.nonce, // <--- MUST be stored for OIDC
      code_challenge: payload.code_challenge,
    });

    // 4. Redirect back to client
    const redirectUrl = buildAuthorizeRedirect(
      payload.redirect_uri,
      code,
      payload.state,
    );
    return sendRedirect(event, redirectUrl);
  },
});

```


- `payload` is strongly typed as `OIDCAuthQuery`.
- Always echo back the `state` parameter.
- Store the `nonce` from `payload.nonce` with the authorization code for inclusion in the `id_token`.


### OIDC Token Endpoint

```ts
// server/api/oidc/token.ts
import {defineOIDCTokenHandler, OIDCTokenResponse} from '@nitrotool/oauth-server'

export default defineOIDCTokenHandler({
  async consumeCode(code) {
    return await consumeAuthCode(code);
  },
  async handler(event, payload, ctx) {
    // 1. Validation (Payload must be auth_code grant for OIDC)
    const error = validateTokenRequest(event, payload as any, ctx);
    if (error) return error;

    // 2. Issue Standard Tokens
    const tokens = await issueTokens(ctx, payload.client_id);

    // 3. Generate OIDC specific ID Token
    const idToken = await encodeJwt(
            {
              sub: ctx.user_id,
              aud: payload.client_id,
              nonce: ctx.nonce,
              type: 'id_token',
            },
            3600,
    );

    return {
      ...tokens,
      id_token: idToken,
    };
  },
});
```

- `payload` is strongly typed as `OIDCTokenPayload`.
- The return type must conform to `OIDCTokenResponse`.
- `access_token` and `expires_in` are required; `refresh_token`, `scope`, and `id_token` are optional
  depending on the grant and requested scopes.

---

### Notes

- **No client code**: This library is purely for implementing an OAuth/OIDC provider (server).
- You must **persist authorization codes**, `nonce`s, and optionally refresh tokens in your database.
- `isValidRedirectUrl` helps safely validate client redirect URIs.