Skip to content

Commit 074ac01

Browse files
authored
Merge pull request #3 from felixheck/release/0.2.0
Release/0.2.0
2 parents f19f75c + da62b13 commit 074ac01

File tree

9 files changed

+338
-76
lines changed

9 files changed

+338
-76
lines changed

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ server.register({
6565
clientId: 'foobar',
6666
secret: '1234-bar-4321-foo'
6767
},
68-
cache: {}
68+
cache: {},
69+
userInfo: ['name', 'email']
6970
}
7071
}, function(err) {
7172
if (err) {
@@ -112,7 +113,10 @@ Required.
112113

113114
- `cache {Object|false}`: The configuration of the [hapi.js cache](https://hapijs.com/api#servercacheoptions) powered by [catbox][catbox].<br/>
114115
If `false` the cache is disabled. Use an empty object to use the built-in default cache.<br/>
115-
Optional. Default: `false`.<br/>
116+
Optional. Default: `false`.
117+
118+
- `userInfo {Array.<?string>}`: List of properties which should be included in the `request.auth.credentials` object besides `scope` and `sub`.<br/>
119+
Optional. Default: `[]`.<br/>
116120

117121
#### `server.kjwt.validate(field {string}, done {Function})`
118122
Uses internally [`GrantManager.prototype.validateAccessToken()`][keycloak-auth-utils-gm-validate].
@@ -166,7 +170,8 @@ server.register({
166170
clientId: 'foobar',
167171
secret: '1234-bar-4321-foo'
168172
},
169-
cache: {}
173+
cache: {},
174+
userInfo: ['name', 'email']
170175
}
171176
}).then(() => {
172177
server.auth.strategy('keycloak-jwt', 'keycloak-jwt');

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"json web token",
1515
"plugin"
1616
],
17-
"version": "0.1.1",
17+
"version": "0.2.0",
1818
"license": "MIT",
1919
"author": {
2020
"name": "Felix Heck",

src/index.js

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,15 @@ const token = require('./token')
44
const { error, fakeReply, verify } = require('./utils')
55
const pkg = require('../package.json')
66

7-
let manager
8-
97
/**
10-
* @function
11-
* @public
12-
*
13-
* Get user information based on token with help of Keycloak.
14-
* If all validations and requests are successful, save the
15-
* token and its user data in memory cache.
8+
* @type Object
9+
* @private
1610
*
17-
* @param {string} token The token to be validated
18-
* @param {Function} reply The callback handler
11+
* Internally used properties
1912
*/
20-
function handleKeycloakUserInfo (tkn, reply) {
21-
manager.userInfo(tkn.get()).then((userInfo) => {
22-
const { scope, expiresIn } = tkn.getData()
23-
const userData = { credentials: Object.assign({ scope }, userInfo) }
24-
25-
cache.set(tkn.get(), userData, expiresIn)
26-
reply.continue(userData)
27-
}).catch((err) => {
28-
reply(error('unauthorized', err))
29-
})
13+
const internals = {
14+
manager: undefined,
15+
userInfoFields: undefined
3016
}
3117

3218
/**
@@ -41,8 +27,16 @@ function handleKeycloakUserInfo (tkn, reply) {
4127
function handleKeycloakValidation (tkn, reply) {
4228
const invalidate = (err) => reply(error('unauthorized', err, error.msg.invalid))
4329

44-
manager.validateAccessToken(tkn.get()).then((res) => {
45-
res ? handleKeycloakUserInfo(tkn, reply) : invalidate()
30+
internals.manager.validateAccessToken(tkn.get()).then((res) => {
31+
if (!res) {
32+
return invalidate()
33+
}
34+
35+
const { expiresIn, credentials } = tkn.getData(internals.userInfoFields)
36+
const userData = { credentials }
37+
38+
cache.set(tkn.get(), userData, expiresIn)
39+
return reply.continue(userData)
4640
}).catch(invalidate)
4741
}
4842

@@ -108,9 +102,11 @@ function strategy (server) {
108102
*/
109103
function plugin (server, opts, next) {
110104
opts = verify(opts)
111-
manager = new GrantManager(opts.client)
112105
cache.init(server, opts.cache)
113106

107+
internals.manager = new GrantManager(opts.client)
108+
internals.userInfoFields = opts.userInfo
109+
114110
server.auth.scheme('keycloak-jwt', strategy)
115111
server.decorate('server', 'kjwt', { validate })
116112

src/token.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ function token (field) {
6565
return (exp - iat) * 1000
6666
}
6767

68+
/**
69+
* @function
70+
* @private
71+
*
72+
* Get necessary user information out of token content.
73+
*
74+
* @param {Object} content The token its content
75+
* @param {Array.<?string>} [fields] The necessary fields
76+
* @returns {Object} The collection of requested user info
77+
*/
78+
function getUserInfo (content, fields = []) {
79+
return _.pick(content, ['sub', ...fields])
80+
}
81+
6882
/**
6983
* @function
7084
* @public
@@ -88,12 +102,14 @@ function token (field) {
88102
*
89103
* @returns {Object} The extracted data
90104
*/
91-
function getData () {
105+
function getData (userInfoFields) {
92106
const content = getContent()
93107

94108
return {
95-
scope: getScope(content),
96-
expiresIn: getExpiration(content)
109+
expiresIn: getExpiration(content),
110+
credentials: Object.assign({
111+
scope: getScope(content)
112+
}, getUserInfo(content, userInfoFields))
97113
}
98114
}
99115

src/utils.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const scheme = joi.object({
1414
}).unknown(true).required(),
1515
cache: joi.alternatives().try(joi.object({
1616
segment: joi.string().default('keycloakJwt')
17-
}), joi.boolean().invalid(true)).default(false)
17+
}), joi.boolean().invalid(true)).default(false),
18+
userInfo: joi.array().items(joi.string().min(1))
1819
}).unknown(true).required()
1920

2021
/**

test/_fixtures.js

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,51 @@ const config = {
1414
secret: 'barfoo'
1515
}
1616

17+
const content = {
18+
userData: {
19+
'exp': 5,
20+
'iat': 1,
21+
'sub': '1234567890',
22+
'name': 'John Doe',
23+
'email': '[email protected]',
24+
'admin': true,
25+
'realm_access': {
26+
'roles': [
27+
'admin'
28+
]
29+
},
30+
'resource_access': {
31+
'account': {
32+
'roles': [
33+
'manage-account',
34+
'manage-account-links',
35+
'view-profile'
36+
]
37+
},
38+
'same': {
39+
'roles': [
40+
'editor'
41+
]
42+
},
43+
'other-app': {
44+
'roles': [
45+
'other-app:creator'
46+
]
47+
}
48+
}
49+
}
50+
}
51+
1752
/**
1853
* @type Object
1954
* @public
2055
*
2156
* Various JSON Web Tokens
2257
*/
2358
const jwt = {
24-
content: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ',
25-
userData: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjUsImlhdCI6MSwic3ViIjoiMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiYWRtaW4iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX0sInNhbWUiOnsicm9sZXMiOlsiZWRpdG9yIl19LCJvdGhlci1hcHAiOnsicm9sZXMiOlsib3RoZXItYXBwOmNyZWF0b3IiXX19fQ._yxUAslOcgCp2Fd2xyO0q3iB24brG8PqqXQ-TCblQ1w',
26-
userDataExp: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJhZG1pbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfSwic2FtZSI6eyJyb2xlcyI6WyJlZGl0b3IiXX0sIm90aGVyLWFwcCI6eyJyb2xlcyI6WyJvdGhlci1hcHA6Y3JlYXRvciJdfX19.Q49BbBtcemvPaDfXyroyuoR56_rbq_pADXeC0ABXyZc'
59+
userData: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjUsImlhdCI6MSwic3ViIjoiMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImVtYWlsIjoiam9obi5kb2VAbWFpbC5jb20iLCJhZG1pbiI6dHJ1ZSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImFkbWluIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19LCJzYW1lIjp7InJvbGVzIjpbImVkaXRvciJdfSwib3RoZXItYXBwIjp7InJvbGVzIjpbIm90aGVyLWFwcDpjcmVhdG9yIl19fX0.uuhtpYNVtFZvPuRAEktWEDn_2u-dvimWnspXVt-gObU',
60+
userDataExp: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huLmRvZUBtYWlsLmNvbSIsImFkbWluIjp0cnVlLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiYWRtaW4iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX0sInNhbWUiOnsicm9sZXMiOlsiZWRpdG9yIl19LCJvdGhlci1hcHAiOnsicm9sZXMiOlsib3RoZXItYXBwOmNyZWF0b3IiXX19fQ.BcTtSEpyiUVBVkUOwVDM0_T9UIy-vk2aaUAR8XM6Hd0',
61+
userDataScope: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjUsImlhdCI6MSwic3ViIjoiMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImVtYWlsIjoiam9obi5kb2VAbWFpbC5jb20iLCJhZG1pbiI6dHJ1ZX0.2tfThhgwSbIEq2cZcoHSRwL2-UCanF23BXlyphm5ehs'
2762
}
2863

2964
/**
@@ -92,6 +127,7 @@ module.exports = {
92127
realmUrl,
93128
clientId,
94129
config,
130+
content,
95131
jwt,
96132
validation,
97133
userInfo

test/index.spec.js

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@ test.cb.serial('throw error if plugin gets registered twice', (t) => {
2121

2222
test.cb.serial('authentication does succeed', (t) => {
2323
prototypes.stub('validateAccessToken', fixtures.validation)
24-
prototypes.stub('userInfo', fixtures.userInfo)
2524

2625
getServer(undefined, (server) => {
2726
server.inject({
2827
method: 'GET',
2928
url: '/',
3029
headers: {
31-
authorization: `bearer ${fixtures.jwt.content}`
30+
authorization: `bearer ${fixtures.jwt.userData}`
3231
}
3332
}, (res) => {
3433
t.truthy(res)
@@ -40,13 +39,12 @@ test.cb.serial('authentication does succeed', (t) => {
4039

4140
test.cb.serial('authentication does succeed – cached', (t) => {
4241
prototypes.stub('validateAccessToken', fixtures.validation)
43-
prototypes.stub('userInfo', fixtures.userInfo)
4442

4543
const mockReq = {
4644
method: 'GET',
4745
url: '/',
4846
headers: {
49-
authorization: `bearer ${fixtures.jwt.content}`
47+
authorization: `bearer ${fixtures.jwt.userData}`
5048
}
5149
}
5250

@@ -66,7 +64,6 @@ test.cb.serial('authentication does succeed – cached', (t) => {
6664

6765
test.cb.serial('authentication does success – valid roles', (t) => {
6866
prototypes.stub('validateAccessToken', fixtures.validation)
69-
prototypes.stub('userInfo', fixtures.userInfo)
7067

7168
getServer(undefined, (server) => {
7269
server.inject({
@@ -85,7 +82,6 @@ test.cb.serial('authentication does success – valid roles', (t) => {
8582

8683
test.cb.serial('authentication does fail – invalid roles', (t) => {
8784
prototypes.stub('validateAccessToken', fixtures.validation)
88-
prototypes.stub('userInfo', fixtures.userInfo)
8985

9086
getServer(undefined, (server) => {
9187
server.inject({
@@ -110,7 +106,7 @@ test.cb.serial('authentication does fail – invalid token', (t) => {
110106
method: 'GET',
111107
url: '/',
112108
headers: {
113-
authorization: `bearer ${fixtures.jwt.content}`
109+
authorization: `bearer ${fixtures.jwt.userData}`
114110
}
115111
}, (res) => {
116112
t.truthy(res)
@@ -140,10 +136,9 @@ test.cb.serial('authentication does fail – invalid header', (t) => {
140136

141137
test.cb.serial('server method validates token', (t) => {
142138
prototypes.stub('validateAccessToken', fixtures.validation)
143-
prototypes.stub('userInfo', fixtures.userInfo)
144139

145140
getServer(undefined, (server) => {
146-
server.kjwt.validate(`bearer ${fixtures.jwt.content}`, (err, res) => {
141+
server.kjwt.validate(`bearer ${fixtures.jwt.userData}`, (err, res) => {
147142
t.falsy(err)
148143
t.truthy(res)
149144
t.truthy(res.credentials)
@@ -152,27 +147,11 @@ test.cb.serial('server method validates token', (t) => {
152147
})
153148
})
154149

155-
test.cb.serial('server method invalidates token – userinfo error', (t) => {
156-
prototypes.stub('validateAccessToken', fixtures.validation)
157-
prototypes.stub('userInfo', new Error('an error'), 'reject')
158-
159-
getServer(undefined, (server) => {
160-
server.kjwt.validate(`bearer ${fixtures.jwt.content}`, (err, res) => {
161-
t.falsy(res)
162-
t.truthy(err)
163-
t.truthy(err.isBoom)
164-
t.is(err.output.statusCode, 401)
165-
t.is(err.output.headers['WWW-Authenticate'], 'Bearer error="Error: an error"')
166-
t.end()
167-
})
168-
})
169-
})
170-
171150
test.cb.serial('server method invalidates token – validation error', (t) => {
172151
prototypes.stub('validateAccessToken', new Error('an error'), 'reject')
173152

174153
getServer(undefined, (server) => {
175-
server.kjwt.validate(`bearer ${fixtures.jwt.content}`, (err, res) => {
154+
server.kjwt.validate(`bearer ${fixtures.jwt.userData}`, (err, res) => {
176155
t.falsy(res)
177156
t.truthy(err)
178157
t.truthy(err.isBoom)
@@ -187,7 +166,7 @@ test.cb.serial('server method invalidates token – invalid', (t) => {
187166
prototypes.stub('validateAccessToken', false)
188167

189168
getServer(undefined, (server) => {
190-
server.kjwt.validate(`bearer ${fixtures.jwt.content}`, (err, res) => {
169+
server.kjwt.validate(`bearer ${fixtures.jwt.userData}`, (err, res) => {
191170
t.falsy(res)
192171
t.truthy(err)
193172
t.truthy(err.isBoom)
@@ -200,7 +179,7 @@ test.cb.serial('server method invalidates token – invalid', (t) => {
200179

201180
test.cb.serial('server method invalidates token – wrong format', (t) => {
202181
getServer(undefined, (server) => {
203-
server.kjwt.validate(fixtures.jwt.content, (err, res) => {
182+
server.kjwt.validate(fixtures.jwt.userData, (err, res) => {
204183
t.falsy(res)
205184
t.truthy(err)
206185
t.truthy(err.isBoom)

test/token.spec.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,10 @@ test('get no bearer token – spaces between', (t) => {
5050
})
5151

5252
test('get decoded content part of token', (t) => {
53-
const jwt = `bearer ${fixtures.jwt.content}`
53+
const jwt = `bearer ${fixtures.jwt.userData}`
5454
const tkn = token(jwt)
5555

56-
t.deepEqual(tkn.getContent(), {
57-
'sub': '1234567890',
58-
'name': 'John Doe',
59-
'admin': true
60-
})
56+
t.deepEqual(tkn.getContent(), fixtures.content.userData)
6157
})
6258

6359
test('get user data of token', (t) => {
@@ -67,7 +63,21 @@ test('get user data of token', (t) => {
6763

6864
t.truthy(data)
6965
t.is(data.expiresIn, 4000)
70-
t.deepEqual(data.scope.sort(), ['editor', 'other-app:creator', 'realm:admin'])
66+
t.is(data.credentials.sub, fixtures.content.userData.sub)
67+
t.falsy(data.credentials.name)
68+
t.deepEqual(data.credentials.scope.sort(), ['editor', 'other-app:creator', 'realm:admin'])
69+
})
70+
71+
test('get user data of token – additional fields', (t) => {
72+
const jwt = `bearer ${fixtures.jwt.userData}`
73+
const tkn = token(jwt)
74+
const data = tkn.getData(['name'])
75+
76+
t.truthy(data)
77+
t.is(data.expiresIn, 4000)
78+
t.is(data.credentials.sub, fixtures.content.userData.sub)
79+
t.is(data.credentials.name, fixtures.content.userData.name)
80+
t.deepEqual(data.credentials.scope.sort(), ['editor', 'other-app:creator', 'realm:admin'])
7181
})
7282

7383
test('get user data of token – default expiration', (t) => {
@@ -77,5 +87,19 @@ test('get user data of token – default expiration', (t) => {
7787

7888
t.truthy(data)
7989
t.is(data.expiresIn, 60000)
80-
t.deepEqual(data.scope.sort(), ['editor', 'other-app:creator', 'realm:admin'])
90+
t.is(data.credentials.sub, fixtures.content.userData.sub)
91+
t.falsy(data.credentials.name)
92+
t.deepEqual(data.credentials.scope.sort(), ['editor', 'other-app:creator', 'realm:admin'])
93+
})
94+
95+
test('get user data of token – default scopes', (t) => {
96+
const jwt = `bearer ${fixtures.jwt.userDataScope}`
97+
const tkn = token(jwt)
98+
const data = tkn.getData()
99+
100+
t.truthy(data)
101+
t.is(data.expiresIn, 4000)
102+
t.is(data.credentials.sub, fixtures.content.userData.sub)
103+
t.falsy(data.credentials.name)
104+
t.deepEqual(data.credentials.scope, [])
81105
})

0 commit comments

Comments
 (0)