Skip to content

Commit

Permalink
#813 Add functionality to enable deploy CLI client to use private key…
Browse files Browse the repository at this point in the history
… JWT (#817)

* Adds functional support for private key JWT authentication
- Adds new configuration options AUTH0_CLIENT_SIGNING_KEY,  AUTH0_CLIENT_SIGNING_ALGORITHM
- Adds validation to ensure either AUTH0_CLIENT_SIGNING_KEY or AUTH0_CLIENT_SECRET are present
- Passes new configiration options through to auth0/auth0 library when creating a new AuthenticationClient

In part, resolves: #813

* Adds test cases to cover new functionalilty for private key JWT authentication

In part, resolves: #813

* Updates changelog and documentation to support new features

In part, resolves: #813

* Fixes changelog formatting

* Continued work

* Changing signing key to path rather than passed in as environment variable

* Adding public and private keys to the git ignore

* Re-recording tests

* Updating changelog

* Undoing erroneous change and fixing PR link

---------

Co-authored-by: Aaron Chilcott <siaison.co>
Co-authored-by: Will Vedder <[email protected]>
  • Loading branch information
aaronchilcott and willvedd authored Aug 3, 2023
1 parent d4e6e13 commit 72470f4
Show file tree
Hide file tree
Showing 12 changed files with 2,721 additions and 3,073 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ config*.json
.idea
.npmrc
yarn-error.log
*.pem
*.pub
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Support for Private Key JWT authentication for authenticating with private key instead of client secret [#817]

## [7.18.0] - 2023-07-14

### Added
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The Auth0 Deploy CLI is a tool that helps you manage your Auth0 tenant configura

- [Using as a CLI](docs/using-as-cli.md)
- [Using as a Node Module](docs/using-as-node-module.md)
- [Authenticating with Tenant](docs/authenticating-with-tenant.md)
- [Configuring the Deploy CLI](docs/configuring-the-deploy-cli.md)
- [Keyword Replacement](docs/keyword-replacement.md)
- [Incorporating Into Multi-environment Workflows](docs/multi-environment-workflow.md)
Expand Down Expand Up @@ -91,7 +92,7 @@ The Deploy CLI can be configured two ways, through a `config.json` file and thro
- `AUTH0_CLIENT_ID`
- `AUTH0_CLIENT_SECRET`

These values can be found in the “Settings” tab within the Auth0 application created in the previous step.
These values can be found in the “Settings” and “Credentials“ tabs within the Auth0 application created in the previous step.

### Calling the Deploy CLI

Expand Down
34 changes: 34 additions & 0 deletions docs/authenticating-with-tenant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Authenticating with your Tenant

There are three available methods of authenticating the Deploy CLI with your tenant:

- [Client Credentials](#client-credentials)
- [Private Key JWT](#private-key-JWT)
- [Access Token](#access-token)

## Client Credentials

Authenticating with a client ID and client secret pair. This option is straightforward and enables the quickest path to setup for the tool. In order to utilize, set both the `AUTH0_CLIENT_ID` and `AUTH0_CLIENT_SECRET` configuration values with the client ID and client secret respectively. These credentials can be found under the "Credentials" tab within the designated application used for the Deploy CLI.

## Private Key JWT

Providing a private key to facilitate asymmetric key pair authentication. This requires the "Private Key JWT" authentication method for the designated client as well as a public key configured on the remote tenant. This may be appealing to developers who do not wish to have credentials stored remotely on Auth0.

To utilize, pass the path of the private key through the `AUTH0_CLIENT_SIGNING_KEY_PATH` configuration property either as an environment variable or property in your `config.json` file. This path is relative to the working directory. Optionally, you can specify the signing algorithm through the `AUTH0_CLIENT_SIGNING_ALGORITHM` configuration property.

**Example: **

```json
{
"AUTH0_CLIENT_SIGNING_KEY_PATH": "./private.pem",
"AUTH0_CLIENT_SIGNING_ALGORITHM": "RSA256"
}
```

See [Configure Private Key JWT Authentication](https://auth0.com/docs/get-started/applications/configure-private-key-jwt) for further documentation

## Access Token

Passing in an access token directly is also supported. This option puts more onus on the developers but can enable flexible and specific workflows when necessary. It can be leveraged by passing the Auth0 access token in via the `AUTH0_ACCESS_TOKEN` environment variable.

[[table of contents]](../README.md#documentation)
8 changes: 8 additions & 0 deletions docs/configuring-the-deploy-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ String. The secret of the designated Auth0 application used to make API requests

String. Short-lived access token for Management API from designated Auth0 application. Can be used in replacement to client ID and client secret combination.

### `AUTH0_CLIENT_SIGNING_KEY_PATH`

String. The path to the private key used by the client when facilitating Private Key JWT authentication. Path relative to the working directory. Also note `AUTH0_CLIENT_SIGNING_ALGORITHM` for specifying signing algorithm.

### `AUTH0_CLIENT_SIGNING_ALGORITHM`

String. Specifies the JWT signing algorithms used by the client when facilitating Private Key JWT authentication. Only used in combination with `AUTH0_CLIENT_SIGNING_KEY_PATH`. Accepted values: `RS256`, `RS384`, `PS256`.

### `AUTH0_ALLOW_DELETE`

Boolean. When enabled, will allow the tool to delete resources. Default: `false`.
Expand Down
50 changes: 42 additions & 8 deletions src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,19 @@ export const setupContext = async (
config: Config,
command: 'import' | 'export'
): Promise<DirectoryContext | YAMLContext> => {
const missingParams: ('AUTH0_DOMAIN' | 'AUTH0_CLIENT_ID' | 'AUTH0_CLIENT_SECRET')[] = [];
const missingParams: (
| 'AUTH0_DOMAIN'
| 'AUTH0_CLIENT_ID'
| 'AUTH0_CLIENT_SECRET or AUTH0_CLIENT_SIGNING_KEY_PATH or AUTH0_ACCESS_TOKEN'
)[] = [];

if (!config.AUTH0_DOMAIN) missingParams.push('AUTH0_DOMAIN');
if (!config.AUTH0_ACCESS_TOKEN) {
if (!config.AUTH0_CLIENT_ID) missingParams.push('AUTH0_CLIENT_ID');
if (!config.AUTH0_CLIENT_SECRET) missingParams.push('AUTH0_CLIENT_SECRET');
if (!config.AUTH0_CLIENT_SECRET && !config.AUTH0_CLIENT_SIGNING_KEY_PATH)
missingParams.push(
'AUTH0_CLIENT_SECRET or AUTH0_CLIENT_SIGNING_KEY_PATH or AUTH0_ACCESS_TOKEN'
);
}

if (missingParams.length > 0) {
Expand Down Expand Up @@ -126,13 +133,40 @@ export const setupContext = async (
})(config);

const accessToken = await (async (): Promise<string> => {
if (!!config.AUTH0_ACCESS_TOKEN) return config.AUTH0_ACCESS_TOKEN;
const {
AUTH0_DOMAIN,
AUTH0_CLIENT_ID,
AUTH0_ACCESS_TOKEN,
AUTH0_CLIENT_SECRET,
AUTH0_CLIENT_SIGNING_KEY_PATH,
AUTH0_CLIENT_SIGNING_ALGORITHM,
} = config;

if (!!AUTH0_ACCESS_TOKEN) return AUTH0_ACCESS_TOKEN;
if (!AUTH0_CLIENT_SECRET && !AUTH0_CLIENT_SIGNING_KEY_PATH) {
throw new Error(
'need to supply either `AUTH0_ACCESS_TOKEN`, `AUTH0_CLIENT_SECRET` or `AUTH0_CLIENT_SIGNING_KEY_PATH`'
);
}

const authClient = new AuthenticationClient({
domain: config.AUTH0_DOMAIN,
clientId: config.AUTH0_CLIENT_ID,
clientSecret: config.AUTH0_CLIENT_SECRET,
});
const authClient: AuthenticationClient = (() => {
if (!!AUTH0_CLIENT_SECRET) {
return new AuthenticationClient({
domain: AUTH0_DOMAIN,
clientId: AUTH0_CLIENT_ID,
clientSecret: AUTH0_CLIENT_SECRET,
});
}

return new AuthenticationClient({
domain: AUTH0_DOMAIN,
clientId: AUTH0_CLIENT_ID,
clientAssertionSigningKey: readFileSync(AUTH0_CLIENT_SIGNING_KEY_PATH),
clientAssertionSigningAlg: !!AUTH0_CLIENT_SIGNING_ALGORITHM
? AUTH0_CLIENT_SIGNING_ALGORITHM
: undefined,
});
})();

const clientCredentials = await authClient.clientCredentialsGrant({
audience: config.AUTH0_AUDIENCE
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ export type Config = {
AUTH0_DOMAIN: string;
AUTH0_CLIENT_ID: string;
AUTH0_CLIENT_SECRET: string;
AUTH0_CLIENT_SIGNING_KEY_PATH: string;
AUTH0_CLIENT_SIGNING_ALGORITHM: string;
AUTH0_INPUT_FILE: string;
AUTH0_ALLOW_DELETE: boolean;
AUTH0_EXCLUDED?: AssetTypes[];
Expand Down
96 changes: 95 additions & 1 deletion test/context/context.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import logger from '../../src/logger';
import { setupContext, filterOnlyIncludedResourceTypes } from '../../src/context';
import directoryContext from '../../src/context/directory';
import yamlContext from '../../src/context/yaml';
import { cleanThenMkdir, testDataDir } from '../utils';
import { cleanThenMkdir, testDataDir, createDir } from '../utils';

chai.use(chaiAsPromised);
chai.use(sinonChai);
Expand All @@ -25,6 +25,100 @@ describe('#context loader validation', async () => {
await expect(setupContext(config), 'import').to.be.eventually.rejectedWith(Error);
});

describe('authentication options', async () => {
let tmpConfig;

beforeEach(() => {
tmpConfig = {
AUTH0_CLIENT_ID: 'fake client ID',
...config,
};
delete tmpConfig.AUTH0_ACCESS_TOKEN;
});

it('should error while attempting authentication, but pass validation with client secret', async () => {
/* Create empty directory */
const dir = path.resolve(testDataDir, 'context');
cleanThenMkdir(dir);

tmpConfig.AUTH0_CLIENT_SECRET = 'fake secret';

const result = await expect(
setupContext({ ...tmpConfig, AUTH0_INPUT_FILE: dir }),
'import'
).to.be.eventually.rejectedWith(Error);

expect(result).to.be.an('object').that.has.property('name').which.eq('access_denied');
});

it('should error while attempting private key JWT generation, but pass validation with client signing key', async () => {
// Proves that config value AUTH0_CLIENT_SIGNING_KEY_PATH is being passed to Auth0 library
/* Create empty directory */
const dir = path.resolve(testDataDir, 'context');
cleanThenMkdir(dir);
createDir(dir, {
'.': {
'private.pem': 'some-invalid-private-key',
},
});

tmpConfig.AUTH0_CLIENT_SIGNING_KEY_PATH = path.join(dir, 'private.pem');

const result = await expect(
setupContext({ ...tmpConfig, AUTH0_INPUT_FILE: dir }),
'import'
).to.be.eventually.rejectedWith(Error);

expect(result)
.to.be.an('Error')
.that.has.property('message')
.which.eq('secretOrPrivateKey must be an asymmetric key when using RS256');
});

it('should error while attempting private key JWT generation because of incorrect value for algorithm, but pass validation with client signing key', async () => {
// Proves that config value AUTH0_CLIENT_SIGNING_ALGORITHM is being passed to Auth0 library
/* Create empty directory */
const dir = path.resolve(testDataDir, 'context');
cleanThenMkdir(dir);
createDir(dir, {
'.': {
'private.pem': 'some-invalid-private-key',
},
});

tmpConfig.AUTH0_CLIENT_SIGNING_KEY_PATH = path.join(dir, 'private.pem');
tmpConfig.AUTH0_CLIENT_SIGNING_ALGORITHM = 'bad value for algorithm';

const result = await expect(
setupContext({ ...tmpConfig, AUTH0_INPUT_FILE: dir }),
'import'
).to.be.eventually.rejectedWith(Error);

expect(result)
.to.be.an('Error')
.that.has.property('message')
.which.eq('"algorithm" must be a valid string enum value');
});

it('should error when secret, private key and auth token are all absent', async () => {
/* Create empty directory */
const dir = path.resolve(testDataDir, 'context');
cleanThenMkdir(dir);

const result = await expect(
setupContext({ ...tmpConfig, AUTH0_INPUT_FILE: dir }),
'import'
).to.be.eventually.rejectedWith(Error);

expect(result)
.to.be.an('Error')
.that.has.property('message')
.which.eq(
'The following parameters were missing. Please add them to your config.json or as an environment variable. ["AUTH0_CLIENT_SECRET or AUTH0_CLIENT_SIGNING_KEY_PATH or AUTH0_ACCESS_TOKEN"]'
);
});
});

it('should load directory context', async () => {
/* Create empty directory */
const dir = path.resolve(testDataDir, 'context');
Expand Down
Loading

0 comments on commit 72470f4

Please sign in to comment.