From d9af0656fec6d24b7a8a8c5ec18da7cea5607ad0 Mon Sep 17 00:00:00 2001 From: Ryan Bolger Date: Tue, 27 Jun 2023 21:34:52 -0700 Subject: [PATCH] Fixed Azure tests --- Posh-ACME/Plugins/Azure.ps1 | 6 +- Tests/Azure.Tests.ps1 | 208 ++++++++++++++++++------------------ 2 files changed, 110 insertions(+), 104 deletions(-) diff --git a/Posh-ACME/Plugins/Azure.ps1 b/Posh-ACME/Plugins/Azure.ps1 index a4687425..894da7a8 100644 --- a/Posh-ACME/Plugins/Azure.ps1 +++ b/Posh-ACME/Plugins/Azure.ps1 @@ -363,6 +363,10 @@ function ConvertFrom-AccessToken { # Anatomy of an access token # https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-token-and-claims + # In this function we're extracting the tenant ID and expiration value from + # the claims in the payload and returning an object that includes them along + # with the original token value. + # grab the payload section of the JWT $null,$payload,$null = $AZAccessToken.Split('.') @@ -377,7 +381,7 @@ function ConvertFrom-AccessToken { # make sure the token hasn't expired $expires = [DateTimeOffset]::FromUnixTimeSeconds($claims.exp) if ((Get-DateTimeOffsetNow) -gt $expires) { - throw "The provided access token has expired as of $($expires.ToString('u'))" + throw "The provided access token expired since $($expires.ToString('u'))" } # return an object that contains the 'expires_on' property along with the token diff --git a/Tests/Azure.Tests.ps1 b/Tests/Azure.Tests.ps1 index ea0c79e2..8fc39720 100644 --- a/Tests/Azure.Tests.ps1 +++ b/Tests/Azure.Tests.ps1 @@ -1,157 +1,159 @@ Describe "Connect-AZTenant" { BeforeAll { + . (Join-Path $PSScriptRoot "..\Posh-ACME\Plugins\Azure.ps1") . (Join-Path $PSScriptRoot "..\Posh-ACME\Private\MockWrappers.ps1") + . (Join-Path $PSScriptRoot "..\Posh-ACME\Private\ConvertTo-Base64Url.ps1") + $script:UseBasic = @{UseBasicParsing = $true} + + $fakeTokenText = [ordered]@{ + aud = 'https://management.core.windows.net/' + exp = [DateTimeOffset]::Parse('2018-07-04T10:00:00Z').ToUnixTimeSeconds() # 1530698400 + tid = '00000000-0000-0000-0000-000000000000' + } | ConvertTo-Json | ConvertTo-Base64Url + $fakeTokenResponse = [pscustomobject]@{ - expires_on = '1530691200' # 2018-07-04 08:00:00 UTC - access_token = 'faketoken' + expires_on = [DateTimeOffset]::Parse('2018-07-04T10:00:00Z').ToUnixTimeSeconds() # 1530698400 + access_token = $fakeTokenText + tenant = '00000000-0000-0000-0000-000000000000' } - $script:UseBasic = @{UseBasicParsing = $true} - Mock Invoke-RestMethod { return $fakeTokenResponse } Mock ConvertFrom-AccessToken { return $fakeTokenResponse } Mock Get-DateTimeOffsetNow { return [DateTimeOffset]::Parse('2018-07-04T09:00:00Z') } - $fakeGoodToken = [pscustomobject]@{ - # just after mocked "Now" - Expires = [DateTimeOffset]::Parse('2018-07-04T09:05:00Z') - AuthHeader = @{ Authorization = 'Bearer fakegoodtoken' } - } - - $fakeExpiredToken = [pscustomobject]@{ - # just before mocked "Now" - Expires = [DateTimeOffset]::Parse('2018-07-04T08:55:00Z') - AuthHeader = @{ Authorization = 'Bearer fakeexpiredtoken' } - } } - Context "Credential param set" { + Context "Token param set" { BeforeAll { - $fakeTenant = '00000000-0000-0000-0000-000000000000' - $fakePass = "fake+p&ss" | ConvertTo-SecureString -AsPlainText -Force - $fakeCred = New-Object System.Management.Automation.PSCredential('fake user', $fakePass) + $fakeAccessToken = "blah.$fakeTokenText.blah" + $mockedBearer = "Bearer $fakeTokenText" } - It "calls Invoke-RestMethod if no existing token" { - $script:AZToken = $null - Connect-AZTenant -AZTenantId $fakeTenant -AZAppCred $fakeCred - Should -Invoke Invoke-RestMethod -Times 1 -Exactly -Scope It -ParameterFilter { - $Body -match "[&?]client_id=fake%20user(&|$)" -and $Body -match "[&?]client_secret=fake%2[Bb]p%26ss(&|$)" - } - Should -Invoke ConvertFrom-AccessToken -Times 0 -Exactly -Scope It - } - - It "calls Invoke-RestMethod if token expired" { - $script:AZToken = $fakeExpiredToken - Connect-AZTenant -AZTenantId $fakeTenant -AZAppCred $fakeCred - Should -Invoke Invoke-RestMethod -Times 1 -Exactly -Scope It - Should -Invoke ConvertFrom-AccessToken -Times 0 -Exactly -Scope It - } + It "Uses the token as-is" -TestCases @( + @{ curToken = $null } + @{ curToken = [pscustomobject]@{ + Expires = [DateTimeOffset]::Parse('2018-07-04T06:00:00Z') # expired before mocked "Now" + AuthHeader = @{ Authorization = 'Bearer fakeexpiredtoken' } + Tenant = '11111111-1111-1111-1111-111111111111' + }} + ) { + # setup an expired cached token + $script:AZToken = $curToken - It "sets new AZToken if token expired" { - $script:AZToken | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader.Authorization | Should -BeExactly 'Bearer faketoken' - $script:AZToken.Expires | Should -Be ([DateTimeOffset]::Parse('2018-07-04T07:55:00Z')) - } + Connect-AZTenant -AZAccessToken $fakeAccessToken - It "calls nothing if current token is valid" { - $script:AZToken = $fakeGoodToken - Connect-AZTenant -AZTenantId $fakeTenant -AZAppCred $fakeCred Should -Invoke Invoke-RestMethod -Times 0 -Exactly -Scope It - Should -Invoke ConvertFrom-AccessToken -Times 0 -Exactly -Scope It - } + Should -Invoke ConvertFrom-AccessToken -Times 1 -Exactly -Scope It - It "does not overwrite existing token if current token is valid" { - $script:AZToken | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader.Authorization | Should -BeExactly $fakeGoodToken.AuthHeader.Authorization - $script:AZToken.Expires | Should -Be $fakeGoodToken.Expires + $script:AZToken.Tenant | Should -Be '00000000-0000-0000-0000-000000000000' + $script:AZToken.Expires | Should -Be ([DateTimeOffset]::Parse('2018-07-04T09:55:00Z')) # 5 min prior to actual + $script:AZToken.AuthHeader.Authorization | Should -BeExactly $mockedBearer } } - Context "Token param set" { + Context "IMDS param set" { BeforeAll { - $fakeAccessToken = 'blah.blah.blah' + $mockedBearer = "Bearer $fakeTokenText" } - It "calls ConvertFrom-AccessToken if no existing token" { - $script:AZToken = $null - Connect-AZTenant -AZAccessToken $fakeAccessToken - Should -Invoke Invoke-RestMethod -Times 0 -Exactly -Scope It - Should -Invoke ConvertFrom-AccessToken -Times 1 -Exactly -Scope It - } + It "Gets new token if no current or expired" -TestCases @( + @{ curToken = $null } + @{ curToken = [pscustomobject]@{ + Expires = [DateTimeOffset]::Parse('2018-07-04T06:00:00Z') + AuthHeader = @{ Authorization = 'Bearer fakeexpiredtoken' } + Tenant = '00000000-0000-0000-0000-000000000000' + }} + ) { + $script:AZToken = $curToken - It "calls ConvertFrom-AccessToken if token expired" { - $script:AZToken = $fakeExpiredToken - Connect-AZTenant -AZAccessToken $fakeAccessToken - Should -Invoke Invoke-RestMethod -Times 0 -Exactly -Scope It + Connect-AZTenant -AZUseIMDS + + Should -Invoke Invoke-RestMethod -Times 1 -Exactly -Scope It -ParameterFilter { + $Uri -eq 'http://169.254.169.254/metadata/identity/oauth2/token' + } Should -Invoke ConvertFrom-AccessToken -Times 1 -Exactly -Scope It - } - It "uses passed in token if AZToken expired" { - $script:AZToken | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader.Authorization | Should -BeExactly 'Bearer faketoken' - $script:AZToken.Expires | Should -Be ([DateTimeOffset]::Parse('2018-07-04T07:55:00Z')) + $script:AZToken.Tenant | Should -Be '00000000-0000-0000-0000-000000000000' + $script:AZToken.Expires | Should -Be ([DateTimeOffset]::Parse('2018-07-04T09:55:00Z')) # 5 min prior to actual + $script:AZToken.AuthHeader.Authorization | Should -BeExactly $mockedBearer } - It "calls nothing if current token is valid" { - $script:AZToken = $fakeGoodToken - Connect-AZTenant -AZAccessToken $fakeAccessToken + It "Uses existing token if still valid" { + $script:AZToken = [pscustomobject]@{ + Expires = [DateTimeOffset]::Parse('2018-07-04T10:00:00Z') + AuthHeader = @{ Authorization = 'Bearer fakeexpiredtoken' } + Tenant = '00000000-0000-0000-0000-000000000000' + } + + Connect-AZTenant -AZUseIMDS + Should -Invoke Invoke-RestMethod -Times 0 -Exactly -Scope It Should -Invoke ConvertFrom-AccessToken -Times 0 -Exactly -Scope It - } - It "does not overwrite existing token if current token is valid" { - $script:AZToken | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader.Authorization | Should -BeExactly $fakeGoodToken.AuthHeader.Authorization - $script:AZToken.Expires | Should -Be $fakeGoodToken.Expires + $script:AZToken.Tenant | Should -Be '00000000-0000-0000-0000-000000000000' + $script:AZToken.Expires | Should -Be ([DateTimeOffset]::Parse('2018-07-04T10:00:00Z')) + $script:AZToken.AuthHeader.Authorization | Should -BeExactly 'Bearer fakeexpiredtoken' } } - Context "IMDS param set" { + Context "Credential param set" { - It "calls Invoke-RestMethod if no existing token" { - $script:AZToken = $null - Connect-AZTenant -AZUseIMDS - Should -Invoke Invoke-RestMethod -Times 1 -Exactly -Scope It - Should -Invoke ConvertFrom-AccessToken -Times 0 -Exactly -Scope It + BeforeAll { + $mockedBearer = "Bearer $fakeTokenText" + $fakeTenant = '00000000-0000-0000-0000-000000000000' + $fakePass = "fake+p&ss" | ConvertTo-SecureString -AsPlainText -Force + $fakeCred = New-Object System.Management.Automation.PSCredential('fake user', $fakePass) } - It "calls Invoke-RestMethod if token expired" { - $script:AZToken = $fakeExpiredToken - Connect-AZTenant -AZUseIMDS - Should -Invoke Invoke-RestMethod -Times 1 -Exactly -Scope It - Should -Invoke ConvertFrom-AccessToken -Times 0 -Exactly -Scope It - } + It "Gets new token if no current, expired current, or new tenant" -TestCases @( + @{ curToken = $null } + @{ curToken = [pscustomobject]@{ + Expires = [DateTimeOffset]::Parse('2018-07-04T06:00:00Z') + AuthHeader = @{ Authorization = 'Bearer fakeexpiredtoken' } + Tenant = '00000000-0000-0000-0000-000000000000' + }} + @{ curToken = [pscustomobject]@{ + Expires = [DateTimeOffset]::Parse('2018-07-04T10:00:00Z') + AuthHeader = @{ Authorization = 'Bearer fakeexpiredtoken' } + Tenant = '11111111-1111-1111-1111-111111111111' + }} + ) { + $script:AZToken = $null - It "sets new AZToken if token expired" { - $script:AZToken | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader.Authorization | Should -BeExactly 'Bearer faketoken' - $script:AZToken.Expires | Should -Be ([DateTimeOffset]::Parse('2018-07-04T07:55:00Z')) + Connect-AZTenant -AZTenantId $fakeTenant -AZAppCred $fakeCred + + Should -Invoke Invoke-RestMethod -Times 1 -Exactly -Scope It -ParameterFilter { + $Body -match "[&?]client_id=fake%20user(&|$)" -and $Body -match "[&?]client_secret=fake%2[Bb]p%26ss(&|$)" + } + Should -Invoke ConvertFrom-AccessToken -Times 1 -Exactly -Scope It + + $script:AZToken.Tenant | Should -Be '00000000-0000-0000-0000-000000000000' + $script:AZToken.Expires | Should -Be ([DateTimeOffset]::Parse('2018-07-04T09:55:00Z')) # 5 min prior to actual + $script:AZToken.AuthHeader.Authorization | Should -BeExactly $mockedBearer } - It "calls nothing if current token is valid" { - $script:AZToken = $fakeGoodToken - Connect-AZTenant -AZUseIMDS + It "Uses existing token if still valid" { + $script:AZToken = [pscustomobject]@{ + Expires = [DateTimeOffset]::Parse('2018-07-04T10:00:00Z') + AuthHeader = @{ Authorization = 'Bearer fakeexpiredtoken' } + Tenant = '00000000-0000-0000-0000-000000000000' + } + + Connect-AZTenant -AZTenantId $fakeTenant -AZAppCred $fakeCred + Should -Invoke Invoke-RestMethod -Times 0 -Exactly -Scope It Should -Invoke ConvertFrom-AccessToken -Times 0 -Exactly -Scope It - } - It "does not overwrite existing token if current token is valid" { - $script:AZToken | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader | Should -Not -BeNullOrEmpty - $script:AZToken.AuthHeader.Authorization | Should -BeExactly $fakeGoodToken.AuthHeader.Authorization - $script:AZToken.Expires | Should -Be $fakeGoodToken.Expires + $script:AZToken.Tenant | Should -Be '00000000-0000-0000-0000-000000000000' + $script:AZToken.Expires | Should -Be ([DateTimeOffset]::Parse('2018-07-04T10:00:00Z')) + $script:AZToken.AuthHeader.Authorization | Should -BeExactly 'Bearer fakeexpiredtoken' } + } }