Skip to content

Commit 6b63d42

Browse files
committed
Merge branch 'kemunga/Set-EntraBetaAppRoleToApplicationUsers' of https://github.com/microsoftgraph/entra-powershell into kemunga/Set-EntraBetaAppRoleToApplicationUsers
2 parents 04ea596 + 565c280 commit 6b63d42

File tree

290 files changed

+5495
-3654
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

290 files changed

+5495
-3654
lines changed

.config/CredScanSuppressions.json

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
{
4141
"file": "test\\Entra\\Users\\New-EntraUser.Tests.ps1",
4242
"_justification": "Unit test file has a sample Password used in mocking."
43+
},
44+
{
45+
"file": "test\\Entra\\CertificateBasedAuthentication\\Get-EntraUserCertificateUserIdsFromCertificate.Tests.ps1",
46+
"_justification": "Unit test file has a sample certificate with only public keys used in mocking."
4347
}
4448
]
4549
}

build/BUILD.md

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Import-Module .\bin\Microsoft.Entra.Applications.psd1 -Force
6060
Import-Module .\bin\Microsoft.Entra.DirectoryManagement.psd1 -Force
6161
Import-Module .\bin\Microsoft.Entra.Governance.psd1 -Force
6262
Import-Module .\bin\Microsoft.Entra.Users.psd1 -Force
63+
Import-Module .\bin\Microsoft.Entra.CertificateBasedAuthentication.psd1 -Force
6364
Import-Module .\bin\Microsoft.Entra.Groups.psd1 -Force
6465
Import-Module .\bin\Microsoft.Entra.Reports.psd1 -Force
6566
Import-Module .\bin\Microsoft.Entra.SignIns.psd1 -Force
@@ -138,6 +139,7 @@ Import-Module .\bin\Microsoft.Entra.Applications.psd1 -Force
138139
Import-Module .\bin\Microsoft.Entra.DirectoryManagement.psd1 -Force
139140
Import-Module .\bin\Microsoft.Entra.Governance.psd1 -Force
140141
Import-Module .\bin\Microsoft.Entra.Users.psd1 -Force
142+
Import-Module .\bin\Microsoft.Entra.CertificateBasedAuthentication.psd1 -Force
141143
Import-Module .\bin\Microsoft.Entra.Groups.psd1 -Force
142144
Import-Module .\bin\Microsoft.Entra.Reports.psd1 -Force
143145
Import-Module .\bin\Microsoft.Entra.SignIns.psd1 -Force
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# ------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
# ------------------------------------------------------------------------------
4+
5+
function Get-EntraUserCBAAuthorizationInfo {
6+
[CmdletBinding()]
7+
param (
8+
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'The unique identifier for the user (User Principal Name or UserId).')]
9+
[Alias('ObjectId', 'UPN', 'Identity', 'UserPrincipalName')]
10+
[ValidateNotNullOrEmpty()]
11+
[ValidateScript({
12+
if ($_ -match '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' -or
13+
$_ -match '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$') {
14+
return $true
15+
}
16+
throw "UserId must be a valid email address or GUID."
17+
})]
18+
[string]$UserId,
19+
20+
[Parameter(Mandatory = $false, HelpMessage = 'If specified, returns the raw response from the API.')]
21+
[Alias('RawResponse')]
22+
[switch]$Raw
23+
)
24+
25+
begin {
26+
# Ensure connection to Microsoft Entra
27+
if (-not (Get-EntraContext)) {
28+
$errorMessage = "Not connected to Microsoft Graph. Use 'Connect-Entra -Scopes Directory.ReadWrite.All, User.ReadWrite.All' to authenticate."
29+
Write-Error -Message $errorMessage -ErrorAction Stop
30+
return
31+
}
32+
}
33+
34+
process {
35+
try {
36+
$customHeaders = New-EntraCustomHeaders -Command $MyInvocation.MyCommand
37+
# Determine if UserId is a GUID (ObjectId) or a UserPrincipalName
38+
$isGuid = $UserId -match "^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$"
39+
$filter = if ($isGuid) { "id eq '$UserId'" } else { "userPrincipalName eq '$UserId'" }
40+
41+
# First look up the user to get the ID if not already a GUID
42+
$uri = "/v1.0/users?`$filter=$filter"
43+
$lookupCustomHeaders = @{} + $customHeaders + @{"ConsistencyLevel" = "eventual" }
44+
$userLookup = Invoke-MgGraphRequest -Uri $uri -Method GET -Headers $lookupCustomHeaders -ErrorAction Stop
45+
46+
if ($userLookup.value.Count -eq 0) {
47+
throw "User '$UserId' not found in Entra ID"
48+
}
49+
50+
$userId = $userLookup.value[0].id
51+
52+
# Now fetch the authorization info
53+
$apiCallUrl = "/v1.0/users/$userId`?`$select=id,displayName,userPrincipalName,userType,authorizationInfo"
54+
$response = Invoke-MgGraphRequest -Uri $apiCallUrl -Method GET -Headers $customHeaders -ErrorAction Stop
55+
56+
# If Raw switch is specified, return the raw response
57+
if ($Raw) {
58+
return $response
59+
}
60+
61+
# Create a structured object for the certificate user IDs
62+
$certificateUserIds = @()
63+
64+
if ($response.authorizationInfo -and $response.authorizationInfo.certificateUserIds) {
65+
foreach ($certId in $response.authorizationInfo.certificateUserIds) {
66+
if ($certId -match 'X509:<([^>]+)>(.+)') {
67+
$type = $matches[1]
68+
$value = $matches[2]
69+
70+
# Create a descriptive name for the certificate type
71+
$typeName = switch ($type) {
72+
"PN" { "PrincipalName" }
73+
"S" { "Subject" }
74+
"I" { "Issuer" }
75+
"SR" { "SerialNumber" }
76+
"SKI" { "SubjectKeyIdentifier" }
77+
"SHA1-PUKEY" { "SHA1PublicKey" }
78+
default { $type }
79+
}
80+
81+
$certificateUserIds += [PSCustomObject]@{
82+
Type = $type
83+
TypeName = $typeName
84+
Value = $value
85+
OriginalString = $certId
86+
}
87+
}
88+
else {
89+
# For any ID that doesn't match the expected format
90+
$certificateUserIds += [PSCustomObject]@{
91+
Type = "Unknown"
92+
TypeName = "Unknown"
93+
Value = $certId
94+
OriginalString = $certId
95+
}
96+
}
97+
}
98+
}
99+
100+
# Create a structured AuthorizationInfo object
101+
$authInfo = [PSCustomObject]@{
102+
CertificateUserIds = $certificateUserIds
103+
RawAuthorizationInfo = $response.authorizationInfo
104+
}
105+
106+
# Create a custom output object with the properties of interest
107+
$result = [PSCustomObject]@{
108+
Id = $response.id
109+
DisplayName = $response.displayName
110+
UserPrincipalName = $response.userPrincipalName
111+
UserType = $response.userType
112+
AuthorizationInfo = $authInfo
113+
}
114+
115+
return $result
116+
}
117+
catch {
118+
$errorDetails = $_.Exception.Message
119+
Write-Error "Failed to retrieve authorization info: $errorDetails"
120+
throw
121+
}
122+
}
123+
124+
end {
125+
# Cleanup if needed
126+
}
127+
}
128+
129+
Set-Alias -Name Get-EntraUserAuthorizationInfo -Value Get-EntraUserCBAAuthorizationInfo -Description "Gets a user's authorization information from Microsoft Entra ID" -Scope Global -Force
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# ------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
# ------------------------------------------------------------------------------
4+
function Get-EntraUserCertificateUserIdsFromCertificate {
5+
[CmdletBinding(DefaultParameterSetName = 'Default')]
6+
param (
7+
[Parameter(Mandatory = $false,
8+
HelpMessage = "Path to the certificate file. The file can be in .cer or .pem format.")]
9+
[string]$Path,
10+
11+
[Parameter(Mandatory = $false,
12+
HelpMessage = "Certificate object. If provided, the Path parameter is ignored.")]
13+
[Alias('CertificateObject', 'Cert')]
14+
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
15+
16+
[Parameter(Mandatory = $false,
17+
ParameterSetName = 'Default',
18+
HelpMessage = "The certificate mapping type to use. Valid values are: PrincipalName, RFC822Name, IssuerAndSubject, Subject, SKI, SHA1PublicKey, IssuerAndSerialNumber.")]
19+
[ValidateSet("PrincipalName", "RFC822Name", "IssuerAndSubject", "Subject",
20+
"SKI", "SHA1PublicKey", "IssuerAndSerialNumber")]
21+
[string]$CertificateMapping
22+
)
23+
24+
function Get-Certificate {
25+
param (
26+
[string]$filePath
27+
)
28+
if ($filePath.EndsWith(".cer")) {
29+
return [System.Security.Cryptography.X509Certificates.X509Certificate]::new($filePath)
30+
}
31+
elseif ($filePath.EndsWith(".pem")) {
32+
$pemContent = Get-Content -Path $filePath -Raw
33+
$pemContent = $pemContent -replace "-----BEGIN CERTIFICATE-----", ""
34+
$pemContent = $pemContent -replace "-----END CERTIFICATE-----", ""
35+
$pemContent = $pemContent -replace "(\r\n|\n|\r)", ""
36+
$pemBytes = [Convert]::FromBase64String($pemContent)
37+
$certificate = [System.Security.Cryptography.X509Certificates.X509Certificate]::new($pemBytes)
38+
39+
return $certificate
40+
}
41+
else {
42+
throw "Unsupported certificate format. Please provide a .cer or .pem file."
43+
}
44+
}
45+
46+
function Get-DistinguishedNameAsString {
47+
param (
48+
[System.Security.Cryptography.X509Certificates.X500DistinguishedName]$distinguishedName
49+
)
50+
51+
$dn = $distinguishedName.Decode([System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags]::UseNewLines -bor [System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags]::DoNotUsePlusSign)
52+
53+
$dn = $dn -replace "(\r\n|\n|\r)", ","
54+
return $dn.TrimEnd(',')
55+
}
56+
57+
function Get-SerialNumberAsLittleEndianHexString {
58+
param (
59+
[System.Security.Cryptography.X509Certificates.X509Certificate2]$cert
60+
)
61+
62+
$littleEndianSerialNumber = $cert.GetSerialNumber()
63+
64+
if ($littleEndianSerialNumber.Length -eq 0) {
65+
return ""
66+
}
67+
68+
[System.Array]::Reverse($littleEndianSerialNumber)
69+
$hexString = -join ($littleEndianSerialNumber | ForEach-Object { $_.ToString("x2") })
70+
return $hexString
71+
}
72+
73+
function Get-SubjectKeyIdentifier {
74+
param (
75+
[System.Security.Cryptography.X509Certificates.X509Certificate2]$cert
76+
)
77+
foreach ($extension in $cert.Extensions) {
78+
if ($extension.Oid.Value -eq "2.5.29.14") {
79+
$ski = New-Object System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension -ArgumentList $extension, $false
80+
return $ski.SubjectKeyIdentifier
81+
}
82+
}
83+
84+
return ""
85+
}
86+
87+
# Function to generate certificate mapping fields
88+
function Get-CertificateMappingFields {
89+
param (
90+
[System.Security.Cryptography.X509Certificates.X509Certificate2]$cert
91+
)
92+
$subject = Get-DistinguishedNameAsString -distinguishedName $cert.SubjectName
93+
$issuer = Get-DistinguishedNameAsString -distinguishedName $cert.IssuerName
94+
$serialNumber = Get-SerialNumberAsLittleEndianHexString -cert $cert
95+
$thumbprint = $cert.Thumbprint
96+
$principalName = $cert.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::UpnName, $false)
97+
$emailName = $cert.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::EmailName, $false)
98+
$subjectKeyIdentifier = Get-SubjectKeyIdentifier -cert $cert
99+
$sha1PublicKey = $cert.GetCertHashString()
100+
101+
return @{
102+
"SubjectName" = $subject
103+
"IssuerName" = $issuer
104+
"SerialNumber" = $serialNumber
105+
"Thumbprint" = $thumbprint
106+
"PrincipalName" = $principalName
107+
"EmailName" = $emailName
108+
"SubjectKeyIdentifier" = $subjectKeyIdentifier
109+
"Sha1PublicKey" = $sha1PublicKey
110+
}
111+
}
112+
113+
function Get-CertificateUserIds {
114+
param (
115+
[System.Security.Cryptography.X509Certificates.X509Certificate2]$cert
116+
)
117+
118+
$mappingFields = Get-CertificateMappingFields -cert $cert
119+
120+
$certUserIDs = @{
121+
"PrincipalName" = ""
122+
"RFC822Name" = ""
123+
"IssuerAndSubject" = ""
124+
"Subject" = ""
125+
"SKI" = ""
126+
"SHA1PublicKey" = ""
127+
"IssuerAndSerialNumber" = ""
128+
}
129+
130+
if (-not [string]::IsNullOrWhiteSpace($mappingFields.PrincipalName)) {
131+
$certUserIDs.PrincipalName = "X509:<PN>$($mappingFields.PrincipalName)"
132+
}
133+
134+
if (-not [string]::IsNullOrWhiteSpace($mappingFields.EmailName)) {
135+
$certUserIDs.RFC822Name = "X509:<RFC822>$($mappingFields.EmailName)"
136+
}
137+
138+
if ((-not [string]::IsNullOrWhiteSpace($mappingFields.IssuerName)) -and (-not [string]::IsNullOrWhiteSpace($mappingFields.SubjectName))) {
139+
$certUserIDs.IssuerAndSubject = "X509:<I>$($mappingFields.IssuerName)<S>$($mappingFields.SubjectName)"
140+
}
141+
142+
if (-not [string]::IsNullOrWhiteSpace($mappingFields.SubjectName)) {
143+
$certUserIDs.Subject = "X509:<S>$($mappingFields.SubjectName)"
144+
}
145+
146+
if (-not [string]::IsNullOrWhiteSpace($mappingFields.SubjectKeyIdentifier)) {
147+
$certUserIDs.SKI = "X509:<SKI>$($mappingFields.SubjectKeyIdentifier)"
148+
}
149+
150+
if (-not [string]::IsNullOrWhiteSpace($mappingFields.Sha1PublicKey)) {
151+
$certUserIDs.SHA1PublicKey = "X509:<SHA1-PUKEY>$($mappingFields.Sha1PublicKey)"
152+
}
153+
154+
if ((-not [string]::IsNullOrWhiteSpace($mappingFields.IssuerName)) -and (-not [string]::IsNullOrWhiteSpace($mappingFields.SerialNumber))) {
155+
$certUserIDs.IssuerAndSerialNumber = "X509:<I>$($mappingFields.IssuerName)<SR>$($mappingFields.SerialNumber)"
156+
}
157+
158+
return $certUserIDs
159+
}
160+
161+
function Main {
162+
$cert = $Certificate
163+
if ($null -eq $cert) {
164+
$cert = Get-Certificate -filePath $Path
165+
}
166+
167+
$mappings = Get-CertificateUserIds -cert $cert
168+
169+
if ($CertificateMapping -eq "") {
170+
return $mappings
171+
}
172+
else {
173+
$value = $mappings[$CertificateMapping]
174+
return "$($value)"
175+
}
176+
}
177+
178+
# Call main function
179+
return Main
180+
}

0 commit comments

Comments
 (0)