-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathRequest-FederationCerts.ps1
More file actions
318 lines (259 loc) · 14.5 KB
/
Request-FederationCerts.ps1
File metadata and controls
318 lines (259 loc) · 14.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
<#
.SYNOPSIS
Displays signing, encrypting, and HTTPS certificates from ADFS or Entra ID federation metadata.
.DESCRIPTION
Retrieves and displays the certificates published in ADFS or Entra ID federation metadata,
including the HTTPS ("SSL") certificate used in the connection itself.
Does not authenticate to the server or investigate individual ADFS farm nodes.
Supports two modes:
- ADFS mode: Query an ADFS farm by FQDN (uses -FarmFqdn parameter)
- Entra ID mode: Query Entra ID federation metadata by URL (uses -MetadataUrl parameter)
DaysToExpiry values are color-coded: Green (90+ days), Yellow (30-89 days), Red (<30 days or expired).
Use -Display $false for pipeline/loop use (returns a PSCustomObject instead of console output).
.PARAMETER FarmFqdn
The FQDN of the ADFS farm to query (ADFS mode).
.PARAMETER MetadataUrl
The full URL to Entra ID or ADFS federation metadata (Entra ID mode).
.PARAMETER Display
When $true (default), writes color-coded output to the console. When $false, returns a
PSCustomObject for pipeline use.
.PARAMETER ExportCsv
Optional file path to export the certificate report as CSV.
.PARAMETER ExportJson
Optional file path to export the certificate report as JSON.
.EXAMPLE
Request-FederationCerts -FarmFqdn adfs.mikecrowley.us
.EXAMPLE
Request-FederationCerts -FarmFqdn adfs.mikecrowley.us -Display $false
.EXAMPLE
Request-FederationCerts -MetadataUrl "https://login.microsoftonline.com/contoso.onmicrosoft.com/federationmetadata/2007-06/federationmetadata.xml"
.EXAMPLE
Request-FederationCerts -MetadataUrl "https://login.microsoftonline.com/50afff0a-571a-4822-915b-d6823ca9fe63/federationmetadata/2007-06/federationmetadata.xml?appid=df3d5533-18a3-400a-9027-ae7da4836fc7"
.EXAMPLE
Request-FederationCerts -FarmFqdn adfs.mikecrowley.us -ExportCsv "C:\reports\certs.csv" -ExportJson "C:\reports\certs.json"
.NOTES
Author: Mike Crowley
https://mikecrowley.us
See also: https://adfshelp.microsoft.com/MetadataExplorer/GetFederationMetadata
.LINK
https://github.com/Mike-Crowley/Public-Scripts
#>
function Request-FederationCerts {
[CmdletBinding(DefaultParameterSetName = 'ADFS')]
param (
[Parameter(ParameterSetName = 'ADFS', ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string]$FarmFqdn,
[Parameter(ParameterSetName = 'Entra', ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string]$MetadataUrl,
[bool]$Display = $true,
[string]$ExportCsv,
[string]$ExportJson
)
$IsPSCore = $PSVersionTable.PSEdition -eq 'Core'
# Validate that at least one parameter is provided
if (-not $FarmFqdn -and -not $MetadataUrl) {
Write-Warning "Please specify either -FarmFqdn or -MetadataUrl"
return
}
if ($FarmFqdn) {
# ADFS mode - test connection first
if (-not (Test-NetConnection -ComputerName $FarmFqdn -Port 443 -InformationLevel Quiet -Verbose)) {
Write-Warning "Cannot connect to: $FarmFqdn"
return
}
$url = "https://$FarmFqdn/FederationMetadata/2007-06/FederationMetadata.xml"
$hostHeader = $FarmFqdn
}
else {
# Entra mode - use the provided URL directly
$url = $MetadataUrl
$hostHeader = ([uri]$MetadataUrl).Host
}
# Ensure TLS 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# For Windows PowerShell, ignore SSL warnings for self-signed certs
if (-not $IsPSCore) {
[Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
}
# Make HTTPS connection and get content
try {
if ($IsPSCore) {
# PowerShell 7+ - use Invoke-WebRequest with -SkipCertificateCheck for self-signed certs
$webResponse = Invoke-WebRequest -Uri $url -UseBasicParsing -SkipCertificateCheck
$content = $webResponse.Content
# Get SSL cert from the connection via TcpClient
$tcpClient = [System.Net.Sockets.TcpClient]::new($hostHeader, 443)
$sslStream = [System.Net.Security.SslStream]::new($tcpClient.GetStream(), $false, { $true })
$sslStream.AuthenticateAsClient($hostHeader)
$SSLCert_x509 = [Security.Cryptography.X509Certificates.X509Certificate2]::new($sslStream.RemoteCertificate)
$sslStream.Close()
$tcpClient.Close()
}
elseif ($MetadataUrl) {
# Windows PowerShell + Entra mode - use Invoke-WebRequest
$webResponse = Invoke-WebRequest -Uri $url -UseBasicParsing
$content = $webResponse.Content
# Get SSL cert from the connection
$tcpClient = [System.Net.Sockets.TcpClient]::new($hostHeader, 443)
$sslStream = [System.Net.Security.SslStream]::new($tcpClient.GetStream(), $false, { $true })
$sslStream.AuthenticateAsClient($hostHeader)
$SSLCert_x509 = [Security.Cryptography.X509Certificates.X509Certificate2]::new($sslStream.RemoteCertificate)
$sslStream.Close()
$tcpClient.Close()
}
else {
# Windows PowerShell + ADFS mode - use HttpWebRequest to handle custom Host header
$request = [Net.HttpWebRequest]::Create($url)
$request.Host = $hostHeader
$request.AllowAutoRedirect = $false
$response = $request.GetResponse()
$HttpsCertBytes = $request.ServicePoint.Certificate.GetRawCertData()
$contentStream = $response.GetResponseStream()
$reader = [IO.StreamReader]::new($contentStream)
$content = $reader.ReadToEnd()
$reader.Close()
$contentStream.Close()
$response.Close()
# Extract HTTPS cert (ADFS calls this the "SSL" cert)
$CertInBase64 = [convert]::ToBase64String($HttpsCertBytes)
$SSLCert_x509 = [Security.Cryptography.X509Certificates.X509Certificate2]([System.Convert]::FromBase64String($CertInBase64))
}
}
catch {
Write-Warning "Failed to retrieve metadata from: $url"
Write-Warning $_.Exception.Message
return
}
# Parse FederationMetadata for certs
# Remove BOM if present (can cause XML parsing issues in PowerShell 7)
$content = $content -replace '^\xEF\xBB\xBF', '' -replace '^\uFEFF', ''
# ADFS uses SPSSODescriptor, Entra ID uses IDPSSODescriptor
$xmlContent = [xml]$content
# Extract EntityID from the metadata
$EntityID = $xmlContent.EntityDescriptor.entityID
$KeyDescriptors = $xmlContent.EntityDescriptor.SPSSODescriptor.KeyDescriptor
if (-not $KeyDescriptors) {
$KeyDescriptors = $xmlContent.EntityDescriptor.IDPSSODescriptor.KeyDescriptor
}
$FirstSigningCert_base64 = ([array]($KeyDescriptors | Where-Object use -eq 'signing').KeyInfo)[0].X509Data.X509Certificate
$FirstSigningCert_x509 = if ($FirstSigningCert_base64) { [Security.Cryptography.X509Certificates.X509Certificate2][System.Convert]::FromBase64String($FirstSigningCert_base64) } else { $null }
$SecondSigningCert_base64 = ([array]($KeyDescriptors | Where-Object use -eq 'signing').KeyInfo)[1].X509Data.X509Certificate
$SecondSigningCert_x509 = if ($SecondSigningCert_base64) { [Security.Cryptography.X509Certificates.X509Certificate2][System.Convert]::FromBase64String($SecondSigningCert_base64) } else { $null }
$EncryptionCert_base64 = ($KeyDescriptors | Where-Object use -eq 'encryption').KeyInfo.X509Data.X509Certificate
$EncryptionCert_x509 = if ($EncryptionCert_base64) { [Security.Cryptography.X509Certificates.X509Certificate2][System.Convert]::FromBase64String($EncryptionCert_base64) } else { $null }
$Now = Get-Date
$CertReportObject = [pscustomobject]@{
EntityID = $EntityID
SSL_Subject = $SSLCert_x509.Subject
SSL_NotAfter = $SSLCert_x509.NotAfter
SSL_Thumbprint = $SSLCert_x509.Thumbprint
SSL_Issuer = $SSLCert_x509.Issuer
SSL_DaysToExpiry = ($SSLCert_x509.NotAfter - $Now).Days
FirstSigning_Subject = $FirstSigningCert_x509.Subject
FirstSigning_NotAfter = $FirstSigningCert_x509.NotAfter
FirstSigning_Thumbprint = $FirstSigningCert_x509.Thumbprint
FirstSigning_Issuer = $FirstSigningCert_x509.Issuer
FirstSigning_DaysToExpiry = if ($FirstSigningCert_x509) { ($FirstSigningCert_x509.NotAfter - $Now).Days } else { $null }
SecondSigning_Subject = $SecondSigningCert_x509.Subject
SecondSigning_NotAfter = $SecondSigningCert_x509.NotAfter
SecondSigning_Thumbprint = $SecondSigningCert_x509.Thumbprint
SecondSigning_Issuer = $SecondSigningCert_x509.Issuer
SecondSigning_DaysToExpiry = if ($SecondSigningCert_x509) { ($SecondSigningCert_x509.NotAfter - $Now).Days } else { $null }
Encryption_Subject = $EncryptionCert_x509.Subject
Encryption_NotAfter = $EncryptionCert_x509.NotAfter
Encryption_Thumbprint = $EncryptionCert_x509.Thumbprint
Encryption_Issuer = $EncryptionCert_x509.Issuer
Encryption_DaysToExpiry = if ($EncryptionCert_x509) { ($EncryptionCert_x509.NotAfter - $Now).Days } else { $null }
}
# Helper function for expiry color coding
function Get-ExpiryColor {
param([int]$Days)
if ($null -eq $Days) { return 'Gray' }
if ($Days -lt 0) { return 'Red' }
if ($Days -lt 30) { return 'Red' }
if ($Days -lt 90) { return 'Yellow' }
return 'Green'
}
if ($Display -eq $true) {
Write-Host "`n EntityID: " -ForegroundColor Cyan -NoNewline
Write-Host $CertReportObject.EntityID
Write-Host " `nSSL (HTTPS) Certificate:`n" -ForegroundColor Green
Write-Host " SSL_Subject: " $CertReportObject.SSL_Subject
Write-Host " SSL_NotAfter: " $CertReportObject.SSL_NotAfter
Write-Host " SSL_Thumbprint: " $CertReportObject.SSL_Thumbprint
Write-Host " SSL_Issuer: " $CertReportObject.SSL_Issuer
Write-Host " SSL_DaysToExpiry: " -NoNewline
Write-Host $CertReportObject.SSL_DaysToExpiry -ForegroundColor (Get-ExpiryColor $CertReportObject.SSL_DaysToExpiry)
if ($null -ne $CertReportObject.Encryption_Subject) {
Write-Host " `nEncryption Certificate:`n" -ForegroundColor DarkMagenta
Write-Host " Encryption_Subject: " $CertReportObject.Encryption_Subject
Write-Host " Encryption_NotAfter: " $CertReportObject.Encryption_NotAfter
Write-Host " Encryption_Thumbprint: " $CertReportObject.Encryption_Thumbprint
Write-Host " Encryption_Issuer: " $CertReportObject.Encryption_Issuer
Write-Host " Encryption_DaysToExpiry: " -NoNewline
Write-Host $CertReportObject.Encryption_DaysToExpiry -ForegroundColor (Get-ExpiryColor $CertReportObject.Encryption_DaysToExpiry)
}
else {
Write-Host " `nEncryption Certificate:`n" -ForegroundColor DarkMagenta
Write-Host " !! No Encryption Certificate Found (typical for Entra ID metadata) !!`n"
}
if ($null -eq $CertReportObject.SecondSigning_Subject) {
Write-Host " `nToken Signing Certificate:`n" -ForegroundColor Yellow
}
else {
Write-Host " `nFirst Token Signing Certificate:`n" -ForegroundColor Yellow
}
Write-Host " FirstSigning_Subject: " $CertReportObject.FirstSigning_Subject
Write-Host " FirstSigning_NotAfter: " $CertReportObject.FirstSigning_NotAfter
Write-Host " FirstSigning_Thumbprint: " $CertReportObject.FirstSigning_Thumbprint
Write-Host " FirstSigning_Issuer: " $CertReportObject.FirstSigning_Issuer
Write-Host " FirstSigning_DaysToExpiry: " -NoNewline
Write-Host $CertReportObject.FirstSigning_DaysToExpiry -ForegroundColor (Get-ExpiryColor $CertReportObject.FirstSigning_DaysToExpiry)
Write-Host "`nSecond Token Signing Certificate:`n" -ForegroundColor DarkYellow
if ($null -ne $CertReportObject.SecondSigning_Subject) {
Write-Host " SecondSigning_Subject: " $CertReportObject.SecondSigning_Subject
Write-Host " SecondSigning_NotAfter: " $CertReportObject.SecondSigning_NotAfter
Write-Host " SecondSigning_Thumbprint: " $CertReportObject.SecondSigning_Thumbprint
Write-Host " SecondSigning_Issuer: " $CertReportObject.SecondSigning_Issuer
Write-Host " SecondSigning_DaysToExpiry: " -NoNewline
Write-Host $CertReportObject.SecondSigning_DaysToExpiry -ForegroundColor (Get-ExpiryColor $CertReportObject.SecondSigning_DaysToExpiry)
Write-Host "`n NOTE: A federation metadata document can have multiple signing keys." -ForegroundColor Gray
Write-Host " When a federation metadata document includes more than one certificate, a service that is validating the tokens should support all certificates in the document." -ForegroundColor Gray
Write-Host " The 'First' certificate in the metadata may not be the 'Primary' certificate in the ADFS configuration"
Write-Host " https://learn.microsoft.com/en-us/entra/identity-platform/federation-metadata#token-signing-certificates"
}
else { Write-Host " !! No Second Token Signing Certificate Found !!`n" }
Write-Host "`n"
}
# Export to CSV if requested
if ($ExportCsv) {
try {
$CertReportObject | Export-Csv -Path $ExportCsv -NoTypeInformation -Encoding UTF8 -ErrorAction Stop
Write-Host "Exported to CSV: $ExportCsv" -ForegroundColor Green
}
catch {
Write-Warning "Failed to export CSV: $_"
}
}
# Export to JSON if requested
if ($ExportJson) {
try {
$CertReportObject | ConvertTo-Json -Depth 3 | Out-File -FilePath $ExportJson -Encoding UTF8 -ErrorAction Stop
Write-Host "Exported to JSON: $ExportJson" -ForegroundColor Green
}
catch {
Write-Warning "Failed to export JSON: $_"
}
}
# Return object if not displaying (for pipeline use)
if ($Display -eq $false) {
return $CertReportObject
}
}
# Examples
# ADFS mode:
# Request-FederationCerts -FarmFqdn adfs.mikecrowley.us -Display $true
# Entra ID mode:
# Request-FederationCerts -MetadataUrl "https://login.microsoftonline.com/contoso.onmicrosoft.com/federationmetadata/2007-06/federationmetadata.xml"