# m2m-token-manager

Use M2MTokenManager to fetch dedicated m2m jwt token.

## Motivation

We introduced this package to reduce the friction when using Dedicated M2M (JWT) Tokens to authenticate a non-human identity (NHI). This is essentially the Typescript port of PP's [Python implementation of `AsyncM2MTokenManager`](https://github.com/theorchard/python-owsclient/blob/746e36918bd2196ff5990b8b7e5590787402b7ec/owsclient/m2m/base.py#L138).

For full details on the architecture and design of the Dedicated M2M (JWT) Token system, please take a look at this [guide](https://www.notion.so/How-to-configure-a-machine-to-have-a-dedicated-Auth0-M2M-Client-and-Automated-JWT-Token-Refresh-2e00d3e763ce497abaf6d2e9a0e947ff).

## Setup

Follow this [guide](https://www.notion.so/Javascript-Package-Management-Setup-9553d5d491c94835aa787fdf0fc4838d) to get your environment set up to work with private packages.

Then, add/install the package:

```sh
npm add @theorchard/backend-m2m-token-manager
```

### Secrets Manager dependency

The `@theorchard/backend-m2m-token-manager` `M2MTokenManager` class requires an implementation of `SecretsManager` matching this [interface](https://github.com/theorchard/backend-js-packages/blob/a1477860af9426ec161ddb42988c478c1e971fd4/packages/backend-m2m-token-manager/src/interfaces/getSecret.ts#L1-L3):

```ts
export interface SecretsManager {
  getSecret(secretName: string): Promise<string>;
}
```

The `getSecret` function should resolve to return a valid JWT token. For most applications, we expect the usage of AWS Secrets Manager, and the PP-supported `lambda-auth0-m2m-token-secret-rotation` function to handle JWT token rotation. You will likely want to follow the guidance in the `@theorchard/backend-secrets-manager-sdk-v3` [README to install the package](https://github.com/theorchard/backend-js-packages/blob/master/packages/backend-secrets-manager-sdk-v3/README.md#setup), and any peer dependencies.

## Usage - M2M Token Manager

You can instantiate an M2MTokenManager singleton to re-use.

```ts
import { M2MTokenManager } from "@theorchard/backend-m2m-token-manager";

// Instantiate using @theorchard/backend-secrets-manager-sdk-v3, or with a custom implementation
const secretsManager = ...;

const m2mTokenManager = new M2MTokenManager({
    secretsManager,
    environment: "qa", // your environment
    // the name of the NHI that needs to be authenticated
    // in order to make calls to a microservice
    serviceName: "pdp-backfill",
  });
```

Then you can use the M2MTokenManager singleton to get the JWT to use to authenticate the request. The example below shows a very low-level usage of `fetch`, passing the JWT into the `Authorization` header as a Bearer token. Your actual code will likely have additional layers of abstraction to effectively pass the `Authorization` header to a request to another microservice.

```ts
const token = await manager.getTokenString(); // the Dedicated M2M JWT
const response = await fetch(
    "https://qa-ows-pdp.theorchard.io/identity/self/check/resources", {
        method: "POST",
        headers: {
            "Authorization": `Bearer ${token}`,
            "Content-Type": "application/json",
            ...
        },
        body: ...
    }
)
```

### `leewaySeconds` parameter

Use the optional `leewaySeconds` parameter to ensure that M2MTokenManager fetches a new token before it expires. For example, setting `leewaySeconds: 60` will prompt M2MTokenManager to retrieve a new token when it is due to expire within one minute. The default leeway is 60 seconds if unset by the client.

```ts
const m2mTokenManager = new M2MTokenManager({
  secretsManager,
  environment: "qa", // your environment
  // the name of the NHI that needs to be authenticated
  // in order to make calls to a microservice
  serviceName: "pdp-backfill",
  leewaySeconds: 60,
});
```

**Never set `leewaySeconds` to a negative number**. The token manager will attempt to use an expired JWT.

### Cache Behavior

By default, M2MTokenManager will try to use an in-memory Map to retrieve the token string first. If the Map does not contain an unexpired token string, M2MTokenManager will fetch from AWS Secrets Manager and store the secret in the cache before returning the token string.

You can use a cache other than an in-memory `Map` by passing an object that implements the `ICache` interface. For example, implementing `ICache` with `ioredis-mock` and using it with M2MTokenManager:

```ts
import Redis from "ioredis-mock";

/**
 * WannabeCache is not production-ready whatsoever and for illustrative purposes only.
 */
export class WannabeCache {
  private cache;
  constructor() {
    this.cache = new Redis();
  }

  async get(key: string): Promise<string> {
    return String(await this.cache.get(key));
  }

  async set(key: string, value: unknown): Promise<void> {
    await this.cache.set(key, String(value));
  }
}

cache = WannabeCache();

const m2mTokenManager = new M2MTokenManager({
  secretsManager,
  environment: "qa",
  serviceName: "pdp-backfill",
  cache,
});
```

Suppose your machine's `M2MTokenManager` is sharing a cache with another machine's `M2MTokenManager`. In this case, you should override the `cacheKey` when instantiating the `M2MTokenManager`.:

```ts
const m2mTokenManagerWithCustomCacheKey = new M2MTokenManager({
  secretsManager,
  environment: "qa",
  serviceName: "pdp-backfill",
  cache,
  cacheKey: "customCacheKeyOfYourChoice",
});
```

### Custom Logger

By default, M2MTokenManager uses a [ConsoleLogger](./src/loggers/ConsoleLogger.ts) for logging. You can provide your own
logger implementation by passing an object that implements the `Logger` interface:

```ts
import {
  M2MTokenManager,
  type Logger,
} from "@theorchard/backend-m2m-token-manager";

// Example: Simple logger that integrates with your application's logging system
class MyCustomLogger implements Logger {
  debug(message: string, context?: Record<string, unknown>): void {
    // Your debug logging implementation
    console.debug(message, context);
  }

  info(message: string, context?: Record<string, unknown>): void {
    // Your info logging implementation
    console.info(message, context);
  }

  warn(
    message: string,
    context?: Record<string, unknown> | Error,
    error?: Error,
  ): void {
    // Your warning logging implementation
    if (error) {
      console.warn(message, context, error);
    } else {
      console.warn(message, context);
    }
  }

  error(
    message: string,
    context?: Record<string, unknown> | Error,
    error?: Error,
  ): void {
    // Your error logging implementation
    if (error) {
      console.error(message, context, error);
    } else {
      console.error(message, context);
    }
  }
}

const m2mTokenManager = new M2MTokenManager({
  secretsManager,
  environment: "qa",
  serviceName: "pdp-backfill",
  logger: new MyCustomLogger(),
});
```

### With`ApolloClient`, as an Authorization Link

#### With `@theorchard/lambda-apollo`

`@theorchard/lambda-apollo` provides an easy-to-configure ApolloClient to authenticate its requests. With full usage of PP-supported packages and Dedicated M2M Token architecture:

```ts
import {
  m2mAuthorizationLink,
  M2MTokenManager,
} from "@theorchard/backend-m2m-token-manager";
import { SecretsManager } from "@theorchard/backend-secrets-manager-sdk-v3";
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import { createClient, apolloLinks } from "@theorchard/lambda-apollo";
import { APPLICATION_NAME, AUTHENTICATION } from "../constants";
import { ENVIRONMENT } from "../config";

const client = createClient({
  authentication: AUTHENTICATION, // These provide Profile Headers, but we are moving toward deprecating them
  lambdaName: APPLICATION_NAME,
  authorizationLink: m2mAuthorizationLink({
    m2mTokenManager: new M2MTokenManager({
      secretsManager: new SecretsManager({
        secretsManagerClient: new SecretsManagerClient(),
      }),
      environment: ENVIRONMENT,
      serviceName: APPLICATION_NAME,
    }),
  }),
});
```

#### With general `ApolloClient`

In general, when creating the [`ApolloClient`](https://github.com/apollographql/apollo-client):

```ts
const secretsManager = ...
const authorizationLink = m2mAuthorizationLink({
  m2mTokenManager: new M2MTokenManager(
    secretsManager,
    environment: config.env,
    serviceName: constants.APPLICATION_NAME,
  )
});

client = new ApolloClient({
        name: constants.APPLICATION_NAME,
        cache: new InMemoryCache(),
        defaultOptions: {
          // ... omitted for brevity
        },
        link: ApolloLink.from([
            authorizationLink, // HERE IT IS!
            new HttpLink({
                uri: env.optStr(
                    'GRAPHQL_URL',
                    `https://${env.name}-graphql-router.theorchard.io/graphql`
                ),
                fetch,
                headers: authentication,
            }),
        ]),
    });
```

## Usage - Impersonation M2M Token Manager

Use [ImpersonationM2MTokenManager](src/impersonation.ts) to obtain M2M JWT tokens with impersonation claims.
This token manager fetches Auth0 client credentials from AWS Secrets Manager and generates tokens that allow your machine to impersonate another identity.

```ts
import { ImpersonationM2MTokenManager } from "@theorchard/backend-m2m-token-manager";

// Instantiate using @theorchard/backend-secrets-manager-sdk-v3, or with a custom implementation
const secretsManager = ...;

// Initialize the token manager
const impersonationManager = ImpersonationM2MTokenManager({
  secrets_manager,
  environment: "qa", // your environment
  // the name of the NHI that needs to be authenticated
  // in order to make calls to a microservice
  serviceName: "pdp-backfill",
  leeway_seconds=60,
)


// Get a token for impersonating a specific identity
const impersonatedIdentityUuid = "4d5f24f5-83f9-4989-9f82-0924a5feaf88";
const token = impersonationManager.getTokenString(impersonatedIdentityUuid)
```

Key Features:

- Credential Caching: Caches Auth0 client credentials to reduce calls to AWS Secrets Manager
- Token Caching: Caches generated tokens per impersonated identity to avoid unnecessary Auth0 API calls

### With`ApolloClient`, as an Authorization Link

#### With `@theorchard/lambda-apollo`

`@theorchard/lambda-apollo` provides an easy-to-configure ApolloClient to authenticate its requests. With full usage of PP-supported packages and Dedicated M2M Token architecture:

```ts
import {
  impersonationM2MAuthorizationLink,
  M2MTokenManager,
} from "@theorchard/backend-m2m-token-manager";
import { SecretsManager } from "@theorchard/backend-secrets-manager-sdk-v3";
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import { createClient, apolloLinks } from "@theorchard/lambda-apollo";
import { APPLICATION_NAME, AUTHENTICATION } from "../constants";
import { ENVIRONMENT } from "../config";

// Create a singleton for the impersonationM2MTokenManager, usually from
// config.py
const impersonationM2MTokenManager = ImpersonationM2MTokenManager({
  secretsManager: new SecretsManager({
    secretsManagerClient: new SecretsManagerClient(),
  }),
  environment: ENVIRONMENT,
  serviceName: APPLICATION_NAME,
});

// This client should be re-used only if it is still
// performing tasks for the same identity.
// A new client MUST BE created and used when a new identity
// should be impersonated.
const impersonatedIdentityClient = createClient({
  authentication: AUTHENTICATION, // These provide Profile Headers, but we are moving toward deprecating them
  lambdaName: APPLICATION_NAME,
  authorizationLink: impersonationM2MAuthorizationLink({
    impersonationM2MTokenManager,
    impersonateIdentityUuid:
      "the UUID4 of the identity the machine needs to impersonate",
  }),
});
```
