Finished implementing searchableDeviceKey modification in AAD.

This commit is contained in:
Michael Grafnetter 2020-07-03 21:43:04 +02:00
parent a1d292d5c0
commit cc76411c7e
12 changed files with 138 additions and 94 deletions

View File

@ -5,13 +5,16 @@ All notable changes to this project will be documented in this file. The format
## [Unreleased]
### Changed
- The PowerShell module now advertizes `Desktop` as the required edition. Note that *PowerShell Core* is not supported because of heavy dependency on Win32 API.
## [4.4] - 2020-07-03
### Added
- The new [Set-AzureADUserEx](PowerShell/Set-AzureADUserEx.md#set-azureaduserex) cmdlet can be used to revoke FIDO2 and NGC keys in Azure Active Directory.
### Changed
- The PowerShell module now advertizes `Desktop` as the required edition. Note that *PowerShell Core* is not supported because of heavy dependency on Win32 API.
## [4.3] - 2020-04-02
### Added
@ -390,7 +393,8 @@ This is a [Chocolatey](https://chocolatey.org/packages/dsinternals-psmodule)-onl
## 1.0 - 2015-01-20
Initial release!
[Unreleased]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.3...HEAD
[Unreleased]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.4...HEAD
[4.4]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.3...v4.4
[4.3]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.2...v4.3
[4.2]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.1...v4.2
[4.1]: https://github.com/MichaelGrafnetter/DSInternals/compare/v4.0...v4.1

View File

@ -354,4 +354,5 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
## RELATED LINKS
[Set-AzureADUserEx](Set-AzureADUserEx.md)
[Get-ADKeyCredential](Get-ADKeyCredential.md)

View File

@ -8,7 +8,7 @@ schema: 2.0.0
# Set-AzureADUserEx
## SYNOPSIS
Registers new or revokes existing FIDO and NGC keys in Azure Active Directory.
Registers new or revokes existing FIDO2 and NGC keys in Azure Active Directory.
## SYNTAX
@ -25,16 +25,33 @@ Set-AzureADUserEx -KeyCredential <KeyCredential[]> -AccessToken <String> -Object
```
## DESCRIPTION
{{ Fill in the Description }}
The Set-AzureADUserEx cmdlet uses an undocumented Azure AD Graph API endpoint to modify the normally hidden searchableDeviceKeys attribute of user accounts.
This attribute holds different types of key credentials, including the FIDO2 and NGC keys that are used by Windows Hello for Business.
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).
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.
## EXAMPLES
### Example 1
```powershell
PS C:\> {{ Add example code here }}
PS C:\> Install-Module -Name AzureAD,DSInternals -Force
PS C:\> Connect-AzureAD
PS C:\> $token = [Microsoft.Open.Azure.AD.CommonLibrary.AzureSession]::AccessTokens['AccessToken'].AccessToken
PS C:\> Set-AzureADUserEx -UserPrincipalName 'john@contoso.com' -KeyCredential @() -Token $token
```
{{ Add example description here }}
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.
### Example 2
```powershell
PS C:\> $user = Get-AzureADUserEx -UserPrincipalName 'john@contoso.com' -AccessToken $token
PS C:\> $newCreds = $user.KeyCredentials | where { $PSItem.FidoKeyMaterial.DisplayName -notlike '*YubiKey*' }
PS C:\> Set-AzureADUserEx -UserPrincipalName 'john@contoso.com' -KeyCredential $newCreds -Token $token
```
Selectively revokes a specific FIDO2 security key based on its display name. Typical use case is a stolen/lost security key.
## PARAMETERS
@ -54,7 +71,7 @@ Accept wildcard characters: False
```
### -KeyCredential
{{ Fill KeyCredential Description }}
Specifies a list of key credentials (typically FIDO2 and NGC keys) that can be used by the target user for authentication.
```yaml
Type: KeyCredential[]
@ -122,7 +139,11 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
## OUTPUTS
### System.Object
### None
## NOTES
## RELATED LINKS
[Get-AzureADUserEx](Get-AzureADUserEx.md)
[Get-ADKeyCredential](Get-ADKeyCredential.md)

View File

@ -52,13 +52,13 @@ namespace DSInternals.Common.AzureAD
Validator.AssertNotNullOrEmpty(userPrincipalName, nameof(userPrincipalName));
var filter = string.Format(CultureInfo.InvariantCulture, UPNFilterParameterFormat, userPrincipalName);
return await GetUserAsync(filter, userPrincipalName);
return await GetUserAsync(filter, userPrincipalName).ConfigureAwait(false);
}
public async Task<AzureADUser> GetUserAsync(Guid objectId)
{
var filter = string.Format(CultureInfo.InvariantCulture, IdFilterParameterFormat, objectId);
return await GetUserAsync(filter, objectId);
return await GetUserAsync(filter, objectId).ConfigureAwait(false);
}
private async Task<AzureADUser> GetUserAsync(string filterParameter, object userIdentifier)
@ -97,53 +97,18 @@ namespace DSInternals.Common.AzureAD
url.Append(UriParameterSeparator);
url.Append(_batchSizeParameter);
// Perform API call
try
using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()))
{
using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()))
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (var streamReader = new StreamReader(responseStream))
// Perform API call
var result = await SendODataRequest<OdataPagedResponse<AzureADUser>>(request).ConfigureAwait(false);
// Update key credential owner references
if (result.Items != null)
{
if (s_odataContentType.MediaType.Equals(response.Content.Headers.ContentType.MediaType, StringComparison.InvariantCultureIgnoreCase))
{
// The response is a JSON document
using (var jsonTextReader = new JsonTextReader(streamReader))
{
if (response.StatusCode == HttpStatusCode.OK)
{
var result = _jsonSerializer.Deserialize<OdataPagedResponse<AzureADUser>>(jsonTextReader);
// Update key credential owner references
if (result.Items != null)
{
result.Items.ForEach(user => user.UpdateKeyCredentialReferences());
}
return result;
}
else
{
// Translate OData response to an exception
var error = _jsonSerializer.Deserialize<OdataErrorResponse>(jsonTextReader);
throw error.GetException();
}
}
}
else
{
// The response is not a JSON document, so we parse its first line as message text
string message = await streamReader.ReadLineAsync().ConfigureAwait(false);
throw new GraphApiException(message, response.StatusCode.ToString());
}
result.Items.ForEach(user => user.UpdateKeyCredentialReferences());
}
}
catch (JsonException e)
{
throw new GraphApiException("The data returned by the REST API call has an unexpected format.", e);
}
catch (HttpRequestException e)
{
// Unpack a more meaningful message, e. g. DNS error
throw new GraphApiException(e?.InnerException.Message ?? "An error occured while trying to call the REST API.", e);
return result;
}
}
@ -153,13 +118,13 @@ namespace DSInternals.Common.AzureAD
Validator.AssertNotNullOrEmpty(userPrincipalName, nameof(userPrincipalName));
var properties = new Hashtable() { { KeyCredentialAttributeName, keyCredentials } };
await SetUserAsync(userPrincipalName, properties);
await SetUserAsync(userPrincipalName, properties).ConfigureAwait(false);
}
public async Task SetUserAsync(Guid objectId, KeyCredential[] keyCredentials)
{
var properties = new Hashtable() { { KeyCredentialAttributeName, keyCredentials } };
await SetUserAsync(objectId.ToString(), properties);
await SetUserAsync(objectId.ToString(), properties).ConfigureAwait(false);
}
private async Task SetUserAsync(string userIdentifier, Hashtable properties)
@ -169,20 +134,52 @@ namespace DSInternals.Common.AzureAD
url.AppendFormat(CultureInfo.InvariantCulture, UsersUrlFormat, _tenantId, userIdentifier);
url.Append(ApiVersionParameter);
// Perform API call
// TODO: Switch to HttpMethod.Patch after migrating to .NET Standard 2.1 / .NET 5
using (var request = new HttpRequestMessage(new HttpMethod("PATCH"), url.ToString()))
{
request.Content = new StringContent(JsonConvert.SerializeObject(properties), Encoding.UTF8, JsonContentType);
await SendODataRequest<object>(request).ConfigureAwait(false);
}
}
private async Task<T> SendODataRequest<T>(HttpRequestMessage request)
{
try
{
// TODO: Switch to HttpMethod.Patch after migrating to .NET Standard 2.1 / .NET 5
using (var request = new HttpRequestMessage(new HttpMethod("PATCH"), url.ToString()))
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
// Build the request body
request.Content = new StringContent(JsonConvert.SerializeObject(properties), Encoding.UTF8, JsonContentType);
// Send the request
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
if(response.StatusCode == HttpStatusCode.NoContent)
{
// TODO: Error handling
// response.StatusCode;
// No objects have been returned, but the call was successful.
return default(T);
}
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (var streamReader = new StreamReader(responseStream))
{
if (s_odataContentType.MediaType.Equals(response.Content.Headers.ContentType.MediaType, StringComparison.InvariantCultureIgnoreCase))
{
// The response is a JSON document
using (var jsonTextReader = new JsonTextReader(streamReader))
{
if (response.StatusCode == HttpStatusCode.OK)
{
return _jsonSerializer.Deserialize<T>(jsonTextReader);
}
else
{
// Translate OData response to an exception
var error = _jsonSerializer.Deserialize<OdataErrorResponse>(jsonTextReader);
throw error.GetException();
}
}
}
else
{
// The response is not a JSON document, so we parse its first line as message text
string message = await streamReader.ReadLineAsync().ConfigureAwait(false);
throw new GraphApiException(message, response.StatusCode.ToString());
}
}
}
}

View File

@ -14,9 +14,7 @@
<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>
<summary>This package is shared between all other DSInternals packages.</summary>
<releaseNotes>
- Added the the AzureADClient class for FIDO2 and NGC key retrieval from Azure Active Directory.
- Both LastLogon and LastLogonTimestamp properties are now exposed on AD user accounts.
- Updated the package logo.
- Added the ability to modify FIDO2 and NGC keys registered in Azure Active Directory.
</releaseNotes>
<copyright>Copyright (c) 2015-2020 Michael Grafnetter. All rights reserved.</copyright>
<tags>ActiveDirectory Security AD AAD Identity Active Directory</tags>

View File

@ -5,8 +5,8 @@ using System.Runtime.InteropServices;
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("DSInternals Common Library")]
[assembly: AssemblyVersion("4.3")]
[assembly: AssemblyFileVersion("4.3")]
[assembly: AssemblyVersion("4.4")]
[assembly: AssemblyFileVersion("4.4")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]

View File

@ -3,7 +3,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<id>DSInternals-PSModule</id>
<version>4.3</version>
<version>4.4</version>
<packageSourceUrl>https://github.com/MichaelGrafnetter/DSInternals/tree/master/Src/DSInternals.PowerShell/Chocolatey</packageSourceUrl>
<owners>MichaelGrafnetter</owners>
<title>DSInternals PowerShell Module</title>
@ -37,10 +37,7 @@
## Disclaimer
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>
<releaseNotes>
* Added the Get-AzureADUserEx cmdlet for FIDO2 and NGC key auditing in Azure Active Directory.
* Both LastLogon and LastLogonTimestamp properties are now exposed on user accounts.
* Improved display format of FIDO2 keys.
* Updated the package logo.
* Added the Set-AzureADUserEx cmdlet for administrative FIDO2 security key revocation in Azure Active Directory.
</releaseNotes>
<dependencies>
<!-- Windows Management Framework 3+. For OS prior to Windows 8 and Windows Server 2012. -->

View File

@ -4,6 +4,7 @@ using DSInternals.Common.Data;
namespace DSInternals.PowerShell.Commands
{
[Cmdlet(VerbsCommon.Set, "AzureADUserEx", DefaultParameterSetName = ParamSetSingleUserUPN)]
[OutputType("None")]
public class SetAzureADUserExCommand : AzureADCommandBase
{
[Parameter(Mandatory = true)]

View File

@ -8,7 +8,7 @@
RootModule = 'DSInternals.Bootstrap.psm1'
# Version number of this module.
ModuleVersion = '4.3'
ModuleVersion = '4.4'
# Supported PSEditions
# CompatiblePSEditions = 'Desktop'
@ -141,10 +141,7 @@ PrivateData = @{
# ReleaseNotes of this module
ReleaseNotes = @"
- Added the Get-AzureADUserEx cmdlet for FIDO2 and NGC key auditing in Azure Active Directory.
- Both LastLogon and LastLogonTimestamp properties are now exposed on user accounts.
- Improved display format of FIDO2 keys.
- Updated the package logo.
- Added the Set-AzureADUserEx cmdlet for administrative FIDO2 security key revocation in Azure Active Directory.
"@
} # End of PSData hashtable

View File

@ -5,8 +5,8 @@ using System.Runtime.InteropServices;
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("DSInternals PowerShell Commands")]
[assembly: AssemblyVersion("4.3")]
[assembly: AssemblyFileVersion("4.3")]
[assembly: AssemblyVersion("4.4")]
[assembly: AssemblyFileVersion("4.4")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]

View File

@ -6077,6 +6077,10 @@ AuthenticatorData
<maml:linkText>Online Version:</maml:linkText>
<maml:uri>https://github.com/MichaelGrafnetter/DSInternals/blob/master/Documentation/PowerShell/Get-AzureADUserEx.md</maml:uri>
</maml:navigationLink>
<maml:navigationLink>
<maml:linkText>Set-AzureADUserEx</maml:linkText>
<maml:uri></maml:uri>
</maml:navigationLink>
<maml:navigationLink>
<maml:linkText>Get-ADKeyCredential</maml:linkText>
<maml:uri></maml:uri>
@ -9567,11 +9571,13 @@ PS C:\&gt; Set-ADDBDomainController -DatabasePath .\ntds.dit -Epoch $currentEpoc
<command:verb>Set</command:verb>
<command:noun>AzureADUserEx</command:noun>
<maml:description>
<maml:para>Registers new or revokes existing FIDO and NGC keys in Azure Active Directory.</maml:para>
<maml:para>Registers new or revokes existing FIDO2 and NGC keys in Azure Active Directory.</maml:para>
</maml:description>
</command:details>
<maml:description>
<maml:para>{{ Fill in the Description }}</maml:para>
<maml:para>The Set-AzureADUserEx cmdlet uses an undocumented Azure AD Graph API endpoint to modify the normally hidden searchableDeviceKeys attribute of user accounts. This attribute holds different types of key credentials, including the FIDO2 and NGC keys that are used by Windows Hello for Business.</maml:para>
<maml:para>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).</maml:para>
<maml:para>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.</maml:para>
</maml:description>
<command:syntax>
<command:syntaxItem>
@ -9591,7 +9597,7 @@ PS C:\&gt; Set-ADDBDomainController -DatabasePath .\ntds.dit -Epoch $currentEpoc
<command:parameter required="true" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="SearchableDeviceKey, KeyCredentialLink">
<maml:name>KeyCredential</maml:name>
<maml:Description>
<maml:para>{{ Fill KeyCredential Description }}</maml:para>
<maml:para>Specifies a list of key credentials (typically FIDO2 and NGC keys) that can be used by the target user for authentication.</maml:para>
</maml:Description>
<command:parameterValue required="true" variableLength="false">KeyCredential[]</command:parameterValue>
<dev:type>
@ -9642,7 +9648,7 @@ PS C:\&gt; Set-ADDBDomainController -DatabasePath .\ntds.dit -Epoch $currentEpoc
<command:parameter required="true" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="SearchableDeviceKey, KeyCredentialLink">
<maml:name>KeyCredential</maml:name>
<maml:Description>
<maml:para>{{ Fill KeyCredential Description }}</maml:para>
<maml:para>Specifies a list of key credentials (typically FIDO2 and NGC keys) that can be used by the target user for authentication.</maml:para>
</maml:Description>
<command:parameterValue required="true" variableLength="false">KeyCredential[]</command:parameterValue>
<dev:type>
@ -9693,7 +9699,7 @@ PS C:\&gt; Set-ADDBDomainController -DatabasePath .\ntds.dit -Epoch $currentEpoc
<command:parameter required="true" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="SearchableDeviceKey, KeyCredentialLink">
<maml:name>KeyCredential</maml:name>
<maml:Description>
<maml:para>{{ Fill KeyCredential Description }}</maml:para>
<maml:para>Specifies a list of key credentials (typically FIDO2 and NGC keys) that can be used by the target user for authentication.</maml:para>
</maml:Description>
<command:parameterValue required="true" variableLength="false">KeyCredential[]</command:parameterValue>
<dev:type>
@ -9752,7 +9758,7 @@ PS C:\&gt; Set-ADDBDomainController -DatabasePath .\ntds.dit -Epoch $currentEpoc
<command:returnValues>
<command:returnValue>
<dev:type>
<maml:name>System.Object</maml:name>
<maml:name>None</maml:name>
</dev:type>
<maml:description>
<maml:para></maml:para>
@ -9767,13 +9773,34 @@ PS C:\&gt; Set-ADDBDomainController -DatabasePath .\ntds.dit -Epoch $currentEpoc
<command:examples>
<command:example>
<maml:title>-------------------------- Example 1 --------------------------</maml:title>
<dev:code>PS C:\&gt; {{ Add example code here }}</dev:code>
<dev:code>PS C:\&gt; Install-Module -Name AzureAD,DSInternals -Force
PS C:\&gt; Connect-AzureAD
PS C:\&gt; $token = [Microsoft.Open.Azure.AD.CommonLibrary.AzureSession]::AccessTokens['AccessToken'].AccessToken
PS C:\&gt; Set-AzureADUserEx -UserPrincipalName 'john@contoso.com' -KeyCredential @() -Token $token</dev:code>
<dev:remarks>
<maml:para>{{ Add example description here }}</maml:para>
<maml:para>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.</maml:para>
</dev:remarks>
</command:example>
<command:example>
<maml:title>-------------------------- Example 2 --------------------------</maml:title>
<dev:code>PS C:\&gt; $user = Get-AzureADUserEx -UserPrincipalName 'john@contoso.com' -AccessToken $token
PS C:\&gt; $newCreds = $user.KeyCredentials | where { $PSItem.FidoKeyMaterial.DisplayName -notlike '*YubiKey*' }
PS C:\&gt; Set-AzureADUserEx -UserPrincipalName 'john@contoso.com' -KeyCredential $newCreds -Token $token</dev:code>
<dev:remarks>
<maml:para>Selectively revokes a specific FIDO2 security key based on its display name. Typical use case is a stolen/lost security key.</maml:para>
</dev:remarks>
</command:example>
</command:examples>
<command:relatedLinks />
<command:relatedLinks>
<maml:navigationLink>
<maml:linkText>Get-AzureADUserEx</maml:linkText>
<maml:uri></maml:uri>
</maml:navigationLink>
<maml:navigationLink>
<maml:linkText>Get-ADKeyCredential</maml:linkText>
<maml:uri></maml:uri>
</maml:navigationLink>
</command:relatedLinks>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10" xmlns:MSHelp="http://msdn.microsoft.com/mshelp">
<command:details>

View File

@ -113,6 +113,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PowerShell", "PowerShell",
..\Documentation\PowerShell\Set-ADDBBootKey.md = ..\Documentation\PowerShell\Set-ADDBBootKey.md
..\Documentation\PowerShell\Set-ADDBDomainController.md = ..\Documentation\PowerShell\Set-ADDBDomainController.md
..\Documentation\PowerShell\Set-ADDBPrimaryGroup.md = ..\Documentation\PowerShell\Set-ADDBPrimaryGroup.md
..\Documentation\PowerShell\Set-AzureADUserEx.md = ..\Documentation\PowerShell\Set-AzureADUserEx.md
..\Documentation\PowerShell\Set-LsaPolicyInformation.md = ..\Documentation\PowerShell\Set-LsaPolicyInformation.md
..\Documentation\PowerShell\Set-SamAccountPasswordHash.md = ..\Documentation\PowerShell\Set-SamAccountPasswordHash.md
..\Documentation\PowerShell\Test-PasswordQuality.md = ..\Documentation\PowerShell\Test-PasswordQuality.md