Skip to content

Commit e3fb264

Browse files
committed
add more code to connect to token endpoint and exchange code/verifier
1 parent 6c433fa commit e3fb264

9 files changed

+134
-8
lines changed

src/MetadataService.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ export class MetadataService {
7070
return this._getMetadataProperty("userinfo_endpoint");
7171
}
7272

73-
getTokenEndpoint() {
74-
return this._getMetadataProperty("token_endpoint", true);
73+
getTokenEndpoint(optional=true) {
74+
return this._getMetadataProperty("token_endpoint", optional);
7575
}
7676

7777
getCheckSessionIframe() {

src/ResponseValidator.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@
44
import { Log } from './Log';
55
import { MetadataService } from './MetadataService';
66
import { UserInfoService } from './UserInfoService';
7+
import { TokenClient } from './TokenClient';
78
import { ErrorResponse } from './ErrorResponse';
89
import { JoseUtil } from './JoseUtil';
910

1011
const ProtocolClaims = ["nonce", "at_hash", "iat", "nbf", "exp", "aud", "iss", "c_hash"];
1112

1213
export class ResponseValidator {
1314

14-
constructor(settings, MetadataServiceCtor = MetadataService, UserInfoServiceCtor = UserInfoService, joseUtil = JoseUtil) {
15+
constructor(settings,
16+
MetadataServiceCtor = MetadataService,
17+
UserInfoServiceCtor = UserInfoService,
18+
joseUtil = JoseUtil,
19+
TokenClientCtor = TokenClient) {
1520
if (!settings) {
1621
Log.error("ResponseValidator.ctor: No settings passed to ResponseValidator");
1722
throw new Error("settings");
@@ -21,6 +26,7 @@ export class ResponseValidator {
2126
this._metadataService = new MetadataServiceCtor(this._settings);
2227
this._userInfoService = new UserInfoServiceCtor(this._settings);
2328
this._joseUtil = joseUtil;
29+
this._tokenClient = new TokenClientCtor(this._settings);
2430
}
2531

2632
validateSigninResponse(state, response) {
@@ -114,6 +120,16 @@ export class ResponseValidator {
114120
return Promise.reject(new Error("Unexpected id_token in response"));
115121
}
116122

123+
if (state.code_verifier && !response.code) {
124+
Log.error("ResponseValidator._processSigninParams: Expecting code in response");
125+
return Promise.reject(new Error("No code in response"));
126+
}
127+
128+
if (!state.code_verifier && response.code) {
129+
Log.error("ResponseValidator._processSigninParams: Not expecting code in response");
130+
return Promise.reject(new Error("Unexpected code in response"));
131+
}
132+
117133
return Promise.resolve(response);
118134
}
119135

@@ -209,10 +225,27 @@ export class ResponseValidator {
209225
return this._validateIdToken(state, response);
210226
}
211227

228+
if (response.code) {
229+
Log.debug("ResponseValidator._validateTokens: Validating code");
230+
return this._validateCode(state, response);
231+
}
232+
212233
Log.debug("ResponseValidator._validateTokens: No id_token to validate");
213234
return Promise.resolve(response);
214235
}
215236

237+
_validateCode(state, response) {
238+
var args = {
239+
code : response.code,
240+
redirect_uri: state.redirect_uri,
241+
code_verifier: state.code_verifier,
242+
};
243+
return this._tokenClient.exchangeCode(args).then(tokenResponse => {
244+
response.id_token = tokenResponse.id_token;
245+
response.access_token = tokenResponse.access_token;
246+
});
247+
}
248+
216249
_validateIdTokenAndAccessToken(state, response) {
217250
return this._validateIdToken(state, response).then(response => {
218251
return this._validateAccessToken(response);

src/SigninRequest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class SigninRequest {
4040

4141
let oidc = SigninRequest.isOidc(response_type);
4242
let code = SigninRequest.isCode(response_type);
43-
this.state = new SigninState({ nonce: oidc, data, client_id, authority, code_verifier: code });
43+
this.state = new SigninState({ nonce: oidc, data, client_id, authority, redirect_uri, code_verifier: code });
4444

4545
url = UrlUtility.addQueryParam(url, "client_id", client_id);
4646
url = UrlUtility.addQueryParam(url, "redirect_uri", redirect_uri);

src/SigninResponse.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class SigninResponse {
1414
this.error_description = values.error_description;
1515
this.error_uri = values.error_uri;
1616

17+
this.code = values.code;
1718
this.state = values.state;
1819
this.id_token = values.id_token;
1920
this.session_state = values.session_state;

src/SigninState.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { JoseUtil } from './JoseUtil';
77
import random from './random';
88

99
export class SigninState extends State {
10-
constructor({nonce, authority, client_id, code_verifier} = {}) {
10+
constructor({nonce, authority, client_id, redirect_uri, code_verifier} = {}) {
1111
super(arguments[0]);
1212

1313
if (nonce === true) {
@@ -18,16 +18,19 @@ export class SigninState extends State {
1818
}
1919

2020
if (code_verifier === true) {
21-
this._code_verifier = random();
21+
// random() produces 32 length
22+
this._code_verifier = random() + random() + random();
2223
}
2324
else if (code_verifier) {
2425
this._code_verifier = code_verifier;
2526
}
2627

2728
if (this.code_verifier) {
28-
this._code_challenge = JoseUtil.hashString(this.code_verifier, "SHA256");
29+
let hash = JoseUtil.hashString(this.code_verifier, "SHA256");
30+
this._code_challenge = JoseUtil.hexToBase64Url(hash);
2931
}
3032

33+
this._redirect_uri = redirect_uri;
3134
this._authority = authority;
3235
this._client_id = client_id;
3336
}
@@ -41,6 +44,9 @@ export class SigninState extends State {
4144
get client_id() {
4245
return this._client_id;
4346
}
47+
get redirect_uri() {
48+
return this._redirect_uri;
49+
}
4450
get code_verifier() {
4551
return this._code_verifier;
4652
}
@@ -56,6 +62,7 @@ export class SigninState extends State {
5662
created: this.created,
5763
nonce: this.nonce,
5864
code_verifier: this.code_verifier,
65+
redirect_uri: this.redirect_uri,
5966
authority: this.authority,
6067
client_id: this.client_id
6168
});

src/TokenClient.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
import { JsonService } from './JsonService';
5+
import { MetadataService } from './MetadataService';
6+
import { Log } from './Log';
7+
8+
export class TokenClient {
9+
constructor(settings, JsonServiceCtor = JsonService, MetadataServiceCtor = MetadataService) {
10+
if (!settings) {
11+
Log.error("TokenClient.ctor: No settings passed");
12+
throw new Error("settings");
13+
}
14+
15+
this._settings = settings;
16+
this._jsonService = new JsonServiceCtor();
17+
this._metadataService = new MetadataServiceCtor(this._settings);
18+
}
19+
20+
exchangeCode(args = {}) {
21+
args.client_id = args.client_id || settings.client_id;
22+
args.grant_type = args.grant_type || "authorization_code";
23+
24+
if (!args.code) {
25+
Log.error("TokenClient.exchangeCode: No code passed");
26+
return Promise.reject(new Error("A code is required"));
27+
}
28+
if (!args.redirect_uri) {
29+
Log.error("TokenClient.exchangeCode: No redirect_uri passed");
30+
return Promise.reject(new Error("A redirect_uri is required"));
31+
}
32+
if (!args.code_verifier) {
33+
Log.error("TokenClient.exchangeCode: No code_verifier passed");
34+
return Promise.reject(new Error("A code_verifier is required"));
35+
}
36+
if (!args.client_id) {
37+
Log.error("TokenClient.exchangeCode: No client_id passed");
38+
return Promise.reject(new Error("A client_id is required"));
39+
}
40+
41+
return this._metadataService.getTokenEndpoint(false).then(url => {
42+
Log.debug("TokenClient.exchangeCode: Received token endpoint");
43+
44+
return this._jsonService.postForm(url, args).then(response => {
45+
Log.debug("TokenClient.exchangeCode: response received", response);
46+
return response;
47+
});
48+
});
49+
}
50+
}

test/unit/ResponseValidator.spec.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,31 @@ describe("ResponseValidator", function () {
397397

398398
});
399399

400+
it("should fail if request was code flow but no code in response", function (done) {
401+
402+
stubResponse.id_token = id_token;
403+
stubState.code_verifier = "secret";
404+
delete stubResponse.code;
405+
406+
subject._processSigninParams(stubState, stubResponse).then(null, err => {
407+
err.message.should.contain("code");
408+
done();
409+
});
410+
411+
});
412+
413+
it("should fail if request was not code flow no code in response", function (done) {
414+
415+
stubResponse.id_token = id_token;
416+
stubResponse.code = "code";
417+
418+
subject._processSigninParams(stubState, stubResponse).then(null, err => {
419+
err.message.should.contain("code");
420+
done();
421+
});
422+
423+
});
424+
400425
it("should return data for successful responses", function (done) {
401426

402427
stubResponse.id_token = id_token;

test/unit/SigninResponse.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ describe("SigninResponse", function () {
3333
subject.state.should.equal("foo");
3434
});
3535

36+
it("should read code", function () {
37+
let subject = new SigninResponse("code=foo");
38+
subject.code.should.equal("foo");
39+
});
40+
3641
it("should read id_token", function () {
3742
let subject = new SigninResponse("id_token=foo");
3843
subject.id_token.should.equal("foo");

test/unit/SigninState.spec.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ describe("SigninState", function() {
3434
subject.nonce.should.be.ok;
3535
});
3636

37+
it("should accept redirect_uri", function() {
38+
var subject = new SigninState({ redirect_uri: "http://cb" });
39+
subject.redirect_uri.should.be.equal("http://cb");
40+
});
41+
3742
it("should accept code_verifier", function() {
3843
var subject = new SigninState({ code_verifier: 5 });
3944
subject.code_verifier.should.be.equal(5);
@@ -61,7 +66,7 @@ describe("SigninState", function() {
6166
});
6267

6368
it("can serialize and then deserialize", function() {
64-
var subject1 = new SigninState({ nonce: true, data: { foo: "test" }, created: 1000, client_id:"client", authority:"authority" });
69+
var subject1 = new SigninState({ nonce: true, data: { foo: "test" }, created: 1000, client_id:"client", authority:"authority", redirect_uri:"http://cb", code_verifier:true });
6570

6671
var storage = subject1.toStorageString();
6772
var subject2 = SigninState.fromStorageString(storage);

0 commit comments

Comments
 (0)