Skip to content

Commit f29b9d9

Browse files
authored
Feature/72 (#86)
* OAuth2 Implicit Grant call is working again. Token Introspection page works with implicit grant results. * All functionality working with the OAuth2 Authorization Grant. * OAuth2 Implicity Grant automated test. * Docker-based test suite is now working with the OAuth2 Implicit Grant. * Added logout test to the OAuth2 Implicit Grant test. * Updating docker-compose file name for containerized tests. Updated environment file path. * Adding repo checkout back into tests pipeline. * Added logout step to OAuth2 Authorization Code test. * OIDC Authorization Code Flow (Public Client) test working with token detail page tests for all token types. * Refresh Token control and results display now works when you return from token_detail or userinfo views. Userinfo has a backend processing option and works correctly with POST method. * Updated oidc_authorization_code.js test script to make a token refresh call and validate the tokens received. * Successful local test of oidc_authorization_code. * Tests that currently work. * Copy new test files for selelium tests.
1 parent 11d2547 commit f29b9d9

27 files changed

+2358
-778
lines changed

api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "1.0.0",
44
"private": true,
55
"description": "idptools client project",
6-
"author": "Robert C. Broeckelmann Jr. <robert@iyasec.io>",
6+
"author": "Robert C. Broeckelmann Jr. <info@iyasec.io>",
77
"scripts": {
88
"start": "node server.js"
99
},

api/server.js

Lines changed: 215 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
// File: server.js
2-
// Author: Robert C. Broeckelmann Jr.
3-
// Date: 05/31/2020
4-
// Notes:
5-
//
61
'use strict';
72

83
var appconfig = require(process.env.CONFIG_FILE);
@@ -20,17 +15,31 @@ const HOST = appconfig.host || '0.0.0.0';
2015
const LOG_LEVEL = appconfig.logLevel || 'debug';
2116
const uiUrl = appconfig.uiUrl || 'http://localhost:3000';
2217

18+
const STATUS_200 = 200;
19+
const STATUS_400 = 400;
20+
const STATUS_401 = 401;
21+
const STATUS_403 = 403;
22+
const STATUS_404 = 404;
23+
const STATUS_500 = 500;
24+
2325
var log = bunyan.createLogger({ name: 'server',
2426
level: LOG_LEVEL });
2527
log.info("Log initialized. logLevel=" + log.level());
2628

29+
var claimDescriptions = "";
30+
var cachedClaimDescriptions = false;
31+
2732
const app = express();
2833
const expressSwagger = require('express-swagger-generator')(app);
2934

3035
app.use(bodyParser.json());
36+
var corsOptions = {
37+
origin: '*',
38+
optionsSuccessStatus: 204
39+
};
3140
// app.use(expressLogging(logger));
32-
app.options('*', cors());
33-
app.use(cors());
41+
app.options("*", cors(corsOptions));
42+
app.use(cors(corsOptions));
3443

3544
/**
3645
* @typedef HealthcheckResponse
@@ -45,28 +54,70 @@ app.use(cors());
4554
* @returns {Error.model} 500 - Unexpected error
4655
*/
4756
app.get('/healthcheck', function (req, res) {
48-
res.json({ message: 'Success' });
57+
res
58+
.status(STATUS_200)
59+
.json({ message: 'Success' });
4960
});
5061

5162
/**
52-
* System healthcheck
63+
* Retrieve Claims Description.
5364
* @route GET /claimdescription
5465
* @group Metadata - Support operations
5566
* @returns {HealthcheckResponse.model} 200 - Claim Description Response
5667
* @returns {Error.model} 400 - Syntax error
5768
* @returns {Error.model} 500 - Unexpected error
5869
*/
5970
app.get('/claimdescription', function(req, res) {
60-
fetch("https://www.iana.org/assignments/jwt/jwt.xml")
61-
.then((response) => {
62-
response.text()
63-
.then( (text) => {
64-
log.debug("Retrieved: " + text);
71+
console.log("Entering GET /claimdescription.");
72+
try {
73+
if(cachedClaimDescriptions) {
74+
console.debug("Using cached claim descriptions.");
6575
res
6676
.append('Content-Type', 'application/xml')
67-
.send(text)
77+
.status(STATUS_200)
78+
.send(claimDescriptions);
79+
} else {
80+
log.debug("Pulling claim descriptions");
81+
fetch("https://www.iana.org/assignments/jwt/jwt.xml")
82+
.then((response) => {
83+
response
84+
.text()
85+
.then( (text) => {
86+
log.debug("Retrieved: " + text);
87+
res
88+
.append('Content-Type', 'application/xml')
89+
.send(text);
90+
cachedClaimDescriptions = true;
91+
claimDescriptions = text;
92+
});
93+
})
94+
.catch(function (error) {
95+
log.error('Error from claimsdescription endpoint: ' + error.stack);
96+
if(!!error.response) {
97+
if(!!error.response.status) {
98+
log.error("Error Status: " + error.response.status);
99+
}
100+
if(!!error.response.data) {
101+
log.error("Error Response body: " + JSON.stringify(error.response.data));
102+
}
103+
if(!!error.response.headers) {
104+
log.error("Error Response headers: " + error.response.headers);
105+
}
106+
if (!!error.response) {
107+
res.status(error.response.status);
108+
res.json(error.response.data);
109+
} else {
110+
res.status(STATUS_500);
111+
res.json(error.message);
112+
}
113+
}
68114
});
69-
});
115+
}
116+
} catch(e) {
117+
log.error("An error occurred while retrieving the claim description XML: " + e.stack);
118+
res.status(STATUS_500)
119+
.render('error', { error: e });
120+
}
70121
});
71122

72123
/**
@@ -176,11 +227,158 @@ app.post('/token', (req, res) => {
176227
});
177228
} catch (e) {
178229
log.error('An error occurred: ' + e);
179-
res.status(500);
230+
res.status(STATUS_500);
180231
res.json({ "error": e });
181232
}
182233
});
183234

235+
/**
236+
* @typedef IntrospectionRequest
237+
* @property {string} grant_type.required - The OAuth2 / OIDC Grant / Flow Type
238+
* @property {string} client_id.required - The OAuth2 client identifier
239+
*/
240+
241+
/**
242+
* @typedef IntrospectionResponse
243+
* @property {string} access_token.required - The OAuth2 Access Token
244+
* @property {string} id_token - The OpenID Connect ID Token
245+
*/
246+
247+
/**
248+
* Wrapper around OAuth2 Introspection Endpoint
249+
* @route POST /introspection
250+
* @group Debugger - Operations for OAuth2/OIDC Debugger
251+
* @param {IntrospectionRequest.model} req.body.required - Token Endpoint Request
252+
* @returns {IntrospectionResponse.model} 200 - Token Endpoint Response
253+
* @returns {Error.model} 400 - Syntax error
254+
* @returns {Error.model} 500 - Unexpected error
255+
*/
256+
app.post('/introspection', (req, res) => {
257+
try {
258+
log.info('Entering app.post for /introspection.');
259+
const body = req.body;
260+
log.debug('body: ' + JSON.stringify(body));
261+
var headers = {
262+
"Authorization": req.headers.authorization,
263+
"Content-Type": "application/x-www-form-urlencoded"
264+
};
265+
var introspectionRequestMessage = {
266+
token: body.token,
267+
token_type_hint: body.token_type_hint
268+
}
269+
const parameterString = JSON.stringify(introspectionRequestMessage);
270+
log.debug("Method: POST");
271+
log.debug("URL: " + body.introspectionEndpoint);
272+
log.debug("headers: " + JSON.stringify(headers));
273+
log.debug("body: " + parameterString);
274+
axios({
275+
method: 'post',
276+
url: body.introspectionEndpoint,
277+
headers: headers,
278+
data: introspectionRequestMessage,
279+
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: true })
280+
})
281+
.then(function (response) {
282+
log.debug('Response from OAuth2 Introspection Endpoint: ' + JSON.stringify(response.data));
283+
log.debug('Headers: ' + response.headers);
284+
res.status(response.status);
285+
res.json(response.data);
286+
})
287+
.catch(function (error) {
288+
log.error('Error from OAuth2 Introspection Endpoint: ' + error);
289+
if(!!error.response) {
290+
if(!!error.response.status) {
291+
log.error("Error Status: " + error.response.status);
292+
}
293+
if(!!error.response.data) {
294+
log.error("Error Response body: " + JSON.stringify(error.response.data));
295+
}
296+
if(!!error.response.headers) {
297+
log.error("Error Response headers: " + error.response.headers);
298+
}
299+
if (!!error.response) {
300+
res.status(error.response.status);
301+
res.json(error.response.data);
302+
} else {
303+
res.status(STATUS_500);
304+
res.json(error.message);
305+
}
306+
}
307+
});
308+
} catch(e) {
309+
log.error("Error from OAuth2 Introspection Endpoint: " + error);
310+
}
311+
});
312+
313+
app.post('/userinfo', (req, res) => {
314+
log.info('Entering app.post for /userinfo.');
315+
userinfo_common(req, res);
316+
log.debug("Leaving app.post for /userinfo.");
317+
});
318+
319+
/**
320+
* Wrapper around OIDC UserInfo Endpoint
321+
* @route POST /userinfo
322+
* @group Debugger - Operations for OAuth2/OIDC Debugger
323+
* @param {UserInfoRequest.model} req.body.required - UserInfo Endpoint Request
324+
* @returns {UserInfoResponse.model} 200 - UserInfo Endpoint Response
325+
* @returns {Error.model} 400 - Syntax error
326+
* @returns {Error.model} 500 - Unexpected error
327+
*/
328+
app.get('/userinfo', (req, res) => {
329+
log.info("Entering app.get for /userinfo.");
330+
userinfo_common(req, res);
331+
log.debug("Leaving app.get for /userinfo.");
332+
});
333+
334+
function userinfo_common(req, res) {
335+
try {
336+
log.info('Entering app.get for /userinfo.');
337+
var headers = {
338+
"Authorization": req.headers.authorization,
339+
};
340+
// All types of requests are converted to GET.
341+
log.debug("Method: GET");
342+
log.debug("URL: " + Buffer.from(req.query.userinfo_endpoint, 'base64').toString('utf-8'));
343+
log.debug("headers: " + JSON.stringify(headers));
344+
axios({
345+
method: 'get',
346+
url: Buffer.from(req.query.userinfo_endpoint, 'base64').toString('utf-8'),
347+
headers: headers,
348+
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: true })
349+
})
350+
.then(function (response) {
351+
log.debug('Response from OIDC UserInfo Endpoint: ' + JSON.stringify(response.data));
352+
log.debug('Headers: ' + response.headers);
353+
res.status(response.status);
354+
res.json(response.data);
355+
})
356+
.catch(function (error) {
357+
log.error('Error from OIDC UserInfo Endpoint: ' + error);
358+
if(!!error.response) {
359+
if(!!error.response.status) {
360+
log.error("Error Status: " + error.response.status);
361+
}
362+
if(!!error.response.data) {
363+
log.error("Error Response body: " + JSON.stringify(error.response.data));
364+
}
365+
if(!!error.response.headers) {
366+
log.error("Error Response headers: " + error.response.headers);
367+
}
368+
if (!!error.response) {
369+
res.status(error.response.status);
370+
res.json(error.response.data);
371+
} else {
372+
res.status(STATUS_500);
373+
res.json(error.message);
374+
}
375+
}
376+
});
377+
} catch(e) {
378+
log.error("Error from OIDC UserInfo Endpoint: " + error);
379+
}
380+
}
381+
184382
let options = {
185383
swaggerDefinition: {
186384
info: {
@@ -200,7 +398,6 @@ let options = {
200398
basedir: __dirname, //app absolute path
201399
files: ['server.js'] //Path to the API handle folder
202400
};
203-
204401
expressSwagger(options)
205402
app.listen(PORT, HOST);
206403
log.info(`Running on http://${HOST}:${PORT}`);

client/public/debugger.html

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,22 @@
2121
<body>
2222

2323
<div class="header_debugger">
24-
<a href="/index.html" class="logo">IDPTools</a>
24+
<a href="/index.html" class="logo" onclick="return debugger.clickLink();">IDPTools</a>
2525
<div class="header-right">
26-
<a class="active" href="#home">Home</a>
27-
<a href="#contact">Contact</a>
28-
<a href="#about">About</a>
26+
<a class="active" href="#home" onclick="return debugger.clickLink();">Home</a>
27+
<a href="#contact" onclick="return debugger.clickLink();">Contact</a>
28+
<a href="#about" onclick="return debugger.clickLink();">About</a>
2929
</div>
3030
</div>
3131

3232
<div class="container">
3333

3434
<script type="text/javascript" src="/js/debugger.js">
3535
</script>
36-
<p>This is a simple Client to use with any <a href="https://medium.com/@robert.broeckelmann/saml2-vs-jwt-understanding-oauth2-4abde9e7ec8b">OAuth2</a> or <a href="https://medium.com/@robert.broeckelmann/understanding-openid-connect-series-37c93d25e92b">OpenID Connect</a> compliant identity provider..
36+
<p>This is a simple Client to use with any <a href="https://medium.com/@robert.broeckelmann/saml2-vs-jwt-understanding-oauth2-4abde9e7ec8b" onclick="return debugger.clickLink();">OAuth2</a> or <a href="https://medium.com/@robert.broeckelmann/understanding-openid-connect-series-37c93d25e92b" onclick="return debugger.clickLink();">OpenID Connect</a> compliant identity provider.
3737
</p>
38+
<P><a href="https://www.rfc-editor.org/rfc/rfc6749" onclick="return debugger.clickLink();">OAuth2 RFC</a></P>
39+
<P><a hreef="https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest">OIDC Spec</a></P>
3840
<p>This page manages interaction with the OAuth2 Authorization Endpoint.</p>
3941
<div class="step0" id="step0">
4042
<legend>OpenID Connect Discovery Endpoint Information
@@ -47,7 +49,7 @@
4749
<table border="0">
4850
<tr>
4951
<td>&nbsp;</td>
50-
<td><p>An OIDC Discovery endpoint uses a path that ends in /.well-known/openid-configuration. See the <a href="https://openid.net/specs/openid-connect-discovery-1_0.html">spec</spec>.<p></td>
52+
<td><p>An OIDC Discovery endpoint uses a path that ends in /.well-known/openid-configuration. See the <a href="https://openid.net/specs/openid-connect-discovery-1_0.html" onclick="return debugger.clickLink();">spec</a>.<p></td>
5153
</tr>
5254
<tr>
5355
<td>
@@ -149,7 +151,7 @@
149151
</div>
150152
</td>
151153
<td>
152-
<input class="stored" id="jwks_endpoint" name="jwks_endpoint" type="text" max="150" /><a href="/jwks.html">Review JWKS meta data</a>
154+
<input class="stored" id="jwks_endpoint" name="jwks_endpoint" type="text" max="150" /><a href="/jwks.html" onclick="return debugger.clickLink();">Review JWKS meta data</a>
153155
</td>
154156
</tr>
155157
<tr>
@@ -159,7 +161,7 @@
159161
</td>
160162
<td>Yes
161163
<input checked="true" id="yesCheckOIDCArtifacts" name="yesno" onclick="debug.displayOIDCArtifacts();" type="radio" value="true" />No
162-
<input id="noCheckOIDCArtifacts" name="yesno" onclick="debug.displayOIDCArtifacts();" type="radio" value="false" />
164+
<input checked="false" id="noCheckOIDCArtifacts" name="yesno" onclick="debug.displayOIDCArtifacts();" type="radio" value="false" />
163165
</td>
164166
</tr>
165167
<tr>
@@ -169,7 +171,7 @@
169171
</td>
170172
<td>Yes
171173
<input checked="true" id="SSLValidate-yes" name="sslValidation" type="radio" value="true" />No
172-
<input id="SSLValidate-no" name="sslValidation" type="radio" value="false" />
174+
<input checked="false" id="SSLValidate-no" name="sslValidation" type="radio" value="false" />
173175
</td>
174176
</tr>
175177
<tr>
@@ -179,7 +181,7 @@
179181
</td>
180182
<td>Yes
181183
<input checked="true" id="useRefreshToken-yes" name="useRefreshToken" onclick="debug.useRefreshTokens();" type="radio" value="true" />No
182-
<input id="useRefreshToken-no" name="useRefreshToken" onclick="debug.useRefreshTokens();" type="radio" value="false" />
184+
<input checked="false" id="useRefreshToken-no" name="useRefreshToken" onclick="debug.useRefreshTokens();" type="radio" value="false" />
183185
</td>
184186
</tr>
185187
<tr>
@@ -189,7 +191,7 @@
189191
</td>
190192
<td>Yes
191193
<input checked="true" id="usePKCE-yes" name="usePKCE" onclick="debug.usePKCERFC();" type="radio" value="true" />No
192-
<input id="usePKCE-no" name="usePKCE" onclick="debug.usePKCERFC();" type="radio" value="false" />
194+
<input checked="false" id="usePKCE-no" name="usePKCE" onclick="debug.usePKCERFC();" type="radio" value="false" />
193195
</td>
194196
</tr>
195197
</tbody>
@@ -357,11 +359,11 @@
357359
</div>
358360

359361
<div class="footer_debugger">
360-
<a href="/index.html" class="logo">IDPTools</a>
362+
<a href="/index.html" class="logo" onclick="return debugger.clickLink();">IDPTools</a>
361363
<div class="header-right">
362-
<a class="active" href="#home">Home</a>
363-
<a href="#contact">Contact</a>
364-
<a href="#about">About</a>
364+
<a class="active" href="#home" onclick="return debugger.clickLink();">Home</a>
365+
<a href="#contact" onclick="return debugger.clickLink();">Contact</a>
366+
<a href="#about" onclick="return debugger.clickLink();">About</a>
365367
</div>
366368
</div>
367369
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">

0 commit comments

Comments
 (0)