Sheep

AngularJS and instagram, a single-page application with OAuth2

We were recently challenged to create a single-page application that allows a user to select, from their personal Instagram account, photos for use in customized imagery.  Though the Instagram developer documentation provides details on how to implement a solution, there are some unique challenges we had to face.

Instagram authentication, and OAuth2 in general, encourages a server-side solution.  However, our task was to create something done entirely in the user's browser.  There are many solutions that can assist with authentication in a single-page application.  The solutions we found, however, all rely on using a redirect URL to return the user to the application with the needed access token as a URL fragment.  Unfortunately, using this method destroys the user's current page.  Since the application we were building did not present Instagram as an option until near the end of the user flow, a redirect that sent the user back to the beginning was unacceptable.  While we did create a method to save the application state for a user to return to later, there were some aspects of the application that could not be saved or restored by design.

To preserve the user's original state in the application we opened the Instagram login in a new window.  Below is the service we used to handle that login and later store our access token.

angular.module('CustomApp').factory("InstagramService", function ($rootScope, $location, $http) {
    var client_id = "XXXXXXXXX";
    var service = {
        login: function () {
            var igPopup = window.open("https://instagram.com/oauth/authorize/?client_id=" + client_id +
                "&redirect_uri=" + $location.absUrl().split('#')[0] +
                "&response_type=token", "igPopup");
        }
    };
    return service;
});

We can bind this method to a button in the frontend view as long as we inject the InstagramService into the controller for that view.

<button ng-click="InstagramService.login()">Login</button>

A user that triggers the login causes a new window to open which takes them through the Instagram authorization process and then redirects the user back to the current location of the single-page application.  We've only taken the URL before the first hash because Instagram ignores URL fragments anyway.  Our routing configuration will now handle this return.

First let's handle a successful authorization.

Here we configure our route handler to accept the standard Instagram return and capture the value of access_token.  We're using UI-Router instead of the ngRoute but the same solution applies.

angular.module('CustomApp').config(function ($stateProvider) {
    $stateProvider.
        state('oauthsuccess', {
            url: "/access_token={access_token}",
            templateUrl: '/Partials/OAuth.html',
            controller: 'OAuthLoginController'
        });
  });

Next we use our OAuthLoginController to process the access_token, pass it back to the parent window and close itself.

angular.module('CustomApp').controller("OAuthLoginController", function ($scope, $stateParams, $window, $state) {
    var $parentScope = $window.opener.angular.element(window.opener.document).scope();
    if (angular.isDefined($stateParams.access_token)) {
        $parentScope.$broadcast("igAccessTokenObtained", { access_token: $stateParams.access_token })
    }
    $window.close();
});

 Finally our InstagramService handles this return and stores the access_token for use in later calls.

angular.module('CustomApp').factory("InstagramService", function ($rootScope, $location, $http) {
    var client_id = "XXXXXXX";

    var service = {
        _access_token: null,
        access_token: function(newToken) {
            if(angular.isDefined(newToken)) {
                this._access_token = newToken;
            }
            return this._access_token;
        },
        login: function () {
            var igPopup = window.open("https://instagram.com/oauth/authorize/?client_id=" + client_id +
                "&redirect_uri=" + $location.absUrl().split('#')[0] +
                "&response_type=token", "igPopup");
        }
    };

    $rootScope.$on("igAccessTokenObtained", function (evt, args) {
        service.access_token(args.access_token);
    });

    return service;
});

 We do this using $broadcast to send a message to the parent window's rootScope and $on to listen for that.  A simple setter/getter then stores the token for later use.

What if we get an error?  Instagram doesn't use a URL fragment to return errors; instead it uses a querystring.  While UI-Router does support matching routes based on querystring we found that without a # in the URL the route matching wasn't working properly.  So, instead, we take a more heavy-handed approach.

var URI = require("URIjs");

angular.module('CustomApp').config(function ($stateProvider, $urlRouterProvider, $locationProvider) {

    var uri = new URI();
    var qs = URI.parseQuery(uri.query());
    if (angular.isDefined(qs.error_reason) || angular.isDefined(qs.error) || angular.isDefined(qs.error_description)) {
        if (angular.isDefined(window.opener) && window.opener != null) {
            var $parentScope = window.opener.angular.element(window.opener.document).scope();
            $parentScope.$broadcast("igAccessTokenError", { error: qs.error, error_reason: qs.error_reason, error_description: qs.error_description });
        }
        window.close();
    }

    $stateProvider.
        state('oauthsuccess', {
            url: "/access_token={access_token}",
            templateUrl: 'Partials/OAuth.html',
            controller: 'OAuthLoginController'
        });
  });

 Here we use URIjs to handle parsing the querystring.  Then we check for an error, error_reason or error_description as defined by the Instagram authentication documentation.  If any of these exist, we broadcast it to the parent window to be handled by the Instagram service and, again, close the child window.

This technique allows us to maintain the original browser window and even allow the user to continue to interact with the application while deciding to authorize Instagram or not.  If you have the option a server side component can certainly make life easier here.  However, if you're faced with the same challenge we were, this technique may provide some inspiration.

Have an application with unique challenges? Let our team of developers help find a solution. Contact us today and get the conversation started.