-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[core-amqp][event-hubs] add support for NamedKeyCredential and SASCre…
…dential (#14423) Adds support for `NamedKeyCredential` and `SASCredential` to `@azure/event-hubs`. ## Background Event Hubs supports using token-based AAD auth via the `@azure/identity` credentials, and both "shared access key" and "shared access signature" via the connection string. This change allows the `EventHubConsumerClient` and `EventHubProducerClient` to support both "shared access key" and "shared access signature" methods of auth via credential objects defined by `@azure/core-auth`. ## It already supports connection strings, so why add 2 more credential types? Both the `AzureNamedKeyCredential` and `AzureSASCredential` classes support rotation by calling their respective `update` methods. Since connection strings can't be rotated within a client, the user would need to close their existing clients (which may be in the process of sending or receiving events!) and create new ones if their keys were rotated. With this change, the user only needs to call `update` on the credential they passed to their Event Hubs client, and the client will automatically use the new values. ## Why is this change in core-amqp as well? Service Bus and Event Hubs both need these changes. Note that the APIs added to core-amqp have the `@hidden` tag so they won't show up in documentation. ## TODO - [x] verify that the SDK retries 'unauthorized' errors. This, and the token expiry, will determine when the clients attempt to use the rotated credentials.
- Loading branch information
Showing
21 changed files
with
864 additions
and
248 deletions.
There are no files selected for viewing
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,123 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
import { | ||
AccessToken, | ||
NamedKeyCredential, | ||
SASCredential, | ||
isNamedKeyCredential, | ||
isSASCredential | ||
} from "@azure/core-auth"; | ||
import jssha from "jssha"; | ||
import { isObjectWithProperties } from "../util/typeGuards"; | ||
|
||
/** | ||
* A SasTokenProvider provides an alternative to TokenCredential for providing an `AccessToken`. | ||
* @hidden | ||
*/ | ||
export interface SasTokenProvider { | ||
/** | ||
* Property used to distinguish SasTokenProvider from TokenCredential. | ||
*/ | ||
isSasTokenProvider: true; | ||
/** | ||
* Gets the token provided by this provider. | ||
* | ||
* This method is called automatically by Azure SDK client libraries. | ||
* | ||
* @param audience - The audience for which the token is desired. | ||
*/ | ||
getToken(audience: string): AccessToken; | ||
} | ||
|
||
/** | ||
* Creates a token provider from the provided shared access data. | ||
* @param data - The sharedAccessKeyName/sharedAccessKey pair or the sharedAccessSignature. | ||
* @hidden | ||
*/ | ||
export function createSasTokenProvider( | ||
data: | ||
| { sharedAccessKeyName: string; sharedAccessKey: string } | ||
| { sharedAccessSignature: string } | ||
| NamedKeyCredential | ||
| SASCredential | ||
): SasTokenProvider { | ||
if (isNamedKeyCredential(data) || isSASCredential(data)) { | ||
return new SasTokenProviderImpl(data); | ||
} else if (isObjectWithProperties(data, ["sharedAccessKeyName", "sharedAccessKey"])) { | ||
return new SasTokenProviderImpl({ name: data.sharedAccessKeyName, key: data.sharedAccessKey }); | ||
} else { | ||
return new SasTokenProviderImpl({ signature: data.sharedAccessSignature }); | ||
} | ||
} | ||
|
||
/** | ||
* A TokenProvider that generates a Sas token: | ||
* `SharedAccessSignature sr=<resource>&sig=<signature>&se=<expiry>&skn=<keyname>` | ||
* | ||
* @internal | ||
*/ | ||
export class SasTokenProviderImpl implements SasTokenProvider { | ||
/** | ||
* Property used to distinguish TokenProvider from TokenCredential. | ||
*/ | ||
get isSasTokenProvider(): true { | ||
return true; | ||
} | ||
|
||
/** | ||
* The SASCredential containing the key name and secret key value. | ||
*/ | ||
private _credential: SASCredential | NamedKeyCredential; | ||
|
||
/** | ||
* Initializes a new instance of SasTokenProvider | ||
* @param credential - The source `NamedKeyCredential` or `SASCredential`. | ||
*/ | ||
constructor(credential: SASCredential | NamedKeyCredential) { | ||
this._credential = credential; | ||
} | ||
|
||
/** | ||
* Gets the sas token for the specified audience | ||
* @param audience - The audience for which the token is desired. | ||
*/ | ||
getToken(audience: string): AccessToken { | ||
if (isNamedKeyCredential(this._credential)) { | ||
return createToken( | ||
this._credential.name, | ||
this._credential.key, | ||
Math.floor(Date.now() / 1000) + 3600, | ||
audience | ||
); | ||
} else { | ||
return { | ||
token: this._credential.signature, | ||
expiresOnTimestamp: 0 | ||
}; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Creates the sas token based on the provided information. | ||
* @param keyName - The shared access key name. | ||
* @param key - The shared access key. | ||
* @param expiry - The time period in unix time after which the token will expire. | ||
* @param audience - The audience for which the token is desired. | ||
* @internal | ||
*/ | ||
function createToken(keyName: string, key: string, expiry: number, audience: string): AccessToken { | ||
audience = encodeURIComponent(audience); | ||
keyName = encodeURIComponent(keyName); | ||
const stringToSign = audience + "\n" + expiry; | ||
|
||
const shaObj = new jssha("SHA-256", "TEXT"); | ||
shaObj.setHMACKey(key, "TEXT"); | ||
shaObj.update(stringToSign); | ||
const sig = encodeURIComponent(shaObj.getHMAC("B64")); | ||
return { | ||
token: `SharedAccessSignature sr=${audience}&sig=${sig}&se=${expiry}&skn=${keyName}`, | ||
expiresOnTimestamp: expiry | ||
}; | ||
} |
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,7 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
import { SasTokenProvider, createSasTokenProvider } from "./auth/tokenProvider"; | ||
import { isSasTokenProvider } from "./util/typeGuards"; | ||
|
||
export { SasTokenProvider, createSasTokenProvider, isSasTokenProvider }; |
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,66 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
import chai from "chai"; | ||
const should = chai.should(); | ||
import { AzureNamedKeyCredential, AzureSASCredential } from "@azure/core-auth"; | ||
import { createSasTokenProvider } from "../src/index"; | ||
|
||
describe("SasTokenProvider", function(): void { | ||
describe("createSasTokenProvider", () => { | ||
it("should work as expected with AzureNamedKeyCredential", async function(): Promise<void> { | ||
const keyName = "myKeyName"; | ||
const key = "importantValue"; | ||
const tokenProvider = createSasTokenProvider(new AzureNamedKeyCredential(keyName, key)); | ||
const now = Math.floor(Date.now() / 1000) + 3600; | ||
const tokenInfo = tokenProvider.getToken("myaudience"); | ||
tokenInfo.token.should.match( | ||
/SharedAccessSignature sr=myaudience&sig=(.*)&se=\d{10}&skn=myKeyName/g | ||
); | ||
tokenInfo.expiresOnTimestamp.should.equal(now); | ||
}); | ||
|
||
it("should work as expected with `shareAccessKeyName` and `sharedAccessKey`", async function(): Promise< | ||
void | ||
> { | ||
// This is how createSasTokenProvider will be called if SAK params are passed through a connection string. | ||
const tokenProvider = createSasTokenProvider({ | ||
sharedAccessKeyName: "sakName", | ||
sharedAccessKey: "sak" | ||
}); | ||
const now = Math.floor(Date.now() / 1000) + 3600; | ||
const tokenInfo = tokenProvider.getToken("sb://hostname.servicebus.windows.net/"); | ||
tokenInfo.token.should.match( | ||
/SharedAccessSignature sr=sb%3A%2F%2Fhostname.servicebus.windows.net%2F&sig=(.*)&se=\d{10}&skn=sakName/g | ||
); | ||
tokenInfo.expiresOnTimestamp.should.equal(now); | ||
}); | ||
}); | ||
|
||
it("should work as expected with AzureSASCredential", async function(): Promise<void> { | ||
const sasTokenProvider = createSasTokenProvider( | ||
new AzureSASCredential("SharedAccessSignature se=<blah>") | ||
); | ||
const accessToken = sasTokenProvider.getToken("audience isn't used"); | ||
|
||
should.equal( | ||
accessToken.token, | ||
"SharedAccessSignature se=<blah>", | ||
"SAS URI we were constructed with should just be returned verbatim without interpretation (and the audience is ignored)" | ||
); | ||
|
||
should.equal( | ||
accessToken.expiresOnTimestamp, | ||
0, | ||
"SAS URI always returns 0 for expiry (ignoring what's in the SAS token)" | ||
); | ||
}); | ||
|
||
it("should work as expected with `sharedAccessSignature`", async function(): Promise<void> { | ||
// This is how createSasTokenProvider will be called if the shared access signature is passed through a connection string. | ||
const tokenProvider = createSasTokenProvider({ sharedAccessSignature: "<blah>" }); | ||
const tokenInfo = tokenProvider.getToken("sb://hostname.servicebus.windows.net/"); | ||
tokenInfo.token.should.match(/<blah>/g); | ||
tokenInfo.expiresOnTimestamp.should.equal(0); | ||
}); | ||
}); |
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.