lwsrbrts / PoSHue

Windows PowerShell 5/PowerShell Core 6 Classes for controlling Philips Hue Bridge and Hue Lights, Groups, Sensors.
GNU Lesser General Public License v3.0
51 stars 7 forks source link

Error requesting remote access token #18

Closed sbrp closed 6 years ago

sbrp commented 6 years ago

After following the link and signing into the Hue site and authorising, the following error is thrown (invalid state):

image

lwsrbrts commented 6 years ago

I can't repro this myself and unfortunately I only have the one account linked to the meethue site which is my Google account.

What do you see if you go here: https://account.meethue.com/apps ?

lwsrbrts commented 6 years ago

Ensure your browser isn't interfering in the process. The state it's referring to is simply a session id that is assigned to you when you visit the first page. When you go off to the other site to authenticate, the session identifier follows you and when you come back, the session id is compared with the one from the original site to ensure you're not falling foul of Cross Site Request Forgery.

This is the code generating that error. (parent file not in the repo since it contains secrets)

It literally compares what was sent back with what was originally stored - if it's different, it stops.

elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {

    unset($_SESSION['oauth2state']);
    exit('Invalid state');
}
sbrp commented 6 years ago

Checked out which apps were already activated - it was already listed (even though I'd not seen it generate one successfully). Deleted it, tried again, worked! Is there a way to automate this process? Wondering about the lifetime of the token and if it can be obtained programmatically.

lwsrbrts commented 6 years ago

Great, glad it worked, interestingly, I had an automatic upgrade to my blog site around the time you were probably trying to get the token so there's a small chance that caused the session to get dismissed through garbage collection.

Well, here's the interesting thing about tokens etc...the point of access tokens is that they grant access (obviously) but they do so for a limited time. Once the access token's valid time expires, the refresh token can be used to request another access (and refresh) token so that the application can continue working...you would obviously need to know the new access token. These types of tokens are typically used in web applications, so when an access token expires and the user tries to use it, the application pulls the refresh token out of a database and, along with the access token, sends it off to the other application (in our case Philips' Remote API) to request another pair of access and refresh tokens.

The problem I have is that the open source toolset I'm using to generate the access and refresh tokens is great at access tokens, not so much for using still valid refresh tokens to get a new set of access and refresh tokens. I have tried in vain to get the sample code the developers provide to work to give a new access token back when a valid refresh token is supplied - PHP just spits out errors.

Unfortunately, without a working method of getting a new access token using the refresh token, there's very little I can do. The intention was to use a method so that the user could provide their refresh token and, once the expiration error was received, the method could be invoked to send a request to my website, along with the expired access but still valid refresh token, that would deal with obtaining the new token pair and then return those back to the user in an automated way.

The reason all this isn't just in PowerShell is because Philips use secrets and this secret is how I identify my application to Philips. The OAuth implementation they use just isn't designed for "applications" like mine.

Admittedly I was probably a little eager to release the Remote API compatibility when I didn't have a workling method of using the refresh tokens but then, at the time, I had zero clue that the access tokens were only valid for one week! That was really frustrating to find out.

So, I'm in a Catch 22 - unless someone drops by to assist with making Oauth Client from PHP League work, the one week valid access tokens will be all I can issue unfortunately.

lwsrbrts commented 6 years ago

No sooner have I returned to "just have a look" and I try one thing and all of a sudden, the refresh of my old token has worked.

The refresh needs implementing (hopefully) as I described, so there'll be a flurry of activity in the repo and then I'll push out 2.1.0. That's likely to be very soon though.

sbrp commented 6 years ago

Excellent!

lwsrbrts commented 6 years ago

Version 2.1.0 which will be on the PowerShell Gallery soon now includes two new methods to deal with this.

The [HueFactory] class, which is inherited by all of the other classes now also has three properties - meaning that these properties always exist in any [HueBridge], [HueLight], [HueGroup] or [HueSensor] object you create. The new properties are:

Since the $RemoteApiRefreshToken and $RemoteApiAccessTokenExpiryDate properties aren't really required to instantiate an object, the properties remain null unless you specifically set them. The reason I do this is to permit flexibility in how a user may choose to store or retrieve and set the refresh token and expiration date properties. That could be in a text file, a NoSQL Database, Azure Automation Variable etc. etc.

For example:

Import-Module PoSHue

$RemoteApiAccessToken = 'WRCuG6Pw9H6eYvpekAGlL93ACgc0'

$WhitelistEntry = 'DZ17DiebfP1cZ4qgijsH3OApZVt6cuMgpjowkrJv'

$Light = [HueLight]::new('Hue go 2', $RemoteApiAccessToken, $WhitelistEntry, $true)

At this stage you would have an object in the $Light variable (as below) as normal but there are two other properties that can be set if desired:

<#
Light                          : 5
LightFriendlyName              : Hue go 2
BridgeIP                       :
APIKey                         : DZ17DiebfP1cZ4qgijsH3OApZVt6cuMgpjowkrJv
JSON                           :
On                             : True
Brightness                     : 254
Hue                            : 8382
Saturation                     : 143
ColourTemperature              : 370
XY                             : {x, y}
Reachable                      : True
ApiUri                         : https://api.meethue.com/bridge/DZ17DiebfP1cZ4qgijsX3OApZVt6cuMgpgowkrJv
RemoteApiAccessToken           : WRCuG6Pw9H6eYvpekAGlL93ACgc0
ColourMode                     : ct
AlertEffect                    : select
RemoteApiRefreshToken          :
RemoteApiAccessTokenExpiryDate : 0
#>

I'll set these using the information obtained from the web page when I got my original access token.

$RefreshToken = 'MVBhA66VtGnGt6MouGufqiaZtSavA3Ga'
$ExpiryDate = 1523536842
$Light.RemoteApiRefreshToken = $RefreshToken
$Light.RemoteApiAccessTokenExpiryDate = $ExpiryDate
$Light
<#
Light                          : 5
LightFriendlyName              : Hue go 2
BridgeIP                       :
APIKey                         : DZ17DiebfP1cZ4qgijsX3OApZVt6cuMgpgowkrJv
JSON                           :
On                             : True
Brightness                     : 254
Hue                            : 8382
Saturation                     : 143
ColourTemperature              : 370
XY                             : {x, y}
Reachable                      : True
ApiUri                         : https://api.meethue.com/bridge/DZ17DiebfP1cZ4qgijsX3OApZVt6cuMgpgowkrJv
RemoteApiAccessToken           : B54qtjNIZcEWrbJJxGC3dnIkCqy6
ColourMode                     : ct
AlertEffect                    : select
RemoteApiRefreshToken          : MVBhA66VtGnGt6MouGufqiaZtSavA3Ga
RemoteApiAccessTokenExpiryDate : 1523536842
#>

To be clear, this assigning variables doesn't have to be done in order to refresh the access token but it shows a complete object. In order to refresh the access token, you must have all three pieces of data (but they don't need to be set in the object). If they are set in the object, you can refer to them easily when refreshing the token...

$Light.RefreshAccessToken($Light.RemoteApiAccessToken, $Light.RemoteApiRefreshToken, $Light.RemoteApiAccessTokenExpiryDate)

Assuming the request for refreshed tokens was successful, the above method does two things - it updates the RemoteApiAccessToken, RemoteApiRefreshToken and RemoteApiAccessTokenExpiryDate properties in the object (whether they were set or not) and it outputs JSON that makes up the set of token data so you can capture the returned information and re-store it if required such that the next time you instantiate, your tokens data are already up-to-date.

In order to auto-refresh the tokens when they expire you would need to catch any errors (using try catch) produced by the other methods in the classes and then refresh the tokens using the above method.

Getting refreshed tokens requires an existing access token so you must still do the manual process of using the website to get the first access token - this is unavoidable since it requires user interaction to click a button that says "Grant permission".

Finally, there's another method ExportAccessTokenToJson() which takes the properties set in the object and just exports them to JSON - handy if you forgot to capture the output to a variable when you refreshed for example. It isn't "smart" so if those properties aren't set in the object, you'll get an incomplete JSON representation of your access, refresh and expiration token data.

$Light.ExportAccessTokenToJson()
<#
{
  "expires": 1523536842,
  "refresh_token": "MVBhA66VtGnGt6MouGufqiaZtSavA3Ga",
  "access_token": "B54qtjNIZcEWrbJJxGC3dnIkCqy6"
}
#>
lwsrbrts commented 6 years ago

I should probably have added that there are instances where you won't be able to instantiate a new object because the token has expired. In cases like that, I've created a static method in the [HueFactory] class that can be used without the need to instantiate any object or light. The only information you need is what you require for refreshing a token anyway

Remember, no need to instantiate an object, you can literally call this after importing the module..

Import-Module PoSHue
$ExpiredAccessToken = 'pouaenrv978yefrrvk jerav987'
$RefreshToken = '08u34toljknef 9834fg098098yuq345g'
$Expiration = 1523552112
[HueFactory]::RefreshAccessToken($ExpiredAccessToken, $RefreshToken, $Expiration, $true)
<#
{
  "access_token_expires_in": "604799",
  "refresh_token_expires_in": "9676799",
  "token_type": "BearerToken",
  "access_token": "tYhnQwSe5QAnG3E61gmKfeMsA4oz",
  "refresh_token": "JfwxnsybZVB51GtXgOTQHC6GnjtHjNhf",
  "expires": 1523552112
}
#>
sbrp commented 6 years ago

Brilliant. I'll try to build a worker script to automate this when I get a chance to give it a spin.

lwsrbrts commented 6 years ago

I've started storing my token data as a JSON file alongside my PowerShell profile which I read when I start a PowerShell session. With this, I could automatically refresh an expired token and update the token data using something like this:

$TokenPath = "$(Split-Path -Path $Profile)\RemoteApiAccessToken.json"
$PoSHueAccessTokens = Get-Content -Raw -Path $TokenPath | ConvertFrom-Json

Try {
    $Light = [HueLight]::new('Hue go 2', $PoSHueAccessTokens.access_token, $UserID, $true)
}
Catch {
    If ($_.Exception.Message -match 'Access Token expired') {
        $TokenRefreshing = [HueFactory]::RefreshAccessToken($PoSHueAccessTokens.access_token, $PoSHueAccessTokens.refresh_token, $PoSHueAccessTokens.expires, $true)
        $TokenRefreshing | ConvertTo-Json | Out-File -Path $TokenPath -Encoding UTF8
    }
    Else { Write-Output $_.Exception.Message }
}

Oh, since the [HueFactory] is inherited by all the other classes, you can call the static method from any of them. All of these would work:

[HueBridge]::RefreshAccessToken($PoSHueAccessTokens.access_token, $PoSHueAccessTokens.refresh_token, $PoSHueAccessTokens.expires, $true)
[HueGroup]::RefreshAccessToken($PoSHueAccessTokens.access_token, $PoSHueAccessTokens.refresh_token, $PoSHueAccessTokens.expires, $true)
[HueLight]::RefreshAccessToken($PoSHueAccessTokens.access_token, $PoSHueAccessTokens.refresh_token, $PoSHueAccessTokens.expires, $true)
[HueSensor]::RefreshAccessToken($PoSHueAccessTokens.access_token, $PoSHueAccessTokens.refresh_token, $PoSHueAccessTokens.expires, $true)