Skip to content

Commit 224bf39

Browse files
committed
Initial Commit
0 parents  commit 224bf39

File tree

8 files changed

+319
-0
lines changed

8 files changed

+319
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.idea
2+
node_modules

.npmignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

LICENSE

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 Sim Kern Cheh
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+

README.md

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Express Header Token Authentication
2+
3+
This package parses Authorization headers from an Express request object for an authorization token.
4+
5+
## Installation
6+
7+
```
8+
npm install express-header-token-auth
9+
```
10+
11+
## Usage
12+
13+
Token validation can be done through a routing middleware or part of the routed action.
14+
15+
Incoming Payload can be simulated by:
16+
17+
`curl -IH "Authorization: Token token=test;param1=option1;param2=option2" http://myapp.com/protected/resource`
18+
19+
Accepted Authorization Header formats are:
20+
21+
22+
Authorization: Token token=test,param1=option1,param2=option2
23+
Authorization: Token token="test",param1="option1",param2="option2"
24+
//Any variation of commas(,), semicolons(;) or tabs( ) are accepted as delimiters
25+
26+
27+
### As routing middleware
28+
29+
30+
router.use('/protected/resource', function(req, res, next) {
31+
new TokenAuth(req, res).authenticateOrRequestWithHttpToken(function(err, token, options) {
32+
if (!err) {
33+
if (isValidToken(token, options))
34+
next();
35+
else {
36+
// send out invalid response
37+
}
38+
}
39+
// `else` case is not needed. If `err` is present, a HTTP response with status 401 will automatically be sent out.
40+
});
41+
});
42+
43+
44+
### As action
45+
46+
47+
router.use('/protected/resource', function(req, res, next) {
48+
new TokenAuth(req, res).authenticateOrRequestWithHttpToken(function(err, token, options) {
49+
if (!err) {
50+
if (isValidToken(token, options))
51+
// valid response
52+
else {
53+
// send out invalid response
54+
}
55+
}
56+
// `else` case is not needed. If `err` is present, a HTTP response with status 401 will automatically be sent out.
57+
});
58+
});
59+
60+
61+
## Methods
62+
63+
`authenticateOrRequestWithHttpToken([realm], callback)`
64+
65+
Used to grab token and options from callback. When token is not found, HTTP status 401 challenge is automatically sent.
66+
The `realm` parameter defaults to `Application`, which is used when challenging for token.
67+
68+
`authenticate(callback, [callbackOnError])`
69+
70+
Similar to `authenticateOrRequestWithHttpToken`, except the HTTP status 401 challenge will not be sent on failure to identify token. User is responsible for deciding the next course of action.
71+
The `callbackOnError` parameter defaults to `true` and should rarely be needed.
72+
73+
74+
`requestHttpTokenAuthentication(realm)`
75+
76+
Issues HTTP Status 401 challenge to current Response object.

lib/TokenAuth.js

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
var _ = require('underscore');
2+
3+
var TokenAuth = function(req, resp) {
4+
this.req = req;
5+
this.resp = resp;
6+
};
7+
8+
_.extend(TokenAuth.prototype, {
9+
TOKEN_KEY: 'token=',
10+
TOKEN_REGEX: /^Token /,
11+
AUTHN_PAIR_DELIMITERS: /\s*(?:,|;|\t+)\s*/,
12+
13+
/**
14+
* Parses token from Authorization header and returns token and options from callback.
15+
* Automatically responds with 401 requesting for Token Authentication.
16+
*
17+
* @param realm - Current authentication realm. Optional, defaults to 'Application'
18+
* @param callback - Can be passed in as first parameter. Parameters - (error, token, options)
19+
*/
20+
authenticateOrRequestWithHttpToken: function(realm, callback) {
21+
if (typeof(realm) === 'function'){
22+
callback = realm;
23+
realm = 'Application';
24+
}
25+
26+
if (!this.resp) {
27+
callback(new Error('Response object required to use `authenticateOrRequestWithHTTPToken`'));
28+
return;
29+
}
30+
return this.authenticate(callback, false) || this.requestHttpTokenAuthentication(realm) && callback(new Error('HTTP Basic: Access denied.'));
31+
},
32+
33+
/**
34+
* Sets 401 response to Response object.
35+
*
36+
* @param realm - Current authentication realm
37+
*/
38+
requestHttpTokenAuthentication: function(realm) {
39+
this.resp.set('WWW-Authenticate', 'Basic realm="' + realm + '"');
40+
this.resp.status(401);
41+
this.resp.send("HTTP Basic: Access denied.\n");
42+
return true;
43+
},
44+
45+
/**
46+
* Attempts to parse Token and options from Authorization header.
47+
*
48+
* @param callback - (error, token, options)
49+
* @param callbackOnError - Optional. Defaults to true. When false, errors will not trigger callback.
50+
* @returns true if token is found, false if otherwise
51+
*/
52+
authenticate: function(callback, callbackOnError) {
53+
if (callbackOnError === undefined)
54+
callbackOnError = true;
55+
56+
var tokenAndOpts = this.tokenAndOptions();
57+
if (tokenAndOpts && tokenAndOpts[0]) {
58+
callback(null, tokenAndOpts[0], tokenAndOpts[1]);
59+
return true;
60+
}
61+
62+
if (callbackOnError) {
63+
callback(new Error('Token not found'));
64+
}
65+
return false;
66+
},
67+
68+
tokenAndOptions: function() {
69+
var auth = this.req.headers.authorization;
70+
if (auth && auth.match(this.TOKEN_REGEX)) {
71+
var params = this.tokenParamsFrom(auth);
72+
return [params.shift(1)[1], _.object(params)];
73+
}
74+
},
75+
76+
tokenParamsFrom: function(auth) {
77+
return this.rewriteParamValues( this.paramsArrayFrom( this.rawParams(auth) ) );
78+
},
79+
80+
rawParams: function(authorizationHeader) {
81+
var rawParams = authorizationHeader.replace(this.TOKEN_REGEX, '').split(this.AUTHN_PAIR_DELIMITERS);
82+
if (!rawParams[0].match(new RegExp('^' + this.TOKEN_KEY))) {
83+
rawParams[0] = this.TOKEN_KEY + rawParams[0];
84+
}
85+
86+
return rawParams;
87+
},
88+
89+
paramsArrayFrom: function(rawParams) {
90+
return _.map(rawParams, function(param) {
91+
return param.split(/=(?=.+)/);
92+
});
93+
},
94+
95+
rewriteParamValues: function(arrayParams) {
96+
return _.map(arrayParams, function(param) {
97+
param[1] = (param[1] || '').replace(/^"|"$/g, '');
98+
return param;
99+
});
100+
}
101+
});
102+
103+
module.exports = TokenAuth;

package.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "express-header-token-auth",
3+
"version": "1.0.0",
4+
"description": "Parses token from Authorization Headers",
5+
"main": "lib/TokenAuth.js",
6+
"scripts": {
7+
"test": "./node_modules/jasmine/bin/jasmine.js"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "[email protected]:KernCheh/express-header-token-auth.git"
12+
},
13+
"keywords": [
14+
"authorization",
15+
"token",
16+
"express"
17+
],
18+
"author": "Sim Kern Cheh",
19+
"license": "MIT",
20+
"dependencies": {
21+
"jasmine": "^2.3.1",
22+
"underscore": "^1.8.3"
23+
}
24+
}

spec/lib/TokenAuthSpec.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
describe('TokenAuth', function() {
2+
var TokenAuth = require('../../lib/TokenAuth'),
3+
fakeRequest = {
4+
headers: {
5+
authorization: 'Token token="mytesttoken", extraparams="info"'
6+
}
7+
},
8+
9+
fakeResponse = {
10+
set: function(key, value) {},
11+
status: function(status) {},
12+
send: function(content) {}
13+
};
14+
15+
beforeEach(function() {
16+
this.token = new TokenAuth(fakeRequest, fakeResponse);
17+
});
18+
19+
describe('rawParams', function() {
20+
it('gathers params from token', function() {
21+
var result = this.token.rawParams('Token token=fake; test=something, param2=true');
22+
expect(result).toEqual([ 'token=fake', 'test=something', 'param2=true' ]);
23+
});
24+
});
25+
26+
describe('paramsArrayFrom', function() {
27+
it('takes raw params and turns into array of parameters', function() {
28+
var result = this.token.paramsArrayFrom(['token=fake', 'param2=true']);
29+
expect(result).toEqual([ [ 'token', 'fake' ], [ 'param2', 'true' ] ]);
30+
});
31+
});
32+
33+
describe('rewriteParamValues', function() {
34+
it('removes inverted commas from params', function() {
35+
var result = this.token.rewriteParamValues([ [ 'token', '"fake"' ], [ 'param2', '"true"' ] ]);
36+
expect(result).toEqual([ [ 'token', 'fake' ], [ 'param2', 'true' ] ]);
37+
});
38+
});
39+
40+
describe('tokenAndOptions', function() {
41+
it('returns token as first item and additional options as a hash', function() {
42+
var result = this.token.tokenAndOptions();
43+
expect(result).toEqual([ 'mytesttoken', { extraparams: 'info' } ]);
44+
});
45+
46+
it('returns undefined if header authorization is not present', function() {
47+
this.token.req.headers.authorization = undefined;
48+
expect(this.token.tokenAndOptions()).toEqual(undefined);
49+
this.token.req.headers.authorization = 'Token token="mytesttoken", extraparams="info"';
50+
})
51+
});
52+
53+
describe('authenticateOrRequestWithHttpToken', function() {
54+
it('defaults to Application realm and calls back if token is found', function(done) {
55+
this.token.authenticateOrRequestWithHttpToken(function(err, token, options) {
56+
expect(token).toEqual('mytesttoken');
57+
expect(options).toEqual({ extraparams: 'info' });
58+
done();
59+
});
60+
});
61+
62+
it('responds with 401 if token is not found', function(done) {
63+
spyOn(fakeResponse, 'set');
64+
spyOn(fakeResponse, 'status');
65+
spyOn(fakeResponse, 'send');
66+
67+
var newRequest = {
68+
headers: {
69+
authorization: undefined
70+
}
71+
},
72+
token = new TokenAuth(newRequest, fakeResponse);
73+
74+
token.authenticateOrRequestWithHttpToken(function(err, token, options) {
75+
expect(fakeResponse.set.calls.allArgs()).toEqual([ [ 'WWW-Authenticate', 'Basic realm="Application"' ] ]);
76+
expect(fakeResponse.status.calls.allArgs()).toEqual([ [401] ]);
77+
expect(fakeResponse.send.calls.allArgs()).toEqual([ ["HTTP Basic: Access denied.\n"] ]);
78+
done();
79+
});
80+
});
81+
});
82+
});

spec/support/jasmine.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"spec_dir": "spec",
3+
"spec_files": [
4+
"**/*[sS]pec.js"
5+
],
6+
"helpers": [
7+
"helpers/**/*.js"
8+
]
9+
}

0 commit comments

Comments
 (0)