Skip to content

Commit

Permalink
Add ability to auto create internal client secrets.
Browse files Browse the repository at this point in the history
Create client secret from Grafana
  • Loading branch information
alukach committed Oct 30, 2024
1 parent f8907ce commit 200446b
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 50 deletions.
29 changes: 15 additions & 14 deletions config/src/veda.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ displayName: Applications
displayNameHtml: VEDA Ecosystem

clients:
- clientId: grafana
- clientId: $(env:GRAFANA_CLIENT_ID)
secret: $(env:GRAFANA_CLIENT_SECRET)
name: Grafana
rootUrl: https://d3art7u1htuei0.cloudfront.net
enabled: true
Expand Down Expand Up @@ -171,16 +172,16 @@ identityProviders:
authenticateByDefault: false
linkOnly: false
config:
userInfoUrl: "https://cilogon.org/oauth2/userinfo"
validateSignature: "true"
tokenUrl: "https://cilogon.org/oauth2/token"
jwksUrl: "https://cilogon.org/oauth2/certs"
issuer: "https://cilogon.org"
useJwksUrl: "true"
pkceEnabled: "false"
metadataDescriptorUrl: "https://cilogon.org/.well-known/openid-configuration"
authorizationUrl: "https://cilogon.org/authorize"
clientAuthMethod: "client_secret_post"
syncMode: "LEGACY"
clientId: $(env:CILOGON_CLIENT_ID)
clientSecret: $(env:CILOGON_CLIENT_SECRET)
userInfoUrl: "https://cilogon.org/oauth2/userinfo"
validateSignature: "true"
tokenUrl: "https://cilogon.org/oauth2/token"
jwksUrl: "https://cilogon.org/oauth2/certs"
issuer: "https://cilogon.org"
useJwksUrl: "true"
pkceEnabled: "false"
metadataDescriptorUrl: "https://cilogon.org/.well-known/openid-configuration"
authorizationUrl: "https://cilogon.org/authorize"
clientAuthMethod: "client_secret_post"
syncMode: "LEGACY"
clientId: $(env:CILOGON_CLIENT_ID)
clientSecret: $(env:CILOGON_CLIENT_SECRET)
26 changes: 7 additions & 19 deletions deploy/app.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { KeycloakStack } from "./lib/KeycloakStack";
import { join } from "path";
import assert = require("assert");
import * as assert from "assert";
import { KeycloakStack } from "./lib/KeycloakStack";
import { getOauthSecrets } from "./lib/utils";

const {
AWS_ACCOUNT_ID,
Expand All @@ -17,11 +18,9 @@ const {
assert(SSL_CERTIFICATE_ARN, "SSL_CERTIFICATE_ARN env var is required");
assert(HOSTNAME, "HOSTNAME env var is required");

const oAuthClientSecrets = getOauthSecrets();
const idpOauthClientSecrets = getOauthSecrets();
console.log(
`Found ${
Object.keys(oAuthClientSecrets).length
} OAuth client secrets for clients: ${Object.keys(oAuthClientSecrets).join(
`Found IdP client secrets for ${Object.keys(idpOauthClientSecrets).join(
", "
)}`
);
Expand All @@ -42,17 +41,6 @@ new KeycloakStack(app, `VedaKeycloakStack-${STAGE}`, {
hostname: HOSTNAME,
keycloakVersion: KEYCLOAK_VERSION,
configDir: join(__dirname, "..", "config"),
oAuthClientSecrets,
idpOauthClientSecrets,
createdOauthClients: ["grafana"],
});

/**
* Helper function to extract OAuth client secrets from the runtime environment
* @returns Record<string, string> - A map of OAuth client IDs to the ARN of their secrets
*/
function getOauthSecrets(): Record<string, string> {
const oauthSecretPrefix = "IDP_SECRET_ARN_";
const clientSecrets = Object.entries(process.env)
.filter(([k, v]) => k.startsWith(oauthSecretPrefix))
.map(([k, v]) => [k.split(oauthSecretPrefix)[1], v]);
return Object.fromEntries(clientSecrets);
}
53 changes: 38 additions & 15 deletions deploy/lib/KeycloakConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ interface KeycloakConfigConstructProps {
adminSecret: secretsManager.ISecret;
hostname: string;
configDir: string;
oAuthClientSecrets: Record<string, string>;
idpOauthClientSecrets: Record<string, string>;
createdOauthClients: string[];
}

type clientSecretTuple = Array<[string, secretsManager.ISecret]>;

export class KeycloakConfig extends Construct {
constructor(
scope: Construct,
Expand All @@ -32,22 +35,42 @@ export class KeycloakConfig extends Construct {
platform: ecrAssets.Platform.LINUX_AMD64,
});

const clientSecrets = Object.fromEntries(
Object.entries(props.oAuthClientSecrets)
.map(([clientSlug, secretArn]): [string, secretsManager.ISecret] => [
clientSlug,
secretsManager.Secret.fromSecretCompleteArn(
this,
`${clientSlug}-client-secret`,
secretArn
),
])
.flatMap(([clientSlug, secret]) =>
// Create a client secret for each private client
const createdClientSecrets: clientSecretTuple =
props.createdOauthClients.map((clientSlug) => [
clientSlug,
new secretsManager.Secret(this, `${clientSlug}-client-secret`, {
generateSecretString: {
excludePunctuation: true,
includeSpace: false,
secretStringTemplate: JSON.stringify({ id: clientSlug }),
generateStringKey: "secret",
passwordLength: 16,
},
}),
]);

// Import the client secrets for each public clients
const importedClientSecrets: clientSecretTuple = Object.entries(
props.idpOauthClientSecrets
).map(([clientSlug, secretArn]): [string, secretsManager.ISecret] => [
clientSlug,
secretsManager.Secret.fromSecretCompleteArn(
this,
`${clientSlug}-client-secret`,
secretArn
),
]);

// Create env vars from secrets for each client, e.g. GRAFANA_CLIENT_ID, GRAFANA_CLIENT_SECRET
const taskClientSecrets = Object.fromEntries(
[...createdClientSecrets, ...importedClientSecrets].flatMap(
([clientSlug, secret]) =>
["id", "secret"].map((key) => [
`${clientSlug}_client_${key}`.toUpperCase(),
`${clientSlug}_CLIENT_${key}`.toUpperCase(),
ecs.Secret.fromSecretsManager(secret, key),
])
)
)
);

configTaskDef.addContainer("ConfigContainer", {
Expand All @@ -71,7 +94,7 @@ export class KeycloakConfig extends Construct {
"password"
),
// Inject the client ID and secret for any OAuth clients
...clientSecrets,
...taskClientSecrets,
},
});

Expand Down
6 changes: 4 additions & 2 deletions deploy/lib/KeycloakStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export interface StackInputProps {
sslCertificateArn: string;
keycloakVersion: string;
configDir: string;
oAuthClientSecrets: Record<string, string>;
idpOauthClientSecrets: Record<string, string>;
createdOauthClients: string[];
}

interface StackProps extends cdk.StackProps, StackInputProps {
Expand Down Expand Up @@ -46,8 +47,9 @@ export class KeycloakStack extends cdk.Stack {
hostname: props.hostname,
subnetIds: vpc.publicSubnets.map((subnet) => subnet.subnetId),
adminSecret: adminSecret,
oAuthClientSecrets: props.oAuthClientSecrets,
configDir: props.configDir,
idpOauthClientSecrets: props.idpOauthClientSecrets,
createdOauthClients: props.createdOauthClients,
});

new KeycloakUrl(this, "url", {
Expand Down
11 changes: 11 additions & 0 deletions deploy/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Helper function to extract OAuth client secrets from the runtime environment
* @returns Record<string, string> - A map of OAuth client IDs to the ARN of their secrets
*/
export function getOauthSecrets(): Record<string, string> {
const oauthSecretPrefix = "IDP_SECRET_ARN_";
const clientSecrets = Object.entries(process.env)
.filter(([k, v]) => k.startsWith(oauthSecretPrefix))
.map(([k, v]) => [k.split(oauthSecretPrefix)[1], v]);
return Object.fromEntries(clientSecrets);
}

0 comments on commit 200446b

Please sign in to comment.