1 |
|
2 |
|
3 |
|
4 |
|
5 | var JWT = require('anvil-connect-jwt')
|
6 | var async = require('async')
|
7 | var request = require('superagent')
|
8 | var UnauthorizedError = require('../errors/UnauthorizedError')
|
9 | var nowSeconds = require('./time-utils.js').nowSeconds
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | var AccessToken = JWT.define({
|
15 |
|
16 | header: {
|
17 | alg: 'RS256'
|
18 | },
|
19 |
|
20 | headers: [
|
21 | 'alg'
|
22 | ],
|
23 |
|
24 |
|
25 | registeredHeaders: {
|
26 | alg: { format: 'StringOrURI', required: true, enum: ['RS256'] }
|
27 | },
|
28 |
|
29 |
|
30 | claims: ['jti', 'iss', 'sub', 'aud', 'exp', 'iat', 'scope'],
|
31 |
|
32 |
|
33 | registeredClaims: {
|
34 | jti: { format: 'String', required: true },
|
35 | iss: { format: 'URI', required: true },
|
36 | iat: { format: 'IntDate', required: true },
|
37 | exp: { format: 'IntDate', required: true },
|
38 | sub: { format: 'String', required: true },
|
39 | aud: { format: 'String', required: true },
|
40 | scope: { format: 'String', required: true }
|
41 | }
|
42 |
|
43 | })
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | AccessToken.refresh = function (refreshToken, options, callback) {
|
49 | if (!options.client_id || !options.client_secret || !refreshToken) {
|
50 | return callback(new UnauthorizedError('Missing client credentials'))
|
51 | }
|
52 | request
|
53 | .post(options.issuer + '/token')
|
54 | .auth(options.client_id, options.client_secret)
|
55 | .set('Content-Type', 'application/json')
|
56 | .send({ refresh_token: refreshToken, grant_type: 'refresh_token' })
|
57 | .end(function (err, response) {
|
58 |
|
59 | if (err) {
|
60 | return callback(err)
|
61 | }
|
62 |
|
63 |
|
64 | if (response.body && response.body.error) {
|
65 | return callback(new UnauthorizedError(response.body))
|
66 | } else {
|
67 | return callback(null, response.body)
|
68 | }
|
69 | })
|
70 | }
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 | AccessToken.verify = function (token, options, callback) {
|
89 | async.parallel({
|
90 | jwt: function (done) {
|
91 | if (token.indexOf('.') !== -1) {
|
92 | var at = AccessToken.decode(token, options.key)
|
93 | if (!at || at instanceof Error) {
|
94 | done(new UnauthorizedError({
|
95 | realm: 'user',
|
96 | error: 'invalid_token',
|
97 | error_description: 'Invalid access token',
|
98 | statusCode: 401
|
99 | }))
|
100 | } else {
|
101 | done(null, at)
|
102 | }
|
103 | } else {
|
104 | done()
|
105 | }
|
106 | },
|
107 | random: function (done) {
|
108 | if (token.indexOf('.') === -1) {
|
109 | request
|
110 | .post(options.issuer + '/token/verify')
|
111 | .auth(options.client_id, options.client_secret)
|
112 | .set('Content-Type', 'application/json')
|
113 | .send({ access_token: token })
|
114 | .end(function (err, response) {
|
115 |
|
116 | if (err) {
|
117 | return done(err)
|
118 | }
|
119 |
|
120 |
|
121 | if (response.body && response.body.error) {
|
122 | done(new UnauthorizedError(response.body))
|
123 | } else {
|
124 | done(null, response.body)
|
125 | }
|
126 | })
|
127 | } else {
|
128 | done()
|
129 | }
|
130 | }
|
131 | }, function (err, result) {
|
132 | if (err) { return callback(err) }
|
133 |
|
134 | var claims = result.random || result.jwt.payload
|
135 | var issuer = options.issuer
|
136 | var clients = options.clients
|
137 | var scope = options.scope
|
138 |
|
139 |
|
140 | if (claims.iss !== issuer) {
|
141 | return callback(new UnauthorizedError({
|
142 | error: 'invalid_token',
|
143 | error_description: 'Mismatching issuer',
|
144 | statusCode: 403
|
145 | }))
|
146 | }
|
147 |
|
148 |
|
149 | if (clients && clients.indexOf(claims.aud) === -1) {
|
150 | return callback(new UnauthorizedError({
|
151 | error: 'invalid_token',
|
152 | error_description: 'Mismatching audience',
|
153 | statusCode: 403
|
154 | }))
|
155 | }
|
156 |
|
157 |
|
158 | if (claims.exp < nowSeconds()) {
|
159 | return callback(new UnauthorizedError({
|
160 | error: 'invalid_token',
|
161 | error_description: 'Expired access token',
|
162 | statusCode: 403
|
163 | }))
|
164 | }
|
165 |
|
166 |
|
167 | if (scope && claims.scope.indexOf(scope) === -1) {
|
168 | return callback(new UnauthorizedError({
|
169 | error: 'insufficient_scope',
|
170 | error_description: 'Insufficient scope',
|
171 | statusCode: 403
|
172 | }))
|
173 | }
|
174 |
|
175 | callback(null, claims)
|
176 | })
|
177 | }
|
178 |
|
179 |
|
180 |
|
181 |
|
182 | module.exports = AccessToken
|