badgateway/oauth2-client

repository·main·Indexed 43 days ago

https://github.com/badgateway/oauth2-client

A lightweight (4KB gzipped), zero-dependency OAuth2 client for Node.js 18+ and browsers. Version 3.3.1 supports multiple grant types, PKCE, automatic token refreshing, and provides an OAuth2Fetch wrapper for seamless API integration with automatic Bearer token handling.

Tokens
9K
Snippets
28
Records
40
Agent score
55%

What's inside badgateway-oauth2-client

  1. Configure authentication method via OAuth2 discovery

    main
    If you initialize the OAuth2Client using an OAuth2 discovery document, the library will automatically respect the server's preference. If the discovery document indicates a preference for client_secret_basic, the client will default to the strict encoding form instead of the standard client_secret_basic_interop default.
  2. Compatibility with older Node.js versions

    main

    The current version of @badgateway/oauth2-client is designed for modern browsers and Node 18+.

    If you are working in an environment with Node 16 or below, you must:

    1. Use a 2.x version of the package.
    2. Provide necessary polyfills for your environment.
  3. Persist tokens with OAuth2Fetch

    main

    To maintain user sessions, implement storeToken and getStoredToken in the OAuth2Fetch configuration. This allows the wrapper to attempt using a previously saved token before attempting a full re-authentication.

    const fetchWrapper = new OAuth2Fetch({
      client: client,
      getNewToken: async () => {
        // Logic to get a fresh token if no stored token is available
        return client.clientCredentials();
      },
      /**
       * Called whenever the active token changes.
       */
      storeToken: (token) => {
        document.localStorage.setItem('token-store', JSON.stringify(token));
      },
      /**
       * Called to try a stored token before re-authenticating.
       * Return null if no token is found.
       */
      getStoredToken: () => {
        const token = document.localStorage.getItem('token-store');
        if (token) return JSON.parse(token);
        return null;
      }
    });
  4. Implement the authorization_code flow with PKCE

    main

    The authorization_code flow is designed for browser-based applications and involves three steps: redirecting the user, handling the redirect back to the app, and exchanging the code for a token. You can use generateCodeVerifier() to enable PKCE (Proof Key for Code Exchange) for enhanced security.

    import { OAuth2Client, generateCodeVerifier } from '@badgateway/oauth2-client';
    
    const client = new OAuth2Client({
      server: 'https://authserver.example/',
      clientId: '...',
      tokenEndpoint: '/token',
      authorizationEndpoint: '/authorize',
    });
    
    // 1. Redirect the user to the authorization server
    const codeVerifier = await generateCodeVerifier();
    
    document.location = await client.authorizationCode.getAuthorizeUri({
      redirectUri: 'https://my-app.example/',
      state: 'some-string',
      codeVerifier,
      scope: ['scope1', 'scope2'],
    });
    
    // 2. Handle the redirect back to the app and obtain token
    // (This part runs on the page the user is redirected to)
    const oauth2Token = await client.authorizationCode.getTokenFromCodeRedirect(
      document.location, // The current URL containing the 'code'
      {
        redirectUri: 'https://my-app.example/',
        state: 'some-string', // Must match the state sent in step 1
        codeVerifier,         // Must match the verifier sent in step 1
      }
    );
  5. Configure OAuth2 client_id and client_secret encoding

    main

    OAuth2 servers vary in how they expect client_id and client_secret to be transmitted. This library defaults to using the Authorization header with client_secret_basic_interop encoding (which only percent-encodes the : character to maximize compatibility with servers like Google and Ebay).

    If your server requires credentials in the POST body or requires strict standard encoding, use the authenticationMethod option during initialization.

    Supported authenticationMethod values:

    • client_secret_post: Encodes credentials in the POST request body. Recommended if your server supports it.
    • client_secret_basic: Encodes credentials in the Authorization header using strict standard percent-encoding rules.
    • client_secret_basic_interop: (Default) Encodes credentials in the Authorization header using less strict rules (only encoding :).
    const client = new OAuth2Client({
      server: 'https://auth-server.example/',
      clientId: '...',
      clientSecret: '...',
      authenticationMethod: 'client_secret_post', // encode in POST body 
    });
  6. How OAuth2Fetch manages token lifecycle

    main

    The OAuth2Fetch wrapper follows a specific lifecycle to ensure requests are authenticated with minimal latency:

    1. Initialization: If getStoredToken is provided, the wrapper awaits its resolution to populate the initial token state.
    2. Request Execution: When .fetch() is called, the wrapper retrieves the current access token via .getAccessToken().
    3. Token Retrieval/Refresh:
      • If a valid token exists, it is used.
      • If the token is expired or near expiry, .refreshToken() is called.
      • If a refreshToken is available, it attempts to use the client.refreshToken() method. If that fails, it falls back to getNewToken().
    4. Automatic Background Refresh: If scheduleRefresh is true, the wrapper sets a timer to refresh the token 1 minute before its expiresAt time. It only schedules a refresh if the token has an expiresAt value and a refreshToken available.
    5. 401 Retry: If a request returns a 401 Unauthorized, the wrapper attempts to refresh the token once and retries the original request with the new token.
  7. Implement the authorization_code grant flow

    main

    The OAuth2AuthorizationCodeClient class provides a specialized implementation for the authorization_code OAuth2 grant flow. This flow is typically used when a user needs to authorize your application via a browser redirect.

    Workflow

    1. Generate a Code Verifier: Use generateCodeVerifier() to create a high-entropy cryptographic string for PKCE support.
    2. Get the Authorization URI: Use getAuthorizeUri() to generate the URL where the user should be redirected to log in.
    3. Handle the Redirect: After the user authenticates, they are redirected back to your redirectUri. Use getTokenFromCodeRedirect() to automatically validate the response and exchange the code for an access token.

    PKCE Support

    For enhanced security (Proof Key for Code Exchange), provide the codeVerifier in both getAuthorizeUri and getToken (or via getTokenFromCodeRedirect). The client automatically handles generating the code_challenge and code_challenge_method (using S256).

    import { OAuth2Client } from '../client.ts';
    import { OAuth2AuthorizationCodeClient, generateCodeVerifier } from './authorization-code.ts';
    
    const client = new OAuth2Client({ clientId: 'YOUR_CLIENT_ID' });
    const authCodeClient = new OAuth2AuthorizationCodeClient(client);
    
    // 1. Setup PKCE
    const codeVerifier = await generateCodeVerifier();
    
    // 2. Get URI for redirection
    const authUri = await authCodeClient.getAuthorizeUri({
      redirectUri: 'https://yourapp.com/callback',
      codeVerifier,
      scope: ['openid', 'profile'],
      state: 'random_state_string'
    });
    
    // ... User visits authUri and is redirected back to https://yourapp.com/callback?code=XYZ&state=random_state_string
    
    // 3. Exchange code for token
    const token = await authCodeClient.getTokenFromCodeRedirect('https://yourapp.com/callback?code=XYZ&state=random_state_string', {
      redirectUri: 'https://yourapp.com/callback',
      state: 'random_state_string',
      codeVerifier
    });
  8. Use OAuth2Fetch to automate authenticated requests

    main

    The OAuth2Fetch class is a wrapper around the standard fetch API that automatically manages Bearer tokens. It handles injecting the Authorization: Bearer <token> header, proactively refreshing tokens before they expire, and retrying requests if a 401 Unauthorized error is encountered.

    To use it, you must provide an OAuth2FetchOptions object containing an OAuth2Client and a getNewToken function to handle initial authentication.

    import { OAuth2Fetch } from '@badgateway/oauth2-client';
    
    const oauth2Fetch = new OAuth2Fetch({
      client: myOAuth2Client,
      getNewToken: async () => {
        // Implement your logic to get a fresh token (e.g., via password or auth code grant)
        return await myOAuth2Client.getToken();
      },
      // Optional: persist tokens to localStorage/database
      storeToken: (token) => localStorage.setItem('token', JSON.stringify(token)),
      getStoredToken: async () => {
        const stored = localStorage.getItem('token');
        return stored ? JSON.parse(stored) : null;
      }
    });
    
    // Use it just like standard fetch
    const response = await oauth2Fetch.fetch('https://api.example.com/data', {
      method: 'GET'
    });
    const data = await response.json();
  9. Configure the OAuth2Client

    main

    To use the library, instantiate the OAuth2Client class with a ClientSettings object. You can provide a server hostname to enable automatic endpoint discovery via the .well-known/oauth-authorization-server endpoint, or manually specify endpoints like tokenEndpoint, authorizationEndpoint, etc.

    If you need to implement custom middleware or headers, you can provide your own fetch implementation in the settings.

    import { OAuth2Client } from '@badgateway/oauth2-client';
    
    const client = new OAuth2Client({
      server: 'https://auth.example.com',
      clientId: 'YOUR_CLIENT_ID',
      clientSecret: 'YOUR_CLIENT_SECRET',
      // authenticationMethod defaults to 'client_secret_basic_interop'
      authenticationMethod: 'client_secret_post',
    });
  10. Use OAuth2Fetch middleware for existing fetch libraries

    main

    If you are using a library like Ketting, you can use the mw() method on an OAuth2Fetch instance to get a middleware function that decorates existing request handling with OAuth2 logic.

    import { Client } from 'ketting';
    import { OAuth2Client, OAuth2Fetch } from '@badgateway/oauth2-client';
    
    const oauth2Client = new OAuth2Client({
      server: 'https://my-auth.example',
      clientId: 'foo',
    });
    
    const oauth2Fetch = new OAuth2Fetch({
      client: oauth2Client,
    });
    
    const ketting = new Client('http://api-root');
    // Add the middleware to Ketting
    ketting.use(oauth2Fetch.mw());