GoogleOAuth2.psm1
[System.Collections.Hashtable]$script:OAuthTokens = @{} $script:ProfileLocation = "$env:USERPROFILE\.google\credentials" [System.String]$script:refresh_token = "refresh_token" [System.String]$script:access_token = "access_token" [System.String]$script:client_secret = "client_secret" [System.String]$script:iss = "iss" [System.String]$script:validity = "validity" [System.String]$script:scope = "scope" [System.String]$script:sub = "sub" [System.String[]]$script:NoUrlScopes = @("https://mail.google.com/", "https://www.google.com/calendar/feeds", "https://www.google.com/m8/feeds", "profile", "email", "openid", "servicecontrol", "cloud-platform-service-control", "service.management-service-management") [System.String[]]$script:EncryptedProperties = @($script:access_token, $script:refresh_token, $script:client_secret) [System.String[]]$script:Scopes = @( "activity", "adexchange.buyer", "adexchange.seller", "admin.datatransfer", "admin.datatransfer.readonly", "admin.directory.customer", "admin.directory.customer.readonly", "admin.directory.device.chromeos", "admin.directory.device.chromeos.readonly", "admin.directory.device.mobile", "admin.directory.device.mobile.action", "admin.directory.device.mobile.readonly", "admin.directory.domain", "admin.directory.domain.readonly", "admin.directory.group", "admin.directory.group.member", "admin.directory.group.member.readonly", "admin.directory.group.readonly", "admin.directory.notifications", "admin.directory.orgunit", "admin.directory.orgunit.readonly", "admin.directory.resource.calendar", "admin.directory.resource.calendar.readonly", "admin.directory.rolemanagement", "admin.directory.rolemanagement.readonly", "admin.directory.user", "admin.directory.user.alias", "admin.directory.user.alias.readonly", "admin.directory.user.readonly", "admin.directory.user.security", "admin.directory.userschema", "admin.directory.userschema.readonly", "admin.reports.audit.readonly", "admin.reports.usage.readonly", "adsense", "adsense.readonly", "adsensehost", "analytics", "analytics.edit", "analytics.manage.users", "analytics.manage.users.readonly", "analytics.provision", "analytics.readonly", "androidenterprise", "androidmanagement", "androidpublisher", "appengine.admin", "apps.groups.migration", "apps.groups.settings", "apps.licensing", "apps.order", "apps.order.readonly", "appstate", "bigquery", "bigquery.insertdata", "blogger", "blogger.readonly", "books", "calendar", "calendar.readonly", "classroom.announcements", "classroom.announcements.readonly", "classroom.courses", "classroom.courses.readonly", "classroom.coursework.me", "classroom.coursework.me.readonly", "classroom.coursework.students", "classroom.coursework.students.readonly", "classroom.guardianlinks.me.readonly", "classroom.guardianlinks.students", "classroom.guardianlinks.students.readonly", "classroom.profile.emails", "classroom.profile.photos", "classroom.push-notifications", "classroom.rosters", "classroom.rosters.readonly", "classroom.student-submissions.me.readonly", "classroom.student-submissions.students.readonly", "cloud.useraccounts", "cloud.useraccounts.readonly", "cloud_debugger", "cloudiot", "cloud-language", "cloud-platform", "cloud-platform.read-only", "cloud-platform-service-control", "cloudruntimeconfig", "cloud-translation", "cloud-vision", "compute", "compute.readonly", "contacts", "contacts.readonly", "content", "datastore", "ddmconversions", "devstorage.full_control", "devstorage.read_only", "devstorage.read_write", "dfareporting", "dfatrafficking", "doubleclickbidmanager", "doubleclicksearch", "drive", "drive.appdata", "drive.file", "drive.metadata", "drive.metadata.readonly", "drive.photos.readonly", "drive.readonly", "drive.scripts", "ediscovery", "ediscovery.readonly", "email", "firebase", "firebase.readonly", "fitness.activity.read", "fitness.activity.write", "fitness.blood_glucose.read", "fitness.blood_glucose.write", "fitness.blood_pressure.read", "fitness.blood_pressure.write", "fitness.body.read", "fitness.body.write", "fitness.body_temperature.read", "fitness.body_temperature.write", "fitness.location.read", "fitness.location.write", "fitness.nutrition.read", "fitness.nutrition.write", "fitness.oxygen_saturation.read", "fitness.oxygen_saturation.write", "fitness.reproductive_health.read", "fitness.reproductive_health.write", "forms", "forms.currentonly", "fusiontables", "fusiontables.readonly", "games", "genomics", "genomics.readonly", "glass.location", "glass.timeline", "gmail.compose", "gmail.insert", "gmail.labels", "gmail.metadata", "gmail.modify", "gmail.readonly", "gmail.send", "gmail.settings.basic", "gmail.settings.sharing", "groups", "https://mail.google.com/", "https://www.google.com/calendar/feeds", "https://www.google.com/m8/feeds", "logging.admin", "logging.read", "logging.write", "manufacturercenter", "monitoring", "monitoring.read", "monitoring.write", "ndev.clouddns.readonly", "ndev.clouddns.readwrite", "ndev.cloudman", "ndev.cloudman.readonly", "openid", "plus.circles.read", "plus.circles.write", "plus.login", "plus.me", "plus.media.upload", "plus.profiles.read", "plus.stream.read", "plus.stream.write", "prediction", "presentations", "presentations.readonly", "profile", "pubsub", "replicapool", "replicapool.readonly", "service.management", "service.management.readonly", "service.management-service-management", "servicecontrol", "siteverification", "siteverification.verify_only", "source.full_control", "source.read_only", "source.read_write", "spanner.admin", "spanner.data", "spreadsheets", "spreadsheets.readonly", "sqlservice.admin", "streetviewpublish", "tagmanager.delete.containers", "tagmanager.edit.containers", "tagmanager.edit.containerversions", "tagmanager.manage.accounts", "tagmanager.manage.users", "tagmanager.publish", "tagmanager.readonly", "taskqueue", "taskqueue.consumer", "tasks", "tasks.readonly", "trace.append", "urlshortener", "user.addresses.read", "user.birthday.read", "user.emails.read", "user.phonenumbers.read", "userinfo.email", "userinfo.profile", "userlocation.beacon.registry", "webmasters", "webmasters.readonly", "xapi.zoo", "youtube", "youtube.force-ssl", "youtube.readonly", "youtube.upload", "youtubepartner", "youtubepartner-channel-audit", "yt-analytics.readonly", "yt-analytics-monetary.readonly" ) #region Scopes Function Convert-GoogleApiScopes { <# .SYNOPSIS Converts the short name scopes used as parameter input to their full values. .DESCRIPTION This cmdlet converts the short name scopes used as parameter input to their full values as required by access codes and JWTs. .PARAMETER Scopes The scopes to convert. .EXAMPLE $Scopes = @("compute", "admin.directory.group.readonly") $NewScopes = $Scopes | Convert-GoogleApiScopes Converts the provided scopes to the scope names that usually begin with https://www.googleapis.com/auth/. .INPUTS System.String[] .OUTPUTS System.String[] .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/27/2018 #> [CmdletBinding()] [OutputType([System.String[]])] Param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [System.String[]]$Scopes ) Begin { [System.String[]]$Temp = @() } Process { $Temp += $Scopes } End { [System.String[]]$FinalScopes = @() foreach ($Item in $Temp) { if ($Item -notin $script:NoUrlScopes) { $FinalScopes += "https://www.googleapis.com/auth/$Item" } elseif ($Item -eq "cloud-platform-service-control") { # cloud-platform is used both with a preceding url for some services and without for cloud service control APIs $FinalScopes += "cloud-platform" } else { $FinalScopes += $Item } } Write-Output -InputObject $FinalScopes } } Function Get-GoogleOAuth2ApiScopes { <# .SYNOPSIS Gets a current set of available OAuth2 API scopes. .DESCRIPTION This cmdlet retrieves the curent set of available OAuth2 API scopes. The format can either be in the short form used by the input parameters of the cmdlets in this module, or as their long full name as used in access code requests or JWT claim set construction. .PARAMETER UseShortNames Specifies that the output is the short names of the scopes as defined in this module. .INPUTS None .OUTPUTS System.String[] .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/27/2018 #> [CmdletBinding()] [OutputType([System.String[]])] Param( [Parameter()] [Switch]$UseShortNames ) Begin { } Process { if ($UseShortNames) { Write-Output -InputObject $script:Scopes } else { Write-Output -InputObject (Convert-GoogleApiScopes -Scopes $script:Scopes) } } End { } } #endregion #region Auth Code Function Get-GoogleOAuth2Code { <# .SYNOPSIS Gets an authorization code for specified scopes to be granted Google OAuth2 credentials. .DESCRIPTION This cmdlet initiates the user approval for access to data and opens a browser window for the user to login and provide consent to the access. After approval, the browser will present an authorization code that should be pasted back into the prompt presented to the user. The code is sent out the pipeline, which should be supplied to Get-GoogleOAuth2Token in order to get Google OAuth2 bearer tokens. .PARAMETER ClientId The supplied client id for OAuth. .PARAMETER ClientSecret The supplied client secret for OAuth. .PARAMETER Email The user's GSuite/Google user email to provide as a login hint to the login and consent page. .PARAMETER Scope The scope or scopes to be authorized in the OAuth tokens. .PARAMETER AccessType Indicates the module can refresh access tokens when the user is not present at the browser. This value instructs the Google authorization server to return a refresh token and an access token the first time that the cmdlet exchages an authorization code for tokens. You should always specify "offline", which is the default. .PARAMETER ResponseType How the Google Authorization server returns the code: Setting to "token" instructs the Google Authorization Server to return the access token as a name=value pair in the hash (#) fragment of the URI to which the user is redirected after completing the authorization process. You must specify "online" as the AccessType with this setting and provide an actual redirect url. Setting to "code" instructs the Google Authorization Server to return the access code as an element in the web browser that can be copy and pasted into PowerShell. You should always specify "code" for this cmdlet, which is the default. .PARAMETER NoPrompt Indicates that the user receives no prompt in the web browser, which will likely result in a failed attempt or an access denied error. You shouldn't specify this parameter. .EXAMPLE $Code = Get-GoogleOAuth2Code -ClientId $Id -ClientSecret $Secret -Email john.smith@google.com -Scope "admin.directory.group.readonly" Gets an authorization code for the user to be able to exchange it for a long-term access token with the ability to have read-only access to groups in GSuite through the Google Directory API. .INPUTS None .OUTPUTS System.String .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/12/2018 #> [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$ClientId, [Parameter()] [ValidateNotNullOrEmpty()] [System.String]$Email, [Parameter()] [ValidateSet("online", "offline")] [System.String]$AccessType = "offline", [Parameter()] [ValidateSet("code", "token")] [System.String]$ResponseType = "code", #[Parameter()] #[Switch]$NoWebBrowser, [Parameter()] [Switch]$NoPrompt ) DynamicParam { $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute] $ParameterAttribute = New-Object -TypeName System.Management.Automation.PARAMETERAttribute $ParameterAttribute.Mandatory = $true $AttributeCollection.Add($ParameterAttribute) $ValidateSetAttribute = New-Object -TypeName System.Management.Automation.ValidateSetAttribute($script:Scopes) $AttributeCollection.Add($ValidateSetAttribute) $RuntimeParameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("Scope", ([System.String[]]), $AttributeCollection) $RuntimeParameterDictionary.Add("Scope", $RuntimeParameter) return $RuntimeParameterDictionary } Begin { # This redirect tells Google to display the authorization code in the web browser [System.String]$Redirect = [System.Uri]::EscapeUriString("urn:ietf:wg:oauth:2.0:oob") } Process { $ClientId = [System.Uri]::EscapeUriString($ClientId) [System.String[]]$Scope = Convert-GoogleApiScopes -Scopes $PSBoundParameters["Scope"] [System.String]$Scopes = [System.Uri]::EscapeUriString($Scope -join ",") [System.String]$StateVariable="ps_state" [System.String]$OAuth = "https://accounts.google.com/o/oauth2/v2/auth?client_id=$ClientId&redirect_uri=$Redirect&scope=$Scopes&access_type=$AccessType&include_granted_scopes=true&response_type=$ResponseType&state=$StateVariable" if ($NoPrompt) { $OAuth += "&prompt=none" } if (-not [System.String]::IsNullOrEmpty($Email)) { $OAuth += "&login_hint=$([System.Uri]::EscapeUriString($Email))" } try { $Code = "" # Get the redirect url [Microsoft.PowerShell.Commands.WebResponseObject]$RedirectResponse = Invoke-WebRequest -Uri $OAuth -Method Get -MaximumRedirection 0 -ErrorAction Ignore -UserAgent PowerShell Write-Verbose -Message "Response Code: $($RedirectResponse.StatusCode)" # If the response is a redirect, that's what we expect if ($RedirectResponse.StatusCode.ToString().StartsWith("30")) { [System.Uri]$Redirect = $RedirectResponse.Headers.Location Write-Verbose -Message "Redirect location: $Redirect" if ($NoWebBrowser) { <# [System.Collections.Hashtable]$Query = @{} # Remove leading "?" $Redirect.Query.Substring(1) -split "&" | ForEach-Object { $Parts = $_ -split "=" $Query.Add($Parts[0], $Parts[1]) } # Get the first page, it could be an account selection page, a password entry page, or a the consent page [Microsoft.PowerShell.Commands.HtmlWebResponseObject]$SignInResponse = Invoke-WebRequest -Uri $Redirect -Method Get $SignInResponse.ParsedHtml.GetElementById("Email").value = $Query["Email"] [Microsoft.PowerShell.Commands.HtmlWebResponseObject]$NextResponse = Invoke-WebRequest -Uri $SignInResponse.Forms[0].Action -Body $SignInResponse.Forms[0] -Method Post $StateWrapper = $NextResponse.ParsedHtml.GetElementById("state_wrapper").value $SignInUrl = "https://accounts.google.com/o/oauth2/approval?hd=$Org&as=$As&pageId=none&xsrfsign=$XSRF" [Microsoft.PowerShell.Commands.HtmlWebResponseObject]$CodeResponse = Invoke-WebRequest -Uri $NextResponse.Forms[0].Action -Method Post # Title looks like: # Success state=<state_var>&code=<oauth_code>&scope=<scope_var> $Title = $CodeResponse.ParsedHtml.GetElementsByTagName("title") | Select-Object -First 1 -ExpandProperty text $Code = ($Title -ireplace "&", "") -split ";" | Where-Object {$_ -ilike "code=*" } | Select-Object -First 1 $Code = ($Code -split "=")[1] #> Write-Warning -Message "No browser option isn't supported yet." } else { Write-Verbose -Message "Please open $Redirect in your browser" try { # This will launch a web browser with the provided url & start $Redirect while ([System.String]::IsNullOrEmpty($Code)) { $Code = Read-Host -Prompt "Enter authorization code from web browser" } } catch [Exception] { if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { Write-Error -Message "Could not open a web browser" -Exception $_.Exception -ErrorAction Stop } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message "Could not open a web browser: $($_.Exception.Message)" } else { Write-Verbose -Message "[ERROR] : Could not open a web browser: $($_.Exception.Message)" } } } # This is where we normally return Write-Output -InputObject $Code } else { Write-Error -Message $RedirectResponse.RawContent } } catch [System.Net.WebException] { [System.Net.WebException]$Ex = $_.Exception [System.Net.HttpWebResponse]$Response = [System.Net.HttpWebResponse]($Ex.Response) [System.IO.Stream]$Stream = $Ex.Response.GetResponseStream() [System.IO.StreamReader]$Reader = New-Object -TypeName System.IO.StreamReader($Stream, [System.Text.Encoding]::UTF8) [System.String]$Content = $Reader.ReadToEnd() [System.Int32]$StatusCode = $Response.StatusCode.value__ [System.String]$Message = "$StatusCode : $Content" if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { [System.Web.HttpException]$NewEx = New-Object -TypeName System.Web.HttpException($Content, $StatusCode) Write-Error -Exception $NewEx -Category NotSpecified -ErrorId $StatusCode } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $Message } else { Write-Verbose -Message "[ERROR] : $Message" } } catch [Exception] { if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { Write-Error -Exception $_.Exception -ErrorAction Stop } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $_.Exception.Message } else { Write-Verbose -Message "[ERROR] : $($_.Exception.Message)" } } } End { } } Function Convert-GoogleOAuth2Code { <# .SYNOPSIS Exchanges an OAuth2 code for an access token. .DESCRIPTION This cmdlet exchanges an OAuth2 code for an access token and refresh token that can used to authenticate a user to Google APIs. .PARAMETER Code The one-time use authorization code received from Google. .PARAMETER ClientId The provided ClientId. .PARAMETER ClientSecret The provided ClientSecret. .PARAMETER GrantType The type of token being exchanged, in this case always authorization_code. .PARAMETER ProfileLocation The location where stored credentials are located. If this is not specified, the default location will be used. .PARAMETER Persist Indicates that the retrieved access tokens and client secret will be persisted on disk in an encrypted format using the Windows DPAPI as well as the local in-memory cache (also encrypted). .EXAMPLE $Code = Get-GoogleOAuth2Code -ClientId $Id -ClientSecret $Secret Convert-GoogleOAuth2Code -Code $Code -ClientId $Id -ClientSecret $Secret -Persist This example retrieves an authorization code and then exchanges it for long term access and refresh tokens. The token data and client secret are persisted to disk in an encrypted format. .INPUTS None .OUTPUTS None .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/17/2018 #> [CmdletBinding()] [OutputType()] Param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$Code, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$ClientId, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$ClientSecret, [Parameter()] [ValidateSet("authorization_code")] [System.String]$GrantType = "authorization_code", [Parameter()] [System.String]$ProfileLocation, [Parameter()] [Switch]$Persist ) Begin { $Base = "https://www.googleapis.com/oauth2/v4/token" $CodeRedirect = [System.Uri]::EscapeUriString("urn:ietf:wg:oauth:2.0:oob") } Process { Write-Verbose -Message "Exchanging OAuth2 code for an access token." $Code = [System.Uri]::EscapeUriString($Code) $ClientId = [System.Uri]::EscapeUriString($ClientId) $ClientSecret = [System.Uri]::EscapeUriString($ClientSecret) $GrantType = "authorization_code" $Url = "$Base`?code=$Code&client_id=$ClientId&client_secret=$ClientSecret&redirect_uri=$CodeRedirect&grant_type=$GrantType" try { [Microsoft.PowerShell.Commands.WebResponseObject]$Response = Invoke-WebRequest -Uri $Url -Method Post -UserAgent PowerShell Write-Verbose -Message $Response.Content [PSCustomObject]$Data = ConvertFrom-Json -InputObject $Response.Content # Update the cache and persisted data Set-GoogleOAuth2Profile -ClientId $ClientId -ClientSecret $ClientSecret -AccessToken $Data.access_token -RefreshToken $Data.refresh_token -ProfileLocation $ProfileLocation -Persist:$Persist [System.Collections.Hashtable]$Token = Get-GoogleOAuth2Profile -ClientId $ClientId -ProfileLocation $ProfileLocation Write-Output -InputObject $Token } catch [System.Net.WebException] { [System.Net.WebException]$Ex = $_.Exception [System.Net.HttpWebResponse]$Response = [System.Net.HttpWebResponse]($Ex.Response) [System.IO.Stream]$Stream = $Ex.Response.GetResponseStream() [System.IO.StreamReader]$Reader = New-Object -TypeName System.IO.StreamReader($Stream, [System.Text.Encoding]::UTF8) [System.String]$Content = $Reader.ReadToEnd() [System.Int32]$StatusCode = $Response.StatusCode.value__ [System.String]$Message = "$StatusCode : $Content" if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { [System.Web.HttpException]$NewEx = New-Object -TypeName System.Web.HttpException($Content, $StatusCode) Write-Error -Exception $NewEx -Category NotSpecified -ErrorId $StatusCode } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $Message } else { Write-Verbose -Message "[ERROR] : $Message" } } catch [Exception] { if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { Write-Error -Exception $_.Exception -ErrorAction Stop } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $_.Exception.Message } else { Write-Verbose -Message "[ERROR] : $($_.Exception.Message)" } } } End { } } #endregion #region Tokens Function Request-GoogleOAuth2Token { <# .SYNOPSIS Requests a token for the google OAuth2 service. .DESCRIPTION This cmdlet wraps multiple ways to retrieve an access token. It can process the following methods 1) Requesting a new authorization code and exchaning it for a token 2) Receiving a currently valid authorization code and exchanging it for a token 3) Receive a refresh token and client secret and exchange for a new access token 4) Use a cached or persisted profile indicated by the client id. If the profile has a current access token, it is returned, if not, if the profile contains means to refresh or renew the access token, it performs that action and returns the new token, otherwise an exception is thrown. 5) Receiving a currently valid JWT produced from GCP service account credentials and exchanging it for a token. .PARAMETER ClientId The OAuth client id or service account email to get an access token for. .PARAMETER Code A valid authorization code to exchange for a set of tokens. .PARAMETER JWT A valid base64 url encoded JWT produced from GCP service account credentials to exchange for an access token. .PARAMETER ClientSecret The client secret to be used with an authorization code or refresh token. .PARAMETER RefreshToken The refresh token to use to request a new access token. .PARAMETER Persist Indicates that the newly retrieved token(s) or refreshed token and associated client data like client secret are persisted to disk. .PARAMETER ProfileLocation The location where stored credentials are located. If this is not specified, the default location will be used. .EXAMPLE Request-GoogleOAuth2Token -ClientId "21268289792-22n7bku0cf8okn8pib505bk78l4k838e.apps.googleusercontent.com" -ClientSecret $Secret -Persist This example initiates the process of retrieving an authorization code and exchaning it for an access token that is persisted to disk. .EXAMPLE Request-GoogleOAuth2Token -ClientId "21268289792-22n7bku0cf8okn8pib505bk78l4k838e.apps.googleusercontent.com" -RefreshToken $RToken -ClientSecret $Secret -Persist This example retrieves a new access token from a provided refresh token and client secret and persists the results to disk. .INPUTS None .OUTPUTS System.Collections.Hashtable .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/12/2018 #> [CmdletBinding(DefaultParameterSetName = "Get")] [OutputType([System.Collections.Hashtable])] Param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$ClientId, [Parameter(Mandatory = $true, ParameterSetName = "Code")] [ValidateNotNullOrEmpty()] [System.String]$Code, [Parameter(Mandatory = $true, ParameterSetName = "JWT")] [ValidateNotNullOrEmpty()] [System.String]$JWT, [Parameter(Mandatory = $true, ParameterSetName = "Code")] [Parameter(Mandatory = $true, ParameterSetName = "Default")] [Parameter(Mandatory = $true, ParameterSetName = "RefreshFromToken")] [ValidateNotNullOrEmpty()] [System.String]$ClientSecret, [Parameter(Mandatory = $true, ParameterSetName = "RefreshFromToken")] [ValidateNotNullOrEmpty()] [System.String]$RefreshToken, [Parameter()] [Switch]$Persist, [Parameter()] [System.String]$ProfileLocation ) DynamicParam { $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute] $ParameterAttribute = New-Object -TypeName System.Management.Automation.PARAMETERAttribute $ParameterAttribute.Mandatory = $true $ParameterAttribute.ParameterSetName = "Default" $AttributeCollection.Add($ParameterAttribute) $ValidateSetAttribute = New-Object -TypeName System.Management.Automation.ValidateSetAttribute($script:Scopes) $AttributeCollection.Add($ValidateSetAttribute) $RuntimeParameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("Scope", ([System.String[]]), $AttributeCollection) $RuntimeParameterDictionary.Add("Scope", $RuntimeParameter) return $RuntimeParameterDictionary } Begin { } Process { switch ($PSCmdlet.ParameterSetName) { "Default" { [System.String[]]$Scope = $PSBoundParameters["Scope"] $Code = Get-GoogleOAuth2Code -ClientId $ClientId -Scope $Scope $Token = Convert-GoogleOAuth2Code -Code $Code -ClientId $ClientId -ClientSecret $ClientSecret -ProfileLocation $ProfileLocation -Persist:$Persist Write-Output -InputObject $Token break } "Code" { $Token = Convert-GoogleOAuth2Code -Code $Code -ClientId $ClientId -ClientSecret $ClientSecret -ProfileLocation $ProfileLocation -Persist:$Persist Write-Output -InputObject $Token break } "RefreshFromToken" { $Token = Update-GoogleOAuth2Token -ClientId $ClientId -RefreshToken $RefreshToken -ClientSecret $ClientSecret -ProfileLocation $ProfileLocation -Persist:$Persist Write-Output -InputObject $Token break } "Get" { # Specify stop for the error action so that an exception is thrown in case # the token can't be renewed/refreshed, i.e. the refresh token or client secret is missing $Token = Get-GoogleOAuth2Token -ClientId $ClientId -ProfileLocation $ProfileLocation -Persist:$Persist -ErrorAction Stop Write-Output -InputObject $Token break } "JWT" { $Token = Convert-GoogleOAuth2JWT -JWT $JWT -ClientId $ClientId -ProfileLocation $ProfileLocation -Persist:$Persist Write-Output -InputObject $Token break } default { Write-Error -Message "Unknown parameter set $($PSCmdlet.ParameterSetName) for $($MyInvocation.MyCommand)." -ErrorAction Stop } } } End { } } Function Get-GoogleOAuth2Token { <# .SYNOPSIS Retrieves a current access token from the in-memory cache or local disk. .DESCRIPTION This cmdlet retrieves the token set for the specified ClientId, either from the in-memory cache or the local disk if it is persisted. The access_token is analyzed to see if it is valid, and if not, it is automatically updated if a refresh token and client secret is present or if the client id specifies a service account profile, if the iss, scope, and client secret properties are present. .PARAMETER ClientId The key value the token set is stored as. .PARAMETER ProfileLocation The location where stored credentials are located. If this is not specified, the default location will be used. .PARAMETER Persist Specifies that if the access token needs to be refreshed during retrieval that the updated access token is persisted to disk. .EXAMPLE $Token = Get-GoogleOAuth2Token -ClientId $Id -Persist This example retrieves the stored tokens and client secret associated with the provided client Id and persists the updated access token if it needs to be refreshed. .INPUTS System.String .OUTPUTS System.Collections.Hashtable .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/18/2018 #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] Param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [System.String]$ClientId, [Parameter()] [System.String]$ProfileLocation, [Parameter()] [Switch]$Persist ) Begin { } Process { Write-Verbose -Message "Getting OAuth2 token from cache." [System.Collections.Hashtable]$Token = Get-GoogleOAuth2Profile -ClientId $ClientId -ProfileLocation $ProfileLocation if ($Token -eq $null) { Write-Error -Exception (New-Object -TypeName System.Collections.Generic.KeyNotFoundException("No stored tokens for profile $ClientId.")) -ErrorAction Stop } if (-not $Token.ContainsKey($script:client_secret)) { Write-Error -Exception (New-Object -TypeName System.Collections.Generic.KeyNotFoundException("The stored token for profile $ClientId does not contain the required client_secret.")) -ErrorAction Stop } Write-Verbose -Message "Cache contains profile information for $ClientId." switch ($Token["type"]) { "client" { # If the token contains an refresh token, we should check to see if we need to get # or renew the access token if ($Token.ContainsKey($script:refresh_token)) { [System.Collections.Hashtable]$TokenToReturn = @{} # If there's an access token and refresh token, let's make sure it's up to date if ($Token.ContainsKey($script:access_token)) { # Check the access token to see if it's expired, if it is, refresh, otherwise, return as is [System.Boolean]$Expired = Test-IsGoogleOAuth2TokenExpired -AccessToken $Token[$script:access_token] -ErrorAction SilentlyContinue if ($Expired) { Write-Verbose -Message "The current access token is expired, getting a new one." # This will update the cache and persisted data store if necessary $TokenToReturn = Update-GoogleOAuth2Token -RefreshToken $Token[$script:refresh_token] -ClientId $ClientId -ClientSecret $Token[$script:client_secret] -Persist:$Persist } else { Write-Verbose -Message "The current access token is valid." # No need to do anything, use the token we found in the cache $TokenToReturn = $Token } } else { # The stored profile doesn't contain a current access_token, go ahead and request one with the # refresh token # Since there wasn't a persisted access_token, either on disk or in the cache, this will add that access_token to the # cache so we can continue to use it later $TokenToReturn = Update-GoogleOAuth2Token -RefreshToken $Token[$script:refresh_token] -ClientId $ClientId -ClientSecret $Token[$script:client_secret] -Persist:$Persist } Write-Output -InputObject $TokenToReturn } elseif ($Token.ContainsKey($script:access_token)) { # There's no refresh token, so just use this and hope it's not expired if (-not (Test-IsGoogleOAuth2TokenExpired -AccessToken $Token[$script:access_token])) { Write-Output -InputObject $Token } else { Write-Error -Message "The stored access token is expired and there is no refresh token available." -ErrorAction Stop } } else { # This shouldn't happen since the cmdlet to modify the profile requires at least 1 token to be set, but # best to check it anyways, might have been edited manually Write-Verbose -Message "No stored tokens found for $ClientId, removing it from the cache and persisted data store." Remove-GoogleOAuth2Profile -ClientId $ClientId -ProfileLocation $ProfileLocation Write-Error -Message "No stored tokens for profile $ClientId." -ErrorAction Stop } break } "sa" { if ($Token.ContainsKey($script:access_token)) { [System.Boolean]$Expired = Test-IsGoogleOAuth2TokenExpired -AccessToken $Token[$script:access_token] -ErrorAction SilentlyContinue if ($Expired) { Write-Verbose -Message "The current access token is expired, getting a new one." if ($Token.ContainsKey($script:scope) -and $Token.ContainsKey($script:iss) -and $Token.ContainsKey($script:client_secret)) { [System.Collections.Hashtable]$JWTSplat = @{} if ($Token.Contains($script:sub) -and -not [System.String]::IsNullOrEmpty($Token[$script:sub])) { $JWTSplat.Add("Subject", $Token[$script:sub]) } if ($Token.Contains($script:validity)) { $JWTSplat.Add("ValidityInSeconds", $Token[$script:validity]) } [System.String]$NewJWT = New-GoogleServiceAccountJWT -ClientSecret $Token[$script:client_secret] -Issuer $Token[$script:iss] -Scope $Token[$script:scope] @JWTSplat [System.Collections.Hashtable]$TokenFromJWT = Convert-GoogleOAuth2JWT -JWT $NewJWT -ClientId $ClientId -ProfileLocation $ProfileLocation -Persist:$Persist Write-Output -InputObject $TokenFromJWT } else { Write-Error -Exception (New-Object -TypeName System.NotSupportedException("The stored profile does not have the required attributes to renew the expired access token for $ClientId.")) -ErrorAction Stop } } else { Write-Output -InputObject $Token } } else { Write-Verbose "No stored access token for $ClientId" if ($Token.ContainsKey($script:scope) -and $Token.ContainsKey($script:iss) -and $Token.ContainsKey($script:client_secret)) { [System.Collections.Hashtable]$SubSplat = @{} if ($Token.Contains($script:sub) -and -not [System.String]::IsNullOrEmpty($Token[$script:sub])) { $SubSplat.Add("Subject", $Token[$script:sub]) } [System.String]$NewJWT = New-GoogleServiceAccountJWT -ClientSecret $Token[$script:client_secret] -Issuer $Token[$script:iss] -Scope $Token[$script:scop] @SubSplat [System.Collections.Hashtable]$TokenFromJWT = Convert-GoogleOAuth2JWT -JWT $NewJWT -ClientId $ClientId -ProfileLocation $ProfileLocation -Persist:$Persist Write-Output -InputObject $TokenFromJWT } else { Write-Verbose -Message "No stored tokens or jwt properties found for $ClientId, removing it from the cache and persisted data store." Remove-GoogleOAuth2Profile -ClientId $ClientId -ProfileLocation $ProfileLocation -Force } } break } } } End { } } Function Update-GoogleOAuth2Token { <# .SYNOPSIS Refreshes a Google OAuth2 access token that was retrieved with an authorization code. .DESCRIPTION This cmdlet refreshes a Google OAuth2 access token. The access token should have been retrieved through an access code exchange and is refreshed with an associated refresh token. The refresh token can either be supplied as a parameter or can be stored in a cached or persisted profile. The updated token data is returned to pipeline regardless if the data is persisted or not. If the data is not persisted, the cache is still updated so the same session can access the new tokens. .PARAMETER RefreshToken The refresh token returned during the initial authorization code exchange. This token is passed to the Google OAuth API to retrieve a new access token. .PARAMETER ClientId The client id associated with the refresh token or that indicates the stored profile to use that contains a persisted refresh token. .PARAMETER ClientSecret The client secret to pass with refresh request. .PARAMETER GrantType The grant type for the refresh, this must be refresh_token, and is the only allowed, and default value, for this parameter. .PARAMETER ProfileLocation The location where stored credentials are located. If this is not specified, the default location will be used. .PARAMETER Persist Specifies that the updated access token will be persisted to disk. If this is specified when RefreshToken and ClientSecret are specified, they are also persisted to disk. .EXAMPLE Update-GoogleOAuth2Token -ClientId "21251290794-21o6bku0cf8oln8pia505bk7813k838a.apps.googleusercontent.com" -Persist This updates the access token for the specified cached or stored profile. The renewed access token is persisted to disk. The profile indicated by the client id must have a refresh_token and client_secret stored in the profile, otherwise an exception is thrown. .EXAMPLE Update-GoogleOAuth2Token -ClientId "21251290794-21o6bku0cf8oln8pia505bk7813k838a.apps.googleusercontent.com" ` -RefreshToken $RToken ` -ClientSecret $Secret ` -Persist In this example, the access token is refreshed with the specified parameters and nothing from the cache or persisted profile store is used. However, the updated access token, refresh token, and client secret are persisted upon a successful request. .INPUTS None .OUTPUTS System.Collections.Hashtable .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/27/2018 #> [CmdletBinding(DefaultParameterSetName = "Stored")] Param( [Parameter(Mandatory = $true, ParameterSetName = "Token")] [ValidateNotNullOrEmpty()] [System.String]$RefreshToken, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$ClientId, [Parameter(Mandatory = $true, ParameterSetName = "Token")] [ValidateNotNullOrEmpty()] [System.String]$ClientSecret, [Parameter(DontShow = $true)] [ValidateSet("refresh_token")] [System.String]$GrantType = "refresh_token", [Parameter()] [System.String]$ProfileLocation, [Parameter()] [Switch]$Persist ) Begin { [System.String]$Base = "https://www.googleapis.com/oauth2/v4/token" } Process { Write-Verbose -Message "Updating the OAuth2 token for $ClientId." switch ($PSCmdlet.ParameterSetName) { "Stored" { # Use currently stored or cached tokens $TokenData = Get-GoogleOAuth2Profile -ClientId $ClientId -ProfileLocation $ProfileLocation if ($TokenData.ContainsKey($script:refresh_token) -and $TokenData.ContainsKey($script:client_secret)) { $ClientSecret = $TokenData[$script:client_secret] $RefreshToken = $TokenData[$script:refresh_token] } else { Write-Error -Exception (New-Object -TypeName System.Collections.Generic.KeyNotFoundException("The specified profile $ClientId does not contain a refresh token and/or a client secret and cannot be refreshed.")) -ErrorAction Stop } break } "Token" { # Do nothing break } default { Write-Error -Message "Unknown parameter set name $($PSCmdlet.ParameterSetName)." -ErrorAction Stop } } $ClientSecret = [System.Uri]::EscapeUriString($ClientSecret) $ClientId = [System.Uri]::EscapeUriString($ClientId) [System.String]$Url = "$Base`?client_id=$ClientId&client_secret=$ClientSecret&refresh_token=$RefreshToken&grant_type=$GrantType" try { [Microsoft.PowerShell.Commands.WebResponseObject]$Response = Invoke-WebRequest -Uri $Url -Method Post -UserAgent PowerShell if ($Response.StatusCode -eq 200) { # The request was successful, convert the JSON response data [PSCustomObject]$Token = (ConvertFrom-Json -InputObject $Response.Content) # Update the local cache with the updated access token, and also possibly the refresh token if it wasn't stored originally # with the profile, or the profile may not have existed at all Set-GoogleOAuth2Profile -AccessToken $Token.access_token -RefreshToken $RefreshToken -ClientSecret $ClientSecret -ClientId $ClientId -ProfileLocation $ProfileLocation -Persist:$Persist # Create the hash table with the returned token [System.Collections.Hashtable]$Temp = @{} foreach ($Property in ($Token | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)) { $Temp.Add($Property, $Token.$Property) } Write-Output -InputObject $Temp } else { if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { Write-Error -Message "There was a problem refreshing the token: $($Response.Content)" -ErrorAction Stop } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message "There was a problem refreshing the token: $($Response.Content)" } else { Write-Verbose -Message "[ERROR] : There was a problem refreshing the token: $($Response.Content)" } } } catch [System.Net.WebException] { [System.Net.WebException]$Ex = $_.Exception [System.Net.HttpWebResponse]$Response = [System.Net.HttpWebResponse]($Ex.Response) [System.IO.Stream]$Stream = $Ex.Response.GetResponseStream() [System.IO.StreamReader]$Reader = New-Object -TypeName System.IO.StreamReader($Stream, [System.Text.Encoding]::UTF8) [System.String]$Content = $Reader.ReadToEnd() [System.Int32]$StatusCode = $Response.StatusCode.value__ [System.String]$Message = "$StatusCode : $Content" if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { [System.Web.HttpException]$NewEx = New-Object -TypeName System.Web.HttpException($Content, $StatusCode) Write-Error -Exception $NewEx -Category NotSpecified -ErrorId $StatusCode } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $Message } else { Write-Verbose -Message "[ERROR] : $Message" } } catch [Exception] { if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { Write-Error -Exception $_.Exception -ErrorAction Stop } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $_.Exception.Message } else { Write-Verbose -Message "[ERROR] : $($_.Exception.Message)" } } } End { } } #endregion #region JWT Function Convert-GoogleOAuth2JWT { <# .SYNOPSIS Converts a GCP service account JWT for an access token. .DESCRIPTION This cmdlet takes a constructed JWT generated from a GCP Service Account credentials and exchanges it for an access token which can be used to authorize subsequent calls to Google APIs. .PARAMETER JWT The base64url encoded JWT generated from the GCP service account credentials. .PARAMETER ClientId The service account email address. .PARAMETER ProfileLocation The location where stored credentials are located. If this is not specified, the default location will be used. .PARAMETER Persist Indicates that the retrieved access token will be persisted on disk in an encrypted format using the Windows DPAPI as well as the local in-memory cache (also encrypted). .EXAMPLE $Token = Convert-GoogleOAuth2JWT -JWT $JWT -ClientId "test-sa@my-project.iam.gserviceaccount.com" Converts the constructed JWT to a token object whose access token can be used to call other APIs. .INPUTS None .OUTPUTS System.Collections.Hashtable .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/27/2018 #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] Param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [ValidateScript({ $_.Split(".").Length -eq 3 })] [System.String]$JWT, [Parameter(Mandatory = $true)] [Alias("ServiceAccountEmail")] [ValidateNotNullOrEmpty()] [System.String]$ClientId, [Parameter()] [System.String]$ProfileLocation, [Parameter()] [Switch]$Persist ) Begin { $BaseUrl = "https://www.googleapis.com/oauth2/v4/token" } Process { $GrantType = [System.Uri]::EscapeUriString("urn:ietf:params:oauth:grant-type:jwt-bearer") [System.String[]]$JWTParts = $JWT.Split(".") # Make sure these are escaped if they weren't already for ($i = 0; $i -lt $JWTParts.Length; $i++) { $JWTParts[$i] = Invoke-Base64UrlEscape -InputObject $JWTParts[$i] } $Assertion = $JWTParts -join "." $Body = "grant_type=$GrantType&assertion=$Assertion" try { Write-Verbose -Message "POST Body: $Body" [Microsoft.PowerShell.Commands.WebResponseObject]$Response = Invoke-WebRequest -Uri $BaseUrl -Method Post -Body $Body -ErrorAction Stop -UserAgent PowerShell [PSCustomObject]$Token = ConvertFrom-Json -InputObject ($Response.Content) [System.Collections.Hashtable]$TokenHashtable = @{} foreach ($Item in (Get-Member -InputObject $Token -MemberType Properties | Select-Object -ExpandProperty Name)) { $TokenHashtable.Add($Item, $Token.$Item) } Set-GoogleOAuth2Profile -ServiceAccountEmail $ClientId -AccessToken $TokenHashtable[$script:access_token] -ProfileLocation $ProfileLocation -Persist:$Persist Write-Output -InputObject $TokenHashtable } catch [System.Net.WebException] { [System.Net.WebException]$Ex = $_.Exception [System.Net.HttpWebResponse]$Response = [System.Net.HttpWebResponse]($Ex.Response) [System.IO.Stream]$Stream = $Ex.Response.GetResponseStream() [System.IO.StreamReader]$Reader = New-Object -TypeName System.IO.StreamReader($Stream, [System.Text.Encoding]::UTF8) [System.String]$Content = $Reader.ReadToEnd() [System.Int32]$StatusCode = $Response.StatusCode.value__ [System.String]$Message = "$StatusCode : $Content" if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { [System.Web.HttpException]$NewEx = New-Object -TypeName System.Web.HttpException($Content, $StatusCode) Write-Error -Exception $NewEx -Category NotSpecified -ErrorId $StatusCode } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $Message } else { Write-Verbose -Message "[ERROR] : $Message" } } catch [Exception] { if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { Write-Error -Exception $_.Exception -ErrorAction Stop } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $_.Exception.Message } else { Write-Verbose -Message "[ERROR] $($_.Exception.Message)" } } } End { } } Function New-GoogleServiceAccountJWT { <# .SYNOPSIS Creates a new JWT from a Google service account. .DESCRIPTION This cmdlet creates a new JWT from Google service account credentials. The JWT can be used to gain an access token for subsequent API calls. The outputted JWT is a base64 url encoded string. .PARAMETER ClientSecret The RSA private key in PEM format associated with the service account. .PARAMETER Issuer The service account email address. .PARAMETER Audience The URL the JWT is sent to for exchange for an access token. You do not need to specify this parameter. .PARAMETER ValidityInSeconds The number of seconds between 1 and 3600 that the JWT (and subsequent access token) is valid. .PARAMETER Subject The email address of the user for which the application is requesting delegated access. .PARAMETER Scope A set of API scopes that the service account is requesting permission for. .EXAMPLE $JWT = New-GoogleServiceAccountJWT -ClientSecret $ClientSecret -Issuer $ClientId -Scope $Scopes -Subject $Email Creates a new JWT from the service account credentials that is used to get an access token. .INPUTS None .OUTPUTS System.String .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/27/2018 #> [CmdletBinding()] [OutputType([System.String])] Param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$ClientSecret, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$Issuer, [Parameter()] [ValidateSet("https://www.googleapis.com/oauth2/v4/token")] [System.String]$Audience = "https://www.googleapis.com/oauth2/v4/token", [Parameter()] [ValidateRange(1, 3600)] [System.Int32]$ValidityInSeconds = 3600, [Parameter()] [System.String]$Subject ) DynamicParam { $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute] $ParameterAttribute = New-Object -TypeName System.Management.Automation.PARAMETERAttribute $ParameterAttribute.Mandatory = $true $AttributeCollection.Add($ParameterAttribute) $ValidateSetAttribute = New-Object -TypeName System.Management.Automation.ValidateSetAttribute($script:Scopes) $AttributeCollection.Add($ValidateSetAttribute) $RuntimeParameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("Scope", ([System.String[]]), $AttributeCollection) $RuntimeParameterDictionary.Add("Scope", $RuntimeParameter) return $RuntimeParameterDictionary } Begin { # eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9 [System.String]$JWTHeader = ConvertTo-Base64UrlEncoding -InputObject (ConvertTo-Json -InputObject @{"alg" = "RS256"; "typ" = "JWT"} -Compress) } Process { [System.String[]]$Scope = Convert-GoogleApiScopes -Scopes $PSBoundParameters["Scope"] [System.Int64]$Now = ([System.TimeSpan](([System.DateTime]::UtcNow) - (New-Object -TypeName System.DateTime(1970, 1, 1, 0, 0, 0, [System.DateTimeKind]::Utc)))).TotalSeconds [System.Collections.Hashtable]$JWT = @{ $script:iss = $Issuer; $script:scope = $Scope -join " "; "aud" = $Audience; "iat" = $Now; "exp" = $Now + $ValidityInSeconds} if ($PSBoundParameters.ContainsKey("Subject") -and -not [System.String]::IsNullOrEmpty($Subject)) { $JWT.Add($script:sub, $Subject) } $JWTClaimSet = ConvertTo-Base64UrlEncoding -InputObject (ConvertTo-Json -InputObject $JWT -Compress) [System.Byte[]]$SigningData = [System.Text.Encoding]::UTF8.GetBytes("$JWTHeader.$JWTClaimSet") [System.Security.Cryptography.RSACryptoServiceProvider]$RSA = ConvertFrom-PEM -PEM $ClientSecret [System.Byte[]]$Sig = $RSA.SignData($SigningData, "SHA256") [System.String]$JWTSignature = ConvertTo-Base64UrlEncoding -Bytes $Sig Write-Output -InputObject "$JWTHeader.$JWTClaimSet.$JWTSignature" } End { } } #endregion #region TokenInfo Function Test-IsGoogleOAuth2TokenExpired { <# .SYNOPSIS Tests whether the provided token or token in a stored profile is expired. .DESCRIPTION This cmdlet tests a provided access token or an access token stored in a client profile to see whether it has expired. The cmdlet will by default return true if the ClientId does not exist or does not contain an access_token property. To throw an exception in these cases use -ErrorAction Stop. .PARAMETER AccessToken The token to test. .PARAMETER ClientId The id of the profile containing the access token to test. If the client profile does not contain an access token, this will return false, unless the ErrorActionPreference is set to stop, in which case an exception is thrown. .PARAMETER Buffer The number of seconds to buffer against the actual expiration. This defaults to 60, and can be between 1 and 60. For example, if the buffer is set to 30, and the token expires in 25 seconds from now, the cmdlet would report true for being expired. This helps ensure a token doesn't expire mid-request and isn't refreshed before being used. .PARAMETER ProfileLocation The location where stored credentials are located. If this is not specified, the default location will be used. .EXAMPLE $Expired = Test-IsGoogleOAuth2TokenExpired -AccessToken $Token Tests whether the token contained in the $Token variable is expired .EXAMPLE try { $Expired = Test-IsGoogleOAuth2TokenExpired -ClientId $Id -ErrorAction Stop } catch [Exception] { Write-Host $_.Exception.Message } This example attempts to test the access token stored with the profile identified by $Id. If the profile is not found, or the profile doesn't contain an access token, an exception is thrown and caught in the catch statement. .INPUTS None .OUTPUTS None .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/27/2018 #> [CmdletBinding()] [OutputType([System.Boolean])] Param( [Parameter(Mandatory = $true, ParameterSetName = "Token")] [ValidateNotNullOrEmpty()] [System.String]$AccessToken = [System.String]::Empty, [Parameter(Mandatory = $true, ParameterSetName = "ClientId")] [ValidateNotNullOrEmpty()] [System.String]$ClientId, [Parameter()] [System.String]$ProfileLocation, [Parameter()] [ValidateRange(1, 60)] [System.Int32]$Buffer = 60 ) Begin { } Process { switch ($PSCmdlet.ParameterSetName) { "ClientId" { [System.Collections.Hashtable]$Token = Get-GoogleOAuth2Profile -ClientId $ClientId -ProfileLocation $ProfileLocation if (-not $Token.ContainsKey($script:access_token)) { # This will end processing if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { Write-Error -Exception (New-Object -TypeName System.Collections.Generic.KeyNotFoundException("There was no access token to verify for $ClientId.")) -ErrorAction Stop } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message "There was no access token to verify for $ClientId." } else { Write-Verbose -Message "[ERROR] : There was no access token to verify for $ClientId." } } else { $AccessToken = $Token[$script:access_token] } break } "Token" { # Do nothing break } default { throw "Unknown parameter set $($PSCmdlet.ParameterSetName) for $($MyInvocation.MyCommand)." } } # This will only be null or empty if the stored item was empty if (-not [System.String]::IsNullOrEmpty($AccessToken)) { try { [PSCustomObject]$TokenDetails = Get-GoogleOAuth2TokenInfo -AccessToken $AccessToken -ErrorAction Stop [System.Int64]$Exp = $TokenDetails.exp [System.DateTime]$Epoch = New-Object -TypeName System.DateTime(1970, 1, 1, 0, 0, 0, [System.DateTimeKind]::Utc) [System.DateTime]$Expiration = $Epoch.AddSeconds($Exp) Write-Verbose -Message "The supplied access token expires $($Expiration.ToString("yyyy-MM-ddTHH:mm:ssZ"))." [System.DateTime]$Now = [System.DateTime]::UtcNow $ExpiredWithoutBuffer = $Now -gt $Expiration $ExpiredWithBuffer = $now -gt $Expiration.AddSeconds($Buffer) if (-not $ExpiredWithoutBuffer -and $ExpiredWithBuffer) { Write-Verbose -Message "Although the access token is not actually expired, it is within the specified buffer of expiration, so should be refreshed." } Write-Output -InputObject $ExpiredWithBuffer } catch [Exception] { if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { Write-Error -Exception $_.Exception -ErrorAction Stop } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $_.Exception.Message } else { Write-Verbose -Message $_.Exception.Message } Write-Output -InputObject $true } } else { if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { Write-Error -Exception (New-Object -TypeName System.NullReferenceException("The access_token property for $ClientId was null or empty.")) -ErrorAction Stop } else { Write-Verbose -Message "There was no stored access token, returning true by default." Write-Output -InputObject $true } } } End { } } Function Get-GoogleOAuth2TokenInfo { <# .SYNOPSIS Retrieves information about an issued access token .DESCRIPTION This cmdlet retrieves information about the access token provided or contained in the client profile. The information includes the following details: azp : 51258299791-22n6bku0cf8oln8pia505bk78l3k838e.apps.googleusercontent.com # The ClientId aud : 51258299791-22n6bku0cf8oln8pia505bk78l3k838e.apps.googleusercontent.com # The ClientId scope : https://www.googleapis.com/auth/admin.directory.group.readonly # The requested scope in the auth code exp : 1515792549 # Expiration represented by seconds past the epoch (unix timestamp) expires_in : 3599 # Seconds from now the token expires in access_type : offline # The originally requested access type .PARAMETER ClientId The id of the profile to get info on. If the ClientId .PARAMETER AccessToken The token to retrieve details about. .PARAMETER ProfileLocation The location where stored credentials are located. If this is not specified, the default location will be used. .EXAMPLE $Details = Get-GoogleOAuth2TokenInfo -ClientId $Id Gets details on the access token stored with key $Id. .INPUTS None .OUTPUTS System.Collections.Hashtable .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/27/2018 #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] Param( [Parameter(Mandatory = $true, ParameterSetName = "ClientId")] [ValidateNotNullOrEmpty()] [System.String]$ClientId, [Parameter(Mandatory = $true, ParameterSetName = "Token")] [System.String]$AccessToken, [Parameter()] [System.String]$ProfileLocation ) Begin { $Base = "https://www.googleapis.com/oauth2/v3/tokeninfo" } Process { switch ($PSCmdlet.ParameterSetName) { "ClientId" { [System.Collections.Hashtable]$Token = Get-GoogleOAuth2Profile -ClientId $ClientId -ProfileLocation $ProfileLocation if (-not $Token.ContainsKey($script:access_token)) { Write-Error -Exception (New-Object -TypeName System.NullReferenceException("There was no access token to verify for $ClientId.")) -ErrorAction Stop } else { $AccessToken = $Token[$script:access_token] } break } "Token" { # Do nothing break } default { throw "Unknown parameter set $($PSCmdlet.ParameterSetName)." } } $Url = "$Base`?access_token=$AccessToken" try { [Microsoft.PowerShell.Commands.HtmlWebResponseObject]$Response = Invoke-WebRequest -Method Post -Uri $Url -UserAgent PowerShell [PSCustomObject]$Data = ConvertFrom-Json -InputObject $Response.Content [System.Collections.Hashtable]$Temp = @{} foreach ($Property in ($Data | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)) { $Temp.Add($Property, $Data.$Property) } Write-Output -InputObject $Temp } catch [System.Net.WebException] { [System.Net.WebException]$Ex = $_.Exception [System.Net.HttpWebResponse]$Response = [System.Net.HttpWebResponse]($Ex.Response) [System.IO.Stream]$Stream = $Ex.Response.GetResponseStream() [System.IO.StreamReader]$Reader = New-Object -TypeName System.IO.StreamReader($Stream, [System.Text.Encoding]::UTF8) [System.String]$Content = $Reader.ReadToEnd() [System.Int32]$StatusCode = $Response.StatusCode.value__ [System.String]$Message = "$StatusCode : $Content" if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { [System.Web.HttpException]$NewEx = New-Object -TypeName System.Web.HttpException($Content, $StatusCode) Write-Error -Exception $NewEx -Category NotSpecified -ErrorId $StatusCode } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $Message } else { Write-Verbose -Message "[ERROR] : $Message" } } catch [Exception] { if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { Write-Error -Exception $_.Exception -ErrorAction Stop } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $_.Exception.Message } else { Write-Verbose -Message "[ERROR] : $($_.Exception.Message)" } } } End { } } #endregion #region Profile Function Get-GoogleOAuth2Profile { <# .SYNOPSIS Retrieves details about a cached profile or lists all available profiles. .DESCRIPTION This cmdlet gets the tokens associated with a specific profile or lists all available profiles. Because the token data is encrypted using the Windows DPAPI, only token data that was stored by the current user can be successfully decrypted. If a specified ClientId is not found in the cache, persisted credentials are synced from disk into the cache and then it is checked again. If a ClientId is not specified, the cache is synced from disk and then all Ids found in the cache are returned. This cmdlet will only throw an exception if a ClientId is specified and not found and -ErrorAction is set to Stop, otherwise, the cmdlet will return null. .PARAMETER ClientId The Id of the stored profile to retrieve. If this is not specified, a list of cached profiles is returned. .PARAMETER ProfileLocation The location where stored credentials are located. If this is not specified, the default location will be used. .EXAMPLE $Profiles = Get-GoogleOAuth2Profile Retrieves a list of profiles cached on the system .EXAMPLE $TokenData = Get-GoogleOAuth2Profile -ClientId $Id Gets the unencrypted token data associated with the profile stored with Id $Id. If $Id is not found, $TokenData will be $null. .INPUTS System.String .OUTPUTS System.Collections.Hashtable or System.String[] .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/17/2018 #> [CmdletBinding()] [OutputType([System.Collections.Hashtable], [System.String[]])] Param( [Parameter(ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [System.String]$ClientId, [Parameter()] [System.String]$ProfileLocation ) Begin { Function Convert-SecureStringToString { Param( [Parameter(Position = 0, ValueFromPipeline = $true, Mandatory = $true)] [System.Security.SecureString]$SecureString ) Begin { } Process { [System.String]$PlainText = [System.String]::Empty [System.IntPtr]$IntPtr = [System.IntPtr]::Zero try { $IntPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($SecureString) $PlainText = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($IntPtr) } finally { if ($IntPtr -ne $null -and $IntPtr -ne [System.IntPtr]::Zero) { [System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($IntPtr) } } Write-Output -InputObject $PlainText } End { } } } Process { if ([System.String]::IsNullOrEmpty($ProfileLocation)) { $ProfileLocation = $script:ProfileLocation } # If the client specified a specific client id, look for its data if ($PSBoundParameters.ContainsKey("ClientId")) { # If the cache doesn't have the client id, sync the persisted data if (-not $script:OAuthTokens.ContainsKey($ClientId)) { Sync-GoogleOAuth2ProfileCache -ProfileLocation $ProfileLocation } # Check again to see if syncing the persisted data loaded it if ($script:OAuthTokens.ContainsKey($ClientId)) { [System.Collections.Hashtable]$Temp = @{} # Need to call GetEnumerator() on a Hastable to iterate its entries foreach ($Property in $script:OAuthTokens[$ClientId].GetEnumerator()) { if ($Property.Name -in $script:EncryptedProperties) { $Temp.Add($Property.Name, (Convert-SecureStringToString -SecureString (ConvertTo-SecureString -String $Property.Value))) } else { $Temp.Add($Property.Name, $Property.Value) } } Write-Output -InputObject $Temp } else { if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { # No need to write output since this will be a terminating error Write-Error -Exception (New-Object -TypeName System.Collections.Generic.KeyNotFoundException("The specified profile $ClientId could not be found.")) -ErrorAction Stop } else { Write-Verbose -Message "The specified profile $ClientId could not be found." Write-Output -InputObject $null } } } else { # Sync whatever's stored on disk first Sync-GoogleOAuth2ProfileCache -ProfileLocation $ProfileLocation # Then return the ClientIds that are used as the key identifiers in the cache Write-Output -InputObject ([System.String[]]($script:OAuthTokens.GetEnumerator() | Select-Object -ExpandProperty Name)) } } End { } } Function Set-GoogleOAuth2Profile { <# .SYNOPSIS Sets the data in a profile. .DESCRIPTION This cmdlet sets data for a specified ClientId profile. The profiles support both OAuth Clients and Service Account based credentials. For client credentials, you must specify either an access token or refresh token. If you specify a refresh token, you should also specify the client secret so the token can be refreshed. For a service account, you can store either an existing access token, or the service account private key and details to construct a JWT which can be exchanged for an access token. .PARAMETER ClientId The profile Id to store the data with, this can be an OAuth2 Client Profile or a Service Account ClientId. .PARAMETER ClientSecret The provided client secret associated with the ClientId. This can be the OAuth2 provided client secret or a private key from a service account. .PARAMETER AccessToken The access token to store in the profile. .PARAMETER RefreshToken The refresh token to store in the profile when using client based OAuth. .PARAMETER ServiceAccount Specifies that the client id and access token provided are for a service account. .PARAMETER Issuer The email address of the service account. .PARAMETER Scope An collection of permissions that the application requests .PARAMETER ProfileLocation The location where stored credentials are located. If this is not specified, the default location will be used. .PARAMETER Persist Specifies if the data should be persisted to disk in an encrytped format or only maintained in the local cache (also encrypted). .EXAMPLE Set-GoogleOAuth2Profile -ClientId $Id -ClientSecret $Secret -AccessToken $Token -RefreshToken $RToken -Persist This example stores the client secret, current access token, and refresh token to the local cache and persists them to disk. .EXAMPLE Set-GoogleOAuth2Profile -ClientId $Id -ClientSecret $Secret -RefreshToken $RToken -Persist This example stores the client secret and refresh token to the local cache and persists them to disk. Because only a refresh token is stored, the next time the token in this profile is accessed, a new access token will be retrieved with the stored refresh token. .INPUTS None .OUTPUTS None .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/17/2018 #> [CmdletBinding(DefaultParameterSetName = "client")] [OutputType()] Param( [Parameter(Mandatory = $true, ParameterSetName = "client")] [ValidateNotNullOrEmpty()] [System.String]$ClientId, [Parameter(ParameterSetName = "client")] [Parameter(ParameterSetName = "sa", Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$ClientSecret, [Parameter(ParameterSetName = "client")] [Parameter(ParameterSetName = "sa_access_token", Mandatory = $true)] [Parameter(ParameterSetName = "sa")] [ValidateNotNullOrEmpty()] [System.String]$AccessToken, [Parameter(ParameterSetName = "client")] [ValidateNotNullOrEmpty()] [System.String]$RefreshToken, [Parameter(ParameterSetName = "sa", Mandatory = $true)] [Parameter(ParameterSetName = "sa_access_token", Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$ServiceAccountEmail, [Parameter(ParameterSetName = "sa", DontShow = $true)] [ValidateSet("https://www.googleapis.com/oauth2/v4/token")] [System.String]$Audience = "https://www.googleapis.com/oauth2/v4/token", [Parameter(ParameterSetName = "sa")] [ValidateNotNullOrEmpty()] [System.String]$Subject, [Parameter(ParameterSetName = "sa")] [ValidateRange(1, 3600)] [System.Int32]$ValidityInSeconds, [Parameter()] [System.String]$ProfileLocation, [Parameter()] [Switch]$Persist ) DynamicParam { $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute] $ParameterAttribute = New-Object -TypeName System.Management.Automation.PARAMETERAttribute $ParameterAttribute.Mandatory = $true $ParameterAttribute.ParameterSetName = "sa" $AttributeCollection.Add($ParameterAttribute) $ValidateSetAttribute = New-Object -TypeName System.Management.Automation.ValidateSetAttribute($script:Scopes) $AttributeCollection.Add($ValidateSetAttribute) $RuntimeParameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("Scope", ([System.String[]]), $AttributeCollection) $RuntimeParameterDictionary.Add("Scope", $RuntimeParameter) return $RuntimeParameterDictionary } Begin { } Process { if ($PSCmdlet.ParameterSetName -ilike "sa*") { $ClientId = $ServiceAccountEmail } Write-Verbose -Message "Setting profile $ClientId." if ([System.String]::IsNullOrEmpty($ProfileLocation)) { $ProfileLocation = $script:ProfileLocation } # Create the profile store if it doesn't exist if (-not (Test-Path -Path $ProfileLocation)) { New-Item -Path $ProfileLocation -ItemType File -Force | Out-Null } # Make sure the cache is loaded so that updates to what's stored in the cache are kept in sync # with what's on disk if ($Persist) { Sync-GoogleOAuth2ProfileCache -ProfileLocation $ProfileLocation } # This will hold the data supplied by the parameters for the token information to store # Use a hashtable so it's easy to check property existence [System.Collections.Hashtable]$Profile = @{} if ($PSBoundParameters.ContainsKey("ClientSecret")) { if ($PSCmdlet.ParameterSetName -eq "sa") { $ClientSecret = $ClientSecret.Replace("\n", "").Replace("\r", "").Replace("`r", "").Replace("`n", "") } $Profile.Add($script:client_secret, (ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force))) } # Mandatory so we know what type of credentials these are if ($PSCmdlet.ParameterSetName -ilike "sa*") { $Profile.Add("type", "sa") } else { $Profile.Add("type", "client") } switch ($PSCmdlet.ParameterSetName) { "sa_access_token" { if ($PSBoundParameters.ContainsKey("AccessToken")) { $Profile.Add($script:access_token, (ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String $AccessToken -AsPlainText -Force))) } break } "sa" { [System.String[]]$Scope = $PSBoundParameters["Scope"] $Profile.Add($script:scope, $Scope) $Profile.Add($script:iss, $ServiceAccountEmail) if (-not [System.String]::IsNullOrEmpty($Subject)) { $Profile.Add($script:sub, $Subject) } if ($PSBoundParameters.ContainsKey("AccessToken")) { $Profile.Add($script:access_token, (ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String $AccessToken -AsPlainText -Force))) } if ($PSBoundParameters.ContainsKey("ValidityInSeconds")) { $Profile.Add($script:validity, $ValidityInSeconds) } break } "client" { if (-not $PSBoundParameters.ContainsKey("AccessToken") -and -not $PSBoundParameters.ContainsKey("RefreshToken")) { Write-Error -Exception (New-Object -TypeName System.ArgumentException("At least AccessToken or RefreshToken must be specified for the Set-GoogleOAuth2Profile cmdlet when specifying OAuth client information.")) -ErrorAction Stop } if ($PSBoundParameters.ContainsKey("AccessToken")) { $Profile.Add($script:access_token, (ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String $AccessToken -AsPlainText -Force))) } if ($PSBoundParameters.ContainsKey("RefreshToken")) { $Profile.Add($script:refresh_token, (ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String $RefreshToken -AsPlainText -Force))) } break } default { Write-Error -Message "Unknown parameter set $($PSCmdlet.ParameterSetName)." -ErrorAction Stop } } # If the profile already exists in the cache, update the information, don't worry about checking to see if it's # different since it's not a big penalty to rewrite to memory if ($script:OAuthTokens.ContainsKey($ClientId)) { foreach ($Property in $Profile.GetEnumerator()) { if ($script:OAuthTokens[$ClientId].ContainsKey($Property.Name)) { $script:OAuthTokens[$ClientId][$Property.Name] = $Property.Value } else { $script:OAuthTokens[$ClientId].Add($Property.Name, $Property.Value) } } } else { $script:OAuthTokens.Add($ClientId, $Profile) } # If the profile is being persisted, merge it with the saved profile data if ($Persist) { # Let's make sure the tokens were different before we decide to write something back to disk [System.Boolean]$ChangeOccured = $false [PSCustomObject]$ProfileData = [PSCustomObject]@{} [System.String]$Content = Get-Content -Path $ProfileLocation -Raw -ErrorAction SilentlyContinue # This will load the persisted data from disk into the cache object if (-not [System.String]::IsNullOrEmpty($Content)) { [PSCustomObject]$ProfileData = ConvertFrom-Json -InputObject $Content } # This could happen if the credential file just contains whitespace and no content # Use this approach since the ProfileData is a PSCustomObject if ($ProfileData -ne $null -and (Get-Member -InputObject $ProfileData -Name $ClientId -MemberType Properties) -ne $null) { Write-Verbose -Message "The profile $ClientId may be overwritten with new data." # Go through each property in the profile and compare it against the stored profile data to see if we # need to add or update fields foreach ($Property in ($Profile.GetEnumerator() | Select-Object -ExpandProperty Name)) { if (($ProfileData.$ClientId | Get-Member -Name $Property -MemberType Properties) -ne $null) { if ($Property -iin $script:EncryptedProperties) { # Since the DPAPI uses a time factor to generate the encryption, the encrypted data is different # each time the encryption is performed, convert the encrypyted string to a secure string # in order to compare them successfully if ( [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((ConvertTo-SecureString -String $ProfileData.$ClientId.$Property))) -ne [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((ConvertTo-SecureString -String $Profile[$Property]))) ) { $ProfileData.$ClientId.$Property = $Profile[$Property] # Note that an update actually happened $ChangeOccured = $true } } else { if ($ProfileData.$ClientId.$Property -ne $Profile["$Property"]) { $ProfileData.$ClientId.$Property = $Profile[$Property] # Note that an update actually happened $ChangeOccured = $true } } } else { $ProfileData.$ClientId | Add-Member -MemberType NoteProperty -Name $Property -Value $Profile[$Property] # Note that an update actually happened $ChangeOccured = $true } } } else { $ProfileData | Add-Member -MemberType NoteProperty -Name $ClientId -Value $Profile # Note that an update actually happened $ChangeOccured = $true } # It's possible no updates were actually made to the existing data, only write to disk if a change # was made if ($ChangeOccured) { Set-Content -Path $ProfileLocation -Value (ConvertTo-Json -InputObject $ProfileData) -Force Write-Verbose -Message "Successfully persisted profile data for $ClientId in $ProfileLocation." } else { Write-Verbose -Message "No profile data changes occured for persisted data, nothing updated on disk." } } Write-Verbose -Message "Successfully created or updated the profile for $ClientId." } End { } } Function Remove-GoogleOAuth2Profile { <# .SYNOPSIS Removes a cached and/or stored Google OAuth profile. .DESCRIPTION This cmdlet will delete the cached and stored profile for the specified client id. If RevokeToken is specified, the set of tokens, including the refresh token will be invalidated. .PARAMETER ClientId The supplied client id for OAuth. .PARAMETER ProfileLocation The location where stored credentials are located. If this is not specified, the default location will be used. .PARAMETER RevokeToken This specifies that any tokens associated with this profile will be revoked permanently. .PARAMETER PassThru If this is specified, the deleted profile data is returned to the pipeline. .EXAMPLE Remove-GoogleOAuth2Profile -ClientId $Id Removes cached and persisted profile data for the id contained in the $Id variable. The user is prompted before the removal occurs. .EXAMPLE Remove-GoogleOAuth2Profile -ClientId $Id -RevokeToken -Force Removes cached and persisted profile data for the id contained in the $Id variable and invalidates all associated tokens that have been issued. The -Force parameter bypasses any confirmation. .INPUTS None .OUTPUTS None or System.Collections.Hashtable The hashtable will contain either an access_token or refresh_token property or both. .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/12/2018 #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")] [OutputType([System.Collections.Hashtable])] Param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$ClientId, [Parameter()] [System.String]$ProfileLocation, [Parameter()] [Switch]$RevokeToken, [Parameter()] [Switch]$Force, [Parameter()] [Switch]$PassThru ) Begin { } Process { Write-Verbose -Message "Removing profile $ClientId." if ([System.String]::IsNullOrEmpty($ProfileLocation)) { $ProfileLocation = $script:ProfileLocation } # Do this before we delete it from the cache so we don't have to go to disk [System.Collections.Hashtable]$Profile = Get-GoogleOAuth2Profile -ClientId $ClientId -ProfileLocation $ProfileLocation -ErrorAction SilentlyContinue if ($script:OAuthTokens.ContainsKey($ClientId)) { $script:OAuthTokens.Remove($ClientId) } else { Write-Verbose -Message "Not profile data for $ClientId found in the cache." } [System.String]$Content = Get-Content -Path $ProfileLocation -Raw -ErrorAction SilentlyContinue # This will load the persisted data from disk into the cache object if (-not [System.String]::IsNullOrEmpty($Content)) { [PSCustomObject]$ProfileData = ConvertFrom-Json -InputObject $Content # The profile contains the clientId to remove if ($Profile -ne $null) { $ConfirmMessage = "You are about to delete profile $ClientId. If you specified -RevokeToken, the REFRESH TOKEN will be revoked and you will need to submit a new authorization code to retrieve a new token." $WhatIfDescription = "Deleted profile $ClientId" $ConfirmCaption = "Delete Google OAuth2 Profile" if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption)) { if ($RevokeToken) { $Token = "" if ($Profile.ContainsKey($script:access_token)) { $Token = $Profile[$script:access_token] } elseif ($Profile.ContainsKey($script:refresh_token)) { $Token = $Profile[$script:refresh_token] } else { Write-Warning -Message "RevokeToken was specified, but no tokens are associated with the profile $ClientId." } if (-not [System.String]::IsNullOrEmpty($Token)) { try { [Microsoft.PowerShell.Commands.WebResponseObject]$Response = Invoke-WebRequest -Uri "https://accounts.google.com/o/oauth2/revoke?token=$Token" -Method Post -UserAgent PowerShell if ($Response.StatusCode -ne 200) { Write-Warning -Message "There was a problem revoking the access token associated with $ClientId." } } catch [System.Net.WebException] { [System.Net.WebException]$Ex = $_.Exception [System.Net.HttpWebResponse]$Response = [System.Net.HttpWebResponse]($Ex.Response) [System.IO.Stream]$Stream = $Ex.Response.GetResponseStream() [System.IO.StreamReader]$Reader = New-Object -TypeName System.IO.StreamReader($Stream, [System.Text.Encoding]::UTF8) [System.String]$Content = $Reader.ReadToEnd() [System.Int32]$StatusCode = $Response.StatusCode.value__ [System.String]$Message = "$StatusCode : $Content" if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { [System.Web.HttpException]$NewEx = New-Object -TypeName System.Web.HttpException($Content, $StatusCode) Write-Error -Exception $NewEx -Category NotSpecified -ErrorId $StatusCode } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $Message } else { Write-Verbose -Message "[ERROR] : $Message" } } catch [Exception] { if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Stop) { Write-Error -Exception $_.Exception } elseif ($ErrorActionPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { Write-Warning -Message $_.Exception.Message } else { Write-Verbose -Message "[ERROR] : $($_.Exception.Message)" } } } } # This returns void, so do it first, then pass the ProfileData variable $ProfileData.PSObject.Properties.Remove($ClientId) $Value = "" if (($ProfileData | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name).Count -gt 0) { $Value = (ConvertTo-Json -InputObject $ProfileData) } if ([System.String]::IsNullOrEmpty($Value)) { Clear-Content -Path $ProfileLocation -Force } else { Set-Content -Path $ProfileLocation -Value $Value -Force } Write-Verbose -Message "Successfully removed profile $ClientId." if ($PassThru) { Write-Output -InputObject $Profile } } } else { Write-Error -Message "No profile matching $ClientId in $ProfileLocation." } } else { Write-Verbose -Message "No persisted profile data found in $ProfileLocation." } } End { } } Function Sync-GoogleOAuth2ProfileCache { <# .SYNOPSIS Syncs the stored profile data with the in memory cache. .DESCRIPTION This cmdlet loads the data stored in local credential file into the in-memory cache of credentials. You typically will not need to call this cmdlet, the other cmdlets that use the profile data will call this on your behalf. .PARAMETER ProfileLocation The location where stored credentials are located. If this is not specified, the default location will be used. .EXAMPLE Sync-GoogleOAuth2ProfileCache This syncs the locally stored profile data to the in-memory cache. .INPUTS None .OUTPUTS None .NOTES AUTHOR: Michael Haken LAST UPDATE: 1/12/2018 #> [CmdletBinding()] [OutputType()] Param( [Parameter()] [System.String]$ProfileLocation ) Begin { } Process { if ([System.String]::IsNullOrEmpty($ProfileLocation)) { $ProfileLocation = $script:ProfileLocation } [System.Boolean]$AddedToCache = $false Write-Verbose -Message "Syncing data from $ProfileLocation into local cache." [System.String]$Content = Get-Content -Path $ProfileLocation -Raw -ErrorAction SilentlyContinue # This will load the persisted data from disk into the cache object if (-not [System.String]::IsNullOrEmpty($Content)) { [PSCustomObject]$ProfileData = ConvertFrom-Json -InputObject $Content # Iterate each key value in the PSCustomObject which represents a ClientId foreach ($Property in ($ProfileData | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name)) { # If the module cache of profiles doesn't contain a token for the persisted client id, add it if (-not $script:OAuthTokens.ContainsKey($Property)) { Write-Verbose -Message "Adding data for $Property into local cache from disk." $AddedToCache = $true $script:OAuthTokens.Add($Property, @{}) } $ProfileDataProperties = ($ProfileData.$Property | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) foreach ($Token in $ProfileDataProperties) { # Add the token values to the cache if ($script:OAuthTokens[$Property].ContainsKey($Token)) { if ($script:OAuthTokens[$Property][$Token] -ne $ProfileData.$Property.$Token) { Write-Verbose -Message "Updating property $Token in profile $Property." $script:OAuthTokens[$Property][$Token] = $ProfileData.$Property.$Token $AddedToCache = $true } } else { Write-Verbose -Message "Adding property $Token to profile $Property." $script:OAuthTokens[$Property].Add($Token, $ProfileData.$Property.$Token) $AddedToCache = $true } } # Remove any existing properties in the profile cache that were not persisted to disk foreach ($Prop in $script:OAuthTokens[$Property].GetEnumerator()) { if ($Prop.Name -inotin $ProfileDataProperties) { Write-Verbose -Message "Removing property $($Prop.Name) from profile $Property." $script:OAuthTokens[$Property].Remove($Prop.Name) } } } if (-not $AddedToCache) { Write-Verbose -Message "No updates required to the profile cache." } } else { Write-Verbose -Message "No persisted profile data found in $ProfileLocation." } } End { } } #endregion |