Skip to content

Commit c016866

Browse files
committed
feat: support for OpenID auto-discovery via domain, show callback URL in main settings page
1 parent 5f87b2d commit c016866

7 files changed

Lines changed: 153 additions & 32 deletions

File tree

lib/controllers.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
const fetch = require('node-fetch');
4+
35
const db = require.main.require('./src/database');
46
const slugify = require.main.require('./src/slugify');
57
const helpers = require.main.require('./src/controllers/helpers');
@@ -16,6 +18,23 @@ Controllers.renderAdminPage = async (req, res) => {
1618
});
1719
};
1820

21+
Controllers.getOpenIdMetadata = async (req, res) => {
22+
const { domain } = req.query;
23+
if (!domain) {
24+
return helpers.formatApiResponse(400, res);
25+
}
26+
27+
try {
28+
const url = new URL(`https://${domain}/.well-known/openid-configuration`);
29+
const response = await fetch(url);
30+
const { authorization_endpoint, token_endpoint, userinfo_endpoint } = await response.json();
31+
32+
helpers.formatApiResponse(200, res, { authorization_endpoint, token_endpoint, userinfo_endpoint });
33+
} catch (e) {
34+
helpers.formatApiResponse(400, res, new Error('Invalid domain supplied'));
35+
}
36+
};
37+
1938
Controllers.getStrategy = async (req, res) => {
2039
const name = slugify(req.params.name);
2140

library.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ OAuth.addRoutes = async ({ router, middleware }) => {
9494
middleware.admin.checkPrivileges,
9595
];
9696

97+
routeHelpers.setupApiRoute(router, 'get', '/oauth2-multiple/discover', middlewares, controllers.getOpenIdMetadata);
98+
9799
routeHelpers.setupApiRoute(router, 'post', '/oauth2-multiple/strategies', middlewares, controllers.editStrategy);
98100
routeHelpers.setupApiRoute(router, 'get', '/oauth2-multiple/strategies/:name', middlewares, controllers.getStrategy);
99101
routeHelpers.setupApiRoute(router, 'delete', '/oauth2-multiple/strategies/:name', middlewares, controllers.deleteStrategy);
@@ -115,6 +117,7 @@ OAuth.listStrategies = async () => {
115117
strategies.forEach((strategy, idx) => {
116118
strategy.name = names[idx];
117119
strategy.enabled = strategy.enabled === 'true';
120+
strategy.callbackUrl = `${nconf.get('url')}/auth/${names[idx]}/callback`;
118121
});
119122

120123
return strategies;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"dependencies": {
3131
"async": "^3.2.0",
3232
"eslint": "8.x",
33+
"node-fetch": "^2",
3334
"passport-oauth": "~1.0.0"
3435
},
3536
"nbbpm": {

static/lib/admin.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
// import * as settings from 'settings';
4-
import { confirm } from 'bootbox';
4+
import { alert, confirm } from 'bootbox';
55
import { get, post, del } from 'api';
66
import { error } from 'alerts';
77
import { render } from 'benchpress';
@@ -25,6 +25,7 @@ export function init() {
2525
title,
2626
message,
2727
callback: handleEditStrategy,
28+
onShown: handleAutoDiscovery,
2829
});
2930

3031
break;
@@ -39,6 +40,7 @@ export function init() {
3940
title,
4041
message,
4142
callback: handleEditStrategy,
43+
onShown: handleAutoDiscovery,
4244
});
4345

4446
break;
@@ -58,6 +60,19 @@ export function init() {
5860
handleDeleteStrategy.call(this, ok, name);
5961
},
6062
});
63+
64+
break;
65+
}
66+
67+
case 'callback-help': {
68+
alert({
69+
title: 'What is the callback URL?',
70+
message: `
71+
When you create a new OAuth2 client at the provider, you need to specify a callback URL.
72+
Each provider is unfamiliar with how individual clients handle the callback.
73+
NodeBB provides the following URL for you to enter into that configuration.
74+
`,
75+
});
6176
}
6277
}
6378
}
@@ -84,6 +99,48 @@ function handleEditStrategy(ok) {
8499
return false;
85100
}
86101

102+
function handleAutoDiscovery() {
103+
const modalEl = this;
104+
105+
const domainEl = modalEl.querySelector('#domain');
106+
const successEl = modalEl.querySelector('#discovery-success');
107+
const failureEl = modalEl.querySelector('#discovery-failure');
108+
const detailsEl = modalEl.querySelector('#details');
109+
const idEl = modalEl.querySelector('#id');
110+
if (![domainEl, successEl, failureEl, detailsEl].every(Boolean)) {
111+
return;
112+
}
113+
114+
domainEl.addEventListener('change', async () => {
115+
try {
116+
detailsEl.classList.add('opacity-50');
117+
successEl.classList.replace('d-flex', 'd-none');
118+
failureEl.classList.replace('d-flex', 'd-none');
119+
const { authorization_endpoint, token_endpoint, userinfo_endpoint } = await get(`/plugins/oauth2-multiple/discover?domain=${encodeURIComponent(domainEl.value)}`);
120+
121+
if (authorization_endpoint) {
122+
modalEl.querySelector('#authUrl').value = authorization_endpoint;
123+
}
124+
125+
if (token_endpoint) {
126+
modalEl.querySelector('#tokenUrl').value = token_endpoint;
127+
}
128+
129+
if (userinfo_endpoint) {
130+
modalEl.querySelector('#userRoute').value = userinfo_endpoint;
131+
}
132+
133+
successEl.classList.replace('d-none', 'd-flex');
134+
} catch (e) {
135+
console.log(e);
136+
failureEl.classList.replace('d-none', 'd-flex');
137+
} finally {
138+
detailsEl.classList.remove('opacity-50');
139+
idEl.focus();
140+
}
141+
});
142+
}
143+
87144
function handleDeleteStrategy(ok, name) {
88145
if (!ok) {
89146
return;

static/templates/admin/plugins/sso-oauth2-multiple.tpl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
<thead>
1616
<th>Name</th>
1717
<th>Enabled</th>
18+
<th>Callback URL <a href="#" data-action="callback-help"><i class="fa fa-question-circle"></i></a></th>
1819
<th><span class="visually-hidden">Actions</span></th>
1920
</thead>
2021
<tbody>
2122
{{{ if !strategies.length }}}
2223
<tr>
23-
<td colspan="3">
24+
<td colspan="4">
2425
<div class="alert alert-info text-center mb-0"><em>No OAuth2 endpoints configured.</em></div>
2526
</td>
2627
</tr>
@@ -31,6 +32,7 @@
3132
<td>
3233
{{{ if ./enabled }}}&check;{{{ else }}}&cross;{{{ end }}}
3334
</td>
35+
<td>{./callbackUrl}</td>
3436
<td class="text-end">
3537
<a href="#" data-action="edit">Edit</a>
3638
&nbsp;&nbsp;&nbsp;
@@ -41,7 +43,7 @@
4143
</tbody>
4244
<tfoot>
4345
<tr>
44-
<td colspan="3">
46+
<td colspan="4">
4547
<button type="button" class="btn btn-success btn-sm pull-right" data-action="new"><i class="fa fa-plus"></i> New Endpoint</button>
4648
</td>
4749
</tr>

static/templates/partials/edit-oauth2-strategy.tpl

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
<label for="enabled" class="form-check-label">Enabled</label>
55
</div>
66

7-
<hr />
8-
97
<div class="mb-3">
108
<label class="form-label" for="name">Name</label>
119
<input type="text" id="name" name="name" title="Name" class="form-control" placeholder="Name" value="{./name}" {{{ if ./name }}}readonly{{{ end }}}>
@@ -15,30 +13,46 @@
1513
</div>
1614

1715
<div class="mb-3">
18-
<label class="form-label" for="authUrl">Authorization URL</label>
19-
<input type="text" id="authUrl" name="authUrl" title="Authorization URL" class="form-control" placeholder="https://..." value="{./authUrl}">
16+
<label class="form-label" for="domain">Domain</label>
17+
<div class="input-group">
18+
<input type="text" id="domain" name="domain" title="domain" class="form-control" placeholder="foo.example.org">
19+
<span class="input-group-text text-success d-none" id="discovery-success">&check;</span>
20+
<span class="input-group-text text-warning d-none" id="discovery-failure">&cross;</span>
21+
</div>
22+
<p class="form-text">
23+
<strong>Optional</strong> — fill in a domain to automatically discover the URLs if provided by the server.
24+
</p>
2025
</div>
2126

22-
<div class="mb-3">
23-
<label class="form-label" for="tokenUrl">Token URL</label>
24-
<input type="text" id="tokenUrl" name="tokenUrl" title="Token URL" class="form-control" placeholder="https://..." value="{./tokenUrl}">
25-
</div>
27+
<hr />
2628

27-
<div class="mb-3">
28-
<label class="form-label" for="id">Client ID</label>
29-
<input type="text" id="id" name="id" title="Client ID" class="form-control" value="{./id}">
30-
</div>
29+
<fieldset id="details">
30+
<div class="mb-3">
31+
<label class="form-label" for="authUrl">Authorization URL</label>
32+
<input type="text" id="authUrl" name="authUrl" title="Authorization URL" class="form-control" placeholder="https://..." value="{./authUrl}">
33+
</div>
3134

32-
<div class="mb-3">
33-
<label class="form-label" for="secret">Client Secret</label>
34-
<input type="text" id="secret" name="secret" title="Client Secret" class="form-control" value="{./secret}">
35-
</div>
35+
<div class="mb-3">
36+
<label class="form-label" for="tokenUrl">Token URL</label>
37+
<input type="text" id="tokenUrl" name="tokenUrl" title="Token URL" class="form-control" placeholder="https://..." value="{./tokenUrl}">
38+
</div>
3639

37-
<div class="mb-3">
38-
<label class="form-label" for="userRoute">User Info URL</label>
39-
<input type="text" id="userRoute" name="userRoute" title="User Info URL" class="form-control" placeholder="/userinfo" value="{./userRoute}">
40-
<p class="form-text">
41-
If a relative path is specified here, we will assume the hostname from the authorization URL.
42-
</p>
43-
</div>
40+
<div class="mb-3">
41+
<label class="form-label" for="id">Client ID</label>
42+
<input type="text" id="id" name="id" title="Client ID" class="form-control" value="{./id}">
43+
</div>
44+
45+
<div class="mb-3">
46+
<label class="form-label" for="secret">Client Secret</label>
47+
<input type="text" id="secret" name="secret" title="Client Secret" class="form-control" value="{./secret}">
48+
</div>
49+
50+
<div class="mb-3">
51+
<label class="form-label" for="userRoute">User Info URL</label>
52+
<input type="text" id="userRoute" name="userRoute" title="User Info URL" class="form-control" placeholder="/userinfo" value="{./userRoute}">
53+
<p class="form-text">
54+
If a relative path is specified here, we will assume the hostname from the authorization URL.
55+
</p>
56+
</div>
57+
</fieldset>
4458
</form>

yarn.lock

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -633,9 +633,9 @@ esutils@^2.0.2:
633633
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
634634

635635
execa@^7.0.0:
636-
version "7.1.1"
637-
resolved "https://registry.yarnpkg.com/execa/-/execa-7.1.1.tgz#3eb3c83d239488e7b409d48e8813b76bb55c9c43"
638-
integrity sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==
636+
version "7.2.0"
637+
resolved "https://registry.yarnpkg.com/execa/-/execa-7.2.0.tgz#657e75ba984f42a70f38928cedc87d6f2d4fe4e9"
638+
integrity sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==
639639
dependencies:
640640
cross-spawn "^7.0.3"
641641
get-stream "^6.0.1"
@@ -1187,6 +1187,13 @@ natural-compare@^1.4.0:
11871187
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
11881188
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
11891189

1190+
node-fetch@^2:
1191+
version "2.6.12"
1192+
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba"
1193+
integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==
1194+
dependencies:
1195+
whatwg-url "^5.0.0"
1196+
11901197
normalize-path@^3.0.0:
11911198
version "3.0.0"
11921199
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@@ -1660,6 +1667,11 @@ to-regex-range@^5.0.1:
16601667
dependencies:
16611668
is-number "^7.0.0"
16621669

1670+
tr46@~0.0.3:
1671+
version "0.0.3"
1672+
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
1673+
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
1674+
16631675
tsconfig-paths@^3.14.2:
16641676
version "3.14.2"
16651677
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
@@ -1671,9 +1683,9 @@ tsconfig-paths@^3.14.2:
16711683
strip-bom "^3.0.0"
16721684

16731685
tslib@^2.1.0:
1674-
version "2.6.0"
1675-
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3"
1676-
integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==
1686+
version "2.6.1"
1687+
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410"
1688+
integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==
16771689

16781690
type-check@^0.4.0, type-check@~0.4.0:
16791691
version "0.4.0"
@@ -1758,6 +1770,19 @@ utils-merge@1.x.x:
17581770
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
17591771
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
17601772

1773+
webidl-conversions@^3.0.0:
1774+
version "3.0.1"
1775+
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
1776+
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
1777+
1778+
whatwg-url@^5.0.0:
1779+
version "5.0.0"
1780+
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
1781+
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
1782+
dependencies:
1783+
tr46 "~0.0.3"
1784+
webidl-conversions "^3.0.0"
1785+
17611786
which-boxed-primitive@^1.0.2:
17621787
version "1.0.2"
17631788
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"

0 commit comments

Comments
 (0)