forked from Azure/azure-sdk-for-js
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[test-credential] Credential relay for browser tests (Azure#29616)
### Packages impacted by this PR - `@azure-tools/dev-tool` - `@azure-tools/test-credential` ### Describe the problem that is addressed by this PR We want to move to user auth and `DefaultAzureCredential` to authenticate our tests, but `DefaultAzureCredential` does not work in the browser, blocking this transition. This PR proposes a solution -- create a short-lived, local-only, server which can create tokens in Node using DefaultAzureCredential. In the browser, createTestCredential provides a credential implementation which calls this server whenever it needs a token, and the server relays the request to DefaultAzureCredential. Here's what's changed: - `test-credential` now returns a credential which requests tokens from a local web endpoint when running browser tests against a live service. - `dev-tool` contains an implementation of this web endpoint. The dev-tool scripts used to run browser tests now start this server automatically when running browser tests. A separate command to start the server is also provided for pipelines. - Update live test pipelines to start the server when running browser tests. ### Provide a list of related PRs _(if any)_ - Draft version with loads of commits and `/azp run` spam: Azure#29581 - Harsha's earlier PR to enable DefaultAzureCredential: Azure#29577
- Loading branch information
Showing
14 changed files
with
787 additions
and
408 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
common/tools/dev-tool/src/commands/run/startBrowserRelay.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license | ||
|
||
import { leafCommand, makeCommandInfo } from "../../framework/command"; | ||
|
||
import { startRelayServer } from "../../util/browserRelayServer"; | ||
|
||
export const commandInfo = makeCommandInfo( | ||
"start-browser-relay", | ||
"Start the browser credential relay, used for authenticating browser tests.", | ||
{ | ||
listenHost: { | ||
kind: "string", | ||
default: "localhost", | ||
description: "Host to listen on", | ||
}, | ||
port: { | ||
kind: "string", | ||
default: "4895", | ||
description: "Port to listen on", | ||
}, | ||
}, | ||
); | ||
|
||
export default leafCommand(commandInfo, async (options) => { | ||
startRelayServer({ | ||
listenHost: options.listenHost, | ||
port: Number(options.port), | ||
}); | ||
|
||
return true; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
import express from "express"; | ||
import type { Express } from "express-serve-static-core"; | ||
import { DefaultAzureCredential, type TokenCredential } from "@azure/identity"; | ||
import { randomUUID } from "node:crypto"; | ||
import { createPrinter } from "./printer"; | ||
|
||
const printer = createPrinter("browser-relay"); | ||
|
||
export interface TestCredentialServerOptions { | ||
/** | ||
* Port to listen on. Defaults to 4895. | ||
*/ | ||
port?: number; | ||
|
||
/** | ||
* Host to listen on. Defaults to `localhost`. Caution: do not expose this server to the network. | ||
*/ | ||
listenHost?: string; | ||
} | ||
|
||
function isValidScopes(scopes: unknown): scopes is string | string[] { | ||
return ( | ||
typeof scopes === "string" || | ||
(Array.isArray(scopes) && scopes.every((s) => typeof s === "string")) | ||
); | ||
} | ||
|
||
function buildServer(app: Express) { | ||
const credentials: Record<string, TokenCredential> = {}; | ||
|
||
app.use(express.json()); | ||
app.use((_req, res, next) => { | ||
res.set("Access-Control-Allow-Methods", "GET, PUT"); | ||
res.set("Access-Control-Allow-Origin", "*"); | ||
next(); | ||
}); | ||
|
||
app.get("/health", (_req, res) => { | ||
res.status(204).send(); | ||
}); | ||
|
||
// Endpoint for creating a new credential | ||
app.put("/credential", (req, res) => { | ||
const id = randomUUID(); | ||
try { | ||
const cred = new DefaultAzureCredential(req.body); | ||
credentials[id] = cred; | ||
res.status(201).send({ id }); | ||
} catch (error: unknown) { | ||
res.status(400).send({ error }); | ||
return; | ||
} | ||
}); | ||
|
||
// Endpoint for getting a token using a pre-created credential | ||
app.get("/credential/:id/token", async (req, res) => { | ||
const credential = credentials[req.params.id]; | ||
if (!credential) { | ||
res.status(404).send({ error: "Credential not found, create a credential first" }); | ||
return; | ||
} | ||
|
||
const scopes = req.query["scopes"]; | ||
|
||
if (!isValidScopes(scopes)) { | ||
res.status(400).send({ error: "Scopes must be provided" }); | ||
return; | ||
} | ||
|
||
const options = JSON.parse(req.query["options"]?.toString() ?? "{}"); | ||
|
||
try { | ||
const token = await credential.getToken(scopes, options); | ||
res.status(200).send(token); | ||
} catch (error: unknown) { | ||
res.status(400).send({ error }); | ||
} | ||
}); | ||
} | ||
|
||
async function isRelayAlive(options: TestCredentialServerOptions = {}): Promise<boolean> { | ||
try { | ||
const res = await fetch( | ||
`http://${options.listenHost ?? "localhost"}:${options.port ?? 4895}/health`, | ||
); | ||
|
||
if (res.ok) { | ||
printer("Browser relay is already alive"); | ||
return true; | ||
} else { | ||
throw new Error(`Browser relay responded with an error: ${await res.text()}`); | ||
} | ||
} catch (e) { | ||
printer("Browser relay is not yet alive"); | ||
return false; | ||
} | ||
} | ||
|
||
export async function shouldStartRelay( | ||
options: TestCredentialServerOptions = {}, | ||
): Promise<boolean> { | ||
const testMode = (process.env.TEST_MODE ?? "playback").toLowerCase(); | ||
if (testMode !== "record" && testMode !== "live") { | ||
printer("Not in record or live mode; not starting relay"); | ||
return false; | ||
} | ||
|
||
return !(await isRelayAlive(options)); | ||
} | ||
|
||
/** | ||
* Create and start the relay server used by test credential to provide credentials to the browser tests. | ||
* @param options Options for the relay server. | ||
* @returns A callback which, when called, will stop the server. | ||
*/ | ||
export function startRelayServer(options: TestCredentialServerOptions = {}): () => void { | ||
const app = express(); | ||
buildServer(app); | ||
|
||
const { listenHost = "localhost", port = 4895 } = options; | ||
|
||
printer(`Starting browser relay on http://${listenHost}:${port}/`); | ||
const server = app.listen(port, listenHost); | ||
return () => server.close(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.