Deprecated: Creation of dynamic property wpdb::$categories is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Creation of dynamic property wpdb::$post2cat is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Creation of dynamic property wpdb::$link2cat is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Return type of WP_Theme::offsetExists($offset) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 554

Deprecated: Return type of WP_Theme::offsetGet($offset) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 595

Deprecated: Return type of WP_Theme::offsetSet($offset, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 535

Deprecated: Return type of WP_Theme::offsetUnset($offset) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 544

Deprecated: Return type of WP_REST_Request::offsetExists($offset) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 960

Deprecated: Return type of WP_REST_Request::offsetGet($offset) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 980

Deprecated: Return type of WP_REST_Request::offsetSet($offset, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 992

Deprecated: Return type of WP_REST_Request::offsetUnset($offset) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 1003

Deprecated: Return type of WP_Block_List::current() should either be compatible with Iterator::current(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 151

Deprecated: Return type of WP_Block_List::next() should either be compatible with Iterator::next(): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 175

Deprecated: Return type of WP_Block_List::key() should either be compatible with Iterator::key(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 164

Deprecated: Return type of WP_Block_List::valid() should either be compatible with Iterator::valid(): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 186

Deprecated: Return type of WP_Block_List::rewind() should either be compatible with Iterator::rewind(): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 138

Deprecated: Return type of WP_Block_List::offsetExists($index) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 75

Deprecated: Return type of WP_Block_List::offsetGet($index) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 89

Deprecated: Return type of WP_Block_List::offsetSet($index, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 110

Deprecated: Return type of WP_Block_List::offsetUnset($index) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 127

Deprecated: Return type of WP_Block_List::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 199

Deprecated: Creation of dynamic property Automattic\Jetpack\Connection\Manager::$error_handler is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php on line 91

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Instagram_Gallery::$is_wpcom is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-instagram-gallery.php on line 23

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Mailchimp::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php on line 17

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Tweetstorm_Gather::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-tweetstorm-gather.php on line 21

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Tweetstorm_Gather::$is_wpcom is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-tweetstorm-gather.php on line 22

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Gutenberg_Available_Extensions::$wpcom_is_site_specific_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php on line 20

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Memberships::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php on line 23

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Memberships::$wpcom_is_site_specific_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php on line 24

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Subscribers::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php on line 15

Deprecated: Creation of dynamic property Automattic\Jetpack\Sync\Queue::$random_int is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php on line 38

Deprecated: Creation of dynamic property Automattic\Jetpack\Sync\Queue::$random_int is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php on line 38

Deprecated: Creation of dynamic property WPCOM_Widget_Goodreads::$shelves is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/modules/widgets/goodreads.php on line 33

Deprecated: DateTime::__construct(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/twodee/blog/wp-includes/script-loader.php on line 348

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_classic_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 347

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 349

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_block_side is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 350

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$panels_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 351

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$used_block_buttons is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 354

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198
» CS 491 Lecture 24 – OAuth2 Access Tokens

teaching machines

CS 491 Lecture 24 – OAuth2 Access Tokens

November 26, 2013 by . Filed under
Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383

Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383

Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383
cs491 mobile, fall 2013, lectures.

Agenda

Exercise

Last time we started integrating access to a Picasa Web account into our mobile apps. All the interaction with the Picasa Web hinged on having a special string—the access token—which we sent along with every request in the Authorization header. We used Google’s OAuth2 playground to get a temporary access token. In today’s exercise, we’ll have our apps get the token directly. This will involve a few steps:

  1. Create a project on Google Cloud Console for our app. Google will generate a client ID and a client secret that Google will use to identify our app.
  2. Authenticate once manually with our client credentials to get our first access token and (more importantly) a refresh token. The access token will expire eventually, but the refresh token lasts into perpetuity. We can use it to generate a new access token when the old one expires.
  3. Store our client ID, client secret, and refresh token in our app or on the web somewhere. If a request from our app fails with an HTTP 403 error (access forbidden), we can use these three values to get a new access token.

Create a Project on Google Cloud Console

Any app that wants to make use of Google’s web services must be registered with Google Cloud Console. Visit the console and create a new project. Give it a meaningful name, like Acknet. The project ID is irrelevant.

Each project can have many apps associated with it. Click on Registered Apps and register a new application on a Native platform. I don’t fully know the implications of this choice. The web options don’t give us a refresh token, and the Android and iOS options seem to assume use of an SDK.

After you register, you will be granted a client ID, a client secret, and two redirect URIs which will be used when you authenticate. You will need the first two in the next steps.

Authenticate Manually

OAuth2 is a three-party authentication protocol. Its designers assume your app sits between a user and the user’s content locked up in some web service. If your app managed the authentication all by itself, you, the developer, might decide to capture a user’s username and password for your own nefarious deeds. OAuth2 attempts to fix that danger by eliminating your app from the authentication process entirely. When it’s time to authenticate, the user is directed to a browser page with a login form. After the user successfully authenticates in the browser, your app gets control again. All your app sees of the authentication process are two strings that the browser sends back: an access token and a refresh token.

In Acknet, the OAuth2 model isn’t a great fit. Our app has a Google account, not the users of our app. We can’t expect the users to authenticate on the app’s behalf. Maybe the app could programmatically authenticate, but that involves a fair bit of work. The authentication system is built around a browser, and we’d end up parsing HTML and pretending we’re a web server. Instead, we’ll authenticate once by hand. We’ll take the access token and refresh token that we get into our app.

Authenticating once should be easy, right? Well, not exactly. We’ll need to create our own little OAuth2 playground for our particular client. The first step is to login in to our app’s account, grant permissions to access Picasa Web, and get back a string called the authorization code. We can use the following PHP script to accomplish this first step. I called it get_authorization_code.php.

<?php
// If we've got at least one request parameter, let's fire off a request
// for authentication, from which we get an authorization code. With that
// we can get an access token.
if (isset($_REQUEST['response_type'])) {

  // According to https://developers.google.com/accounts/docs/OAuth2InstalledApp#formingtheurl.
  $url = "https://accounts.google.com/o/oauth2/auth";
  $params = array(
    "response_type" => $_REQUEST['response_type'],
    "client_id" => $_REQUEST['client_id'],
    "redirect_uri" => $_REQUEST['redirect_uri'],
    "scope" => $_REQUEST['scope']
  );

  $request_to = $url . '?' . http_build_query($params);

  // Now, redirect to a page that asks for authentication and gives the
  // authorization code.                                                                                                                                               
  header("Location: " . $request_to);
}

// Otherwise, let's pop up a form that does requests the authentication
// request parameters.
else {
?>

<!DOCTYPE html>
<html>
<body>

<form method="post" action="https://accounts.google.com/o/oauth2/auth">
response_type: <input type="text" name="response_type" value="code" size="100" /><br />
client_id: <input type="text" name="client_id" size="100" /><br />
redirect_uri: <input type="text" name="redirect_uri" value="urn:ietf:wg:oauth:2.0:oob" size="100" /><br />
scope: <input type="text" name="scope" value="https://picasaweb.google.com/data/" size="100" /><br />
<input type="submit" />
</form>

</body>
</html>

<?php
}
?>

With the authorization code in hand, we can then request an access token. We’ll need to send the authorization code we just got from Google, our client ID, and our client secret back to Google according to their OAuth2 documentation. The following PHP script, which I call get_tokens.html, accomplishes this step:

<!DOCTYPE html>
<html>
<body>

<form method="post" action="https://accounts.google.com/o/oauth2/token">
code: <input type="text" name="code" size="100" /><br />
client_id: <input type="text" name="client_id" size="100" /><br />
client_secret: <input type="text" name="client_secret" size="100" /><br />
redirect_uri: <input type="text" name="redirect_uri" value="urn:ietf:wg:oauth:2.0:oob" size="100" /><br />
grant_type: <input type="text" name="grant_type" value="authorization_code" size="100" /><br />                                                                        
<input type="submit" />
</form>
</li>

</body>
</html>

The result is pure treasure:

{
  "access_token" : "...",
  "token_type" : "Bearer",
  "expires_in" : 3600,
  "refresh_token" : "..."
}

Grab the access token and the refresh token for the next step. These are precious and hard-won.

Get a Fresh Access Token

We can use the initial access token get from the previous step for the next hour. Once it expires, all HTTP requests that include it in the Authorization header will return with an HTTP response with status 403. That means Forbidden. I don’t see that this status code is documented, so maybe it’ll change some day.

If we get a 403 status, we can issue a POST request that includes our client ID, client secret, and the refresh token to get a new access token.

On iOS, this can be done with the following non-robust code. Note that I’ve switched the NSURLResponse from the last lecture’s exercise to an NSHTTPURLResponse in order to have access to the HTTP status code.

// Make a request to Picasa Web. Prefer the access token from preferences.
NSURL *url = [[NSURL alloc] initWithString:@"https://picasaweb.google.com/data/feed/api/user/default?alt=json&prettyprint=true"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"GET"];
NSString *accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:@"accessToken"];
if (!accessToken) {
    accessToken = ACCESS_TOKEN;
}
[request setValue:[NSString stringWithFormat:@"Bearer %@", accessToken] forHTTPHeaderField:@"Authorization"];

// Issue request and catch result.
NSError *error = nil;
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] init];
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
NSLog(@"%d", response.statusCode);

// Our requested was Forbidden. Time to get a new access token!
if (response.statusCode == 403) {
    NSURL *refreshURL = [NSURL URLWithString:@"https://accounts.google.com/o/oauth2/token"];
    NSMutableURLRequest *refreshRequest = [NSMutableURLRequest requestWithURL:refreshURL];
    [refreshRequest setHTTPMethod:@"POST"];

    // Google says POST with the following parameters.
    NSMutableDictionary *postDictionary = [[NSMutableDictionary alloc] init];        
    [postDictionary setValue:REFRESH_TOKEN forKey:@"refresh_token"];
    [postDictionary setValue:CLIENT_ID forKey:@"client_id"];
    [postDictionary setValue:CLIENT_SECRET forKey:@"client_secret"];
    [postDictionary setValue:@"refresh_token" forKey:@"grant_type"];

    // iOS doesn't make it easy to add POST parameters. Let's walk through the
    // parameter dictionary, form the key=value entries in an array, join up
    // the results as key1=value1&key2=value2..., and escape it all.
    NSMutableArray *postArray = [[NSMutableArray alloc] init];
    for (NSString *key in postDictionary) {
        NSString *keyValue = [NSString stringWithFormat:@"%@=%@", [key stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding], [postDictionary[key] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
        [postArray addObject:keyValue];
    }
    NSString *postQuery = [postArray componentsJoinedByString:@"&"];
    NSLog(@"%@", postQuery);

    // The request body needs to be NSData, not NSString.
    NSData *postQueryAsData = [postQuery dataUsingEncoding:NSUTF8StringEncoding];
    refreshRequest.HTTPBody = postQueryAsData;

    NSError *refreshError = nil;
    NSHTTPURLResponse *refreshResponse = [[NSHTTPURLResponse alloc] init];
    NSData *refreshData = [NSURLConnection sendSynchronousRequest:refreshRequest returningResponse:&refreshResponse error:&refreshError];

    // Assuming that worked...

    // Fetch the new access token and throw it in preferences.
    NSDictionary *refreshResult = [NSJSONSerialization JSONObjectWithData:refreshData options:0 error:&refreshError];
    accessToken = refreshResult[@"access_token"];
    [[NSUserDefaults standardUserDefaults] setObject:accessToken forKey:@"accessToken"];

    // Now, let's update our previous Forbidden request and try again.
    [request setValue:[NSString stringWithFormat:@"Bearer %@", accessToken] forHTTPHeaderField:@"Authorization"];
    error = nil;
    response = [[NSHTTPURLResponse alloc] init];
    data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
}

On Android, this can be done in almost identical fashion with:

// Make a request to Picasa Web. Prefer the access token from preferences.
URL url = new URL("https://picasaweb.google.com/data/feed/api/user/default?alt=json&prettyprint=true");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
String accessToken = preferences.getString("accessToken", Constants.ACCESS_TOKEN);
connection.addRequestProperty("Authorization", "Bearer " + accessToken);

int responseCode = connection.getResponseCode();
Log.d("FOO", String.format("Response code: %d", responseCode));

// Our request was Forbidden. Time to get a new access token!
if (responseCode == 403) {
  URL refreshUrl = new URL("https://accounts.google.com/o/oauth2/token");
  HttpURLConnection refreshConnection = (HttpURLConnection) refreshUrl.openConnection();
  refreshConnection.setRequestMethod("POST");
  refreshConnection.setDoOutput(true);

  // Google says POST with the following parameters.
  HashMap<String, String> postDictionary = new HashMap<String, String>();
  postDictionary.put("refresh_token", Constants.REFRESH_TOKEN);
  postDictionary.put("client_id", Constants.CLIENT_ID);
  postDictionary.put("client_secret", Constants.CLIENT_SECRET);
  postDictionary.put("grant_type", "refresh_token");

  // Android doesn't make it easy to add POST parameters. Let's walk
  // through the parameter dictionary, form the key=value entries in an
  // array, join up the results as key1=value1&key2=value2..., and escape
  // it all.
  ArrayList<String> postArray = new ArrayList<String>();
  for (Entry<String, String> entry : postDictionary.entrySet()) {
    String keyValue = String.format("%s=%s", URLEncoder.encode(entry.getKey(), "UTF-8"), URLEncoder.encode(entry.getValue(), "UTF-8"));
    postArray.add(keyValue);
  }
  String postQuery = TextUtils.join("&", postArray);

  OutputStream out = refreshConnection.getOutputStream();
  out.write(postQuery.getBytes("UTF-8"));
  out.close();

  int refreshStatus = refreshConnection.getResponseCode();
  Log.d("FOO", "refresh status: " + refreshStatus);

  // Assuming that worked...

  // Let's read the response body up into a string.
  InputStream is = refreshConnection.getInputStream();

  BufferedReader in = new BufferedReader(new InputStreamReader(is));
  StringBuffer buffer = new StringBuffer();
  String line = in.readLine();
  while (line != null) {
    Log.d("FOO", line);
    buffer.append(line);
    line = in.readLine();
  }
  in.close();

  // Fetch the new access token and throw it in preferences.
  JSONObject refreshResult = new JSONObject(buffer.toString());
  Log.d("FOO", refreshResult.toString());
  accessToken = refreshResult.getString("access_token");
  preferences.edit().putString("accessToken", accessToken).apply();

  // Now, let's update our previous Forbidden request and try again.
  connection = (HttpURLConnection) url.openConnection();
  connection.setRequestMethod("GET");
  connection.addRequestProperty("Authorization", "Bearer " + accessToken);
}

Use the same refresh token in both Android and iOS, as only one will be granted per user-app pair. If you request a second refresh token, the first one will become invalid. This brittleness would probably best be handled by putting the access token and refresh token up in some web service of your own, a practice which presents problems of its own. Security is hard.

It’d be a good idea to store your access token in preferences. Only request a new one if the one retrieved from preferences fails.

Haiku

Who invented keys?
Evil poured the foundation
Good built keys on top