ts-oauth2-server

repository·main·Indexed 43 days ago

https://github.com/jasonraimondi/ts-oauth2-server

A standards-compliant OAuth 2.0 and OpenID Connect (OIDC) authorization server implementation written in TypeScript for Node.js 22+. It supports RFC6749, RFC6750, RFC7009, RFC7519, RFC7636, RFC7662, RFC8693, and RFC9068, providing support for various grant types including authorization code, client credentials, and refresh grants. The library is framework-agnostic with adapters for Express, Fastify, H3, and VanillaJS.

Tokens
58.5K
Snippets
144
Records
221
Agent score
54%

What's inside ts-oauth2-server

  1. Overview of @jmondi/oauth2-server

    main

    @jmondi/oauth2-server is a standards-compliant implementation of an OAuth 2.0 authorization server written in TypeScript. It is designed to be used within various web frameworks and supports multiple OAuth 2.0 and OpenID Connect specifications.

    Requirements

    • node >= 22

    Supported Specifications

    • RFC6749 (OAuth 2.0)
    • RFC6750 (Bearer Token Usage)
    • RFC7009 (Token Revocation)
    • RFC7519 (JSON Web Token)
    • RFC7636 (PKCE)
    • RFC7662 (Token Introspection)
    • RFC8693 (Token Exchange)
    • RFC9068 (JWT Profile for OAuth 2.0 Access Tokens)
    • OpenID Connect Core 1.0
    • OpenID Connect Discovery 1.0

    Supported Grant Types

    • Authorization code grant
    • Client credentials grant
    • Refresh grant
    • Implicit grant (not recommended)
    • Resource owner password credentials grant (not recommended)
  2. Choose an adapter for your web framework

    main

    To connect @jmondi/oauth2-server to your web framework, you must use an adapter. Adapters provide helper functions that bridge the OAuth2 server logic with the specific request/response handling of your chosen framework.

    Select the adapter based on your framework:

    • Express: Use @jmondi/oauth2-server/express.
    • Fastify: Use @jmondi/oauth2-server/fastify.
    • h3 / Nuxt / Nitro: Use @jmondi/oauth2-server/h3.
    • Standard Web APIs (Hono, SvelteKit, Next.js, etc.): Use @jmondi/oauth2-server/vanilla if your framework uses native Request and [Response] objects.
  3. Manage and rotate OIDC keys

    main

    Key Storage Best Practices

    • Private Key: Store in a secure secret manager (e.g., AWS Secrets Manager, GCP Secret Manager, or Vault) or an environment variable. Never commit the private key to version control.
    • Public Key: The server handles this automatically; it is only published at the JWKS endpoint.

    Key Rotation

    In the current version, OIDC uses a single active RSA key. To rotate keys:

    1. Update your configuration with a new key.
    2. The server will sign new tokens with the new key.
    3. Note: Once the new key is deployed, all tokens signed by the old key will become invalid immediately because the old key is removed from the JWKS.

    To minimize the impact of rotation, keep the lifetime of Access Tokens short. This reduces the window during which clients are using tokens signed by the old key.

  4. Framework Adapters and Custom Implementations

    main

    The library provides helper functions called adapters to integrate with common web frameworks. While these adapters simplify setup, the core server is framework-agnostic and can be used with any tool.

    Supported adapters include:

    • VanillaJS
    • Express
    • Fastify
    • H3

    You can create custom adapters for other frameworks by following the patterns found in the existing adapter implementations.

  5. Simplified Array-Based Schema

    main

    For simpler deployments, you can store scopes as arrays (e.g., TEXT[]) directly on the oauth_clients, oauth_auth_codes, and oauth_tokens tables instead of using pivot tables.

    Warning: This approach sacrifices referential integrity. There is no foreign key validation for scope names, it is harder to query 'which clients have this scope', and deleting a scope from the oauth_scopes table will not automatically remove it from the arrays in other tables.

    CREATE TABLE oauth_clients (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        name VARCHAR(255) NOT NULL,
        secret VARCHAR(255),
        redirect_uris TEXT[] NOT NULL DEFAULT '{}',
        allowed_grants VARCHAR(50)[] NOT NULL DEFAULT '{}',
        scopes TEXT[] NOT NULL DEFAULT '{}', -- Array instead of pivot table
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        updated_at TIMESTAMPTZ
    );
    
    -- Example query for array-based scopes:
    -- SELECT * FROM oauth_clients WHERE 'admin' = ANY(scopes);
  6. Understand why there is no `verifyToken()` method

    main

    The library does not provide a single verifyToken() method because token validation requirements vary significantly between applications (e.g., specific route scopes, audience requirements, business logic, or database access patterns).

    Instead, the library provides the necessary building blocks for you to assemble your own validation logic:

    • JwtService.verify(): To validate the cryptographic signature of the JWT.
    • OAuthTokenRepository.getByAccessToken(): To retrieve the token metadata from your storage.
  7. Understand the different token types

    main

    The library manages three primary types of tokens, each with distinct purposes and audiences:

    • Access Token: A JWT used as a bearer token to access protected resources. If an audience parameter is supplied in the request, it becomes the aud claim (the resource audience). If no audience is requested, the token has no aud claim.
    • ID Token: A JWT returned only when the openid scope is granted. It asserts that an end-user authenticated. Its aud claim is the Client, which is intentionally different from an Access Token's audience.
    • Refresh Token: A token used to obtain new Access Tokens. Note that in v1, the refresh flow is OIDC-unaware and does not return an ID Token.

    Opaque Tokens: You can configure the server to return random strings instead of signed JWTs using useOpaqueAuthorizationCodes and useOpaqueRefreshTokens. When using opaque tokens, the persisted database row is the sole source of truth, as the token itself carries no claims.

  8. How the /userinfo request and response work

    main

    Request

    The server extracts the Access Token from the following locations (in order):

    1. The Authorization: Bearer <token> header.
    2. The access_token parameter in the form body.

    Note: The server does not accept the token via query parameters for security reasons.

    Response

    A successful response returns 200 OK with Content-Type: application/json and Cache-Control: no-store.

    The body contains the sub claim and any other claims permitted by the granted scopes. The library uses your getUserClaims(subject) callback to fetch attributes and then filters them based on the authorized scopes.

    Important: The sub claim is controlled by the server and is derived from the canonical subject of the Access Token. It is identical to the sub in the ID Token and cannot be modified by your getUserClaims callback.

    GET /userinfo HTTP/1.1
    Host: auth.example.com
    Authorization: Bearer <access_token>
  9. How to choose between `getUserClaims` and `getIdTokenClaims`

    main

    The OIDC layer provides two distinct callbacks for supplying user information. Choosing the correct one depends on whether the data should be retrieved via a separate UserInfo request or embedded directly in the ID Token.

    Comparison Table

    FeaturegetUserClaims
    RequirementRequired (part of the oidc block)
    SuppliesThe UserInfo response
    Best Use CaseScope-derived claims like name, email, or address
    FilteringThe library removes claims not permitted by the granted scopes
    ExecutionCalled at each UserInfo request
    sub claimThe library always replaces sub with the canonical subject
    FeaturegetIdTokenClaims
    RequirementOptional
    SuppliesThe payload of the ID Token
    Best Use CaseCustom claims like roles, tenant, or acr
    FilteringThe library adds your claims but removes all Protocol Claims
    ExecutionCalled once when the server generates an ID Token at /token
    sub claimYou cannot change sub or other Protocol Claims

    Decision Rule: If a claim describes the user profile and the client can fetch it via a second request, use getUserClaims. If the claim must be available to the client immediately without a second request, use getIdTokenClaims.

  10. Understand OIDC Access Token differences in ts-oauth2-server

    main

    Note that the Access Token implementation in this server differs from RFC 9068 in two specific ways:

    1. Client Identification: The token identifies the Client using the cid claim instead of client_id.
    2. Audience: The token carries an aud claim only when the request explicitly supplies an audience or aud parameter in the query string or request body.

    Refer to the Access Token Format documentation for full details.

  11. How token introspection and revocation trust works

    main

    The server uses a 'verified, persisted state' trust model for introspection and revocation. This means a token's claims are only trusted after the configured JwtInterface.verify succeeds.

    Key behaviors:

    • Verification: Tokens are verified using the configured algorithm. Expiry (exp) and Not-Before (nbf) are ignored during the initial parse to ensure expired tokens can still be revoked, but the active status in the introspection response is determined by the stored state (checking if the record exists, is not expired, and has not been revoked).
    • Token Identification: The token type is identified by the verified payload shape rather than the token_type_hint. A payload with jti is treated as an access token, while a payload with refresh_token_id is treated as a refresh token.
    • Response Priority: In introspection responses, the verified payload is processed first, but persisted database fields (active, scope, client_id, token_type) always take precedence to prevent response spoofing.
    • Error Handling: If a token is unknown or cannot be found in the repository, the introspection response returns active: false instead of a 500 error, adhering to RFC 7662 §2.2.
    • Opaque Tokens: When useOpaqueRefreshTokens is enabled, non-JWT strings are resolved via the grant's RefreshTokenEncoder.