In the last part I referred to the refreshTokens() method, called in the CompanionAppAuthManager class. It uses a client ID and a so called “Refresh token” to authenticate automatically with the LWA (Login with Amazon) web service.
Looking at the documentation I refer to in my last post, this Refresh token is not something that is known from the start. It’s something that is requested once from a companion app and is to be stored by the connected device to be passed to every subsequent request for authentication. Looking at the code it appears to be coming from the device configuration, but as we can see in the current configuration, the refresh token is nowhere to be found.
That’s because we didn’t do any authentication yet.
Let’s look at the getCompanionAppInfo() method called in the refreshTokens() method. It is part of the DeviceConfig class.
All it does is return a variable. This variable is assigned in the constructor of the DeviceConfig class where it is passed as an argument.
If we dig deeper, then we find that the only place where a new DeviceConfig is created is in the DeviceConfigUtils class, where the token is being read from the configuration file. Checking out the readConfigFile() method of the DeviceConfigUtils class you will find no place where the refresh token is set.
However, let’s look more closely at the the CompanionAppInformation class. If we look carefully there is a method called setRefreshToken() where the refresh token is set to the passed argument.
Time to back track this one. Doing that is easy. Just right-click the setRefreshTokens() method and click “Find Usages“. The output at the bottom of the screen will reveal all the places where this function is used.
Apart from the definition, the function is only used in one place : in the CompanionAppAuthManager class. Let’s check that out.
So what do we see here. An object instance called tokens of type OAuth2TokensForPkce is passed along to a method called set tokens. Using the CompanionAppInformation class instance info, the refresh token is set by using the getRefreshToken() method of the tokens argument. After that the configuration is saved. This must be the place where the initial request token is saved to the configuration file.
Let’s see what getRefreshToken() does.
All it does is return a local variable which is passed along in the constructor. That means that the heavy lifting has already been done before calling the setTokens() method. Let’s find the usages of the setTokens() method. Here we go :
The method exchangeCompanionInfoForTokens in the CompanionAppAuthManager is responsible for setting the tokens. It uses 5 variables as an input to be passed to a method called exchangeAuthCodeForTokens of the OAuth2TokensForPkce class. Those 5 are the sessionId, the clientId, authCode, redirectURI and codeVerifier. Let’s look and that method.
So this is where the magic happens. We can see that a post request is being made to a HttpURLConnection using data in the form of a Json Object. The Json object is constructed in a separate method called prepareExchangeAuthCodeForTokensData, and passed on to the postRequest method. This itself returns another Json object from which the access code, refresh token and a variable indicating when the refresh token will expire is stored.
The postRequest method can be found below it :
This is an important piece of code. What it does here is set some properties of the connection, telling it, it will POST a request of content application/json. Next, a stream is obtained from the connection and to that stream, the data is sent to the Amazon server. The stream is flushed and closed ,and a response code is returned. Inside the try-catch block the response is being read by reading from the connection’s input stream. A JsonReader class instance is responsible for parsing the response from the connection. It is reformed into a Json object using the readObject function. The rest is some plumbing code taking care of closing the connection and handling an exception, but essentially, this piece of code is the communication between our Javaclient and the LWA authentication service.
Let’s back track a bit and focus on the exchangeCompantionInfoForTokens() method. There 5 variables were passed to be able to do the postRequest.
4 out of the 5 seem to originate in the same class of type CompanionAppProvisionInfo. This is just a container class with no further logic than storing the 4 variables. That means we need to find where the exchangeCompanionInfoForTokens() method is called. When we search for the usage, we find only one place, and that’s in the CompanionInfoHandler class. Let’s check that out.
This is where things get a little tricky. It is called in the overridden handle() method where it handles a Http request and parses out the 4 different variables from the input stream. But when is this handle called? The handler is part of the CompanionInfoHandler class which extends AbstractHandler. AbstractHandler is a java class from the eclipse core commands, and it needs to be overridden. That’s why it’s called abstract. It requires the class that inherits from it to implement the handle function. This class (in this case CompanionInfoHandler) is then used in a listener, and whenever the listener fires, the handler’s handle() function is called. So next, we need to find the listener that uses this handler.
There’s only one place where this handler is assigned. That’s in the startServer() method of the CompanionAppProvisioningServer.
The startServer uses a Jetty web server. You can find more info here.
So it effectively starts a web server on a certain port (defined in the deviceConfig), and it creates 2 different handlers. These handlers (one of which is the CompanionInfoHandler) are passed into a context, which in turn is passed into the jetty Server. Additionally, the http connection is setup security using SSL (secure socket layers) in which the data is encrypted in all communications to and from the web server.
The handlers are really important here. A handler hooks a piece of code to a URL path. Meaning, if a user goes to a URL path in a browser, a HTTP GET is sent to the handler listening to that specific post. The same thing applies for HTTP POST commands of course. You can see there are 2 handlers . The first one (deviceInfo) is responsible for return the information the companion app needs to get an authorization code.
The second one (companionInfo) is responsible for receiving an authentication code and requesting the refresh token.
Finally, a new connector is made, allowing connections to be made to the jetty server (multiple connectors could be made on multiple ports and using other protocols possibly). This time it will use the port as defined in the deviceConfig and use HTTPS as a protocol in combination with SSL. The server is started.
This means the circle is round. This provisioning service is called in the startProvisioningThread() method we discussed in part 1.
So when putting it all together :
The main class starts instantiates an AuthSetup Class. It essentially does 2 things :
- Create an CompanionAppAuthManager which is responsible for refreshing the access token in time, and making sure the current access token is stored in the config file that is to be used to call AVS.
- It starts a new thread that starts a provisioning server, which is a jetty web server. All it does is post and receive data from the companion app whenever needed.
When the tokens are received, the CompanionAppAuthManager stores the tokens and schedules a new timer to refresh the tokens the next time they expire. At that time the provisioning server is no longer used as the refresh token is already obtained using the companion app.
That’s great, that means that everything is done for us, and we don’t have to do any code writing. The only thing to do is change the configuration in config.json. The screenshot below shows what I changed : Provisionmethod is now “companionApp”.
There’s another small thing we can’t forget. One of the things the Companion App needs when accessing the provisioning server inside the Java Client is the Client ID.
The Client ID can be found in your device configuration on the Amazon Developer Portal :
Let’s enter the Client ID into the configuration
Save it, and we’re all ready to run. The next thing we need to do is get the Companion App going in order to get the refresh token. I talk about that in my next post.