ep_openid_connect 3.0.24 → 3.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/index.js +31 -6
- package/package.json +1 -1
- package/static/tests/backend/specs/tests.js +35 -0
package/README.md
CHANGED
|
@@ -49,6 +49,11 @@ Etherpad's `requireAuthentication` setting must be `true`.
|
|
|
49
49
|
* `client_id` (required): The OAuth2 client ID issued by the identity provider.
|
|
50
50
|
* `client_secret` (required): The OAuth2 client secret issued by the identity
|
|
51
51
|
provider.
|
|
52
|
+
* `token_endpoint_auth_method` (optional; defaults to `client_secret_basic`):
|
|
53
|
+
Client authentication method to use for token endpoint requests. Supported
|
|
54
|
+
values are `client_secret_basic` and `client_secret_post`. Use
|
|
55
|
+
`client_secret_post` if your identity provider expects the client secret in
|
|
56
|
+
the token request body instead of the HTTP `Authorization` header.
|
|
52
57
|
* `base_url` (required): The public base Etherpad URL. When registering Etherpad
|
|
53
58
|
with your identity provider, the redirect URL (a.k.a. callback URL) is this
|
|
54
59
|
base URL plus `/ep_openid_connect/callback`.
|
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@ for (const level of ['debug', 'info', 'warn', 'error']) {
|
|
|
10
10
|
const defaultSettings = {
|
|
11
11
|
prohibited_usernames: ['admin', 'guest'],
|
|
12
12
|
scope: ['openid'],
|
|
13
|
+
token_endpoint_auth_method: 'client_secret_basic',
|
|
13
14
|
user_properties: {},
|
|
14
15
|
};
|
|
15
16
|
let settings;
|
|
@@ -33,6 +34,7 @@ const validSettings = new Ajv().compile({
|
|
|
33
34
|
issuer_metadata: {},
|
|
34
35
|
prohibited_usernames: {elements: {type: 'string'}},
|
|
35
36
|
scope: {elements: {type: 'string'}},
|
|
37
|
+
token_endpoint_auth_method: {enum: ['client_secret_basic', 'client_secret_post']},
|
|
36
38
|
user_properties: {values: {
|
|
37
39
|
optionalProperties: {
|
|
38
40
|
claim: {type: 'string'},
|
|
@@ -50,6 +52,15 @@ const validSettings = new Ajv().compile({
|
|
|
50
52
|
const ep = (endpoint) => `/ep_openid_connect/${endpoint}`;
|
|
51
53
|
const endpointUrl = (endpoint) => new URL(ep(endpoint).substr(1), settings.base_url).toString();
|
|
52
54
|
|
|
55
|
+
const callbackUrlFromRequest = (req) => {
|
|
56
|
+
const callbackUrl = new URL(endpointUrl('callback'));
|
|
57
|
+
const requestUrl = new URL(req.originalUrl || req.url, settings.base_url);
|
|
58
|
+
for (const [key, value] of requestUrl.searchParams) {
|
|
59
|
+
callbackUrl.searchParams.append(key, value);
|
|
60
|
+
}
|
|
61
|
+
return callbackUrl;
|
|
62
|
+
};
|
|
63
|
+
|
|
53
64
|
const validateSubClaim = (sub) => {
|
|
54
65
|
if (typeof sub !== 'string' || // 'sub' claim must exist as a string per OIDC spec.
|
|
55
66
|
sub === '' || // Empty string doesn't make sense.
|
|
@@ -80,8 +91,8 @@ const discoverConfig = async (issuerUrl, clientId, clientAuth) => {
|
|
|
80
91
|
// openid-client@6 rejects http:// issuers by default; opt back in for
|
|
81
92
|
// localhost / private-network providers (matches v5 behaviour).
|
|
82
93
|
const options = url.protocol === 'http:'
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
? {execute: [oidc.allowInsecureRequests]}
|
|
95
|
+
: undefined;
|
|
85
96
|
try {
|
|
86
97
|
return await oidc.discovery(url, clientId, undefined, clientAuth, options);
|
|
87
98
|
} catch (err) {
|
|
@@ -100,11 +111,25 @@ const discoverConfig = async (issuerUrl, clientId, clientAuth) => {
|
|
|
100
111
|
}
|
|
101
112
|
};
|
|
102
113
|
|
|
114
|
+
const getClientAuth = (settings) => {
|
|
115
|
+
switch (settings.token_endpoint_auth_method) {
|
|
116
|
+
case 'client_secret_basic':
|
|
117
|
+
// eslint-disable-next-line new-cap
|
|
118
|
+
return oidc.ClientSecretBasic(settings.client_secret);
|
|
119
|
+
case 'client_secret_post':
|
|
120
|
+
// eslint-disable-next-line new-cap
|
|
121
|
+
return oidc.ClientSecretPost(settings.client_secret);
|
|
122
|
+
default:
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Unsupported token endpoint auth method: ${settings.token_endpoint_auth_method}`);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
103
128
|
const buildConfig = async (settings) => {
|
|
104
129
|
// openid-client@5 defaulted the token endpoint auth method to
|
|
105
130
|
// `client_secret_basic`; v6 defaults to `client_secret_post`. Pin the
|
|
106
|
-
// method explicitly so existing IdP registrations keep working.
|
|
107
|
-
const clientAuth =
|
|
131
|
+
// default method explicitly so existing IdP registrations keep working.
|
|
132
|
+
const clientAuth = getClientAuth(settings);
|
|
108
133
|
if (settings.issuer) {
|
|
109
134
|
const config =
|
|
110
135
|
await discoverConfig(settings.issuer, settings.client_id, clientAuth);
|
|
@@ -170,8 +195,7 @@ exports.expressCreateServer = (hookName, {app}) => {
|
|
|
170
195
|
}
|
|
171
196
|
const oidcSession = req.session.ep_openid_connect || {};
|
|
172
197
|
if (oidcSession.callbackChecks == null) throw new Error('missing authentication checks');
|
|
173
|
-
const
|
|
174
|
-
const tokens = await oidc.authorizationCodeGrant(oidcConfig, currentUrl, {
|
|
198
|
+
const tokens = await oidc.authorizationCodeGrant(oidcConfig, callbackUrlFromRequest(req), {
|
|
175
199
|
expectedNonce: oidcSession.callbackChecks.nonce,
|
|
176
200
|
expectedState: oidcSession.callbackChecks.state,
|
|
177
201
|
pkceCodeVerifier: oidcSession.callbackChecks.code_verifier,
|
|
@@ -310,6 +334,7 @@ exports.preAuthorize = (hookName, {req}) => {
|
|
|
310
334
|
};
|
|
311
335
|
|
|
312
336
|
exports.exportedForTestingOnly = {
|
|
337
|
+
callbackUrlFromRequest,
|
|
313
338
|
defaultSettings,
|
|
314
339
|
validSettings,
|
|
315
340
|
};
|
package/package.json
CHANGED
|
@@ -113,6 +113,16 @@ describe(__filename, function () {
|
|
|
113
113
|
});
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
it('callback URL uses configured base URL with request query parameters', async function () {
|
|
117
|
+
const callbackUrl = epOpenidConnect.exportedForTestingOnly.callbackUrlFromRequest({
|
|
118
|
+
originalUrl: '/ep_openid_connect/callback?code=the_code&state=the_state',
|
|
119
|
+
});
|
|
120
|
+
const expectedUrl = new URL('/ep_openid_connect/callback', pluginSettings.base_url);
|
|
121
|
+
expectedUrl.searchParams.set('code', 'the_code');
|
|
122
|
+
expectedUrl.searchParams.set('state', 'the_state');
|
|
123
|
+
assert.equal(callbackUrl.toString(), expectedUrl.toString());
|
|
124
|
+
});
|
|
125
|
+
|
|
116
126
|
it('normalUser can create and edit a pad', async function () {
|
|
117
127
|
const padId = common.randomString();
|
|
118
128
|
const url = new URL(`/p/${padId}`, common.baseUrl).toString();
|
|
@@ -125,6 +135,31 @@ describe(__filename, function () {
|
|
|
125
135
|
assert.equal(msg.data.readonly, false);
|
|
126
136
|
});
|
|
127
137
|
|
|
138
|
+
describe('token_endpoint_auth_method', function () {
|
|
139
|
+
for (const tokenEndpointAuthMethod of ['client_secret_basic', 'client_secret_post']) {
|
|
140
|
+
it(`supports ${tokenEndpointAuthMethod}`, async function () {
|
|
141
|
+
await epOpenidConnect.loadSettings('loadSettings', {settings: {ep_openid_connect: {
|
|
142
|
+
...pluginSettings,
|
|
143
|
+
token_endpoint_auth_method: tokenEndpointAuthMethod,
|
|
144
|
+
}}});
|
|
145
|
+
const padId = common.randomString();
|
|
146
|
+
const url = new URL(`/p/${padId}`, common.baseUrl).toString();
|
|
147
|
+
const res = await login(agent, issuer, url, 'normalUser');
|
|
148
|
+
assert.equal(res.request.url, url);
|
|
149
|
+
assert.equal(res.status, 200);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
it('rejects unsupported methods', async function () {
|
|
154
|
+
await epOpenidConnect.loadSettings('loadSettings', {settings: {ep_openid_connect: {
|
|
155
|
+
...pluginSettings,
|
|
156
|
+
token_endpoint_auth_method: 'unsupported',
|
|
157
|
+
}}});
|
|
158
|
+
await agent.get(new URL('/ep_openid_connect/login', common.baseUrl))
|
|
159
|
+
.expect(401);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
128
163
|
it('noCreate user is unable to create a pad', async function () {
|
|
129
164
|
const padId = common.randomString();
|
|
130
165
|
const url = new URL(`/p/${padId}`, common.baseUrl).toString();
|