-
Notifications
You must be signed in to change notification settings - Fork 538
Authentication
How do we authorise users' browsers to create annotations on a Consumer's behalf? There are three entities involved:
- The Service Provider (e.g. http://annotateit.org/ or something similar) - where annotations are stored
- The Consumer (e.g. OpenShakespeare) - the website that authenticates users, and frequently provides documents for the User (see below) to annotate
- The User (and the User Agent) - the person/browser doing the annotations
Initially, the Consumer must register with the Service Provider: "I would like you to store my users' annotations on my behalf".
The Consumer is provided with a Account Key and an Account Secret and then provides to the user agent:
- A set of headers
- An auth-token consisting of set of headers 'signed' with Account Secret (via SHA256 hash)
The following are the current required set of headers (which should be prefixed with 'x-annotator-':
- auth-token
- account-id
- user-id
We also optionally support a TTL header:
- auth-token-valid-until (iso8601 formatted)
The auth-token is computed as:
- token = sha256(account-key + user-id + auth-token-valid-until).hexdigest()
- Annotations are stored on the annotation store Service Provider (SP) -- e.g. annotations.okfn.org
- Texts to be annotated, and configuration of the annotator, is provided by the Consumer (C) -- e.g. openshakespeare.org
- User (U) visits client site using a User Agent (UA)
To aid comprehension, parallels are drawn with OAuth standard terminology. However, for clarity, it should be noted that communication is not needed between SP and C at the time U comes into contact with C's website. Issue of a Consumer Key (accountId) has already occurred by this point.
Unless otherwise specified, the meanings of the following are:
-
accountId = Key for Consumer as issued by SP
-
accountSecret = Secret for Consumer as issued by SP
- NB: accountSecret must never be sent over the wire after initial issue to the Consumer. It must never be revealed by the Consumer to any User.
-
authTokenTTL = Authentication token lifetime (in seconds) as agreed by C and SP
-
authTokenValidUntil = ISO8601-formatted time at which authToken was issued
-
authToken = SHA256(accountSecret + userId + authTokenValidUntil)
- NB: + indicates string concatenation
- NB: authToken must be computed on the Consumer's servers, not in the UA.
-
annotation = JSON-serialized representation of an annotation
-
userId = unique user identifier as defined by the Consumer
NB: this does not need to be globally unique, but must be locally unique to each of the consumer's users. If users share their userId, they will be able to forge each other's requests to the SP.
-
U opens C's site.
-
U logs in to site, and is authenticated to C as userId.
-
UA sends a GET request to C to retrieve an authentication token and associated information. If U is logged in as userId, C responds with a JSON dictionary, the "authentication envelope", containing the following keys:
{ accountId , authToken , authTokenValidUntil , userId }
- If U is not logged in, should respond with an HTTP 401 status code.
- NB: Strictly, accountId, and userId need not be sent in every such response, but the protocol is simpler if we ignore this.
-
C website javascript saves AuthToken object in page scope. Before every annotation request, it should call isValid() on the token. This should, at the very least, ensure that:
time.now < authTokenValidUntil
-
If the authToken is valid, then the annotation request can be made, including all the key-value pairs in the authentication envelope in the request. If not, we should return to step 3 and retrieve a new authentication token before attempting the request.
-
The SP receives the request. It retrieves the accountSecret corresponding to the provided accountId from its database. It computes SHA256(getConsumerSecretForConsumer(request.accountId) + request.userId + request.authTokenValidUntil). If the result is not identical to authToken, the request can be discarded at this point.
-
If this condition is fulfilled, the request is both authentic and valid, and should be dealt with appropriately.
-
If step 6 fails, the request should return an HTTP 401 status code.
Here is how we configure for standard situation where service provider and consumer are different.
- account_id: account of consumer on service provider (e.g. annotateit.org)
- userid: user id of user on the consumer
jQuery(function ($) { var elem = $('#text-to-annotate'); var account_id = '39fc339cf058bd22176771b3e3036609'; var userid = 'user-id-on-your-site'; // user name if different from userid (could be ip address if no real user) var username = 'user-name'; var options = {}; options.permissions = {}; options.permissions.user = { // you may wish 'name': user }; if(userid) { elem.data('annotator:headers', {'x-annotator-user-id': userid}); // set user options.permissions.user.id = userid; // configure permissions (only owner can edit, everyone can read) options.permissions.permissions = { 'read': [], 'update': [userid], 'delete': [userid], 'admin': [userid] } } var annotator = elem.annotator().data('annotator'); annotator.addPlugin('Store', { prefix: annotator_store, annotationData: { 'uri': 'http://openshakespeare.org/work/hamlet', // set account id 'account_id': account_id }, loadFromSearch: { 'uri': 'http://openshakespeare.org/work/hamlet', limit: -1 } }); // when loading annotations from backend determine how to get the user id options.permissions.userId = function(user) { if (user) { return user.id; } }; // ditto for human readable string for user options.permissions.userString = function(user) { if (user) { return user.name; } }; annotator.addPlugin('Permissions', options.permissions); })
Copied from original at: