Adding Facebook Login to an AngularJS App using Satellizer

As front-end frameworks like AngularJS become increasingly sophisticated, more and more application logic, including data manipulation and validation, as well as authentication, is being handled on the client-side. Satellizer, a user-friendly token-based authentication module for AngularJS, simplifies the process of implementing authentication mechanisms within AngularJS applications. The library offers built-in support for a variety of accounts, including Google, Facebook, LinkedIn, Twitter, Instagram, GitHub, Bitbucket, Yahoo, Twitch, and Microsoft (Windows Live).

This article will guide you through building a straightforward web application resembling here, enabling users to log in and view their information.

Authentication vs. Authorization

These two terms frequently arise when incorporating a user system into your application. Wikipedia defines them as follows:

Authentication is the process of verifying the authenticity of a claimed attribute associated with a specific piece of data.

Authorization involves defining access rights to resources related to information security and computer security, particularly access control.

To illustrate, consider a blog website with multiple contributors: bloggers write articles, and a manager approves the content. While everyone can authenticate (log in) to the system, their permissions (authorization) differ. Bloggers cannot approve content, while the manager can.

Why Satellizer?

While you can build custom authentication systems in AngularJS using tutorials like this comprehensive JSON Web Token Tutorial: An Example in Laravel and AngularJS, Satellizer offers significant advantages. This tutorial provides a good understanding of JWT (JSON Web Token) and demonstrates a simple authentication implementation in AngularJS using local storage and HTTP interceptors.

The primary reason to choose Satellizer is its support for numerous social network logins, such as Facebook, Twitter, etc. In today’s mobile-first world, users expect seamless experiences and often find typing usernames and passwords tedious. Social logins provide a convenient alternative. Integrating each social network’s SDK and navigating their documentation can be repetitive. Satellizer streamlines this process, enabling support for various social logins with minimal effort.

Furthermore, Satellizer is an active GitHub project. This is crucial because SDKs are frequently updated, and you don’t want to constantly refer to their documentation (anyone who has worked with the Facebook SDK understands this pain).

AngularJS App with Facebook Login

This is where things get interesting.

We will develop a web application with traditional login/registration (using username and password) alongside social login support. The application will have three simple pages:

  • Home page: Accessible to everyone.
  • Login page: For entering username/password.
  • Secret page: Only accessible to logged-in users.

We’ll use Python and Flask for the backend. Python and Flask are highly expressive, making code porting to other languages/frameworks relatively straightforward. AngularJS will power the front-end. For social logins, we’ll focus on Facebook integration due to its popularity.

Let’s begin!

Step #1: Bootstrap the Project

Here’s the code structure:

1
2
3
4
5
6
7
8
9
- app.py
- static/
	- index.html
- app.js
	- bower.json
	- partials/
		- login.tpl.html
		- home.tpl.html
		- secret.tpl.html

The backend code resides in app.py. Frontend code is placed in the static/ folder, which Flask serves automatically. Partial views are located in static/partials/ and managed by the ui.router module.

To begin backend coding, you’ll need Python 2.7.* and the necessary libraries installed via pip. Using virtualenv for Python environment isolation is recommended. Add the following required Python modules to requirements.txt:

1
2
3
4
Flask==0.10.1
PyJWT==1.4.0
Flask-SQLAlchemy==1.0
requests==2.7.0

Install dependencies using:

1
pip install -r requirements.txt

In app.py, include the initial Flask bootstrap code (import statements are omitted for brevity):

1
2
3
4
5
6
7
8
app = Flask(__name__)

@app.route('/')
def index():
    return flask.redirect('/static/index.html')

if __name__ == '__main__':
    app.run(debug=True)

Initialize bower and install AngularJS and ui.router:

1
2
bower init # here you will need to answer some question. when in doubt, just hit enter :)
bower install angular angular-ui-router --save # install and save these dependencies into bower.json

Include AngularJS and ui-router in index.html and create routings for the home, login, and secret pages:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<body ng-app="DemoApp">

<a ui-sref="home">Home</a>
<a ui-sref="login">Login</a>
<a ui-sref="secret">Secret</a>
<div ui-view></div>

<script src="bower_components/angular/angular.min.js"></script>
<script src="bower_components/angular-ui-router/release/angular-ui-router.min.js"></script>
<script src="main.js"></script>
</body>

Add the following code to main.js for routing configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var app = angular.module('DemoApp', ['ui.router']);

app.config(function ($stateProvider, $urlRouterProvider) {
  $stateProvider
    .state('home', {
      url: '/home',
      templateUrl: 'partials/home.tpl.html'
    })
    .state('secret', {
      url: '/secret',
      templateUrl: 'partials/secret.tpl.html',
    })
    .state('login', {
      url: '/login',
      templateUrl: 'partials/login.tpl.html'
    });
  $urlRouterProvider.otherwise('/home');

});

Running the server (python app.py) should now display a basic interface at http://localhost:5000:

Basic login interface

The Home, Login, and Secret links should function, displaying the corresponding template content.

Congratulations on setting up the skeleton! If you encounter any errors, refer to code on GitHub.

Step #2: Login and Register

This step implements user registration and login functionality using email and password.

First, configure the backend with a User model and a method to generate JWT tokens. The simplified User model below omits basic checks like email format or password strength.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(100), nullable=False)
    password = db.Column(db.String(100))

    def token(self):
        payload = {
            'sub': self.id,
            'iat': datetime.utcnow(),
            'exp': datetime.utcnow() + timedelta(days=14)
        }
        token = jwt.encode(payload, app.config['TOKEN_SECRET'])
        return token.decode('unicode_escape')

The Python jwt module generates the JWT payload. iat and exp represent the token’s creation and expiration timestamps. This code sets token expiration to two weeks.

After creating the User model, add “login” and “register” endpoints. The code for both is similar; only the “register” part is shown here. Satellizer defaults to calling /auth/login and /auth/signup endpoints for “login” and “register,” respectively.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@app.route('/auth/signup', methods=['POST'])
def signup():
    data = request.json

    email = data["email"]
    password = data["password"]

    user = User(email=email, password=password)
    db.session.add(user)
    db.session.commit()

    return jsonify(token=user.token())

Test the endpoint using curl:

1
curl localhost:5000/auth/signup -H "Content-Type: application/json" -X POST -d '{"email":"test@abcd.com","password":"xyz"}'

The result should resemble:

1
2
3
{
  "token": "very long string…."
}

With the backend ready, move to the frontend. Install satellizer and add it as a dependency in main.js:

1
bower install satellizer --save

Add satellizer as a dependency:

1
var app = angular.module('DemoApp', ['ui.router', 'satellizer']);

Login and signup in satellizer are straightforward:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$scope.signUp = function () {
    $auth
      .signup({email: $scope.email, password: $scope.password})
      .then(function (response) {
        // set the token received from server
        $auth.setToken(response);
        // go to secret page
        $state.go('secret');
      })
      .catch(function (response) {
        console.log("error response", response);
      })
  };

Refer to code on GitHub if you encounter any issues during setup.

Step #3: Securing the Secret View

Currently, anyone can access the secret page without authentication. We’ll implement an AngularJS interceptor to redirect unauthorized users to the login page.

Add a requiredLogin flag to differentiate the secret page:

1
2
3
4
5
6
    .state('secret', {
      url: '/secret',
      templateUrl: 'partials/secret.tpl.html',
      controller: 'SecretCtrl',
      data: {requiredLogin: true}
    })

The “data” part will be used in the $stateChangeStart event, triggered on each routing change:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
app.run(function ($rootScope, $state, $auth) {
  $rootScope.$on('$stateChangeStart',
    function (event, toState) {
      var requiredLogin = false;
      // check if this state need login
      if (toState.data && toState.data.requiredLogin)
        requiredLogin = true;
      
      // if yes and if this user is not logged in, redirect him to login page
      if (requiredLogin && !$auth.isAuthenticated()) {
        event.preventDefault();
        $state.go('login');
      }
    });
});

Users are now redirected to the login page if they attempt to access the secret page without authentication.

You can find the code for this step in here.

Step #4: Displaying User-Specific Information

Let’s personalize the secret page by adding user-specific content.

Create a backend endpoint accessible only to authenticated users with valid tokens. The /user endpoint returns the user_id and email associated with the token.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@app.route('/user')
def user_info():
    # the token is put in the Authorization header
    if not request.headers.get('Authorization'):
        return jsonify(error='Authorization header missing'), 401
    
    # this header looks like this: Authorization: Bearer {token}
    token = request.headers.get('Authorization').split()[1]
    try:
        payload = jwt.decode(token, app.config['TOKEN_SECRET'])
    except DecodeError:
        return jsonify(error='Invalid token'), 401
    except ExpiredSignature:
        return jsonify(error='Expired token'), 401
    else:
        user_id = payload['sub']
        user = User.query.filter_by(id=user_id).first()
        if user is None:
            return jsonify(error='Should not happen ...'), 500
        return jsonify(id=user.id, email=user.email), 200
    return jsonify(error="never reach here..."), 500

Again, we use the jwt module to decode the JWT token from the ‘Authorization’ header and handle expired or invalid tokens.

Test this endpoint using curl. First, obtain a valid token:

1
curl localhost:5000/auth/signup -H "Content-Type: application/json" -X POST -d '{"email":"test@abcd.com","password":"xyz"}'

Then, use the token:

1
curl localhost:5000/user -H "Authorization: Bearer {put the token here}"

This returns:

1
2
3
4
{
  "email": "test@abcd.com",
  "id": 1
}

Now, include this endpoint in the Secret Controller. Use the $http module to call the endpoint. Satellizer automatically adds the token to the header, simplifying token handling.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  getUserInfo();

  function getUserInfo() {
    $http.get('/user')
      .then(function (response) {
        $scope.user = response.data;
      })
      .catch(function (response) {
        console.log("getUserInfo error", response);
      })
  }

Finally, the secret page displays personalized information.

The secret page, showing a user email and id.

The code for this step is available in GitHub.

Step #5: Enabling Facebook Login with Satellizer

Satellizer simplifies social login integration. This step enables users to log in using their Facebook accounts.

Facebook OAuth authentication.

Create a Facebook developer account and a website application following developers.facebook.com/docs/apps/register to obtain an application_id and secret code. Once completed, you’ll have the application ID and secret as shown in the screenshot below.

Getting the application secret.

When a user chooses Facebook login, Satellizer sends an authorization code to the /auth/facebook endpoint. The backend retrieves an access token from Facebook’s /oauth endpoint using this code, allowing calls to the Facebook Graph API to fetch user information like location, friends, email, etc.

To track whether an account is created via Facebook or regular signup, add facebook_id to the User model.

1
facebook_id = db.Column(db.String(100)) 

Configure the Facebook secret using the FACEBOOK_SECRET environment variable in app.config.

1
app.config['FACEBOOK_SECRET'] = os.environ.get('FACEBOOK_SECRET')

Set this environment variable when launching app.py:

1
FACEBOOK_SECRET={your secret} python app.py

This method handles Facebook logins. Satellizer defaults to calling the /auth/facebook endpoint.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@app.route('/auth/facebook', methods=['POST'])
def auth_facebook():
    access_token_url = 'https://graph.facebook.com/v2.3/oauth/access_token'
    graph_api_url = 'https://graph.facebook.com/v2.5/me?fields=id,email'

    params = {
        'client_id': request.json['clientId'],
        'redirect_uri': request.json['redirectUri'],
        'client_secret': app.config['FACEBOOK_SECRET'],
        'code': request.json['code']
    }

    # Exchange authorization code for access token.
    r = requests.get(access_token_url, params=params)
    # use json.loads instead of urlparse.parse_qsl
    access_token = json.loads(r.text)

    # Step 2. Retrieve information about the current user.
    r = requests.get(graph_api_url, params=access_token)
    profile = json.loads(r.text)

    # Step 3. Create a new account or return an existing one.
    user = User.query.filter_by(facebook_id=profile['id']).first()
    if user:
        return jsonify(token=user.token())

    u = User(facebook_id=profile['id'], email=profile['email'])
    db.session.add(u)
    db.session.commit()
    return jsonify(token=u.token())

The requests module facilitates communication with the Facebook server. This completes the complex backend part. On the frontend, adding Facebook login is straightforward. Inform Satellizer about your facebook_id within the app.config function:

1
2
3
4
5
$authProvider.facebook({
    clientId: {your facebook app id},
    // by default, the redirect URI is http://localhost:5000
    redirectUri: 'http://localhost:5000/static/index.html'
  });

To initiate Facebook login, call:

1
$auth.authenticate(facebook)

As usual, refer to code on GitHub for the complete code.

The web application now offers complete login/registration functionality, allowing users to use their email and password or Facebook. Upon login, they can access their personalized secret page.

Enhancing the User Interface

Let’s improve the appearance using Bootstrap for layout and the angular toaster module for handling error messages gracefully (e.g., failed logins).

The code for this beautification is available in here.

Conclusion

This article provides a step-by-step guide to integrating Satellizer into a simple AngularJS web application. Satellizer simplifies the addition of other social logins like Twitter, LinkedIn, and more. While the frontend code remains similar, backend implementation may vary due to differences in social network SDK endpoints and protocols. You can explore https://github.com/sahat/satellizer/blob/master/examples/server/python/app.py, which contains examples for Facebook, Github, Google, Linkedin, Twitter, and Bitbucket. When in doubt, consult the documentation at https://github.com/sahat/satellizer.

Licensed under CC BY-NC-SA 4.0