Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor (plus fixes): rewrite auth logic for simplification #7

Merged
merged 11 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions .env.example
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reordered keys in this file to better associate them with each other.

Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
# Required for Administration API tutorial, to acquire auth tokens
# See https://docs.camunda.io/docs/apis-tools/administration-api/authentication/#client-credentials-and-scopes
# Shared by various API tutorials
CLUSTER_ID=fill-this-in

------------------------------------

# Administration API tutorial

## Capture these values from an API client created at the organization level, with access to the Cluster scope.
## See https://docs.camunda.io/docs/apis-tools/administration-api/authentication/#client-credentials-and-scopes for more details.
### This value comes from the `CAMUNDA_CONSOLE_CLIENT_ID`.
ADMINISTRATION_CLIENT_ID=fill-this-in
### This value comes from the `CAMUNDA_CONSOLE_CLIENT_SECRET`.
ADMINISTRATION_CLIENT_SECRET=fill-this-in

# Required for Administration API tutorial, to access API
ADMINISTRATION_API_URL=https://api.cloud.camunda.io
## These values will only change if you're not using SaaS.
ADMINISTRATION_AUDIENCE=api.cloud.camunda.io
CLUSTER_ID=fill-this-in
ADMINISTRATION_API_URL=https://api.cloud.camunda.io

------------------------------------

# Required for non-Administration API tutorials, to acquire auth tokens
COMPONENTS_CLIENT_ID=fill-this-in
COMPONENTS_CLIENT_SECRET=fill-this-in
# Optimize API tutorial

# Required for Optimize API tutorial, to access API
## Capture these values from an API client created at the cluster level, with access to the Optimize scope.
### This value comes from the `ZEEBE_CLIENT_ID`.
OPTIMIZE_CLIENT_ID=fill-this-in
### This value comes from the `ZEEBE_CLIENT_SECRET`.
OPTIMIZE_CLIENT_SECRET=fill-this-in
### This value comes from the `CAMUNDA_OPTIMIZE_BASE_URL`.
OPTIMIZE_BASE_URL=fill-this-in

## This value will only change if you're not using SaaS.
OPTIMIZE_AUDIENCE=optimize.camunda.io
83 changes: 36 additions & 47 deletions auth.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
import { AuthorizationCode } from "simple-oauth2";

const adminConfiguration = {
// These credentials come from your .env file.
clientID: process.env.ADMINISTRATION_CLIENT_ID,
clientSecret: process.env.ADMINISTRATION_CLIENT_SECRET
};
// This function can be used by callers to retrieve a token prior to their API calls.
/**
* @param {Object} config - A configuration object for authorizing the API client.
* @param {string} config.clientId - The client ID for the API client.
* @param {string} config.clientSecret - The client secret for the API client.
* @param {string} config.audience - The audience associated with the target API.
* @returns {string}
*/
export async function getAccessToken(config) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this function to the top of the file, because it is the entry point to this logic, and for users who want to read the code and understand how to use a library to handle auth for their code, this is where they'll want to start.

It also now takes a config object, which contains three properties: clientId, clientSecret, and audience. Previously we were passing a magic string and doing some lookups within this file to get the correct environment variables, but that was obfuscating important details -- you need the client id, secret, and audience in order to authorize.

try {
const client = configureAuthorizationClient(config);
const tokenParams = getTokenParams(config);

const componentConfiguration = {
// These credentials come from your .env file.
clientID: process.env.COMPONENTS_CLIENT_ID,
clientSecret: process.env.COMPONENTS_CLIENT_SECRET
};
const result = await client.getToken(tokenParams);
const accessToken = client.createToken(result);

// Configure our authorization request.
function configureAuthorizationClient(targetApi) {
const configSource =
targetApi === "administration"
? adminConfiguration
: componentConfiguration;
// Return the actual token that can be passed as an Authorization header in each request.
return accessToken.token.token.access_token;
} catch (error) {
throw new Error(error.message);
}
}

// Configure our authorization request.
/**
* @param {Object} config - A configuration object for authorizing the API client.
* @param {string} config.clientId - The client ID for the API client.
* @param {string} config.clientSecret - The client secret for the API client.
* @returns {Object} A configured authorization client.
*/
function configureAuthorizationClient({ clientId, clientSecret }) {
const config = {
client: {
id: configSource.clientID,
secret: configSource.clientSecret
id: clientId,
secret: clientSecret
},
auth: {
// This is the URL for our auth server.
Expand All @@ -36,37 +48,14 @@ function configureAuthorizationClient(targetApi) {
}

// Define additional parameters for the authorization request.
function getTokenParams(audience) {
/**
* @param {Object} config - A configuration object for authorizing the API client.
* @param {string} config.audience - The audience associated with the target API.
* @returns {Object} Token parameters for the authorization request.
*/
function getTokenParams({ audience }) {
return {
// This audience is specific to the Camunda API we are calling.
audience
};
}

// This function can be used by callers to retrieve a token prior to their API calls.
/**
*
* @param {"administration" | "components"} targetApi Use "administration" for only the administration API; "components" for all other component APIs.
* @param {string} audience The `audience` required by the target API.
* @returns
*/
export async function getAccessToken(targetApi, audience) {
if (targetApi !== "administration" && targetApi !== "components") {
throw new Error(
"Unexpected targetApi value. Expecing 'administration' or 'components'."
);
}

try {
const client = configureAuthorizationClient(targetApi);
const tokenParams = getTokenParams(audience);

const result = await client.getToken(tokenParams);
const accessToken = client.createToken(result);

// Return the actual token that can be passed as an Authorization header in each request.
return accessToken.token.token.access_token;
} catch (error) {
throw new Error(error.message);
}
}
34 changes: 10 additions & 24 deletions completed/administration.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import axios from "axios";
import { getAccessToken } from "./auth.js";

const authorizationConfiguration = {
clientId: process.env.ADMINISTRATION_CLIENT_ID,
clientSecret: process.env.ADMINISTRATION_CLIENT_SECRET,
audience: process.env.ADMINISTRATION_AUDIENCE
};
Comment on lines +4 to +8
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of assigning this config object once per API client, rather than every single time. Open to feedback on it though.


// An action that lists all clients.
async function listClients() {
const administrationAudience = process.env.ADMINISTRATION_AUDIENCE;

// Every request needs an access token.
const accessToken = await getAccessToken(
"administration",
administrationAudience
);
const accessToken = await getAccessToken(authorizationConfiguration);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this call gets simplified, and a little more clear IMO.


// These settings come from your .env file.
const administrationApiUrl = process.env.ADMINISTRATION_API_URL;
Expand Down Expand Up @@ -45,13 +46,8 @@ async function listClients() {

// An action that adds a new client.
async function addClient([clientName]) {
const administrationAudience = process.env.ADMINISTRATION_AUDIENCE;

// Every request needs an access token.
const accessToken = await getAccessToken(
"administration",
administrationAudience
);
const accessToken = await getAccessToken(authorizationConfiguration);

// These settings come from your .env file.
const administrationApiUrl = process.env.ADMINISTRATION_API_URL;
Expand Down Expand Up @@ -95,13 +91,8 @@ async function addClient([clientName]) {

// An action that views one client.
async function viewClient([clientId]) {
const administrationAudience = process.env.ADMINISTRATION_AUDIENCE;

// Every request needs an access token.
const accessToken = await getAccessToken(
"administration",
administrationAudience
);
const accessToken = await getAccessToken(authorizationConfiguration);

// These settings come from your .env file.
const administrationApiUrl = process.env.ADMINISTRATION_API_URL;
Expand Down Expand Up @@ -136,13 +127,8 @@ async function viewClient([clientId]) {

// An action that deletes a client.
async function deleteClient([clientId]) {
const administrationAudience = process.env.ADMINISTRATION_AUDIENCE;

// Every request needs an access token.
const accessToken = await getAccessToken(
"administration",
administrationAudience
);
const accessToken = await getAccessToken(authorizationConfiguration);

// These settings come from your .env file.
const administrationApiUrl = process.env.ADMINISTRATION_API_URL;
Expand Down
18 changes: 11 additions & 7 deletions completed/optimize.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import axios from "axios";
import { getAccessToken } from "./auth.js";

const authorizationConfiguration = {
clientId: process.env.OPTIMIZE_CLIENT_ID,
clientSecret: process.env.OPTIMIZE_CLIENT_SECRET,
audience: process.env.OPTIMIZE_AUDIENCE
};

async function listDashboards([collectionId]) {
const optimizeAudience = process.env.OPTIMIZE_AUDIENCE;
const accessToken = await getAccessToken("components", optimizeAudience);
const accessToken = await getAccessToken(authorizationConfiguration);

const optimizeApiUrl = process.env.OPTIMIZE_BASE_URL;
// This is the API endpoint to list your existing dashboard IDs
Expand Down Expand Up @@ -33,11 +38,10 @@ async function listDashboards([collectionId]) {
async function deleteDashboard([dashboardId]) {
console.log(`deleting dashboard ${dashboardId}`);

const optimizeAudience = process.env.OPTIMIZE_AUDIENCE;
const accessToken = await getAccessToken("components", optimizeAudience);
const optimizeApiUrl = process.env.OPTIMIZE_API_URL;
const accessToken = await getAccessToken(authorizationConfiguration);

const url = `${optimizeApiUrl}/public/dashboard/${dashboardId}`;
const optimizeApiUrl = process.env.OPTIMIZE_BASE_URL;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug here: the key name didn't match what's in our .env.example, or the other client function (listDashboards).

const url = `${optimizeApiUrl}/api/public/dashboard/${dashboardId}`;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug here: the /api was missing at the beginning, resulting in a failed API call.


// Configure the API call.
const options = {
Expand All @@ -55,7 +59,7 @@ async function deleteDashboard([dashboardId]) {

// Process the results from the API call.
if (response.status === 204) {
console.log(`Dashboard ${clientId} was deleted!`);
console.log(`Dashboard ${dashboardId} was deleted!`);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug here: clientId was copy-pasta, and this line was throwing an undefined variable error.

} else {
// Emit an unexpected error message.
console.error("Unable to delete dashboard!");
Expand Down