AngularJS and ASP.Net WebAPI Social Login to Mobile Browser

I follow this article on social inputs with AngularJS and ASP.Net WebAPI (which is pretty good):

ASP.NET Web API 2 external logins with Facebook and Google in AngularJS application

To a large extent, the code works great when you launch social login through a desktop browser (e.g. Chrome, FF, IE, Edge). The social login opens in a new window (not in the form of tabs), and you can use your Google or Facebook account, and as soon as you log in through any of them, you are redirected to the callback page (authComplete.html), and to The callback page has a JS file defined (authComplete.js) that will close the window and execute the command in the parent window.

angularJS controller, which calls an external login URL and opens a popup window (not a tab) on desktop browsers:

loginController.js

'use strict'; app.controller('loginController', ['$scope', '$location', 'authService', 'ngAuthSettings', function ($scope, $location, authService, ngAuthSettings) { $scope.loginData = { userName: "", password: "", useRefreshTokens: false }; $scope.message = ""; $scope.login = function () { authService.login($scope.loginData).then(function (response) { $location.path('/orders'); }, function (err) { $scope.message = err.error_description; }); }; $scope.authExternalProvider = function (provider) { var redirectUri = location.protocol + '//' + location.host + '/authcomplete.html'; var externalProviderUrl = ngAuthSettings.apiServiceBaseUri + "api/Account/ExternalLogin?provider=" + provider + "&response_type=token&client_id=" + ngAuthSettings.clientId + "&redirect_uri=" + redirectUri; window.$windowScope = $scope; var oauthWindow = window.open(externalProviderUrl, "Authenticate Account", "location=0,status=0,width=600,height=750"); }; $scope.authCompletedCB = function (fragment) { $scope.$apply(function () { if (fragment.haslocalaccount == 'False') { authService.logOut(); authService.externalAuthData = { provider: fragment.provider, userName: fragment.external_user_name, externalAccessToken: fragment.external_access_token }; $location.path('/associate'); } else { //Obtain access token and redirect to orders var externalData = { provider: fragment.provider, externalAccessToken: fragment.external_access_token }; authService.obtainAccessToken(externalData).then(function (response) { $location.path('/orders'); }, function (err) { $scope.message = err.error_description; }); } }); } }]); 

authComplete.html

 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> </head> <body> <script src="scripts/authComplete.js"></script> </body> </html> 

authComplete.js

 window.common = (function () { var common = {}; common.getFragment = function getFragment() { if (window.location.hash.indexOf("#") === 0) { return parseQueryString(window.location.hash.substr(1)); } else { return {}; } }; function parseQueryString(queryString) { var data = {}, pairs, pair, separatorIndex, escapedKey, escapedValue, key, value; if (queryString === null) { return data; } pairs = queryString.split("&"); for (var i = 0; i < pairs.length; i++) { pair = pairs[i]; separatorIndex = pair.indexOf("="); if (separatorIndex === -1) { escapedKey = pair; escapedValue = null; } else { escapedKey = pair.substr(0, separatorIndex); escapedValue = pair.substr(separatorIndex + 1); } key = decodeURIComponent(escapedKey); value = decodeURIComponent(escapedValue); data[key] = value; } return data; } return common; })(); var fragment = common.getFragment(); window.location.hash = fragment.state || ''; window.opener.$windowScope.authCompletedCB(fragment); window.close(); 

The problem is that when the application is launched on the mobile device (Safari, Chrome for Mobile), the social window for entering the new tab opens and the JS function that was supposed to transfer the fragment to the main application window is not executed, and the new tab does not close.

In fact, you can try this behavior both on the desktop and on the mobile browser through the application:

http://ngauthenticationapi.azurewebsites.net/

What I tried so far in this context is in the login controller, I modified the function so that the external login URL opens in the same window:

 $scope.authExternalProvider = function (provider) { var redirectUri = location.protocol + '//' + location.host + '/authcomplete.html'; var externalProviderUrl = ngAuthSettings.apiServiceBaseUri + "api/Account/ExternalLogin?provider=" + provider + "&response_type=token&client_id=" + ngAuthSettings.clientId + "&redirect_uri=" + redirectUri; window.location = externalProviderUrl; }; 

And changed the authComplete.js common.getFragment function to return to the login page, adding the access token provided by the social login as the query string:

 common.getFragment = function getFragment() { if (window.location.hash.indexOf("#") === 0) { var hash = window.location.hash.substr(1); var redirectUrl = location.protocol + '//' + location.host + '/#/login?ext=' + hash; window.location = redirectUrl; } else { return {}; } }; 

And in the input controller, I added a function to parse the request and try to call the $ scope.authCompletedCB function (fragment), for example:

 var vm = this; var fragment = null; vm.testFn = function (fragment) { $scope.$apply(function () { if (fragment.haslocalaccount == 'False') { authenticationService.logOut(); authenticationService.externalAuthData = { provider: fragment.provider, userName: fragment.external_user_name, externalAccessToken: fragment.external_access_token }; $location.path('/associate'); } else { //Obtain access token and redirect to orders var externalData = { provider: fragment.provider, externalAccessToken: fragment.external_access_token }; authenticationService.obtainAccessToken(externalData).then(function (response) { $location.path('/home'); }, function (err) { $scope.message = err.error_description; }); } }); } init(); function parseQueryString(queryString) { var data = {}, pairs, pair, separatorIndex, escapedKey, escapedValue, key, value; if (queryString === null) { return data; } pairs = queryString.split("&"); for (var i = 0; i < pairs.length; i++) { pair = pairs[i]; separatorIndex = pair.indexOf("="); if (separatorIndex === -1) { escapedKey = pair; escapedValue = null; } else { escapedKey = pair.substr(0, separatorIndex); escapedValue = pair.substr(separatorIndex + 1); } key = decodeURIComponent(escapedKey); value = decodeURIComponent(escapedValue); data[key] = value; } return data; } function init() { var idx = window.location.hash.indexOf("ext="); if (window.location.hash.indexOf("#") === 0) { fragment = parseQueryString(window.location.hash.substr(idx)); vm.testFn(fragment); } } 

But obviously this gives me an error related to angular (which I don't have about at the moment):

https://docs.angularjs.org/error/$rootScope/inprog?p0=$digest

So, pretty much this is a dead end for me at this point.

Any ideas or materials would be highly appreciated.

Gracias!

Update: I managed to resolve the angular error about root crop failure, but, unfortunately, solving the problem does not fix the main problem. If I tried to open a social login on the same browser tab where my application is, Google can log in and return to the application and transfer the necessary tokens. This is another story for Facebook, where there is a warning in the developer tools console that does not appear to allow Facebook to display the login page.

To a large extent, the original method with which a new window (or tab) is opened is the way forward, but fixing the same for a mobile browser seems more complicated.

+7
javascript angularjs asp.net-web-api
source share
1 answer

On the desktop, when the auth window (not tab) appears, it has the opener property set in the window that opened this pop-up window, on your mobile phone, as you said, this is not a pop-up window, but a new tab. when a new tab opens in the browser, the opener property is null , so you actually have an exception:

window.opener.$windowScope.authCompletedCB

because you cannot refer to the $windowScope property of zero value ( window.opener ) so that each line of code does not execute after that - that’s why the window is not closed on the mobile device.

Decision

In your authComplete.js file, instead of trying to call window.opener.$windowScope.authCompletedCB and pass the user fragment, save the fragment in localStorage or in a cookie (after the authComplete.html page is in the same place as your application) with using JSON.stringify() and just close the window using window.close() .

In loginController.js do $interval for something like 100ms to check the value in localStorage or in the cookie (don't forget to clear the interval when $scope is $destroy ), if there is a fragment, you can JSON.parse its value with JSON.parse from store, delete it from the store and call $scope.authCompletedCB with the parsed value.

UPDATE - added code examples

authComplete.js

 ... var fragment = common.getFragment(); // window.location.hash = fragment.state || ''; // window.opener.$windowScope.authCompletedCB(fragment); localStorage.setItem("auth_fragment", JSON.stringify(fragment)) window.close(); 

loginController.js

 app.controller('loginController', ['$scope', '$interval', '$location', 'authService', 'ngAuthSettings', function ($scope, $interval, $location, authService, ngAuthSettings) { ... // check for fragment every 100ms var _interval = $interval(_checkForFragment, 100); function _checkForFragment() { var fragment = localStorage.getItem("auth_fragment"); if(fragment && (fragment = JSON.parse(fragment))) { // clear the fragment from the storage localStorage.removeItem("auth_fragment"); // continue as usual $scope.authCompletedCB(fragment); // stop looking for fragmet _clearInterval(); } } function _clearInterval() { $interval.cancel(_interval); } $scope.$on("$destroy", function() { // clear the interval when $scope is destroyed _clearInterval(); }); }]); 
+4
source share

All Articles