Skip to content

Commit 623f5e2

Browse files
committed
add structure for response processing/validation
1 parent 1059ebc commit 623f5e2

13 files changed

Lines changed: 622 additions & 209 deletions

src/OidcClientService.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import Log from './Log';
22
import OidcClientSettings from './OidcClientSettings';
33
import MetadataService from './MetadataService';
44
import SigninRequest from './SigninRequest';
5+
import SigninResponse from './SigninResponse';
56
import SignoutRequest from './SignoutRequest';
67
import WebStorageStateStore from './WebStorageStateStore';
8+
import ResponseValidator from './ResponseValidator';
79

810
export default class OidcClientService {
911
constructor(settings,
1012
stateStore = new WebStorageStateStore(),
13+
ResponseValidatorCtor = ResponseValidator,
1114
MetadataServiceCtor = MetadataService
1215
){
1316
if (!settings) {
@@ -17,6 +20,7 @@ export default class OidcClientService {
1720

1821
this._settings = new OidcClientSettings(settings);
1922
this._stateStore = stateStore;
23+
this._validator = new ResponseValidatorCtor(this._settings);
2024
this._metadataService = new MetadataServiceCtor(this._settings);
2125
}
2226

@@ -67,6 +71,28 @@ export default class OidcClientService {
6771

6872
processSigninResponse(url){
6973
Log.info("OidcClientService.processSigninResponse");
74+
75+
var response = new SigninResponse(url);
76+
if (!response.state) {
77+
Log.error("No state in response");
78+
return Promise.reject(new Error("No state in response"));
79+
}
80+
81+
var stateKey = response.state;
82+
83+
return this._stateStore.remove(stateKey).then(state => {
84+
if (!state){
85+
Log.error("No matching state found in storage");
86+
throw new Error("Failed to process response");
87+
}
88+
89+
Log.info("Received state from storage; validating response");
90+
return this._validator.validateSigninResponse(state, response);
91+
92+
}, err => {
93+
Log.error("Failed to process response", err);
94+
return Promise.reject(new Error("Failed to process response"));
95+
});
7096
}
7197

7298
createSignoutRequest({id_token_hint, data, post_logout_redirect_uri}={}){
@@ -95,6 +121,7 @@ export default class OidcClientService {
95121

96122
processSignoutResponse(url){
97123
Log.info("OidcClientService.processSignoutResponse");
124+
98125
}
99126

100127
// OidcClient.prototype.processResponseAsync = function (queryString) {

src/OidcClientSettings.js

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export default class OidcClientSettings {
1414
// optional protocol
1515
prompt, display, max_age, ui_locales, acr_values,
1616
// behavior flags
17-
filterProtocolClaims = true
17+
filterProtocolClaims = true, loadUserInfo = true
18+
1819
}) {
1920
if (!client_id) {
2021
Log.error("No client_id on settings passed to OidcClientSettings");
@@ -39,10 +40,10 @@ export default class OidcClientSettings {
3940
this._acr_values = acr_values;
4041

4142
this._filterProtocolClaims = !!filterProtocolClaims;
43+
this._loadUserInfo = !!loadUserInfo;
4244
}
4345

44-
// config values
45-
// client
46+
// client config
4647
get client_id() {
4748
return this._client_id;
4849
}
@@ -59,6 +60,7 @@ export default class OidcClientSettings {
5960
return this._post_logout_redirect_uri;
6061
}
6162

63+
6264
// optional protocol params
6365
get prompt(){
6466
return this._prompt;
@@ -76,6 +78,7 @@ export default class OidcClientSettings {
7678
return this._acr_values;
7779
}
7880

81+
7982
// metadata
8083
get authority() {
8184
return this._authority;
@@ -95,12 +98,7 @@ export default class OidcClientSettings {
9598
return this._metadataUrl;
9699
}
97100

98-
// behavior
99-
get filterProtocolClaims() {
100-
return this._filterProtocolClaims;
101-
}
102-
103-
// settable/cachable values
101+
// settable/cachable metadata values
104102
get metadata() {
105103
return this._metadata;
106104
}
@@ -114,4 +112,13 @@ export default class OidcClientSettings {
114112
set signingKeys(value) {
115113
this._signingKeys = value;
116114
}
115+
116+
117+
// behavior flags
118+
get filterProtocolClaims() {
119+
return this._filterProtocolClaims;
120+
}
121+
get loadUserInfo() {
122+
return this._loadUserInfo;
123+
}
117124
}

src/ResponseValidator.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import Log from './Log';
2+
import MetadataService from './MetadataService';
3+
import UserInfoService from './UserInfoService';
4+
5+
const ProtocolClaims = ["nonce", "at_hash", "iat", "nbf", "exp", "aud", "iss", "c_hash"];
6+
7+
export default class ResponseValidator {
8+
9+
constructor(settings, MetadataServiceCtor = MetadataService, UserInfoServiceCtor = UserInfoService) {
10+
if (!settings) {
11+
Log.error("No settings passed to ResponseValidator");
12+
throw new Error("settings");
13+
}
14+
15+
this._settings = settings;
16+
this._metadataService = new MetadataServiceCtor(this._settings);
17+
this._userInfoService = new UserInfoServiceCtor(this._settings);
18+
}
19+
20+
validateSigninResponse(state, response) {
21+
Log.info("ResponseValidator.validateSigninResponse");
22+
23+
if (state.id !== response.state) {
24+
Log.error("State does not match");
25+
return Promise.reject(new Error("State does not match"));
26+
}
27+
28+
// now that we know the state matches, take the stored data
29+
// and set it into the response so callers can get their state
30+
// this is important for both success & error outcomes
31+
response.state = state.data;
32+
33+
if (response.error) {
34+
Log.warn("Response was error", response.error);
35+
return Promise.resolve(response);
36+
}
37+
38+
if (state.nonce && !response.id_token) {
39+
Log.error("Expecting id_token in response");
40+
return Promise.reject(new Error("No id_token in response"));
41+
}
42+
43+
let p = null;
44+
45+
if (response.id_token) {
46+
if (response.access_token) {
47+
Log.info("Validating id_token and access_token");
48+
p = validateIdTokenAndAccessToken(state, response);
49+
}
50+
else {
51+
Log.info("Validating id_token");
52+
p = validateIdToken(state, response);
53+
}
54+
}
55+
else {
56+
Log.info("No id_token to validate");
57+
p = Promise.resolve(response);
58+
}
59+
60+
return p.then(response => {
61+
response.profile = this.filterProtocolClaims(response.profile);
62+
63+
// if (this._settings.loadUserInfo) {
64+
65+
// Log.info("loading user info");
66+
// return this._userInfoService.getClaims(response.access_token).then(claims => {
67+
68+
// Log.info("user info claims received");
69+
// response.profile = this.mergeClaims(response.profile, claims);
70+
71+
// return response;
72+
// });
73+
// }
74+
75+
return response;
76+
});
77+
}
78+
79+
// mergeClaims(claims1, claims2){
80+
// return claims1;
81+
// }
82+
83+
filterProtocolClaims(claims){
84+
if (claims && this._settings.filterProtocolClaims) {
85+
ProtocolClaims.forEach(type => {
86+
delete claims[type];
87+
});
88+
89+
Log.info("protocol claims filtered", claims);
90+
}
91+
return claims;
92+
}
93+
94+
validateIdTokenAndAccessToken(state, response) {
95+
return this.validateIdToken(state, response).then(response => {
96+
return this.validateAccessToken(response);
97+
});
98+
}
99+
100+
validateIdToken(state, response) {
101+
return Promise.resolve(response);
102+
103+
// log("OidcClient.validateIdTokenAsync");
104+
105+
// var client = this;
106+
// var settings = client._settings;
107+
108+
// return client.loadX509SigningKeyAsync().then(function (cert) {
109+
110+
// var jws = new KJUR.jws.JWS();
111+
// if (jws.verifyJWSByPemX509Cert(id_token, cert)) {
112+
// var id_token_contents = JSON.parse(jws.parsedJWS.payloadS);
113+
114+
// if (nonce !== id_token_contents.nonce) {
115+
// return error("Invalid nonce");
116+
// }
117+
118+
// return client.loadMetadataAsync().then(function (metadata) {
119+
120+
// if (id_token_contents.iss !== metadata.issuer) {
121+
// return error("Invalid issuer");
122+
// }
123+
124+
// if (id_token_contents.aud !== settings.client_id) {
125+
// return error("Invalid audience");
126+
// }
127+
128+
// var now = parseInt(Date.now() / 1000);
129+
130+
// // accept tokens issues up to 5 mins ago
131+
// var diff = now - id_token_contents.iat;
132+
// if (diff > (5 * 60)) {
133+
// return error("Token issued too long ago");
134+
// }
135+
136+
// if (id_token_contents.exp < now) {
137+
// return error("Token expired");
138+
// }
139+
140+
// if (access_token && settings.load_user_profile) {
141+
// // if we have an access token, then call user info endpoint
142+
// return client.loadUserProfile(access_token, id_token_contents).then(function (profile) {
143+
// return copy(profile, id_token_contents);
144+
// });
145+
// }
146+
// else {
147+
// // no access token, so we have all our claims
148+
// return id_token_contents;
149+
// }
150+
151+
// });
152+
// }
153+
// else {
154+
// return error("JWT failed to validate");
155+
// }
156+
157+
// });
158+
}
159+
160+
validateAccessToken(response) {
161+
return Promise.resolve(response);
162+
163+
// log("OidcClient.validateAccessTokenAsync");
164+
165+
// if (!id_token_contents.at_hash) {
166+
// return error("No at_hash in id_token");
167+
// }
168+
169+
// var hash = KJUR.crypto.Util.sha256(access_token);
170+
// var left = hash.substr(0, hash.length / 2);
171+
// var left_b64u = hextob64u(left);
172+
173+
// if (left_b64u !== id_token_contents.at_hash) {
174+
// return error("at_hash failed to validate");
175+
// }
176+
177+
// return resolve();
178+
}
179+
}

src/SigninResponse.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import Log from './Log';
2+
import UrlUtility from './UrlUtility';
3+
4+
export default class SigninResponse {
5+
constructor(url) {
6+
7+
var values = UrlUtility.parseUrlFragment(url);
8+
9+
this._state = values.state;
10+
11+
if (values.error){
12+
this._error = values.error;
13+
this._error_description = values.error_description;
14+
this._error_uri = values.error_uri;
15+
}
16+
else {
17+
this._id_token = values.id_token;
18+
this._session_state = values.session_state;
19+
this._access_token = values.access_token;
20+
this._token_type = values.token_type;
21+
this._scope = values.scope;
22+
23+
let expires_in = parseInt(values.expires_in);
24+
if (typeof expires_in === 'number' && expires_in > 0){
25+
let now = parseInt(Date.now() / 1000);
26+
this._expires_at = now + expires_in;
27+
}
28+
}
29+
}
30+
31+
get state(){
32+
return this._state;
33+
}
34+
set state(value){
35+
this._state = value;
36+
}
37+
38+
get error(){
39+
return this._error;
40+
}
41+
get error_description(){
42+
return this._error_description;
43+
}
44+
get error_uri(){
45+
return this._error_uri;
46+
}
47+
48+
get id_token(){
49+
return this._id_token;
50+
}
51+
get session_state(){
52+
return this._session_state;
53+
}
54+
get access_token(){
55+
return this._access_token;
56+
}
57+
get token_type(){
58+
return this._token_type;
59+
}
60+
get scope(){
61+
return this._scope;
62+
}
63+
get expires_in(){
64+
if (this.expires_at){
65+
let now = parseInt(Date.now() / 1000);
66+
return this.expires_at - now;
67+
}
68+
return undefined;
69+
}
70+
get expires_at(){
71+
return this._expires_at;
72+
}
73+
74+
get profile(){
75+
return this._profile;
76+
}
77+
set profile(value){
78+
this._profile = value;
79+
}
80+
}

0 commit comments

Comments
 (0)