Skip to content

Commit cc76411

Browse files
Finished implementing searchableDeviceKey modification in AAD.
1 parent a1d292d commit cc76411

File tree

12 files changed

+137
-93
lines changed

12 files changed

+137
-93
lines changed

Documentation/CHANGELOG.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ All notable changes to this project will be documented in this file. The format
55

66
## [Unreleased]
77

8+
### Changed
9+
10+
- The PowerShell module now advertizes `Desktop` as the required edition. Note that *PowerShell Core* is not supported because of heavy dependency on Win32 API.
11+
12+
## [4.4] - 2020-07-03
13+
814
### Added
915

1016
- The new [Set-AzureADUserEx](PowerShell/Set-AzureADUserEx.md#set-azureaduserex) cmdlet can be used to revoke FIDO2 and NGC keys in Azure Active Directory.
1117

12-
### Changed
13-
- The PowerShell module now advertizes `Desktop` as the required edition. Note that *PowerShell Core* is not supported because of heavy dependency on Win32 API.
14-
1518
## [4.3] - 2020-04-02
1619

1720
### Added
@@ -390,7 +393,8 @@ This is a [Chocolatey](https://chocolatey.org/packages/dsinternals-psmodule)-onl
390393
## 1.0 - 2015-01-20
391394
Initial release!
392395

393-
[Unreleased]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.3...HEAD
396+
[Unreleased]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.4...HEAD
397+
[4.4]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.3...v4.4
394398
[4.3]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.2...v4.3
395399
[4.2]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.1...v4.2
396400
[4.1]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.0...v4.1

Documentation/PowerShell/Get-AzureADUserEx.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,5 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
354354
355355
## RELATED LINKS
356356
357+
[Set-AzureADUserEx](Set-AzureADUserEx.md)
357358
[Get-ADKeyCredential](Get-ADKeyCredential.md)

Documentation/PowerShell/Set-AzureADUserEx.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ schema: 2.0.0
88
# Set-AzureADUserEx
99

1010
## SYNOPSIS
11-
Registers new or revokes existing FIDO and NGC keys in Azure Active Directory.
11+
Registers new or revokes existing FIDO2 and NGC keys in Azure Active Directory.
1212

1313
## SYNTAX
1414

@@ -25,16 +25,33 @@ Set-AzureADUserEx -KeyCredential <KeyCredential[]> -AccessToken <String> -Object
2525
```
2626

2727
## DESCRIPTION
28-
{{ Fill in the Description }}
28+
The Set-AzureADUserEx cmdlet uses an undocumented Azure AD Graph API endpoint to modify the normally hidden searchableDeviceKeys attribute of user accounts.
29+
This attribute holds different types of key credentials, including the FIDO2 and NGC keys that are used by Windows Hello for Business.
30+
31+
This cmdlet also enables Global Admins to selectively revoke security keys registered by other users. This is a unique feature, as Microsoft only supports self-service FIDO2 security key registration and revocation (at least at the time of publishing this cmdlet).
32+
33+
This cmdlet is not intended to replace the Set-AzureADUser cmdlet from Microsoft's AzureAD module. Authentication fully relies on the official Connect-AzureAD cmdlet.
2934

3035
## EXAMPLES
3136

3237
### Example 1
3338
```powershell
34-
PS C:\> {{ Add example code here }}
39+
PS C:\> Install-Module -Name AzureAD,DSInternals -Force
40+
PS C:\> Connect-AzureAD
41+
PS C:\> $token = [Microsoft.Open.Azure.AD.CommonLibrary.AzureSession]::AccessTokens['AccessToken'].AccessToken
42+
PS C:\> Set-AzureADUserEx -UserPrincipalName 'john@contoso.com' -KeyCredential @() -Token $token
3543
```
3644

37-
{{ Add example description here }}
45+
Revokes all FIDO2 security keys and NGC keys (Windows Hello for Business) that were previously registered by the specified user. Typical use case includes stolen devices and other security incidents.
46+
47+
### Example 2
48+
```powershell
49+
PS C:\> $user = Get-AzureADUserEx -UserPrincipalName 'john@contoso.com' -AccessToken $token
50+
PS C:\> $newCreds = $user.KeyCredentials | where { $PSItem.FidoKeyMaterial.DisplayName -notlike '*YubiKey*' }
51+
PS C:\> Set-AzureADUserEx -UserPrincipalName 'john@contoso.com' -KeyCredential $newCreds -Token $token
52+
```
53+
54+
Selectively revokes a specific FIDO2 security key based on its display name. Typical use case is a stolen/lost security key.
3855

3956
## PARAMETERS
4057

@@ -54,7 +71,7 @@ Accept wildcard characters: False
5471
```
5572
5673
### -KeyCredential
57-
{{ Fill KeyCredential Description }}
74+
Specifies a list of key credentials (typically FIDO2 and NGC keys) that can be used by the target user for authentication.
5875
5976
```yaml
6077
Type: KeyCredential[]
@@ -122,7 +139,11 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
122139
123140
## OUTPUTS
124141
125-
### System.Object
142+
### None
143+
126144
## NOTES
127145
128146
## RELATED LINKS
147+
148+
[Get-AzureADUserEx](Get-AzureADUserEx.md)
149+
[Get-ADKeyCredential](Get-ADKeyCredential.md)

Src/DSInternals.Common/AzureAD/AzureADClient.cs

Lines changed: 54 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ public async Task<AzureADUser> GetUserAsync(string userPrincipalName)
5252
Validator.AssertNotNullOrEmpty(userPrincipalName, nameof(userPrincipalName));
5353

5454
var filter = string.Format(CultureInfo.InvariantCulture, UPNFilterParameterFormat, userPrincipalName);
55-
return await GetUserAsync(filter, userPrincipalName);
55+
return await GetUserAsync(filter, userPrincipalName).ConfigureAwait(false);
5656
}
5757

5858
public async Task<AzureADUser> GetUserAsync(Guid objectId)
5959
{
6060
var filter = string.Format(CultureInfo.InvariantCulture, IdFilterParameterFormat, objectId);
61-
return await GetUserAsync(filter, objectId);
61+
return await GetUserAsync(filter, objectId).ConfigureAwait(false);
6262
}
6363

6464
private async Task<AzureADUser> GetUserAsync(string filterParameter, object userIdentifier)
@@ -97,53 +97,18 @@ public async Task<OdataPagedResponse<AzureADUser>> GetUsersAsync(string nextLink
9797
url.Append(UriParameterSeparator);
9898
url.Append(_batchSizeParameter);
9999

100-
// Perform API call
101-
try
100+
using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()))
102101
{
103-
using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()))
104-
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
105-
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
106-
using (var streamReader = new StreamReader(responseStream))
102+
// Perform API call
103+
var result = await SendODataRequest<OdataPagedResponse<AzureADUser>>(request).ConfigureAwait(false);
104+
105+
// Update key credential owner references
106+
if (result.Items != null)
107107
{
108-
if (s_odataContentType.MediaType.Equals(response.Content.Headers.ContentType.MediaType, StringComparison.InvariantCultureIgnoreCase))
109-
{
110-
// The response is a JSON document
111-
using (var jsonTextReader = new JsonTextReader(streamReader))
112-
{
113-
if (response.StatusCode == HttpStatusCode.OK)
114-
{
115-
var result = _jsonSerializer.Deserialize<OdataPagedResponse<AzureADUser>>(jsonTextReader);
116-
// Update key credential owner references
117-
if (result.Items != null)
118-
{
119-
result.Items.ForEach(user => user.UpdateKeyCredentialReferences());
120-
}
121-
return result;
122-
}
123-
else
124-
{
125-
// Translate OData response to an exception
126-
var error = _jsonSerializer.Deserialize<OdataErrorResponse>(jsonTextReader);
127-
throw error.GetException();
128-
}
129-
}
130-
}
131-
else
132-
{
133-
// The response is not a JSON document, so we parse its first line as message text
134-
string message = await streamReader.ReadLineAsync().ConfigureAwait(false);
135-
throw new GraphApiException(message, response.StatusCode.ToString());
136-
}
108+
result.Items.ForEach(user => user.UpdateKeyCredentialReferences());
137109
}
138-
}
139-
catch (JsonException e)
140-
{
141-
throw new GraphApiException("The data returned by the REST API call has an unexpected format.", e);
142-
}
143-
catch (HttpRequestException e)
144-
{
145-
// Unpack a more meaningful message, e. g. DNS error
146-
throw new GraphApiException(e?.InnerException.Message ?? "An error occured while trying to call the REST API.", e);
110+
111+
return result;
147112
}
148113
}
149114

@@ -153,13 +118,13 @@ public async Task SetUserAsync(string userPrincipalName, KeyCredential[] keyCred
153118
Validator.AssertNotNullOrEmpty(userPrincipalName, nameof(userPrincipalName));
154119

155120
var properties = new Hashtable() { { KeyCredentialAttributeName, keyCredentials } };
156-
await SetUserAsync(userPrincipalName, properties);
121+
await SetUserAsync(userPrincipalName, properties).ConfigureAwait(false);
157122
}
158123

159124
public async Task SetUserAsync(Guid objectId, KeyCredential[] keyCredentials)
160125
{
161126
var properties = new Hashtable() { { KeyCredentialAttributeName, keyCredentials } };
162-
await SetUserAsync(objectId.ToString(), properties);
127+
await SetUserAsync(objectId.ToString(), properties).ConfigureAwait(false);
163128
}
164129

165130
private async Task SetUserAsync(string userIdentifier, Hashtable properties)
@@ -169,20 +134,52 @@ private async Task SetUserAsync(string userIdentifier, Hashtable properties)
169134
url.AppendFormat(CultureInfo.InvariantCulture, UsersUrlFormat, _tenantId, userIdentifier);
170135
url.Append(ApiVersionParameter);
171136

172-
// Perform API call
137+
// TODO: Switch to HttpMethod.Patch after migrating to .NET Standard 2.1 / .NET 5
138+
using (var request = new HttpRequestMessage(new HttpMethod("PATCH"), url.ToString()))
139+
{
140+
request.Content = new StringContent(JsonConvert.SerializeObject(properties), Encoding.UTF8, JsonContentType);
141+
await SendODataRequest<object>(request).ConfigureAwait(false);
142+
}
143+
}
144+
145+
private async Task<T> SendODataRequest<T>(HttpRequestMessage request)
146+
{
173147
try
174148
{
175-
// TODO: Switch to HttpMethod.Patch after migrating to .NET Standard 2.1 / .NET 5
176-
using (var request = new HttpRequestMessage(new HttpMethod("PATCH"), url.ToString()))
149+
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
177150
{
178-
// Build the request body
179-
request.Content = new StringContent(JsonConvert.SerializeObject(properties), Encoding.UTF8, JsonContentType);
151+
if(response.StatusCode == HttpStatusCode.NoContent)
152+
{
153+
// No objects have been returned, but the call was successful.
154+
return default(T);
155+
}
180156

181-
// Send the request
182-
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
157+
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
158+
using (var streamReader = new StreamReader(responseStream))
183159
{
184-
// TODO: Error handling
185-
// response.StatusCode;
160+
if (s_odataContentType.MediaType.Equals(response.Content.Headers.ContentType.MediaType, StringComparison.InvariantCultureIgnoreCase))
161+
{
162+
// The response is a JSON document
163+
using (var jsonTextReader = new JsonTextReader(streamReader))
164+
{
165+
if (response.StatusCode == HttpStatusCode.OK)
166+
{
167+
return _jsonSerializer.Deserialize<T>(jsonTextReader);
168+
}
169+
else
170+
{
171+
// Translate OData response to an exception
172+
var error = _jsonSerializer.Deserialize<OdataErrorResponse>(jsonTextReader);
173+
throw error.GetException();
174+
}
175+
}
176+
}
177+
else
178+
{
179+
// The response is not a JSON document, so we parse its first line as message text
180+
string message = await streamReader.ReadLineAsync().ConfigureAwait(false);
181+
throw new GraphApiException(message, response.StatusCode.ToString());
182+
}
186183
}
187184
}
188185
}

Src/DSInternals.Common/DSInternals.Common.nuspec

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
<description>This package is shared between all other DSInternals packages. Its main features are Azure AD Graph API and ADSI clients for for retrieval of cryptographic material. It contains implementations of common hash functions used by Windows, including NT hash, LM hash and OrgId hash. It also contains methods for SysKey/BootKey retrieval.</description>
1515
<summary>This package is shared between all other DSInternals packages.</summary>
1616
<releaseNotes>
17-
- Added the the AzureADClient class for FIDO2 and NGC key retrieval from Azure Active Directory.
18-
- Both LastLogon and LastLogonTimestamp properties are now exposed on AD user accounts.
19-
- Updated the package logo.
17+
- Added the ability to modify FIDO2 and NGC keys registered in Azure Active Directory.
2018
</releaseNotes>
2119
<copyright>Copyright (c) 2015-2020 Michael Grafnetter. All rights reserved.</copyright>
2220
<tags>ActiveDirectory Security AD AAD Identity Active Directory</tags>

Src/DSInternals.Common/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
// set of attributes. Change these attribute values to modify the information
66
// associated with an assembly.
77
[assembly: AssemblyTitle("DSInternals Common Library")]
8-
[assembly: AssemblyVersion("4.3")]
9-
[assembly: AssemblyFileVersion("4.3")]
8+
[assembly: AssemblyVersion("4.4")]
9+
[assembly: AssemblyFileVersion("4.4")]
1010
[assembly: AssemblyDescription("")]
1111
[assembly: AssemblyConfiguration("")]
1212
[assembly: AssemblyCompany("")]

Src/DSInternals.PowerShell/Chocolatey/dsinternals-psmodule.nuspec

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
44
<metadata>
55
<id>DSInternals-PSModule</id>
6-
<version>4.3</version>
6+
<version>4.4</version>
77
<packageSourceUrl>https://github.com/MichaelGrafnetter/DSInternals/tree/master/Src/DSInternals.PowerShell/Chocolatey</packageSourceUrl>
88
<owners>MichaelGrafnetter</owners>
99
<title>DSInternals PowerShell Module</title>
@@ -37,10 +37,7 @@
3737
## Disclaimer
3838
Features exposed through these tools are not supported by Microsoft. Improper use might cause irreversible damage to domain controllers or negatively impact domain security.</description>
3939
<releaseNotes>
40-
* Added the Get-AzureADUserEx cmdlet for FIDO2 and NGC key auditing in Azure Active Directory.
41-
* Both LastLogon and LastLogonTimestamp properties are now exposed on user accounts.
42-
* Improved display format of FIDO2 keys.
43-
* Updated the package logo.
40+
* Added the Set-AzureADUserEx cmdlet for administrative FIDO2 security key revocation in Azure Active Directory.
4441
</releaseNotes>
4542
<dependencies>
4643
<!-- Windows Management Framework 3+. For OS prior to Windows 8 and Windows Server 2012. -->

Src/DSInternals.PowerShell/Commands/AzureAD/SetAzureADUserExCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace DSInternals.PowerShell.Commands
55
{
66
[Cmdlet(VerbsCommon.Set, "AzureADUserEx", DefaultParameterSetName = ParamSetSingleUserUPN)]
7+
[OutputType("None")]
78
public class SetAzureADUserExCommand : AzureADCommandBase
89
{
910
[Parameter(Mandatory = true)]

Src/DSInternals.PowerShell/DSInternals.psd1

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
RootModule = 'DSInternals.Bootstrap.psm1'
99

1010
# Version number of this module.
11-
ModuleVersion = '4.3'
11+
ModuleVersion = '4.4'
1212

1313
# Supported PSEditions
1414
# CompatiblePSEditions = 'Desktop'
@@ -141,10 +141,7 @@ PrivateData = @{
141141

142142
# ReleaseNotes of this module
143143
ReleaseNotes = @"
144-
- Added the Get-AzureADUserEx cmdlet for FIDO2 and NGC key auditing in Azure Active Directory.
145-
- Both LastLogon and LastLogonTimestamp properties are now exposed on user accounts.
146-
- Improved display format of FIDO2 keys.
147-
- Updated the package logo.
144+
- Added the Set-AzureADUserEx cmdlet for administrative FIDO2 security key revocation in Azure Active Directory.
148145
"@
149146
} # End of PSData hashtable
150147

Src/DSInternals.PowerShell/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
// set of attributes. Change these attribute values to modify the information
66
// associated with an assembly.
77
[assembly: AssemblyTitle("DSInternals PowerShell Commands")]
8-
[assembly: AssemblyVersion("4.3")]
9-
[assembly: AssemblyFileVersion("4.3")]
8+
[assembly: AssemblyVersion("4.4")]
9+
[assembly: AssemblyFileVersion("4.4")]
1010
[assembly: AssemblyDescription("")]
1111
[assembly: AssemblyConfiguration("")]
1212
[assembly: AssemblyCompany("")]

0 commit comments

Comments
 (0)