Zemke / starter-laravel-angular

Laravel and AngularJS Starter Application Boilerplate featuring Laravel 5.3 and AngularJS 1.5.8
https://starter-laravel-angular.herokuapp.com
Other
369 stars 119 forks source link

Authentication - Restricting access to Routes #26

Closed spirant closed 8 years ago

spirant commented 8 years ago

Hi

Thank you for the great starter app. I am working with this and note that I cannot see an example of a restricted API route. I have tested the 'middleware' => 'auth.basic' which works as expected, but I am struggling to use token based authentication to restrict API routes with for example 'jwt.auth'.

app \ routes.php

Route::group(array('prefix' => 'api/', 'middleware' => 'jwt.auth'), function () {
    Route::resource('todo', 'TodoController');
});

app \ Kernal.php

    protected $routeMiddleware = [
        'auth' => 'Todo\Http\Middleware\Authenticate',
        'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
        'guest' => 'Todo\Http\Middleware\RedirectIfAuthenticated',
        'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken',
        'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken',
    ];

However the default responses from jwt.auth are not handled by the AngularJS interceptor well. I plan on using my own middleware with the responses set out in a format which works with the Todo app, but any other ideas on approaching this would be gratefully received.

Many thanks

Tim

giorgioprovenzale commented 8 years ago

Hi Tim, I have the same problem because I would like to create some restricted routes. Does your method works or not? Can you share here your solution, please?

Thanks a lot

spirant commented 8 years ago

Hi Giorgio

I haven't worked on this for a while, but I believe I got a prototype working. The logging off may need some work (I was not sure exactly how this works server side to remove the token). Hopefully I haven't forgotten any changes I made, but please let me know any ideas / thoughts.

On Angular:

I altered the $httpProvider.intercepters...

$httpProvider.interceptors.push(['$rootScope', '$q', '$localStorage',
      function ($rootScope, $q, $localStorage) {
        return {
          request: function (config) {
            config.params = config.params || {};
            config.headers = config.headers || {};
            if ($localStorage.token) {
              config.headers.Authorization = 'Bearer ' + $localStorage.token;
              config.params.token = $localStorage.token; 
            }
            return config;
          },
          response: function (res) {
            return res || $q.when(res);
          },
          'responseError': function(response) {
              if(response.status === 401 || response.status === 400) {
                //console.log("Not logged in");
                // Handle unauthenticated user
                $rootScope.$broadcast('unauthorized');
                //$location.path('auth/login');
              }
              return $q.reject(response);
          }
        };
      }
    ]);

MainController.js added a function to redirect to login form when API returns unauthorized (this may be better handled by hiding the current content and showing a login form rather than redirecting?). You must add $rootScope to the dependencies for the module.

    $rootScope.$on('unauthorized', function() {
      // Logged out on server so logout client side too (if not already)
      $scope.logout();
      // Load Login Form
      $location.path('auth/login');
    });

On Laravel

Add the new middleware file in App > Http > Middleware > SpirantJwtGetUserFromToken.php

<?php

namespace Todo\Http\Middleware;

use Closure;
use Illuminate\Http\Response;
use Tymon\JWTAuth\JWTAuth;

//use Tymon\jwtAuthAuth\Exceptions\jwtAuthException;
//use Tymon\jwtAuthAuth\Exceptions\TokenExpiredException;

class SpirantJwtGetUserFromToken {

    protected $jwtAuth;

    public function __construct(JWTAuth $jwtAuth) {
        $this->jwtAuth = $jwtAuth;
    }
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, \Closure $next) {

        if (!$token = $request->input('token')) {
            //if (!$token = $this->jwtAuth->setRequest($request)->getToken()) {
            $status = Response::HTTP_BAD_REQUEST;
            return response()->json(['error' => 'Token Not provided', 'status' => $status], $status);
        }

        try {
            $user = $this->jwtAuth->authenticate($token);
        } catch (TokenExpiredException $e) {
            $status = Response::HTTP_BAD_REQUEST;
            return response()->json(['error' => 'Token Expired', 'status' => $status], $status);
            //return $this->respond('tymon.jwtAuth.expired', 'token_expired', $e->getStatusCode(), [$e]);
        } catch (jwtAuthException $e) {
            $status = Response::HTTP_BAD_REQUEST;
            return response()->json(['error' => 'Token Invalid', 'status' => $status], $status);
            //return $this->respond('tymon.jwtAuth.invalid', 'token_invalid', $e->getStatusCode(), [$e]);
        }

        if (!$user) {
            $status = Response::HTTP_NOT_FOUND;
            return response()->json(['error' => 'Token Invalid', 'status' => $status], $status);
            //return $this->respond('tymon.jwtAuth.user_not_found', 'user_not_found', 404);
        }

        //$this->events->fire('tymon.jwtAuth.valid', $user);

        return $next($request);
    }
}

Add the new route filter:

Route::group(array('prefix' => 'api/', 'middleware' => 'spirant.jwt.auth'), function () {
    Route::resource('todo', 'TodoController');
    //Route::resource('users', 'UserController');
});

Add to app > Http > Kernal.php into the $routeMiddleware: '

'spirant.jwt.auth' => 'Todo\Http\Middleware\SpirantJwtGetUserFromToken',

I will hopefully be looking at this again in a month or two so will update this with any improvements.

jonaths commented 8 years ago

Newbie question... how did you add $rootScope to the dependencies for the module. Help me please?

angular.module('MainController', []).controller('MainController', ['$scope', '$location', '$localStorage', 'User', function ($scope, $location, $localStorage, User) { /* * Responsible for highlighting the currently active menu item in the navbar. * @param route * @returns {boolean} */ $scope.isActive = function (route) { return route === $location.path(); };

/**
 * Query the authenticated user by the Authorization token from the header.
 *
 * @param user {object} If provided, it won't query from database, but take this one.
 * @returns {null}
 */
$scope.getAuthenticatedUser = function (user) {
  if (user) {
    $scope.authenticatedUser = user;
    return;
  }

  if (typeof $localStorage.token === 'undefined') {
    return null;
  }

  new User().$getByToken(function (user) {
    $scope.authenticatedUser = user;
  }, function (err) {
    console.log(err);
  });
};

$scope.logout = function () {
  delete $localStorage.token;
  $scope.authenticatedUser = null;
  $location.path('/auth/login');
};

$rootScope.$on('unauthorized', function() {
  // Logged out on server so logout client side too (if not already)
  $scope.logout();
  // Load Login Form
  $location.path('auth/login');
});

} ]);

jonaths commented 8 years ago

Never mind... Thanks anyway...

angular.module('MainController', []).controller('MainController', ['$scope', '$rootScope','$location', '$localStorage', 'User', function ($scope, $rootScope, $location, $localStorage, User) { /* * Responsible for highlighting the currently active menu item in the navbar. * @param route * @returns {boolean} */ $scope.isActive = function (route) { return route === $location.path(); };

/**
 * Query the authenticated user by the Authorization token from the header.
 *
 * @param user {object} If provided, it won't query from database, but take this one.
 * @returns {null}
 */
$scope.getAuthenticatedUser = function (user) {
  if (user) {
    $scope.authenticatedUser = user;
    return;
  }

  if (typeof $localStorage.token === 'undefined') {
    return null;
  }

  new User().$getByToken(function (user) {
    $scope.authenticatedUser = user;
  }, function (err) {
    console.log(err);
  });
};

$scope.logout = function () {
  delete $localStorage.token;
  $scope.authenticatedUser = null;
  $location.path('/auth/login');
};

$rootScope.$on('unauthorized', function() {
  // Logged out on server so logout client side too (if not already)
  $scope.logout();
  // Load Login Form
  $location.path('auth/login');
});

} ]);