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"
]