For one of my projects I need my users to log in to an 3rd party api (in my case strava) using oauth2.0
Framework of choice: AngularJS 1.5 in plain javascript, with the Angular 1.5 componentrouter. Since I have apikeys which I do not want to be known the the client, I also need my own mini server using NodeJS, express and a bit of request.
OAuth2 in a nutshell:
Someone wants to log in to my application and presses the “Login button”. When the user presses the button there is redirect to the 3rd party website where the user actually logs in. The redirect is done in the client, because when I do a res.redirect() to the url in the backend with express, I get an CORS error.
The user is redirect to my callback page with a code in the url. I take this code to Strava to trade it for an access token so the user can get data. The trading also makes use of my api key so this is all done in the server.
How to do this:
I have a maincomponent with a login button, when clicking this button the user is redirected to the 3rd party login.
angular.module('myapp',['ngComponentRouter',"how","search"]) .value('$routerRootComponent', 'app') .component('main', { template: ' <button ng-click="$ctrl.login()" class="stravaButton"></button>' + '<nav>' + ' <a ng-link="[\'How\']">How page</a>' + ' <a ng-link="[\'Search\']">Search page</a>' + '</nav>' + '<ng-outlet></ng-outlet>', controller:MainComponent, $routeConfig: [ {path: '/how', name: 'How', component: 'how', useAsDefault: true}, {path: '/search', name: 'Search', component: 'search' } ] }); function MainComponent($window){ var $ctrl = this; $ctrl.login = function(){ $window.location.href = "https://www.strava.com/oauth/authorize?client_id=<MY_CLIENT_ID>+&response_type=code&amp;redirect_uri=http://localhost:3000/callback"; } }
As we can see in the $window.location.href, after the user is logged in , the user will be redirected to http://localhost:3000/callback For this we make a callback component: which we add to the $routeConfig in the mainComponent:
$routeConfig: [ {path: '/how', name: 'How', component: 'how', useAsDefault: true}, {path: '/search', name: 'Search', component: 'search' }, {path: '/callback', name: 'Callback', component: 'callback' } ] });
Now for the callback component. In this component we get the code that was send from the 3rd party using location.search().code. This login component does a request to the backend to exchange the code we got for an access token.
This is how the component looks like:
angular.module('callback', []) .component('callback', { controller:CallbackComponent }) function CallbackComponent($location,loginService){ var code = $location.search().code; loginService.loginUser(code).then(function(data){ }) }
This is how our (clientside) loginService looks like:
angular.module('myapp') .factory('loginService',function($http, $q){ var loginUrl = '/api/login'; loginUser = function(code){ var login = $http.get(loginUrl + '/' + code).then(function(response){ var data = response.data; if(data.access_token !== undefined && data.access_token !== null){ //success } }) return login; } return { loginUser:loginUser }; });
Note: for the location.search() to work we need to enable HTML5 in the configuration.
angular.module('myapp') .config(function($locationProvider){ $locationProvider.html5Mode(true); });
In the backend we send a POST request to the endpoint that will give us the token. We send the client id and client secret with the POST. If everything goes well, we receive the token which we send back to the client.
var express = require('express'); var request = require('request') var app = express.Router(); app.get('/:code', function(req,res){ var mycode = req.params.code; var endpoint = 'https://www.strava.com/oauth/token'; var url = endpoint , options = { url: url , method: 'POST' , json: true ,form : { client_id : "MYCLIENTID" , client_secret : "<MY_CLIENT_SECRET>" , code : mycode } }; request(options, function (err, response, payload) { if (err || response.statusCode !== 200) { console.log('api call error'); console.log(err); } res.status(response.statusCode).send(payload) }); }); module.exports = app;
When the component was succesfull we store the accesToken in the localStorage using ngStorage, so the user does not need to login again when he/she comes back to the website. I made a separate clientside service for that, called the UserService, which is called in the login component.
Changes to the login component:
angular.module('myapp') .factory('loginService',function($http, $q, userService){ var loginUrl = '/api/login'; loginUser = function(code){ var login = $http.get(loginUrl + '/' + code).then(function(response){ var data = response.data; if(data.access_token !== undefined && data.access_token !== null){ userService.setToken(data.access_token) } }) return login; } return { loginUser:loginUser }; });
The userservice:
angular.module('myapp') .factory('userService',function($http, $localStorage){ isLoggedIn = function(){ return($localStorage.user !== null); } setToken = function(token){ $localStorage.userToken = token } getToken = function(){ return $localStorage.userToken; } return { getToken:getToken, setToken:setToken, isLoggedIn:isLoggedIn }; });
ngStorage also has the advantage that we can put an ng-if on some DOMelements that watches the storage. In that case we can make some elements only appear if the user is logged in (or not), like the login button and items in the menu I only want to show if the user is logged in. Let’s do that as well in the mainComponent.
"use strict"; angular.module('myapp', ['ngComponentRouter', 'ngStorage',"how","callback","search"]) .value('$routerRootComponent', 'app') .component('app', { template: '<button ng-if="$ctrl.$storage.userToken === undefined" ng-click="$ctrl.login()">LOG IN</button>' + '</div>' + '<nav>' + ' <a ng-link="[\'How\']">How does it work</a>' + ' <a ng-if="$ctrl.$storage.userToken !== undefined" ng-link="[\'Search\']">Search</a>' + '</nav>' + '<ng-outlet></ng-outlet></div>', controller: MainComponent, $routeConfig: [ { path: '/how', name: 'How', component: 'how', useAsDefault: true }, { path: '/search', name: 'Search', component: 'activities' }, { path: '/callback', name: 'Callback', component: 'callback' } ] }); function MainComponent($window, $localStorage) { var $ctrl = this; $ctrl.$storage = $localStorage; $ctrl.login = function () { $window.location.href = "https://www.strava.com/oauth/authorize?client_id=<MY_CLIENT_ID>+&response_type=code&redirect_uri=http://localhost:3000/callback"; }
Also, if the login was succesfull we redirect the user to the component of the website where the data is shown we are getting from the 3rd party api.
angular.module('myapp', []) .component('callback', { controller:LoginComponent }) function LoginComponent($location,loginService,$rootRouter){ var code = $location.search().code; loginService.loginUser(code).then(function(data){ $rootRouter.navigate(['Search']); }) };
And that is it, we can now login in with a 3rd party API using OAuth2.0, the Angular1.5 Componentrouter and NodeJS 🙂
A full working example can be found at my Github page, don’t forget to put your own Strava api keys in, or change it to match your 3rd party api!
Blogs I found helpfull:
http://jasonwatmore.com/post/2015/03/10/AngularJS-User-Registration-and-Login-Example.aspx
https://www.codementor.io/nodejs/tutorial/how-to-implement-twitter-sign-expressjs-oauth
https://www.thepolyglotdeveloper.com/2015/03/using-oauth-2-0-in-your-web-browser-with-angularjs/