-
Notifications
You must be signed in to change notification settings - Fork 3
/
authorize.js
161 lines (139 loc) · 5.78 KB
/
authorize.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
* Checks the `Authorization` header for a JWT and verifies
* that it is legitimate and valid before allowing the
* request to proceed.
*/
const errors = require('@feathersjs/errors')
const jwt = require('jsonwebtoken')
const rp = require('request-promise')
module.exports = ({
// These two parameters allow users to customize which models/services
// they want to use with this hook
userService = 'users',
keysService = 'keys',
// the actual hook
authorize = async (context) => {
// Throw if the hook is being called from an unexpected location.
if (context.type !== 'before')
throw new errors.NotAuthenticated('`authorize()` can only be used as a `before` hook.', context)
// get the Authorization header
const header = (context.params.headers || {}).authorization || null
// throw an error if the Authorization header is not set
if (!header) throw new errors.NotAuthenticated('`Authorization` header not set.', context)
// extract the raw token from the header
const currentToken = header.replace('Bearer ', '').trim()
// decode it
let token = jwt.decode(currentToken, { complete: true })
// throw an error if the token was malformed or missing
if (!token) throw new errors.NotAuthenticated('The token was malformed or missing.')
// get the user ID from the token payload
const user_id = token.payload.sub
// check to see if we have a member with this ID in the database
let member
try {
member = await context.app.service(userService).find({
paginate: false,
query: { user_id, $limit: 1 }
}).then(results => {
if (results[0]) return results[0]
throw 'member does not exist'
})
} catch (err) {
// throw an error if no such member exists
throw new errors.NotAuthenticated('No member with this ID exists.', context)
}
// if the member already has a valid, current token, stop here
if (member.currentToken && member.currentToken === currentToken) return context
// otherwise, get the kid from the token header
const kid = token.header.kid
// create a JWKS retrieval client
const client = getJWKS(context.app.get('jwksUri'))
// get the signing key from the JWKS endpoint at Auth0
const key = await getKey(kid, context.app.service(keysService), client)
// verify the raw JWT
try {
jwt.verify(currentToken, key, context.app.get('jwtOptions'))
} catch (err) {
throw new errors.NotAuthenticated('Token could not be verified.', err.message)
}
// OK! The JWT is valid, store it in the member profile
// (It's okay if this fails)
context.app.service(userService).patch(
null,
{ currentToken },
{ query: { user_id: member.user_id } }
)
// If we made it this far, we're all good!
return context
},
/**
* Takes a JWKS endpoint URI and returns a function that can retrieve an
* array of JWKs, i.e. a JWKS. The resulting function may throw any of
* [the errors described here]{@link https://github.com/request/promise-core/blob/master/lib/errors.js}
*
* @param {string} uri The URI of the JWKS endpoint
* @returns {function} A function that can retrieve a JWKS from the endpoint
*/
getJWKS = uri => () => rp({ uri, json: true }),
/**
* Takes a JWK object and returns a valid key in PEM format. Throws
* a GeneralError if there are no x5c items stored on the JWK.
*
* @param {string} jwk The JWK to be parsed
* @returns {string} The key in PEM format from the first x5c entry
* @throws {GeneralError} Throws a GeneralError if there are no x5c items
*/
x5cToPEM = jwk => {
if (!jwk.x5c.length > 0) throw new errors.GeneralError('Stored JWK has no x5c property.')
const lines = jwk.x5c[0].match(/.{1,64}/g).join('\n')
return `-----BEGIN CERTIFICATE-----\n${lines}\n-----END CERTIFICATE-----\n`
},
/**
* Takes a `kid`, a reference to an in-memory Feathers service (`svc`)
* for storing JWKs, and a `client` for retrieving signing keys from a
* JWKS endpoint. Returns a valid signing key in PEM format or throws
* a `SigningKeyNotFoundError`. If a key is successfully retrieved from
* the endpoint, it tries to store this value using the `svc`.
*
* @async
* @param {string} kid The `kid` for the JWK to be retrieved
* @param {object} svc The Feathers service used to store JWKs in memory
* @param {function} jwksClient A function that takes a `kid` and returns a key
* @returns {string} The retrieved signing key in PEM format
* @throws {GeneralError} Thrown by the `client` if `kid` is not found
*/
getKey = async (kid, svc, jwksClient) => {
try {
// get the signing key from the in-memory service, if it exists
const storedKey = await svc.get(kid)
// if the storedKey exists, return it
if (storedKey) return x5cToPEM(storedKey)
} catch (err) {
// nothing to see here. please move along...
}
// otherwise, we need to get it from our JWKS endpoint
let jwk
try {
const jwks = await jwksClient()
jwk = jwks.keys.find(k => k.kid === kid)
} catch (err) {
// throw an error if we still don't have a signing key
throw new errors.GeneralError('Could not retrieve JWKS', err)
}
// throw an error if there were no JWKs that contained our kid
if (!jwk) throw new errors.GeneralError('Could not find a JWK matching given kid')
// get the signing key from the retrieved JWK
const key = x5cToPEM(jwk)
// store the jwk in our in-memory service
try { svc.create(jwk) } catch (e) { /* no problem if this fails */ }
// and return the key
return key
}
} = {}) => ({
userService,
keysService,
authorize,
getJWKS,
x5cToPEM,
getKey
})