Skip to content

Commit 9fab017

Browse files
authored
feature(authorize): allow custom implementations of validateRedirectUri via model #89 p.4
- support custom validateRedirectUri() - allow to implement model.validateRedirectUri - updated AuthorizeHandler - default conforms with RFC 6819 Section-5.2.3.5 - thanks to @FStefanni and @jorenvandeweyer
2 parents 92bea82 + 29e081b commit 9fab017

File tree

5 files changed

+197
-3
lines changed

5 files changed

+197
-3
lines changed

docs/model/overview.rst

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Model functions used by the authorization code grant:
3737
- :ref:`Model#saveAuthorizationCode`
3838
- :ref:`Model#revokeAuthorizationCode`
3939
- :ref:`Model#validateScope`
40+
- :ref:`Model#validateRedirectUri`
4041

4142
--------
4243

docs/model/spec.rst

+42
Original file line numberDiff line numberDiff line change
@@ -984,3 +984,45 @@ Returns ``true`` if the access token passes, ``false`` otherwise.
984984
let authorizedScopes = token.scope.split(' ');
985985
return requestedScopes.every(s => authorizedScopes.indexOf(s) >= 0);
986986
}
987+
988+
--------
989+
990+
.. _Model#validateRedirectUri:
991+
992+
``validateRedirectUri(redirectUri, client, [callback])``
993+
================================================================
994+
995+
Invoked to check if the provided ``redirectUri`` is valid for a particular ``client``.
996+
997+
This model function is **optional**. If not implemented, the ``redirectUri`` should be included in the provided ``redirectUris`` of the client.
998+
999+
**Invoked during:**
1000+
1001+
- ``authorization_code`` grant
1002+
1003+
**Arguments:**
1004+
1005+
+-----------------+----------+---------------------------------------------------------------------+
1006+
| Name | Type | Description |
1007+
+=================+==========+=====================================================================+
1008+
| redirect_uri | String | The redirect URI to validate. |
1009+
+-----------------+----------+---------------------------------------------------------------------+
1010+
| client | Object | The associated client. |
1011+
+-----------------+----------+---------------------------------------------------------------------+
1012+
1013+
**Return value:**
1014+
1015+
Returns ``true`` if the ``redirectUri`` is valid, ``false`` otherwise.
1016+
1017+
**Remarks:**
1018+
When implementing this method you should take care of possible security risks related to ``redirectUri``.
1019+
.. _rfc6819: https://datatracker.ietf.org/doc/html/rfc6819
1020+
1021+
Section-5.2.3.5 is implemented by default.
1022+
.. _Section-5.2.3.5: https://datatracker.ietf.org/doc/html/rfc6819#section-5.2.3.5
1023+
1024+
::
1025+
1026+
function validateRedirectUri(redirectUri, client) {
1027+
return client.redirectUris.includes(redirectUri);
1028+
}

lib/handlers/authorize-handler.js

+19-3
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ AuthorizeHandler.prototype.getAuthorizationCodeLifetime = function() {
165165
*/
166166

167167
AuthorizeHandler.prototype.getClient = function(request) {
168+
const self = this;
168169
const clientId = request.body.client_id || request.query.client_id;
169170

170171
if (!clientId) {
@@ -198,10 +199,17 @@ AuthorizeHandler.prototype.getClient = function(request) {
198199
throw new InvalidClientError('Invalid client: missing client `redirectUri`');
199200
}
200201

201-
if (redirectUri && !client.redirectUris.includes(redirectUri)) {
202-
throw new InvalidClientError('Invalid client: `redirect_uri` does not match client value');
202+
if (redirectUri) {
203+
return self.validateRedirectUri(redirectUri, client)
204+
.then(function(valid) {
205+
if (!valid) {
206+
throw new InvalidClientError('Invalid client: `redirect_uri` does not match client value');
207+
}
208+
return client;
209+
});
210+
} else {
211+
return client;
203212
}
204-
return client;
205213
});
206214
};
207215

@@ -295,6 +303,14 @@ AuthorizeHandler.prototype.saveAuthorizationCode = function(authorizationCode, e
295303
return promisify(this.model.saveAuthorizationCode, 3).call(this.model, code, client, user);
296304
};
297305

306+
307+
AuthorizeHandler.prototype.validateRedirectUri = function(redirectUri, client) {
308+
if (this.model.validateRedirectUri) {
309+
return promisify(this.model.validateRedirectUri, 2).call(this.model, redirectUri, client);
310+
}
311+
312+
return Promise.resolve(client.redirectUris.includes(redirectUri));
313+
};
298314
/**
299315
* Get response type.
300316
*/

test/integration/handlers/authorize-handler_test.js

+59
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,65 @@ describe('AuthorizeHandler integration', function() {
655655
});
656656
});
657657

658+
describe('validateRedirectUri()', function() {
659+
it('should support empty method', function() {
660+
const model = {
661+
getAccessToken: function() {},
662+
getClient: function() {},
663+
saveAuthorizationCode: function() {}
664+
};
665+
666+
const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model });
667+
668+
handler.validateRedirectUri('http://example.com/a', { redirectUris: ['http://example.com/a'] }).should.be.an.instanceOf(Promise);
669+
});
670+
671+
it('should support promises', function() {
672+
const model = {
673+
getAccessToken: function() {},
674+
getClient: function() {},
675+
saveAuthorizationCode: function() {},
676+
validateRedirectUri: function() {
677+
return Promise.resolve(true);
678+
}
679+
};
680+
681+
const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model });
682+
683+
handler.validateRedirectUri('http://example.com/a', { }).should.be.an.instanceOf(Promise);
684+
});
685+
686+
it('should support non-promises', function() {
687+
const model = {
688+
getAccessToken: function() {},
689+
getClient: function() {},
690+
saveAuthorizationCode: function() {},
691+
validateRedirectUri: function() {
692+
return true;
693+
}
694+
};
695+
696+
const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model });
697+
698+
handler.validateRedirectUri('http://example.com/a', { }).should.be.an.instanceOf(Promise);
699+
});
700+
701+
it('should support callbacks', function() {
702+
const model = {
703+
getAccessToken: function() {},
704+
getClient: function() {},
705+
saveAuthorizationCode: function() {},
706+
validateRedirectUri: function(redirectUri, client, callback) {
707+
callback(null, false);
708+
}
709+
};
710+
711+
const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model });
712+
713+
handler.validateRedirectUri('http://example.com/a', { }).should.be.an.instanceOf(Promise);
714+
});
715+
});
716+
658717
describe('getClient()', function() {
659718
it('should throw an error if `client_id` is missing', function() {
660719
const model = {

test/unit/handlers/authorize-handler_test.js

+76
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,80 @@ describe('AuthorizeHandler', function() {
9999
.catch(should.fail);
100100
});
101101
});
102+
103+
describe('validateRedirectUri()', function() {
104+
it('should call `model.validateRedirectUri()`', function() {
105+
const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] };
106+
const redirect_uri = 'http://example.com/cb/2';
107+
const model = {
108+
getAccessToken: function() {},
109+
getClient: sinon.stub().returns(client),
110+
saveAuthorizationCode: function() {},
111+
validateRedirectUri: sinon.stub().returns(true)
112+
};
113+
const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model });
114+
const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, query: {} });
115+
116+
return handler.getClient(request)
117+
.then(function() {
118+
model.getClient.callCount.should.equal(1);
119+
model.getClient.firstCall.args.should.have.length(2);
120+
model.getClient.firstCall.args[0].should.equal(12345);
121+
model.getClient.firstCall.thisValue.should.equal(model);
122+
123+
model.validateRedirectUri.callCount.should.equal(1);
124+
model.validateRedirectUri.firstCall.args.should.have.length(2);
125+
model.validateRedirectUri.firstCall.args[0].should.equal(redirect_uri);
126+
model.validateRedirectUri.firstCall.args[1].should.equal(client);
127+
model.validateRedirectUri.firstCall.thisValue.should.equal(model);
128+
})
129+
.catch(should.fail);
130+
});
131+
132+
it('should be successful validation', function () {
133+
const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] };
134+
const redirect_uri = 'http://example.com/cb';
135+
const model = {
136+
getAccessToken: function() {},
137+
getClient: sinon.stub().returns(client),
138+
saveAuthorizationCode: function() {},
139+
validateRedirectUri: function (redirectUri, client) {
140+
return client.redirectUris.includes(redirectUri);
141+
}
142+
};
143+
144+
const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model });
145+
const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, query: {} });
146+
147+
return handler.getClient(request)
148+
.then((client) => {
149+
client.should.equal(client);
150+
});
151+
});
152+
153+
it('should be unsuccessful validation', function () {
154+
const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] };
155+
const redirect_uri = 'http://example.com/callback';
156+
const model = {
157+
getAccessToken: function() {},
158+
getClient: sinon.stub().returns(client),
159+
saveAuthorizationCode: function() {},
160+
validateRedirectUri: function (redirectUri, client) {
161+
return client.redirectUris.includes(redirectUri);
162+
}
163+
};
164+
165+
const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model });
166+
const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, query: {} });
167+
168+
return handler.getClient(request)
169+
.then(() => {
170+
throw Error('should not resolve');
171+
})
172+
.catch((err) => {
173+
err.name.should.equal('invalid_client');
174+
err.message.should.equal('Invalid client: `redirect_uri` does not match client value');
175+
});
176+
});
177+
});
102178
});

0 commit comments

Comments
 (0)