Skip to content

Commit bff756b

Browse files
Merge pull request from GHSA-g8x5-p9qc-cf95
* feat: pass request into checkStateFunction * feat: add state in cookies * fix: check presence * fix: check cookie only * fix: change defaultGenerateStateFunction * Update index.js Co-authored-by: Filip Skokan <[email protected]> * fix: unsign * fix: assume stateCookie is always present * renamed state * fix: use session --------- Co-authored-by: Filip Skokan <[email protected]>
1 parent 85c1057 commit bff756b

File tree

4 files changed

+81
-20
lines changed

4 files changed

+81
-20
lines changed

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,10 @@ fastify.register(oauthPlugin, {
125125
## Set custom state
126126

127127
The `generateStateFunction` accepts a function to generate the `state` parameter for the OAUTH flow. This function receives the Fastify instance's `request` object as parameter.
128+
The `state` parameter will be also set into a `httpOnly`, `sameSite: Lax` cookie.
128129
When you set it, it is required to provide the function `checkStateFunction` in order to validate the states generated.
129130

130131
```js
131-
const validStates = new Set()
132-
133132
fastify.register(oauthPlugin, {
134133
name: 'facebookOAuth2',
135134
credentials: {
@@ -146,12 +145,12 @@ When you set it, it is required to provide the function `checkStateFunction` in
146145
// custom function to generate the state
147146
generateStateFunction: (request) => {
148147
const state = request.query.customCode
149-
validStates.add(state)
148+
request.session.state = state
150149
return state
151150
},
152151
// custom function to check the state is valid
153-
checkStateFunction: (returnedState, callback) => {
154-
if (validStates.has(returnedState)) {
152+
checkStateFunction: (request, callback) => {
153+
if (request.query.state === request.session.state) {
155154
callback()
156155
return
157156
}

index.js

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22

3-
const defaultState = require('crypto').randomBytes(10).toString('hex')
3+
const crypto = require('crypto')
44

55
const fp = require('fastify-plugin')
66
const { AuthorizationCode } = require('simple-oauth2')
@@ -10,11 +10,13 @@ const promisify = require('util').promisify
1010
const callbackify = require('util').callbackify
1111

1212
function defaultGenerateStateFunction () {
13-
return defaultState
13+
return crypto.randomBytes(16).toString('base64url')
1414
}
1515

16-
function defaultCheckStateFunction (state, callback) {
17-
if (state === defaultState) {
16+
function defaultCheckStateFunction (request, callback) {
17+
const state = request.query.state
18+
const stateCookie = request.cookies['oauth2-redirect-state']
19+
if (stateCookie && state === stateCookie) {
1820
callback()
1921
return
2022
}
@@ -60,6 +62,10 @@ function fastifyOauth2 (fastify, options, next) {
6062
return next(new Error('options.schema should be a object'))
6163
}
6264

65+
if (!fastify.hasReplyDecorator('cookie')) {
66+
fastify.register(require('@fastify/cookie'))
67+
}
68+
6369
const name = options.name
6470
const credentials = options.credentials
6571
const callbackUri = options.callbackUri
@@ -73,9 +79,8 @@ function fastifyOauth2 (fastify, options, next) {
7379
const tags = options.tags || []
7480
const schema = options.schema || { tags }
7581

76-
function generateAuthorizationUri (requestObject) {
77-
const state = generateStateFunction(requestObject)
78-
const urlOptions = Object.assign({}, generateCallbackUriParams(callbackUriParams, requestObject, scope, state), {
82+
function generateAuthorizationUri (request, state) {
83+
const urlOptions = Object.assign({}, generateCallbackUriParams(callbackUriParams, request, scope, state), {
7984
redirect_uri: callbackUri,
8085
scope,
8186
state
@@ -86,9 +91,13 @@ function fastifyOauth2 (fastify, options, next) {
8691
}
8792

8893
function startRedirectHandler (request, reply) {
89-
const authorizationUri = generateAuthorizationUri(request)
94+
const state = generateStateFunction(request)
95+
const authorizationUri = generateAuthorizationUri(request, state)
9096

91-
reply.redirect(authorizationUri)
97+
reply.setCookie('oauth2-redirect-state', state, {
98+
httpOnly: true,
99+
sameSite: 'lax'
100+
}).redirect(authorizationUri)
92101
}
93102

94103
const cbk = function (o, code, callback) {
@@ -102,9 +111,8 @@ function fastifyOauth2 (fastify, options, next) {
102111

103112
function getAccessTokenFromAuthorizationCodeFlowCallbacked (request, callback) {
104113
const code = request.query.code
105-
const state = request.query.state
106114

107-
checkStateFunction(state, function (err) {
115+
checkStateFunction(request, function (err) {
108116
if (err) {
109117
callback(err)
110118
return

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"tsd": "^0.28.0"
3939
},
4040
"dependencies": {
41+
"@fastify/cookie": "^8.3.0",
4142
"fastify-plugin": "^4.0.0",
4243
"simple-oauth2": "^5.0.0"
4344
},

test/index.test.js

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const t = require('tap')
44
const nock = require('nock')
55
const createFastify = require('fastify')
6-
6+
const crypto = require('crypto')
77
const fastifyOauth2 = require('..')
88

99
nock.disableNetConnect()
@@ -54,7 +54,10 @@ function makeRequests (t, fastify) {
5454

5555
fastify.inject({
5656
method: 'GET',
57-
url: '/?code=my-code&state=' + state
57+
url: '/?code=my-code&state=' + state,
58+
cookies: {
59+
'oauth2-redirect-state': state
60+
}
5861
}, function (err, responseEnd) {
5962
t.error(err)
6063

@@ -426,7 +429,6 @@ t.test('options.generateStateFunction with request', t => {
426429
})
427430

428431
t.test('generateAuthorizationUri redirect with request object', t => {
429-
t.plan(4)
430432
const fastify = createFastify()
431433

432434
fastify.register(fastifyOauth2, {
@@ -448,7 +450,7 @@ t.test('generateAuthorizationUri redirect with request object', t => {
448450
})
449451

450452
fastify.get('/gh', function (request, reply) {
451-
const redirectUrl = this.theName.generateAuthorizationUri(request)
453+
const redirectUrl = this.theName.generateAuthorizationUri(request, 'generated_code')
452454
return reply.redirect(redirectUrl)
453455
})
454456

@@ -463,6 +465,7 @@ t.test('generateAuthorizationUri redirect with request object', t => {
463465
t.equal(responseStart.statusCode, 302)
464466
const matched = responseStart.headers.location.match(/https:\/\/github\.com\/login\/oauth\/authorize\?response_type=code&client_id=my-client-id&redirect_uri=%2Fcallback&scope=notifications&state=generated_code/)
465467
t.ok(matched)
468+
t.end()
466469
})
467470
})
468471

@@ -799,3 +802,53 @@ t.test('preset configuration generate-callback-uri-params', t => {
799802
t.equal(typeof fastifyOauth2[configName].authorizePath, 'string')
800803
}
801804
})
805+
806+
t.test('options.generateStateFunction with signing key', t => {
807+
t.plan(5)
808+
const fastify = createFastify()
809+
810+
const hmacKey = 'hello'
811+
const expectedState = crypto.createHmac('sha1', hmacKey).update('foo').digest('hex')
812+
813+
fastify.register(require('@fastify/cookie'))
814+
815+
fastify.register(fastifyOauth2, {
816+
name: 'the-name',
817+
credentials: {
818+
client: {
819+
id: 'my-client-id',
820+
secret: 'my-secret'
821+
},
822+
auth: fastifyOauth2.GITHUB_CONFIGURATION
823+
},
824+
startRedirectPath: '/login/github',
825+
callbackUri: '/callback',
826+
generateStateFunction: (request) => {
827+
const state = crypto.createHmac('sha1', hmacKey).update(request.headers.foo).digest('hex')
828+
t.ok(request, 'the request param has been set')
829+
return state
830+
},
831+
checkStateFunction: (request) => {
832+
const generatedState = crypto.createHmac('sha1', hmacKey).update(request.headers.foo).digest('hex')
833+
return generatedState === request.query.state
834+
},
835+
scope: ['notifications']
836+
})
837+
838+
t.teardown(fastify.close.bind(fastify))
839+
840+
fastify.listen({ port: 0 }, function (err) {
841+
t.error(err)
842+
fastify.inject({
843+
method: 'GET',
844+
url: '/login/github',
845+
query: { code: expectedState },
846+
headers: { foo: 'foo' }
847+
}, function (err, responseStart) {
848+
t.error(err)
849+
t.equal(responseStart.statusCode, 302)
850+
const matched = responseStart.headers.location.match(/https:\/\/github\.com\/login\/oauth\/authorize\?response_type=code&client_id=my-client-id&redirect_uri=%2Fcallback&scope=notifications&state=1e864fbd840212c1ed9ce60175d373f3a48681b2/)
851+
t.ok(matched)
852+
})
853+
})
854+
})

0 commit comments

Comments
 (0)