View
@@ -0,0 +1,139 @@
var JWT_BEARER = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
var _authPromise = null;
var _tokenPromise = null;
var _tokenResponse = null;
fetchToken.$inject = ['$http', 'jwtHelper', 'serviceUrl', 'session']
function fetchToken ( $http, jwtHelper, serviceUrl, session ) {
var tokenUrl = new URL('token', serviceUrl).href;
var token = (_tokenResponse === null) ? null : _tokenResponse;
if (token === null || jwtHelper.isTokenExpired(token)) {
if (_tokenPromise === null) {
// Set up the token request data.
var data = {
assertion: session.state.id_token,
grant_type: JWT_BEARER
};
// Skip JWT authorization for the token request itself.
var config = {
skipAuthorization: true
};
// Make the request.
var request = $http.post(tokenUrl, data, config);
// Extract and save the response data.
_tokenPromise = request.then(function (response) {
_tokenPromise = null;
_tokenResponse = response.data;
return _tokenResponse;
});
}
// Return a promise of the access token.
return _tokenPromise;
} else {
// The token is available and not expired.
return token;
}
}
tokenGetter.$inject = ['$injector', 'config', 'serviceUrl'];
function tokenGetter ( $injector, config, serviceUrl ) {
var requestUrl = config.url;
// Only send the token on requests to the annotation storage service
// and only if it is not the token request itself.
if (requestUrl !== serviceUrl) {
if (requestUrl.slice(0, serviceUrl.length) === serviceUrl) {
return _authPromise
.then(function () {
return $injector.invoke(fetchToken);
})
.catch(function () {
return null;
});
}
}
return null;
}
configureIdentity.$inject = ['identityProvider', 'jwtInterceptorProvider'];
function configureIdentity ( identityProvider, jwtInterceptorProvider ) {
identityProvider.checkAuthentication = [
'$injector', '$q', 'session', function($injector, $q, session) {
if (_authPromise === null) {
var deferred = $q.defer();
_authPromise = deferred.promise;
session.load().$promise
.then(function (data) {
if (data.userid) {
$q.when($injector.invoke(fetchToken))
.then(function (token) {
deferred.resolve(token);
});
} else {
deferred.reject('no session');
}
})
.catch(function () {
deferred.reject('request failure');
});
}
return _authPromise;
}
];
identityProvider.forgetAuthentication = [
'$q', 'flash', 'session', function($q, flash, session) {
return session.logout({}).$promise
.then(function() {
_authPromise = $q.reject('no session');
_tokenPromise = null;
_tokenResponse = null;
return null;
})
.catch(function(err) {
flash.error('Sign out failed!');
throw err;
});
}
];
identityProvider.requestAuthentication = [
'$injector', '$q', '$rootScope', function($injector, $q, $rootScope) {
return _authPromise.catch(function () {
var deferred = $q.defer();
_authPromise = deferred.promise;
$rootScope.$on('auth', function(event, err, data) {
if (err) {
deferred.reject(err);
} else {
$q.when($injector.invoke(fetchToken))
.then(function (token) {
deferred.resolve(token);
});
}
});
return _authPromise;
});
}
];
// Provide tokens from the token service to the JWT request interceptor.
jwtInterceptorProvider.tokenGetter = tokenGetter;
}
module.exports = configureIdentity;
View
@@ -0,0 +1,9 @@
var angular = require('angular');
var configureHttp = require('./http');
var configureIdentity = require('./identity');
module.exports = angular.module('h')
.config(configureHttp)
.config(configureIdentity)
;
View
@@ -42,11 +42,11 @@ errorMessage = (reason) ->
###
AnnotationController = [
'$scope', '$timeout', '$q', '$rootScope', '$document',
'auth', 'drafts', 'flash', 'permissions', 'tags', 'time',
'annotationUI', 'annotationMapper'
'drafts', 'flash', 'permissions', 'tags', 'time',
'annotationUI', 'annotationMapper', 'session'
($scope, $timeout, $q, $rootScope, $document,
auth, drafts, flash, permissions, tags, time,
annotationUI, annotationMapper) ->
drafts, flash, permissions, tags, time,
annotationUI, annotationMapper, session) ->
@annotation = {}
@action = 'view'
@@ -108,7 +108,12 @@ AnnotationController = [
###
this.authorize = (action) ->
return false unless model?
permissions.permits action, model, auth.user
# TODO: this should use auth instead of permissions but we might need
# an auth cache or the JWT -> userid decoding might start to be a
# performance bottleneck and we would need to get the id token into the
# session, which we should probably do anyway (and move to opaque bearer
# tokens for the access token).
return permissions.permits action, model, session.state.userid
###*
# @ngdoc method
@@ -233,7 +238,7 @@ AnnotationController = [
reply = annotationMapper.createAnnotation({references, uri})
if auth.user?
if session.state.userid
if permissions.isPublic model.permissions
reply.permissions = permissions.public()
else
@@ -321,7 +326,7 @@ AnnotationController = [
# Propagate an update event up the thread (to pulse changing threads),
# but only if this is someone else's annotation.
if model.user != auth.user
if model.user != session.state.userid
$scope.$emit('annotationUpdate')
# Save highlights once logged in.
@@ -338,6 +343,12 @@ AnnotationController = [
this.render()
, true
# Watch the current user
# TODO: fire events instead since watchers are not free and auth is rare
$scope.$watch (-> session.state.userid), (userid) ->
model.permissions ?= {}
model.user ?= userid
# Start editing brand new annotations immediately
unless model.id? or (this.isHighlight() and highlight) then this.edit()
View
@@ -12,12 +12,12 @@ describe 'annotation', ->
isolateScope = null
fakeAnnotationMapper = null
fakeAnnotationUI = null
fakeAuth = null
fakeDrafts = null
fakeFlash = null
fakeMomentFilter = null
fakePermissions = null
fakePersonaFilter = null
fakeSession = null
fakeStore = null
fakeTags = null
fakeTime = null
@@ -40,8 +40,6 @@ describe 'annotation', ->
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeAuth =
user: 'acct:bill@localhost'
fakeAnnotationMapper =
createAnnotation: sandbox.stub().returns
permissions:
@@ -66,6 +64,11 @@ describe 'annotation', ->
private: sandbox.stub().returns({read: ['justme']})
}
fakePersonaFilter = sandbox.stub().returnsArg(0)
fakeSession =
state:
userid: 'acct:bill@localhost'
fakeTags = {
filter: sandbox.stub().returns('a while ago'),
store: sandbox.stub()
@@ -78,12 +81,12 @@ describe 'annotation', ->
$provide.value 'annotationMapper', fakeAnnotationMapper
$provide.value 'annotationUI', fakeAnnotationUI
$provide.value 'auth', fakeAuth
$provide.value 'drafts', fakeDrafts
$provide.value 'flash', fakeFlash
$provide.value 'momentFilter', fakeMomentFilter
$provide.value 'permissions', fakePermissions
$provide.value 'personaFilter', fakePersonaFilter
$provide.value 'session', fakeSession
$provide.value 'store', fakeStore
$provide.value 'tags', fakeTags
$provide.value 'time', fakeTime
@@ -118,10 +121,11 @@ describe 'annotation', ->
it 'persists upon login', ->
delete annotation.id
delete annotation.user
fakeSession.state.userid = null
createDirective()
$scope.$digest()
assert.notCalled annotation.$create
annotation.user = 'acct:ted@wyldstallyns.com'
fakeSession.state.userid = 'acct:ted@wyldstallyns.com'
$scope.$digest()
assert.calledOnce annotation.$create
@@ -383,7 +387,7 @@ describe 'annotation', ->
assert.notCalled(isolateScope.$emit)
it "fires when another user's annotation is updated", ->
fakeAuth.user = 'acct:jane@localhost'
fakeSession.state.userid = 'acct:jane@localhost'
annotation.updated = '456'
$scope.$digest()
assert.calledWith(isolateScope.$emit, 'annotationUpdate')
@@ -548,19 +552,19 @@ describe("AnnotationController", ->
Return an annotation directive instance and stub services etc.
###
createAnnotationDirective = ({annotation, personaFilter, momentFilter,
urlencodeFilter, auth, drafts, flash,
permissions, tags, time, annotationUI,
urlencodeFilter, drafts, flash,
permissions, session, tags, time, annotationUI,
annotationMapper}) ->
locals = {
personaFilter: personaFilter or {}
momentFilter: momentFilter or {}
urlencodeFilter: urlencodeFilter or {}
auth: auth or {}
drafts: drafts or {
add: ->
}
flash: flash or {}
permissions: permissions or {}
session: session or {state: {}}
tags: tags or {}
time: time or {
toFuzzyString: ->
@@ -573,10 +577,10 @@ describe("AnnotationController", ->
$provide.value("personaFilter", locals.personaFilter)
$provide.value("momentFilter", locals.momentFilter)
$provide.value("urlencodeFilter", locals.urlencodeFilter)
$provide.value("auth", locals.auth)
$provide.value("drafts", locals.drafts)
$provide.value("flash", locals.flash)
$provide.value("permissions", locals.permissions)
$provide.value("session", locals.session)
$provide.value("tags", locals.tags)
$provide.value("time", locals.time)
$provide.value("annotationUI", locals.annotationUI)
View
@@ -6,7 +6,7 @@
# This service can set default permissions to annotations properly and
# offers some utility functions regarding those.
###
module.exports = ['auth', (auth) ->
module.exports = ['session', (session) ->
ALL_PERMISSIONS = {}
GROUP_WORLD = 'group:__world__'
ADMIN_PARTY = [{
@@ -37,10 +37,10 @@ module.exports = ['auth', (auth) ->
# Typical use: annotation.permissions = permissions.private()
###
private: ->
read: [auth.user]
update: [auth.user]
delete: [auth.user]
admin: [auth.user]
read: [session.state.userid]
update: [session.state.userid]
delete: [session.state.userid]
admin: [session.state.userid]
###*
# @ngdoc method
@@ -51,9 +51,9 @@ module.exports = ['auth', (auth) ->
###
public: ->
read: [GROUP_WORLD]
update: [auth.user]
delete: [auth.user]
admin: [auth.user]
update: [session.state.userid]
delete: [session.state.userid]
admin: [session.state.userid]
###*
# @ngdoc method
@@ -87,6 +87,7 @@ module.exports = ['auth', (auth) ->
# @param {String} user the userId
#
# User access-level-control function
# TODO: this should move to the auth service and take multiple principals
###
permits: (action, context, user) ->
acl = _acl context
View
@@ -0,0 +1,15 @@
serviceUrl.$inject = ['$document'];
function serviceUrl ( $document ) {
return $document
.find('link')
.filter(function () {
return (this.rel === 'service' &&
this.type === 'application/annotatorsvc+json');
})
.filter(function () {
return this.href;
})
.prop('href');
}
module.exports = serviceUrl;
View
@@ -41,7 +41,7 @@ describe 'AppController', ->
}
fakeAuth = {
user: undefined
userid: sandbox.stub()
}
fakeDrafts = {
@@ -112,6 +112,29 @@ describe 'AppController', ->
afterEach ->
sandbox.restore()
it 'watches the identity service for identity change events', ->
createController()
assert.calledOnce(fakeIdentity.watch)
it 'sets the user to null when the identity has been checked', ->
createController()
{onready} = fakeIdentity.watch.args[0][0]
onready()
assert.isNull($scope.auth.user)
it 'sets auth.user to the authorized user at login', ->
createController()
fakeAuth.userid.withArgs('test-assertion').returns('acct:hey@joe')
{onlogin} = fakeIdentity.watch.args[0][0]
onlogin('test-assertion')
assert.equal($scope.auth.user, 'acct:hey@joe')
it 'sets auth.user to null at logout', ->
createController()
{onlogout} = fakeIdentity.watch.args[0][0]
onlogout()
assert.strictEqual($scope.auth.user, null)
it 'does not show login form for logged in users', ->
createController()
assert.isFalse($scope.accountDialog.visible)
@@ -187,10 +210,10 @@ describe 'AppController', ->
fakeDrafts.contains.withArgs(annotation1).returns(true)
fakeDrafts.contains.withArgs(annotation2).returns(false)
fakeAuth.user = null
$scope.auth.user = null
$scope.$digest()
fakeAuth.user = 'acct:loki@example.com'
$scope.auth.user = 'acct:loki@example.com'
$scope.$digest()
assert.neverCalledWith($scope.$emit, 'annotationDeleted', annotation1)
View

This file was deleted.

Oops, something went wrong.
View
@@ -0,0 +1,58 @@
var inject = angular.mock.inject;
var module = angular.mock.module;
describe('h', function () {
var auth = null;
var fakeJwtHelper = null;
var sandbox = null;
before(function () {
angular.module('h', [])
.service('auth', require('../auth'));
});
beforeEach(function () {
module('h');
});
beforeEach(module(function ($provide) {
sandbox = sinon.sandbox.create();
fakeJwtHelper = {
decodeToken: sandbox.stub(),
isTokenExpired: sandbox.stub()
};
$provide.value('jwtHelper', fakeJwtHelper);
}));
beforeEach(inject(function (_auth_) {
auth = _auth_
}));
afterEach(function () {
sandbox.restore()
});
it('returns the subject of a valid jwt', function () {
var identity = 'fake-identity';
fakeJwtHelper.isTokenExpired.withArgs(identity).returns(false);
fakeJwtHelper.decodeToken.withArgs(identity).returns({sub: 'pandora'});
userid = auth.userid(identity);
assert.equal(userid, 'pandora');
});
it('returns null for an expired jwt', function () {
var identity = 'fake-identity';
fakeJwtHelper.isTokenExpired.withArgs(identity).returns(true);
userid = auth.userid(identity);
assert.isNull(userid);
});
it('returns null for an invalid jwt', function () {
var identity = 'fake-identity';
fakeJwtHelper.decodeToken.withArgs(identity).throws('Error');
userid = auth.userid(identity);
assert.isNull(userid);
});
});
View
@@ -2,7 +2,7 @@
describe 'h:permissions', ->
sandbox = null
fakeAuth = null
fakeSession = null
before ->
angular.module('h', [])
@@ -12,11 +12,13 @@ describe 'h:permissions', ->
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeAuth = {
user: 'acct:flash@gordon'
fakeSession = {
state: {
userid: 'acct:flash@gordon'
}
}
$provide.value 'auth', fakeAuth
$provide.value 'session', fakeSession
return
afterEach ->
View

This file was deleted.

Oops, something went wrong.
View
@@ -4,6 +4,7 @@
"version": "0.0.0",
"description": "The Internet, peer reviewed.",
"dependencies": {
"angular-jwt": "0.0.9",
"babelify": "^6.1.3",
"base-url": "^1.0.0",
"browserify": "^9.0.3",
@@ -64,7 +65,6 @@
},
"browser": {
"annotator": "./h/static/scripts/vendor/annotator.js",
"annotator-auth": "./h/static/scripts/vendor/annotator.auth.js",
"angular": "./h/static/scripts/vendor/angular.js",
"es6-promise": "./node_modules/es6-promise/dist/es6-promise.js",
"hammerjs": "./node_modules/hammerjs/hammer.js",
@@ -79,12 +79,6 @@
"jquery"
]
},
"annotator-auth": {
"depends": [
"jquery",
"annotator"
]
},
"angular": {
"exports": "global:angular",
"depends": [