Skip to content

Commit 18fe07e

Browse files
committed
feat: generate 1-click signed login URL for OpenSearch UI
After setup, generates a SigV4-signed /_login/ URL that opens the Application directly in the browser without additional auth. URL expires in 5 minutes. Uses the same approach as cliu123/SigV4-Signer. Signed-off-by: Kyle Hounslow <kylhouns@amazon.com>
1 parent fb8b09b commit 18fe07e

1 file changed

Lines changed: 59 additions & 0 deletions

File tree

aws/cli-installer/src/main.mjs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { writeFileSync } from 'node:fs';
2+
import { createHash } from 'node:crypto';
23
import { parseCli, applySimpleDefaults, validateConfig, fillDryRunPlaceholders } from './cli.mjs';
34
import { renderPipeline } from './render.mjs';
45
import {
@@ -87,6 +88,54 @@ export async function run() {
8788
* Execute the full pipeline creation flow.
8889
* Shared by the CLI path (main.mjs) and the REPL create command.
8990
*/
91+
async function generateSignedLoginUrl(appEndpoint, region) {
92+
try {
93+
const { SignatureV4 } = await import('@aws-sdk/signature-v4');
94+
const { Sha256 } = await import('@aws-crypto/sha256-js');
95+
const { HttpRequest } = await import('@smithy/protocol-http');
96+
const { defaultProvider } = await import('@aws-sdk/credential-provider-node');
97+
98+
const parsed = new URL(appEndpoint);
99+
const signer = new SignatureV4({
100+
credentials: defaultProvider(),
101+
region,
102+
service: 'opensearch',
103+
sha256: Sha256,
104+
});
105+
106+
const request = new HttpRequest({
107+
method: 'GET',
108+
protocol: 'https:',
109+
hostname: parsed.hostname,
110+
path: '/_login/',
111+
headers: { host: parsed.hostname },
112+
});
113+
114+
const signed = await signer.sign(request);
115+
const auth = signed.headers['authorization'];
116+
const amzDate = signed.headers['x-amz-date'];
117+
const token = signed.headers['x-amz-security-token'];
118+
119+
let credential = '', signedHeaders = '', signature = '';
120+
for (const p of auth.split(', ')) {
121+
if (p.includes('Credential=')) credential = p.split('Credential=')[1];
122+
if (p.includes('SignedHeaders=')) signedHeaders = p.split('SignedHeaders=')[1];
123+
if (p.includes('Signature=')) signature = p.split('Signature=')[1];
124+
}
125+
126+
const params = new URLSearchParams({
127+
'X-Amz-Date': amzDate,
128+
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
129+
'X-Amz-Credential': credential,
130+
'X-Amz-SignedHeaders': signedHeaders,
131+
});
132+
if (token) params.set('X-Amz-Security-Token', token);
133+
params.set('X-Amz-Signature', signature);
134+
135+
return `https://${parsed.hostname}/_login/?${params.toString()}`;
136+
} catch { return null; }
137+
}
138+
90139
export async function executePipeline(cfg) {
91140
await checkRequirements(cfg);
92141
printSummary(cfg);
@@ -187,6 +236,16 @@ export async function executePipeline(cfg) {
187236
'',
188237
], { color: 'primary', padding: 2 });
189238

239+
// Generate a signed 1-click login URL (expires in 5 min)
240+
if (cfg.appEndpoint) {
241+
const signedUrl = await generateSignedLoginUrl(cfg.appEndpoint, cfg.region);
242+
if (signedUrl) {
243+
console.error();
244+
printInfo('1-click login URL (expires in 5 minutes):');
245+
console.error(` ${signedUrl}`);
246+
}
247+
}
248+
190249
}
191250

192251
// ── Summary ─────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)