mardi 5 mai 2015

What is best practise for user authentication across subdomains between an AngularJS UI and a Lithium API?

I have setup a lithium REST API with an AngularJS UI as two separate projects. For the setup of the UI I originally followed this tutorial before going down a slightly different approach. The UI has a session service that effectively syncs session data between itself and a PHP session which is started on the first request to the API. I have been building the apps and testing them locally on a vagrant box and they have been working fine with one and other but when it came to pushing them to the staging environment each call to the API would create a new session key and would not authenticate further than the single login request.

I have read some articles on cross domain security issues but from the sounds of it CORS is secure and may be what I'm looking for. I have had a little play setting the headers on my apache virtual host config in the staging environment to try to make the sessions persist with the UI ajax calls but without any luck.

So I guess my question is: is there a way to make this type of setup work across domains api.domain.com and ui.domain.com and is it secure?

The API currently only outs the standard 401 authentication status code and the UI uses an auth interceptor service on the $httpProvider to detect the status. The interceptor then calls $rootScope.$broadcast AUTH_EVENTS.unauthorized to broadcast that the app has lost authentication with the API and calls $state.go "login" to prompt the user to login.

I plan to add the 403 forbidden status down the line once I have implemented some form of ACL.

Lithium session bootstrap

use lithium\storage\Session;

$name = basename(LITHIUM_APP_PATH);
Session::config([
    'default' => ['adapter' => 'Php', 'session.name' => $name]
]);

use lithium\security\Auth;

Auth::config([
    'default' => [
        'adapter' => 'Form',
        'model' => 'Users',
        'fields' => ['email', 'password']
    ]
]);

use lithium\action\Dispatcher;
use lithium\action\Response;

Dispatcher::applyFilter('_callable', function($self, $params, $chain) {
    $controller = $chain->next($self, $params, $chain);
    $request = isset($params['request']) ? $params['request'] : null;
    $action = $params['params']['action'];
    $publicActions = isset($controller->publicActions) ? (array) $controller->publicActions : [];

    if (!in_array($action, $publicActions) && !Auth::check('default', $request)) {
        return function() use ($request) {
            return new Response(['status' => 401]);
        };
    }

    return $controller;
});

Bulk of the AngularJS

app = angular.module "App", ["ui.router"]

app.constant "AUTH_EVENTS",
    authenticated: "auth-authenticated"
    unauthorized: "auth-unauthorized"
    forbidden: "auth-forbidden"

app.config [
    "$httpProvider"
    "$stateProvider"
    "$urlRouterProvider"
    ($httpProvider, $stateProvider, $urlRouterProvider) ->

        $httpProvider.interceptors.push [
            "$injector",
            ($injector) ->
                $injector.get "AuthInterceptorFactory"
        ]

        $urlRouterProvider
        .otherwise "/home"

        $stateProvider
        .state "login",
            controller: "AuthController"
            controllerAs: "Auth"
            url: "/login"
            templateUrl: "./partials/login.html"
        .state "home",
            controller: "HomeController"
            controllerAs: "Home"
            url: "/home"
            templateUrl: "./partials/home.html"

]

app.run [
    "$rootScope"
    "$state"
    "AUTH_EVENTS"
    "AuthFactory"
    ($rootScope, $state, AUTH_EVENTS, AuthFactory) ->

        $rootScope.$on AUTH_EVENTS.authenticated, ->
            $state.go "home"

        $rootScope.$on AUTH_EVENTS.unauthorized, ->
            $state.go "login"

        $rootScope.$on "$stateChangeStart", (event, toState) ->
            if toState.name isnt "login" and not do AuthFactory.check
                do event.preventDefault
                $rootScope.$broadcast AUTH_EVENTS.unauthorized

]

app.factory "AuthInterceptorFactory", [
    "$rootScope"
    "$q"
    "AUTH_EVENTS"
    ($rootScope, $q, AUTH_EVENTS) ->
        responseError: (response) ->
            statuses =
                401: AUTH_EVENTS.unauthorized
                403: AUTH_EVENTS.forbidden
            $rootScope.$broadcast statuses[response.status], response
            $q.reject response
]

app.factory "SessionFactory", [
    "$http"
    "$q"
    ($http, $q) ->

        get: ->
            deferred = do $q.defer
            $http
            .get "api/session.json"
            .success (data) ->
                deferred.resolve data
            .error ->
                deferred.reject "Unable to get session."
            deferred.promise

        create: (key, value) ->
            deferred = do $q.defer
            data =
                key: key
                value: value
            $http
            .post "api/session.json", data
            .success (data) ->
                if data.success is undefined
                    deferred.reject "Unable to create session."
                else
                    do deferred.resolve
            .error ->
                deferred.reject "Unable to create session."
            deferred.promise

        remove: (key) ->
            deferred = do $q.defer
            $http
            .delete "api/session.json?key=" + key
            .success (data) ->
                if data.success is undefined
                    deferred.reject "Unable to delete session."
                else
                    do deferred.resolve
            .error ->
                deferred.reject "Unable to delete session."
            deferred.promise

]

class SessionService

    key: null

    data: {}

    constructor: (@$q, @$rootScope, @AUTH_EVENTS, @SessionFactory) ->
        do @get

    get: ->
        deferred = do @$q.defer
        get = do @SessionFactory.get
        get.then(
            (data) =>
                @key = data.key
                @data = data.data
                if @data.user
                    @$rootScope.$broadcast @AUTH_EVENTS.authenticated
                do deferred.resolve
            (reason) =>
                deferred.reject reason
        )
        deferred.promise

    write: (key, value) ->
        deferred = do @$q.defer
        create = @SessionFactory.create key, value
        create.then(
            =>
                @data[key] = value
                do deferred.resolve
            (reason) =>
                deferred.reject reason
        )
        deferred.promise

    read: (key) ->
        if @data[key] is undefined then null else @data[key]

    remove: (key) ->
        deferred = do @$q.defer
        remove = @SessionFactory.remove key
        remove.then(
            =>
                delete @data[key]
                do deferred.resolve
            (reason) =>
                deferred.reject reason
        )
        deferred.promise

    destroy: ->
        @key = null
        @data = {}

app.service "SessionService", [
    "$q"
    "$rootScope"
    "AUTH_EVENTS"
    "SessionFactory"
    SessionService
]

app.factory "AuthFactory", [
    "$http"
    "$q"
    "$rootScope"
    "AUTH_EVENTS"
    "SessionService"
    ($http, $q, $rootScope, AUTH_EVENTS, SessionService) ->

        login: (credentials) ->
            deferred = do $q.defer
            $http
            .post "api/users/login.json", credentials
            .success (data) ->
                if data.user
                    writeSession = SessionService.write "user", data.user
                    writeSession.then ->
                        $rootScope.$broadcast AUTH_EVENTS.authenticated
                        do deferred.resolve
                else
                    deferred.reject "Unable to login."
            .error ->
                deferred.reject "Unable to login."
            deferred.promise

        logout: ->
            deferred = do $q.defer
            $http
            .get "api/users/logout.json"
            .success (data) ->
                if data.success
                    do SessionService.destroy
                    $rootScope.$broadcast AUTH_EVENTS.unauthorized
                    do deferred.resolve
                else
                    deferred.reject "Unable to logout."
            .error ->
                deferred.reject "Unable to logout."
            deferred.promise

        check: ->
            not not SessionService.read "user"

]

Aucun commentaire:

Enregistrer un commentaire