Skip to content

Commit f484bdc

Browse files
Merge pull request #10 from solid/refactor-error-response
Refactor AuthenticationResponse.errorResponse method
2 parents e7b71ea + ff54089 commit f484bdc

7 files changed

Lines changed: 130 additions & 57 deletions

File tree

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"dist": "webpack --progress",
1515
"prepare": "npm run build && npm run dist && npm run test",
1616
"preversion": "npm test",
17-
"test": "mocha"
17+
"test": "mocha --timeout=10000"
1818
},
1919
"repository": {
2020
"type": "git",
@@ -43,6 +43,7 @@
4343
"@trust/webcrypto": "0.9.2",
4444
"base64url": "^3.0.0",
4545
"node-fetch": "^2.1.2",
46+
"standard-http-error": "^2.0.1",
4647
"text-encoding": "^0.6.4",
4748
"whatwg-url": "^6.4.1"
4849
},

src/AuthenticationResponse.js

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,29 @@ const FormUrlEncoded = require('./FormUrlEncoded')
1111
const IDToken = require('./IDToken')
1212
const Session = require('./Session')
1313
const onHttpError = require('./onHttpError')
14+
const HttpError = require('standard-http-error')
1415

1516
/**
1617
* AuthenticationResponse
1718
*/
1819
class AuthenticationResponse {
20+
/**
21+
* @param rp {RelyingParty}
22+
* @param [redirect] {string} req.query
23+
* @param [body] {string} req.body.text
24+
* @param session {Session|Storage} req.session or localStorage or similar
25+
* @param params {object} hashmap
26+
* @param mode {string} 'query'/'fragment'/'form_post',
27+
* determined in `parseResponse()`
28+
*/
29+
constructor ({rp, redirect, body, session, mode, params = {}}) {
30+
this.rp = rp
31+
this.redirect = redirect
32+
this.body = body
33+
this.session = session
34+
this.mode = mode
35+
this.params = params
36+
}
1937

2038
/**
2139
* validateResponse
@@ -24,14 +42,15 @@ class AuthenticationResponse {
2442
* Authentication response validation.
2543
*
2644
* @param {string|Object} response
27-
* @returns {Promise}
45+
*
46+
* @returns {Promise<Session>}
2847
*/
2948
static validateResponse (response) {
3049
return Promise.resolve(response)
3150
.then(this.parseResponse)
51+
.then(this.errorResponse)
3252
.then(this.matchRequest)
3353
.then(this.validateStateParam)
34-
.then(this.errorResponse)
3554
.then(this.validateResponseMode)
3655
.then(this.validateResponseParams)
3756
.then(this.exchangeAuthorizationCode)
@@ -42,15 +61,16 @@ class AuthenticationResponse {
4261
/**
4362
* parseResponse
4463
*
45-
* @param {Object} response
46-
* @returns {Promise}
64+
* @param {object} response
65+
*
66+
* @returns {object}
4767
*/
4868
static parseResponse (response) {
4969
let {redirect, body} = response
5070

5171
// response must be either a redirect uri or request body, but not both
5272
if ((redirect && body) || (!redirect && !body)) {
53-
throw new Error('Invalid response mode')
73+
throw new HttpError(400, 'Invalid response mode')
5474
}
5575

5676
// parse redirect uri
@@ -59,7 +79,7 @@ class AuthenticationResponse {
5979
let {search, hash} = url
6080

6181
if ((search && hash) || (!search && !hash)) {
62-
throw new Error('Invalid response mode')
82+
throw new HttpError(400, 'Invalid response mode')
6383
}
6484

6585
if (search) {
@@ -82,6 +102,37 @@ class AuthenticationResponse {
82102
return response
83103
}
84104

105+
/**
106+
* errorResponse
107+
*
108+
* @param {AuthenticationResponse} response
109+
*
110+
* @throws {Error} If response params include the OAuth2 'error' param,
111+
* throws an error based on it.
112+
*
113+
* @returns {AuthenticationResponse} Chainable
114+
*
115+
* @todo Figure out HTTP status code (typically 400, 401 or 403)
116+
* based on the OAuth2/OIDC `error` code, probably using an external library
117+
*/
118+
static errorResponse (response) {
119+
const errorCode = response.params.error
120+
121+
if (errorCode) {
122+
const errorParams = {}
123+
errorParams['error'] = errorCode
124+
errorParams['error_description'] = response.params['error_description']
125+
errorParams['error_uri'] = response.params['error_uri']
126+
errorParams['state'] = response.params['state']
127+
128+
const error = new Error(`AuthenticationResponse error: ${errorCode}`)
129+
error.info = errorParams
130+
throw error
131+
}
132+
133+
return response
134+
}
135+
85136
/**
86137
* matchRequest
87138
*
@@ -130,22 +181,6 @@ class AuthenticationResponse {
130181
})
131182
}
132183

133-
/**
134-
* errorResponse
135-
*
136-
* @param {Object} response
137-
* @returns {Promise}
138-
*/
139-
static errorResponse (response) {
140-
let error = response.params.error
141-
142-
if (error) {
143-
return Promise.reject(error)
144-
}
145-
146-
return Promise.resolve(response)
147-
}
148-
149184
/**
150185
* validateResponseMode
151186
*
@@ -321,14 +356,25 @@ class AuthenticationResponse {
321356
/**
322357
* decodeIDToken
323358
*
324-
* @param {Object} response
325-
* @returns {Promise}
359+
* Note: If the `id_token` is not present in params, this method does not
360+
* get called (short-circuited in `validateIDToken()`).
361+
*
362+
* @param response {AuthenticationResponse}
363+
* @param response.params {object}
364+
* @param [response.params.id_token] {string} IDToken encoded as a JWT
365+
*
366+
* @returns {AuthenticationResponse} Chainable
326367
*/
327368
static decodeIDToken (response) {
328369
let jwt = response.params.id_token
329370

330-
if (jwt) {
371+
try {
331372
response.decoded = IDToken.decode(jwt)
373+
} catch (decodeError) {
374+
const error = new HttpError(400, 'Error decoding ID Token')
375+
error.cause = decodeError
376+
error.info = { id_token: jwt }
377+
throw error
332378
}
333379

334380
return response

src/RelyingParty.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -233,19 +233,21 @@ class RelyingParty extends JSONDocument {
233233
*
234234
* @param response {string} req.query or req.body.text
235235
* @param session {Session|Storage} req.session or localStorage or similar
236-
* @returns {Promise<Object>} Custom response object, with `params` and
237-
* `mode` properties
236+
*
237+
* @returns {Promise<Session>}
238238
*/
239-
validateResponse (response, session) {
240-
session = session || this.store
239+
validateResponse (response, session = this.store) {
240+
let options
241241

242242
if (response.match(/^http(s?):\/\//)) {
243-
response = { rp: this, redirect: response, session }
243+
options = { rp: this, redirect: response, session }
244244
} else {
245-
response = { rp: this, body: response, session }
245+
options = { rp: this, body: response, session }
246246
}
247247

248-
return AuthenticationResponse.validateResponse(response)
248+
const authResponse = new AuthenticationResponse(options)
249+
250+
return AuthenticationResponse.validateResponse(authResponse)
249251
}
250252

251253
/**

src/Session.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ class Session {
4545
/**
4646
* @param response {AuthenticationResponse}
4747
*
48-
* @returns {Session}
48+
* @returns {Session} RelyingParty Session object
4949
*/
5050
static fromAuthResponse (response) {
5151
const RelyingParty = require('./RelyingParty') // import here due to circular dep
5252

53-
let payload = response.decoded.payload
53+
let idClaims = response.decoded && response.decoded.payload || {}
5454

5555
let { rp } = response
5656

@@ -65,14 +65,14 @@ class Session {
6565
let options = {
6666
credentialType,
6767
sessionKey,
68-
issuer: payload.iss,
68+
issuer: idClaims.iss,
69+
idClaims,
6970
authorization: {
7071
client_id: registration['client_id'],
7172
access_token: response.params['access_token'],
7273
id_token: response.params['id_token'],
7374
refresh_token: response.params['refresh_token']
74-
},
75-
idClaims: response.decoded && response.decoded.payload,
75+
}
7676
}
7777

7878
return Session.from(options)

test/AuthenticationResponseSpec.js

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -166,18 +166,29 @@ describe('AuthenticationResponse', () => {
166166
* errorResponse
167167
*/
168168
describe('errorResponse', () => {
169-
it('should reject with error response', () => {
170-
return AuthenticationResponse.errorResponse({
171-
params: {
172-
error: 'access_denied'
173-
}
174-
}).should.be.rejectedWith('access_denied')
169+
it('should throw an error if error param is present', done => {
170+
const errorParams = {
171+
error: 'access_denied',
172+
error_description: 'Access denied',
173+
error_uri: 'https://example.com/123',
174+
state: '$tate'
175+
}
176+
const response = new AuthenticationResponse({ params: {...errorParams} })
177+
try {
178+
AuthenticationResponse.errorResponse(response)
179+
} catch (error) {
180+
error.message.should
181+
.equal('AuthenticationResponse error: access_denied')
182+
error.info.should.eql(errorParams)
183+
done()
184+
}
175185
})
176186

177-
it('should resolve with its argument', () => {
178-
let response = { params: {} }
179-
return AuthenticationResponse.errorResponse(response)
180-
.should.eventually.equal(response)
187+
it('should return its argument if no errors', () => {
188+
let response = new AuthenticationResponse({})
189+
190+
AuthenticationResponse.errorResponse(response)
191+
.should.equal(response)
181192
})
182193
})
183194

@@ -477,10 +488,11 @@ describe('AuthenticationResponse', () => {
477488
.should.be.instanceof(JWT)
478489
})
479490

480-
it('should ignore response without id_token', () => {
481-
delete response.params.id_token
482-
expect(AuthenticationResponse.decodeIDToken(response).decoded)
483-
.to.be.undefined
491+
it('should throw an error on invalid id_token', () => {
492+
response.params.id_token = 'inva1id'
493+
expect(() => {
494+
AuthenticationResponse.decodeIDToken(response)
495+
}).to.throw('Error decoding ID Token')
484496
})
485497

486498
it('should return its argument', () => {

test/RelyingPartySpec.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -543,8 +543,8 @@ describe('RelyingParty', () => {
543543
})
544544

545545
it('should create an AuthenticationResponse instance', () => {
546-
let response = {}
547-
sinon.stub(AuthenticationResponse, 'validateResponse').resolves(response)
546+
const session = {}
547+
sinon.stub(AuthenticationResponse, 'validateResponse').resolves(session)
548548

549549
let store = {}
550550
let rp = new RelyingParty({ store })
@@ -553,9 +553,8 @@ describe('RelyingParty', () => {
553553

554554
return rp.validateResponse(uri)
555555
.then(res => {
556-
expect(res).to.equal(response)
557-
expect(AuthenticationResponse.validateResponse).to.have.been
558-
.calledWith({ rp, session: store, redirect: uri })
556+
expect(res).to.equal(session)
557+
expect(AuthenticationResponse.validateResponse).to.have.been.called()
559558
})
560559
})
561560
})

0 commit comments

Comments
 (0)