src/client/GraphIdentity.ps1
# Copyright 2021, Adam Edwards # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. . (import-script ../graphservice/GraphEndpoint) . (import-script GraphApplication) . (import-script ../auth/AuthProvider) . (import-script ../auth/V2AuthProvider) ScriptClass GraphIdentity { $App = strict-val [PSCustomObject] $Token = strict-val [PSCustomObject] $null $GraphEndpoint = strict-val [PSCustomObject] $null $TenantName = $null $TenantDisplayId = $null $TenantDisplayName = $null $AllowMSA = $false function __initialize([PSCustomObject] $app, [PSCustomObject] $graphEndpoint, [String] $tenantName, [boolean] $allowMSA) { $this.App = $app $this.GraphEndpoint = $graphEndpoint $this.TenantName = $tenantName $this.AllowMSA = $allowMSA $defaultAppId = $::.Application.DefaultAppId.tostring() $chinaEndpointUri = ( $::.GraphEndpoint |=> GetCloudEndpoint ChinaCloud ).Graph.tostring().trimend('/') if ( ( $graphEndpoint.Graph.tostring().trimend('/') -eq $chinaEndpointUri ) -and ( $app.AppId.tostring() -eq $defaultAppId ) ) { write-warning "Initializing connection to China cloud using the default application identifier '$defaultAppId', but this public cloud app may not be available in the China cloud, so authentication may fail. Consider creating a new public client application in a China cloud tenant and specify that application's application identifier (also known as client id) with Connect-GraphApi or related commands via their AppId parameter if you experience authentication failures and retry the failing command." } $this |=> __UpdateTenantDisplayInfo } function GetUserInformation { if ( $this.App.AuthType -eq 'Delegated' ) { $providerInstance = $::.AuthProvider |=> GetProviderInstance $providerInstance |=> GetUserInformation $this.token } else { [PSCustomObject]@{ AppId = $this.App.AppId userId = $null scopes = $null userObjectId = $null } } } function Authenticate($scopes = $null, $noBrowserUI = $false, $groupId = $null, [securestring] $certificatePassword) { if ( $this.token ) { $tokenTimeLeft = $this.token.expireson - [DateTime]::UtcNow write-verbose ("Found existing token with {0} minutes left before expiration" -f $tokenTimeLeft.TotalMinutes) } write-verbose ("Getting token for resource {0} from auth endpoint: {1} for groupid '{2}'" -f $this.graphEndpoint.GraphResourceUri, $this.graphEndpoint.Authentication, $groupId) $this.Token = getGraphToken $this.graphEndpoint $scopes $noBrowserUI $groupId $certificatePassword if ($this.token -eq $null) { throw "Failed to acquire token, no additional error information" } $this |=> __UpdateTenantDisplayInfo } function ClearAuthentication($groupId) { if ( $this.token -and $this.app.AuthType -eq 'Delegated' ) { $authUri = $this.graphEndpoint |=> GetAuthUri $this.TenantName $this.AllowMSA $providerInstance = $::.AuthProvider |=> GetProviderInstance $authContext = $providerInstance |=> GetAuthContext $this.app $this.graphEndpoint.GraphResourceUri $authUri $groupId $providerInstance |=> ClearToken $authContext $this.token } $this.token = $null } function getGraphToken($graphEndpoint, $scopes, $noBrowserUI, $groupId, [securestring] $certificatePassword) { write-verbose "Attempting to get token in tenant '$($this.tenantName)' for '$($graphEndpoint.GraphResourceUri)' ..." write-verbose "Using app id '$($this.App.AppId)'" $isConfidential = ($this.app |=> IsConfidential) write-verbose ("Is confidential client: '{0}'" -f $isConfidential) $existingToken = if ( $this.token ) { write-verbose "Current token expires on $($this.token.ExpiresOn)" $bufferMinutes = 5 if ( ( $this.token.ExpiresOn - [DateTimeOffset]::now ).TotalMinutes -ge $bufferMinutes ) { write-verbose "Using existing token since it is not within $bufferMinutes minutes of expiration" $this.token } else { write-verbose "Existing token is near expiration, new token will be requested" } } if ( $existingToken ) { $existingToken } else { write-verbose ("Adding scopes to request: {0}" -f ($scopes -join ';')) $authUri = $graphEndpoint |=> GetAuthUri $this.TenantName $this.AllowMSA write-verbose ("Sending auth request to auth uri '{0}'" -f $authUri) write-verbose ("Using redirect uri (reply url) '{0}'" -f $this.App.RedirectUri) $providerInstance = $::.AuthProvider |=> GetProviderInstance $authContext = $providerInstance |=> GetAuthContext $this.app $graphEndpoint.GraphResourceUri $authUri $groupId $certificatePassword $authResult = if ( $this.token ) { $providerInstance |=> AcquireRefreshedToken $authContext $this.token } else { if ( $this.App.AuthType -eq 'Apponly' ) { $providerInstance |=> AcquireFirstAppToken $authContext } else { # The latest version of posh-git seems to corrupt the state of the thread if it is imported into the PS session. # This causes auth to fail with a ThreadStateException from MSAL with a message indicating that an MTA operation # is being attempted in an STA thread, and that is illegal. This occurs when MSAL is trying to show the auth web dialog # and may impact device code auth as well. # The good news though is that apparently with a retry MSAL will be just fine -- we can just try again on this # same thread as a workaround until posh-git (or MSAL?) fixes the issue. We do this only when we encounter this specific # error. # It's not clear what additional instability is caused by posh-git here, so regardless of whether the workaround here # unblocks this module's functionality, it may be wise to remove posh-git whenever strange behavior arises with any module # to see if that fixes things. In general limiting posh-git to use cases where you really need it (e.g working # with a source control system) is advisable to avoid non-determinism and hard to troubleshoot errors. # Note that this workaround is only needed in the interactive case -- if no UX thread (e.g. a web dialog) is shown, # there is no MTA / STA issue. $remainingAttempts = 2 $interactiveTokenResult = $null do { $interactiveTokenResult = if ( $isConfidential ) { $providerInstance |=> AcquireFirstUserTokenConfidential $authContext $scopes } else { $providerInstance |=> AcquireFirstUserToken $authContext $scopes $noBrowserUI } # This is terrible -- since we're in PowerShell and async thread operations are inconvenient, we'll # just synchronously wait for the result :(. If we don't do this, we can't check the status # as it can change asynchronously $interactiveTokenResult.Result | out-null $isThreadException = if ( $interactiveTokenResult.Status -eq 'Faulted' ) { if ( $interactiveTokenResult | gm exception -erroraction ignore ) { ( $interactiveTokenResult.Exception -is [Exception] ) -and ( $interactiveTokenResult.Exception.InnerException -is [System.Threading.ThreadStateException] ) } } if ( ! $isThreadException ) { $remainingAttempts = 0 } elseif ( $remainingAttempts -gt 1 ) { write-verbose "Encountered thread exception accessing MSAL, a retry will be attempted" } } while ( --$remainingAttempts -gt 0 ) if ( $interactiveTokenResult ) { $interactiveTokenResult } } } write-verbose ("`nToken request status: {0}" -f $authResult.Status) if ( $authResult.Status -eq 'Faulted' ) { throw "Failed to acquire token for uri '$($graphEndpoint.GraphResourceUri)' for AppID '$($this.App.AppId)'`n" + $authResult.exception, $authResult.exception } $result = $authResult.Result if ( $authResult.IsFaulted ) { write-verbose $authResult.Exception throw [Exception]::new(("An authentication error occurred: '{0}'. See verbose output for additional details" -f $authResult.Exception.message), $authResult.Exception) } if ( ! $this.tenantDisplayId -and ( $result | gm -erroraction ignore tenantid ) ) { if ( $result.tenantid ) { $this.tenantDisplayId = $result.tenantid } } $result } } function GetTenantId($specifiedTenantId) { if ( $specifiedTenantId ) { $specifiedTenantId } else { $this |=> __UpdateTenantDisplayInfo $this.tenantDisplayId } } function __UpdateTenantDisplayInfo { $tenant = if ( $this.token -and ( $this.token | gm authority -erroraction ignore ) ) { (([uri] $this.token.authority).segments | select -last 1).trimend('/') } if ( ! $tenant ) { $tenant = if ( $this.token -and ( $this.token | gm user -erroraction ignore ) ) { if ( $this.token.user | gm identityprovider -erroraction ignore ) { (([uri] $this.token.user.identityprovider).segments | select -first 2 | select -last 1).trimend('/') } } } $tenantName = $null $tenantId = try { if ( $this.token ) { $this.token.tenantId } } catch { } $isGuid = $false $parsedTenantId = $null if ( $tenant ) { $outputGuid = (new-guid).guid $isGuid = [guid]::TryParse($tenant, [ref] $outputguid) if ( $isGuid ) { $parsedTenantid = $outputguid } } if ( ! $isGuid ) { $tenantName = $tenant } if ( ! $tenantName ) { $tenantName = $this.tenantName } if ( ! $tenantId ) { if ( $parsedTenantId ) { $tenantId = $parsedTenantId } else { # Last resort is to hope that the tenant name is actually # the same as the tenant id -- this is quite often the case $tenantGuid = (new-guid).guid $isTenantNameGuid = [guid]::TryParse($tenantName, [ref] $tenantGuid) if ( $isTenantNameGuid ) { $tenantId = $tenantGuid } } } $this.tenantDisplayId = $tenantId $this.tenantDisplayName = $tenantName } } |