Symfony2 and Angular. User authentication

All we need is an easy explanation of the problem, so here it is.

I am developing a web application that involves Symfony2 and AngularJs. I have a question about the right way of authenticate users in the site.

I have built a function in my API REST (built in Symfony) that authenticates an user through the params passed in the request.

/**
 * Hace el login de un usuario
 * 
 * @Rest\View()
 * @Rest\Post("/user/login")
 * @RequestParam(name="mail", nullable=false, description="user email")
 * @RequestParam(name="password", nullable=false, description="user password")
 */
public function userLoginAction(Request $request, ParamFetcher $paramFetcher) {
    $mail = $paramFetcher->get('mail');
    $password = $paramFetcher->get("password");
    $response = [];
    $userManager = $this->get('fos_user.user_manager');
    $factory = $this->get('security.encoder_factory');
    $user = $userManager->findUserByUsernameOrEmail($mail);          
    if (!$user) {
        $response = [
            'error' => 1,
            'data' => 'No existe ese usuario'
        ];
    } else {
        $encoder = $factory->getEncoder($user);
        $ok = ($encoder->isPasswordValid($user->getPassword(),$password,$user->getSalt()));

        if ($ok) {
            $token = new UsernamePasswordToken($user, null, "main", $user->getRoles());
            $this->get("security.context")->setToken($token);
            $event = new InteractiveLoginEvent($request, $token);
            $this->get("event_dispatcher")->dispatch("security.interactive_login", $event);
            if ($user->getType() == 'O4FUser') {
                $url = $this->generateUrl('user_homepage'); 
            } else {
                $url = $this->generateUrl('gym_user_homepage'); 
            }
            $response = [
                'url' => $url
            ];
        } else {
            $response = [
                'error' => 1,
                'data' => 'La contraseña no es correcta'
            ];
        }
    }
    return $response;
}

As you can see, the function set the token and everything works fine.

But yesterday, I have been reading that is preferable to use a stateless system, using for that a JSON Token like the provided by this bundle:

https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Resources/doc/index.md

So my question is what of the two options is better.

Thanks!

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

As I done recently an authentication implementation with Symfony2 and Angular, and after a lot of research doing this the best way I finally chosen API-Platform (that uses JSON-LD / Hydra new vocabulary to provide REST-API, instead of FOSRest that I suppose you use) and restangular from Angular front app.

Regarding stateless, it’s true it’s a better solution but you have to build up your login scenario to choose the best technology.

Login system and JWT is not incompatible together and both solutions could be used. Before going with JWT, I made a lot of research with OAuth and it’s clearly a pain to implements and require a full developers team. JWT offers best and simple way to achieve this.

You should consider first using FOSUser bundle as @chalasr suggests.
Also, using API-Platform and JWT Bundle from Lexik and you will need NelmioCors for CrossDomain errors that should appears :

(Read docs of this bundles carefully)

HTTPS protocol is MANDATORY to communicate between api and front !

In the following example code, I used a specific entities mapping.
Contact Entity got abstract CommunicationWays which got Phones. I’ll put full mapping and class examples later).

Adapt following your needs.

# composer.json

// ...
    "require": {
        // ...
        "friendsofsymfony/user-bundle": "[email protected]",
        "lexik/jwt-authentication-bundle": "^1.4",
        "nelmio/cors-bundle": "~1.4",
        "dunglas/api-bundle": "[email protected]"
// ...


# app/AppKernel.php

    public function registerBundles()
    {
        $bundles = array(
            // ...
            new Symfony\Bundle\SecurityBundle\SecurityBundle(),
            new FOS\UserBundle\FOSUserBundle(),
            new Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle(),
            new Nelmio\CorsBundle\NelmioCorsBundle(),
            new Dunglas\ApiBundle\DunglasApiBundle(),
            // ...
        );

Then update your config :

# app/config/config.yml

imports:
    // ...
    - { resource: security.yml }
// ...
framework:
    // ...
    csrf_protection: ~
    form: ~
    session:
        handler_id: ~
    // ...
fos_user:
    db_driver: orm
    firewall_name: main
    user_class: AppBundle\Entity\User
lexik_jwt_authentication:
    private_key_path: %jwt_private_key_path%
    public_key_path:  %jwt_public_key_path%
    pass_phrase:      %jwt_key_pass_phrase%
    token_ttl:        %jwt_token_ttl%
// ...
dunglas_api:
    title:       "%api_name%"
    description: "%api_description%"
    enable_fos_user: true
nelmio_cors:
    defaults:
        allow_origin:   ["%cors_allow_origin%"]
        allow_methods:  ["POST", "PUT", "GET", "DELETE", "OPTIONS"]
        allow_headers:  ["content-type", "authorization"]
        expose_headers: ["link"]
        max_age:       3600
    paths:
        '^/': ~
// ...

And parameters dist file :

parameters:
    database_host:     127.0.0.1
    database_port:     ~
    database_name:     symfony
    database_user:     root
    database_password: ~
    # You should uncomment this if you want use pdo_sqlite
    # database_path: "%kernel.root_dir%/data.db3"

    mailer_transport:  smtp
    mailer_host:       127.0.0.1
    mailer_user:       ~
    mailer_password:   ~

    jwt_private_key_path: %kernel.root_dir%/var/jwt/private.pem
    jwt_public_key_path:  %kernel.root_dir%/var/jwt/public.pem
    jwt_key_pass_phrase : 'test'
    jwt_token_ttl:        86400

    cors_allow_origin: http://localhost:9000

    api_name:          Your API name
    api_description:   The full description of your API

    # A secret key that's used to generate certain security-related tokens
    secret: ThisTokenIsNotSecretSoChangeIt

Create user class that extends baseUser with ORM yml file :

# src/AppBundle/Entity/User.php

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;

class User extends BaseUser
{
    protected $id;
    protected $username;
    protected $email;
    protected $plainPassword;
    protected $enabled;
    protected $roles;
}

# src/AppBundle/Resources/config/doctrine/User.orm.yml

AppBundle\Entity\User:
    type:  entity
    table: fos_user
    id:
        id:
            type: integer
            generator:
                strategy: AUTO

Then put security.yml config :

# app/config/security.yml

security:
    encoders:
        FOS\UserBundle\Model\UserInterface: bcrypt

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        api:
            pattern: ^/api
            stateless: true
            lexik_jwt:
                authorization_header:
                    enabled: true
                    prefix: Bearer
                query_parameter:
                    enabled: true
                    name: bearer
                throw_exceptions: false
                create_entry_point: true

        main:
            pattern: ^/
            provider: fos_userbundle
            stateless: true
            form_login: 
                check_path: /login_check
                username_parameter: username
                password_parameter: password
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
                require_previous_session: false
            logout: true
            anonymous: true


    access_control:
        - { path: ^/api, role: IS_AUTHENTICATED_FULLY }

And services.yml :

# app/config/services.yml

services:
    // ...
    fos_user.doctrine_registry:
        alias: doctrine

And finally routing file :

# app/config/routing.yml

api:
    resource: "."
    type:     "api"
    prefix: "/api"

api_login_check:
    path: "/login_check"

At this point, composer update, create database / update schema with doctrine console commands, create a fosuser user and generate SSL public and private files required by JWT Lexik bundle (see doc).

You should be able (using POSTMAN for example) to send api calls now or generate a token using a post request to http://your_vhost/login_check

We are done for Symfony api part normally here. Do your tests !

Now, how the api will be handled from Angular ?

Here’s come our scenario :

  1. Throught a login form, send a POST request to Symfony login_check url, that will return a JSON Web Token
  2. Store that token in session / localstorage
  3. Pass this stored token in every api calls we make to headers and access our data

Here is the angular part :

First have required angular global modules installed :

$ npm install -g yo generator-angular bower
$ npm install -g ruby sass compass less
$ npm install -g grunt-cli karma-cli jshint node-gyp registry-url

Launch angular installation with yeoman :

$ yo angular

Answer asked questions :

  • … Gulp……………….. No
  • … Sass/Compass… Yes
  • … Bootstrap………. Yes
  • … Bootstrap-Sass. Yes

and uncheck all other asked modules.

Install local npm packages :

$ npm install karma jasmine-core grunt-karma karma-jasmine --save-dev
$ npm install phantomjs phantomjs-prebuilt karma-phantomjs-launcher --save-dev

And finally bower packages :

$ bower install --save lodash#3.10.1
$ bower install --save restangular

Open index.html file and set it as follow :

# app/index.html

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
    <link rel="stylesheet" href="styles/main.css" rel="nofollow noreferrer noopener">
  </head>
  <body ng-app="angularApp">
    <div class="container">
    <div ng-include="'views/main.html'" ng-controller="MainCtrl"></div>
    <div ui-view></div>

    <script src="bower_components/jquery/dist/jquery.js"></script>
    <script src="bower_components/angular/angular.js"></script>
    <script src="bower_components/bootstrap-sass-official/assets/javascripts/bootstrap.js"></script>

    <script src="bower_components/restangular/dist/restangular.js"></script>
    <script src="bower_components/lodash/lodash.js"></script>

    <script src="scripts/app.js"></script>
    <script src="scripts/controllers/main.js"></script>
  </body>
</html>

Configure restangular :

# app/scripts/app.js

'use strict';

angular
    .module('angularApp', ['restangular'])
    .config(['RestangularProvider', function (RestangularProvider) {
        // URL ENDPOINT TO SET HERE !!!
        RestangularProvider.setBaseUrl('https://your_vhost/api');

        RestangularProvider.setRestangularFields({
            id: '@id'
        });
        RestangularProvider.setSelfLinkAbsoluteUrl(false);

        RestangularProvider.addResponseInterceptor(function (data, operation) {
            function populateHref(data) {
                if (data['@id']) {
                    data.href = data['@id'].substring(1);
                }
            }

            populateHref(data);

            if ('getList' === operation) {
                var collectionResponse = data['hydra:member'];
                collectionResponse.metadata = {};

                angular.forEach(data, function (value, key) {
                    if ('hydra:member' !== key) {
                        collectionResponse.metadata[key] = value;
                    }
                });

                angular.forEach(collectionResponse, function (value) {
                    populateHref(value);
                });

                return collectionResponse;
            }

            return data;
        });
    }])
;

Configure the controller :

# app/scripts/controllers/main.js

'use strict';

angular
    .module('angularApp')
    .controller('MainCtrl', function ($scope, $http, $window, Restangular) {
        // fosuser user
        $scope.user = {username: 'johndoe', password: 'test'};

        // var to display login success or related error
        $scope.message = '';

        // In my example, we got contacts and phones
        var contactApi = Restangular.all('contacts');
        var phoneApi = Restangular.all('telephones');

        // This function is launched when page is loaded or after login
        function loadContacts() {
            // get Contacts
            contactApi.getList().then(function (contacts) {
                $scope.contacts = contacts;
            });

            // get Phones (throught abstrat CommunicationWays alias moyensComm)
            phoneApi.getList().then(function (phone) {
                $scope.phone = phone;
            });

            // some vars set to default values
            $scope.newContact = {};
            $scope.newPhone = {};
            $scope.contactSuccess = false;
            $scope.phoneSuccess = false;
            $scope.contactErrorTitle = false;
            $scope.contactErrorDescription = false;
            $scope.phoneErrorTitle = false;
            $scope.phoneErrorDescription = false;

            // contactForm handling
            $scope.createContact = function (form) {
                contactApi.post($scope.newContact).then(function () {
                    // load contacts & phones when a contact is added
                    loadContacts();

                    // show success message
                    $scope.contactSuccess = true;
                    $scope.contactErrorTitle = false;
                    $scope.contactErrorDescription = false;

                    // re-init contact form
                    $scope.newContact = {};
                    form.$setPristine();

                    // manage error handling
                }, function (response) {
                    $scope.contactSuccess = false;
                    $scope.contactErrorTitle = response.data['hydra:title'];
                    $scope.contactErrorDescription = response.data['hydra:description'];
                });
            };

            // Exactly same thing as above, but for phones
            $scope.createPhone = function (form) {
                phoneApi.post($scope.newPhone).then(function () {
                    loadContacts();

                    $scope.phoneSuccess = true;
                    $scope.phoneErrorTitle = false;
                    $scope.phoneErrorDescription = false;

                    $scope.newPhone = {};
                    form.$setPristine();
                }, function (response) {
                    $scope.phoneSuccess = false;
                    $scope.phoneErrorTitle = response.data['hydra:title'];
                    $scope.phoneErrorDescription = response.data['hydra:description'];
                });
            };
        }

        // if a token exists in sessionStorage, we are authenticated !
        if ($window.sessionStorage.token) {
            $scope.isAuthenticated = true;
            loadContacts();
        }

        // login form management
        $scope.submit = function() {
            // login check url to get token
            $http({
                method: 'POST',
                url: 'http://your_vhost/login_check',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                data: $.param($scope.user)

                // with success, we store token to sessionStorage
            }).success(function(data) {
                $window.sessionStorage.token = data.token;
                $scope.message = 'Successful Authentication!';
                $scope.isAuthenticated = true;

                // ... and we load data
                loadContacts();

                // with error(s), we update message
            }).error(function() {
                $scope.message = 'Error: Invalid credentials';
                delete $window.sessionStorage.token;
                $scope.isAuthenticated = false;
            });
        };

        // logout management
        $scope.logout = function () {
            $scope.message = '';
            $scope.isAuthenticated = false;
            delete $window.sessionStorage.token;
        };

        // This factory intercepts every request and put token on headers
    }).factory('authInterceptor', function($rootScope, $q, $window) {
    return {
        request: function (config) {
            config.headers = config.headers || {};

            if ($window.sessionStorage.token) {
                config.headers.Authorization = 'Bearer ' + $window.sessionStorage.token;
            }
            return config;
        },
        response: function (response) {
            if (response.status === 401) {
                // if 401 unauthenticated
            }
            return response || $q.when(response);
        }
    };
// call the factory ...
}).config(function ($httpProvider) {
    $httpProvider.interceptors.push('authInterceptor');
});

And finally we need our main.html file with forms :

<!—Displays error or success messages-->
<span>{{message}}</span><br><br>

<!—Login/logout form-->
<form ng-show="!isAuthenticated" ng-submit="submit()">
    <label>Login Form:</label><br>
    <input ng-model="user.username" type="text" name="user" placeholder="Username" disabled="true" />
    <input ng-model="user.password" type="password" name="pass" placeholder="Password" disabled="true" />
    <input type="submit" value="Login" />
</form>
<div ng-show="isAuthenticated">
    <a ng-click="logout()" href="">Logout</a>
</div>
<div ui-view ng-show="isAuthenticated"></div>
<br><br>

<!—Displays contacts list-->
<h1 ng-show="isAuthenticated">Liste des Contacts</h1>
<article ng-repeat="contact in contacts" ng-show="isAuthenticated" id="{{ contact['@id'] }}" class="row marketing">
    <h2>{{ contact.nom }}</h2>
    <!—Displays contact phones list-->
    <h3 ng-repeat="moyenComm in contact.moyensComm">Tél : {{ moyenComm.numero }}</h3>
</article><hr>

<!—Create contact form-->
<form name="createContactForm" ng-submit="createContact(createContactForm)" ng-show="isAuthenticated" class="row marketing">
    <h2>Création d'un nouveau contact</h2>
    <!—Displays error / success message on creating contact-->
    <div ng-show="contactSuccess" class="alert alert-success" role="alert">Contact publié.</div>
    <div ng-show="contactErrorTitle" class="alert alert-danger" role="alert">
        <b>{{ contactErrorTitle }}</b><br>
        {{ contactErrorDescription }}
    </div>
    <div class="form-group">
        <input ng-model="newContact.nom" placeholder="Nom" class="form-control">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

<!—Phone form-->
<form name="createPhoneForm" ng-submit="createPhone(createPhoneForm)" ng-show="isAuthenticated" class="row marketing">
    <h2>Création d'un nouveau téléphone</h2>
    <div ng-show="phoneSuccess" class="alert alert-success" role="alert">Téléphone publié.</div>
    <div ng-show="phoneErrorTitle" class="alert alert-danger" role="alert">
        <b>{{ phoneErrorTitle }}</b><br>
        {{ phoneErrorDescription }}
    </div>
    <div class="form-group">
        <input ng-model="newPhone.numero" placeholder="Numéro" class="form-control">
    </div>
    <div class="form-group">
        <label for="contact">Contact</label>
        <!—SelectBox de liste de contacts-->
        <select ng-model="newPhone.contact" ng-options="contact['@id'] as contact.nom for contact in contacts" id="contact"></select>
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

Well, I know it’s a lot of condensed code, but you have all weapons to kick-off a full api system using Symfony & Angular here. I’ll make a blog post one day for this to be more clear and update this post some times.

I just hope it helps.

Best Regards.

Method 2

The bundle you linked is a better solution than your current.
It’s because of the differences between security needs of a REST Api and a classic form-based application.

Look at the jwt.io introduction to Json Web Token, and after, you should try to implement the LexikJWTAuthenticationBundle which is very clean, easy-to-use, securely and powerful.

JWT will provide more security and a ready-to-use login process, only need a few lines of configuration. Of course, you can easily manage, register and create token from users retrieved/registered by your user provider (for me it’s the FOSUserBundle).

A JWT is a real signature representing your user. Read more in the link I’ve given you.

See also this JWTAuthenticationBundle Sandbox for a real example with AngularJS.

Method 3

You can check following repositories. It contains basic setup and configuration for Symfony + Angular (it also contains some bundles like FOSUser, NelmioApiDocBundle as well as simple Auth). Angular setup supports server side rendering. some ban work as default skeleton for Symfony + Angular projects https://github.com/vazgen/sa-standard-be and https://github.com/vazgen/sa-standard-fe

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply