Skip to content

Commit 19f7dc4

Browse files
authored
feature(pkce): save code challenge with authorization code
Merge pull request #161 from martinssonj/save-codechallenge-for-pkce
2 parents 6bafe0e + c597a24 commit 19f7dc4

File tree

4 files changed

+87
-4
lines changed

4 files changed

+87
-4
lines changed

index.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ declare namespace OAuth2Server {
306306
*
307307
*/
308308
saveAuthorizationCode(
309-
code: Pick<AuthorizationCode, 'authorizationCode' | 'expiresAt' | 'redirectUri' | 'scope'>,
309+
code: Pick<AuthorizationCode, 'authorizationCode' | 'expiresAt' | 'redirectUri' | 'scope' | 'codeChallenge' | 'codeChallengeMethod'>,
310310
client: Client,
311311
user: User,
312312
callback?: Callback<AuthorizationCode>): Promise<AuthorizationCode | Falsey>;
@@ -410,6 +410,8 @@ declare namespace OAuth2Server {
410410
scope?: string | string[] | undefined;
411411
client: Client;
412412
user: User;
413+
codeChallenge?: string;
414+
codeChallengeMethod?: string;
413415
[key: string]: any;
414416
}
415417

lib/handlers/authorize-handler.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,10 @@ AuthorizeHandler.prototype.handle = function(request, response) {
114114
})
115115
.then(function(authorizationCode) {
116116
ResponseType = this.getResponseType(request);
117+
const codeChallenge = this.getCodeChallenge(request);
118+
const codeChallengeMethod = this.getCodeChallengeMethod(request);
117119

118-
return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user);
120+
return this.saveAuthorizationCode(authorizationCode, expiresAt, scope, client, uri, user, codeChallenge, codeChallengeMethod);
119121
})
120122
.then(function(code) {
121123
const responseType = new ResponseType(code.authorizationCode);
@@ -293,13 +295,20 @@ AuthorizeHandler.prototype.getRedirectUri = function(request, client) {
293295
* Save authorization code.
294296
*/
295297

296-
AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user) {
297-
const code = {
298+
AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, expiresAt, scope, client, redirectUri, user, codeChallenge, codeChallengeMethod) {
299+
let code = {
298300
authorizationCode: authorizationCode,
299301
expiresAt: expiresAt,
300302
redirectUri: redirectUri,
301303
scope: scope
302304
};
305+
306+
if(codeChallenge && codeChallengeMethod){
307+
code = Object.assign({
308+
codeChallenge: codeChallenge,
309+
codeChallengeMethod: codeChallengeMethod
310+
}, code);
311+
}
303312
return promisify(this.model.saveAuthorizationCode, 3).call(this.model, code, client, user);
304313
};
305314

@@ -369,6 +378,18 @@ AuthorizeHandler.prototype.updateResponse = function(response, redirectUri, stat
369378
response.redirect(url.format(redirectUri));
370379
};
371380

381+
AuthorizeHandler.prototype.getCodeChallenge = function(request) {
382+
return request.body.code_challenge;
383+
};
384+
385+
/**
386+
* Get code challenge method from request or defaults to plain.
387+
* https://www.rfc-editor.org/rfc/rfc7636#section-4.3
388+
*/
389+
AuthorizeHandler.prototype.getCodeChallengeMethod = function(request) {
390+
return request.body.code_challenge_method || 'plain';
391+
};
392+
372393
/**
373394
* Export constructor.
374395
*/

test/integration/handlers/authorize-handler_test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,4 +1321,44 @@ describe('AuthorizeHandler integration', function() {
13211321
response.get('location').should.equal('http://example.com/cb?state=foobar');
13221322
});
13231323
});
1324+
1325+
describe('getCodeChallengeMethod()', function() {
1326+
it('should get code challenge method', function() {
1327+
const model = {
1328+
getAccessToken: function() {},
1329+
getClient: function() {},
1330+
saveAuthorizationCode: function() {}
1331+
};
1332+
const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); const request = new Request({ body: {code_challenge_method: 'S256'}, headers: {}, method: {}, query: {} });
1333+
1334+
const codeChallengeMethod = handler.getCodeChallengeMethod(request);
1335+
codeChallengeMethod.should.equal('S256');
1336+
});
1337+
1338+
it('should get default code challenge method plain if missing', function() {
1339+
const model = {
1340+
getAccessToken: function() {},
1341+
getClient: function() {},
1342+
saveAuthorizationCode: function() {}
1343+
};
1344+
const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); const request = new Request({ body: {}, headers: {}, method: {}, query: {} });
1345+
1346+
const codeChallengeMethod = handler.getCodeChallengeMethod(request);
1347+
codeChallengeMethod.should.equal('plain');
1348+
});
1349+
});
1350+
1351+
describe('getCodeChallenge()', function() {
1352+
it('should get code challenge', function() {
1353+
const model = {
1354+
getAccessToken: function() {},
1355+
getClient: function() {},
1356+
saveAuthorizationCode: function() {}
1357+
};
1358+
const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); const request = new Request({ body: {code_challenge: 'challenge'}, headers: {}, method: {}, query: {} });
1359+
1360+
const codeChallengeMethod = handler.getCodeChallenge(request);
1361+
codeChallengeMethod.should.equal('challenge');
1362+
});
1363+
});
13241364
});

test/unit/handlers/authorize-handler_test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,26 @@ describe('AuthorizeHandler', function() {
9898
})
9999
.catch(should.fail);
100100
});
101+
102+
it('should call `model.saveAuthorizationCode()` with code challenge', function() {
103+
const model = {
104+
getAccessToken: function() {},
105+
getClient: function() {},
106+
saveAuthorizationCode: sinon.stub().returns({})
107+
};
108+
const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model });
109+
110+
return handler.saveAuthorizationCode('foo', 'bar', 'qux', 'biz', 'baz', 'boz', 'codeChallenge', 'codeChallengeMethod')
111+
.then(function() {
112+
model.saveAuthorizationCode.callCount.should.equal(1);
113+
model.saveAuthorizationCode.firstCall.args.should.have.length(3);
114+
model.saveAuthorizationCode.firstCall.args[0].should.eql({ authorizationCode: 'foo', expiresAt: 'bar', redirectUri: 'baz', scope: 'qux', codeChallenge: 'codeChallenge', codeChallengeMethod: 'codeChallengeMethod' });
115+
model.saveAuthorizationCode.firstCall.args[1].should.equal('biz');
116+
model.saveAuthorizationCode.firstCall.args[2].should.equal('boz');
117+
model.saveAuthorizationCode.firstCall.thisValue.should.equal(model);
118+
})
119+
.catch(should.fail);
120+
});
101121
});
102122

103123
describe('validateRedirectUri()', function() {

0 commit comments

Comments
 (0)