discord-strategy 2.2.0

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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 No Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # Discord OAuth2 Strategy for Passport.js
2
+
3
+ ## Overview
4
+
5
+ This repository contains a custom OAuth2 strategy for authenticating with Discord using Passport.js. It facilitates user authentication via Discord and enables the retrieval of user data, including profile information, guilds, and connections.
6
+
7
+ ## Installation
8
+
9
+ To use this strategy, first install Passport.js and then the custom strategy:
10
+
11
+ ```bash
12
+ npm install passport discord-strategy
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ Integrate the strategy into your Express application as follows:
18
+
19
+ ### Example Setup
20
+
21
+ ```javascript
22
+ const express = require("express");
23
+ const passport = require("passport");
24
+ const Strategy = require("discord-strategy");
25
+
26
+ const app = express();
27
+
28
+ // Define options for the Strategy
29
+ const options = {
30
+ clientID: "YOUR_CLIENT_ID",
31
+ clientSecret: "YOUR_CLIENT_SECRET",
32
+ callbackURL: "http://localhost:3000/auth/discord/callback",
33
+ scope: ["identify", "email", "guilds", "connections", "guilds.members.read"], // Example scopes
34
+ };
35
+
36
+ // Create a new instance of the Strategy
37
+ passport.use(new Strategy(options, verify));
38
+
39
+ async function verify(accessToken, refreshToken, profile, done, consume) {
40
+ try {
41
+ // Fetch connections, guilds, guild member concurrently
42
+ await Promise.all([
43
+ consume.connections(),
44
+ consume.guilds(),
45
+ console.memeber("613425648685547541"), //https://discord.com/developers/docs/resources/user#get-current-user
46
+ ]);
47
+ profile = consume.profile();
48
+ console.log("Authentication successful!");
49
+ done(null, profile);
50
+ } catch (error) {
51
+ done(error?.data || error, null);
52
+ }
53
+ }
54
+
55
+ // Initialize Passport
56
+ app.use(passport.initialize());
57
+
58
+ // Define routes
59
+ app.get("/auth/discord", passport.authenticate("discord"));
60
+
61
+ app.get(
62
+ "/auth/discord/callback",
63
+ passport.authenticate("discord", { session: false }),
64
+ (req, res) => {
65
+ res.send(`
66
+ <h1>Authentication successful!</h1>
67
+ <h2>User Profile:</h2>
68
+ <pre>${JSON.stringify(req.user, null, 2)}</pre>
69
+ `);
70
+ }
71
+ );
72
+
73
+ app.listen(3000, () => {
74
+ console.log("Login via http://localhost:3000/auth/discord");
75
+ });
76
+ ```
77
+
78
+ ## Eample 2, Basic info Only
79
+
80
+ For scenarios where only basic user information is needed:
81
+
82
+ ```javascript
83
+ function verify(accessToken, refreshToken, profile, done, consume) {
84
+ console.log("Fetched", profile);
85
+ return done(null, profile);
86
+ }
87
+ ```
88
+
89
+ ## Strategy Options
90
+
91
+ - **`clientID`**: Your Discord application's Client ID.
92
+ - **`clientSecret`**: Your Discord application's Client Secret.
93
+ - **`callbackURL`**: The URL to which Discord will redirect after authorization.
94
+ - **`scope`**: An array of scopes specifying the level of access (default: `["identify", "email"]`).
95
+
96
+ ## Consumable Functions
97
+
98
+ With the v2.0 patch, all utility functions are now encapsulated in a new `consume` parameter.
99
+
100
+ List of Consumable Functions
101
+
102
+ - **`guilds(callback?)`**: Fetches the user's connections. Requires the `connections` scope.
103
+
104
+ ```js
105
+ function verify(accessToken, refreshToken, profile, done, consume) {
106
+ try {
107
+ await consume.guilds(); //retuns void;
108
+ profile = consume.profile(); //update profile, includes guild in `guilds` property.
109
+ console.log(profile.guild);
110
+ done(null, profile);
111
+ } catch (err) {
112
+ done(err, null);
113
+ }
114
+ ```
115
+
116
+ - **`connections(callback?)`**: Fetches the guilds the user is part of. Requires the `guilds` scope.
117
+
118
+ ```js
119
+ await consume.connections(); //retuns void;
120
+ profile = consume.profile(); //update profile, includes guild in `connections` property.
121
+ console.log(profile.connections);
122
+ ```
123
+
124
+ - **`guildJoiner(botToken: string, serverId: string, nickname: string, roles: string[], callback)`**: join the specified guild.
125
+
126
+ ```js
127
+ await consume.guildJoiner(
128
+ "botToken",
129
+ "serverId",
130
+ undefined,
131
+ undefined,
132
+ (err, result) =>
133
+ !err && !result ? console.log("Joined") : console.log(err, result)
134
+ ); //retuns void;
135
+ ```
136
+
137
+ - **`member(guild_id: string)`**: Returns a guild member object for the current user and creates a member property inside the profile. Within the member property, there is a guild_id. If profile.member.guild_id is null, the user is not in that guild. This requires the guilds.members.read OAuth2 scope.
138
+
139
+ ```js
140
+ await consume.member("id");
141
+ profile = consume.profile();
142
+ done(null, profile);
143
+ ```
144
+
145
+ - **`resolver(key, api)`**: Fetches data from a specified API endpoint and stores it under the given key in the profile.
146
+
147
+ ```js
148
+ await consume.resolver("guilds", "users/@me/guilds");
149
+ profile = consume.profile();
150
+ done(null, profile);
151
+ ```
152
+
153
+ - **`consume.resolverCallbackBased(key, api, callback)`**: Allows customization of data fetching with more complex API interactions. The access token is sent as a query parameter btw.
154
+
155
+ - **`consume.profile()`**: Returns the updated user profile.
156
+
157
+ ```js
158
+ done(null, consume.profile());
159
+ ```
160
+
161
+ - **`consume.linkedRole.get()`**: Returns the application role connection for the user. Requires an `role_connections.write` scope.
162
+
163
+ - **`consume.linkedRole.set(platform_name?, platform_username?, metadata, done?)`**: Updates and returns the application role connection for the user. Requires an `role_connections.write` scope.
164
+
165
+ ```js
166
+ // Role register Example
167
+ // fetch(
168
+ // `https://discord.com/api/v10/applications/APPLICATION_ID_HERE/role-connections/metadata`,
169
+ // {
170
+ // method: "PUT",
171
+ // body: JSON.stringify([
172
+ // {
173
+ // key: "cool",
174
+ // name: "Cool ppl",
175
+ // description: "You are cool ppl",
176
+ // type: 7, //type 7: BOOLEAN_EQUAL
177
+ // },
178
+ // ]),
179
+ // headers: {
180
+ // "Content-Type": "application/json",
181
+ // Authorization: `Bot BOT_TOKEN`,
182
+ // },
183
+ // }
184
+ // )
185
+ // .then((r) => r.json())
186
+ // .then(console.log);
187
+ //
188
+ // After registration do not forget to place url in designated field.
189
+ // Bot setting -> General Information -> Linked Roles Verification URL
190
+
191
+ await consume.set(undefined, undefined, {
192
+ //key: value,
193
+ cool: 1, //for type 7, value 1 represent true
194
+ });
195
+ done(null, profile);
196
+ ```
197
+
198
+ ### Example Usage
199
+
200
+ ## Concurrent Data Fetching
201
+
202
+ ```js
203
+ async function verify(accessToken, refreshToken, profile, done, consume) {
204
+ try {
205
+ await Promise.all([consume.connections(), consume.guilds()]);
206
+ profile = consume.profile();
207
+ console.log("[Asynchronous] Authentication successful!", profile);
208
+ done(null, profile);
209
+ } catch (err) {
210
+ done(err, null);
211
+ }
212
+ }
213
+ ```
214
+
215
+ **Callback based Data Fetching (Not Recommended // extreme-slow)**
216
+
217
+ ```js
218
+ async function verify(accessToken, refreshToken, profile, done, consume) {
219
+ consume.connections((err) => {
220
+ if (err) return done(err, false);
221
+ consume.guilds((err) => {
222
+ if (err) return done(err, false);
223
+ console.log("[Synchronous] Authentication successful!", profile);
224
+ done(null, profile);
225
+ });
226
+ });
227
+ }
228
+ ```
229
+
230
+ ### Resolver Functions
231
+
232
+ ## Basic Get Resolver
233
+
234
+ ```javascript
235
+ async function verify(accessToken, refreshToken, profile, done, consume) {
236
+ try {
237
+ await consume.resolver("guilds", "users/@me/guilds");
238
+ profile = consume.profile();
239
+ done(null, profile);
240
+ } catch (err) {
241
+ done(err, null);
242
+ }
243
+ }
244
+ ```
245
+
246
+ ## Refresh Tokens and Additional Handling
247
+
248
+ If you need to store the `refreshToken`, manage sessions, or handle other processes unrelated to Discord OAuth, please refer to the Passport.js documentation for more information on managing these tasks or explore other strategies that might be necessary for additional handling.
249
+
250
+ ## Changelog
251
+
252
+ ### v2.2 Patch
253
+
254
+ - Added `consume.linkedRole.get` & `consume.linkedRole.get`. https://discord.com/developers/docs/resources/user#get-current-user-guild-member & https://discord.com/developers/docs/resources/user#get-current-user-application-role-connection
255
+
256
+ - Resolver function now rejects the promise instead of throwing an error.
257
+
258
+ ### v2.1 Patch
259
+
260
+ - Added `consume.member("guild_id")`,Returns a guild member object for the current user. https://discord.com/developers/docs/resources/user#get-current-user-guild-member
261
+
262
+ - Resolver function now rejects the promise instead of throwing an error.
263
+
264
+ ### v2.0.1 Patch
265
+
266
+ - Fixed typo and doc error
267
+
268
+ ### v2.0 Patch
269
+
270
+ - Switched to an asynchronous approach `async/await` for non-blocking operations.
271
+ - Significantly improved performance compared to the previous version.
272
+ - Introduced the `consume` parameter to encapsulate utility functions.
273
+ - Removed the clean function, as the profile object is no longer need to be sanitize, replacement `consume.profile()` now returns a latest profile object.
274
+ - Added support for both asynchronous and callback-based resolvers.
275
+
276
+ ### v1.1 Patch
277
+
278
+ - No longer required to pass the access token to the consumable functions.
279
+ - Added two new consumable functions: `complexResolver()` and `guildJoiner()`.
280
+
281
+ ### v1.0.1 Patch
282
+
283
+ - Bound the cleaner function to the `clean` property of the profile (`profile.clean()`).
package/index.js ADDED
@@ -0,0 +1,608 @@
1
+ const OAuth2Strategy = require("passport-oauth2");
2
+ const { InternalOAuthError } = require("passport-oauth2");
3
+ const url = require("node:url");
4
+ const utils = require("passport-oauth2/lib/utils");
5
+ const base64url = require("base64url");
6
+ const crypto = require("crypto");
7
+
8
+ const API_BASE = "https://discord.com/api/";
9
+
10
+ /**
11
+ * Represents the Discord OAuth2 strategy for Passport.
12
+ * Extends the base OAuth2Strategy to provide custom behavior for Discord's API.
13
+ * @param {Object} options - Configuration options for the strategy.
14
+ * @param {Function} verify - Verification callback for the strategy.
15
+ * @throws Will throw an error if required options are missing.
16
+ */
17
+ class Strategy extends OAuth2Strategy {
18
+ constructor(options, verify) {
19
+ options = options || {};
20
+ options.authorizationURL =
21
+ options.authorizationURL || "https://discord.com/api/oauth2/authorize";
22
+ options.tokenURL =
23
+ options.tokenURL || "https://discord.com/api/oauth2/token";
24
+ options.scopeSeparator = options.scopeSeparator || " ";
25
+ options.scope = options.scope || ["identify", "email"];
26
+
27
+ if (!options.callbackURL) throw new Error("Missing callbackURL property");
28
+ if (!options.clientID) throw new Error("Missing clientID property");
29
+ if (!options.clientSecret) throw new Error("Missing clientSecret property");
30
+ super(options, verify);
31
+ this.options = options;
32
+ this.verify = verify;
33
+ this.name = "discord";
34
+ this._oauth2.useAuthorizationHeaderforGET(true);
35
+ }
36
+
37
+ /**
38
+ * Fetches the user profile from Discord using the provided access token.
39
+ * @param {string} accessToken - The access token for the user.
40
+ * @param {Function} done - Callback to handle the user profile.
41
+ */
42
+ async userProfile(accessToken, done) {
43
+ try {
44
+ const profile = await this.resolveApi("users/@me", accessToken);
45
+ const consumable = {
46
+ guilds: this.getGuilds.bind(this, profile, accessToken),
47
+ guildJoiner: this.guildJoiner.bind(this, profile, accessToken),
48
+ connections: this.getConnection.bind(this, profile, accessToken),
49
+ member: this.getMember.bind(this, profile, accessToken),
50
+ linkedRole: {
51
+ get: this.getRoleConnectionMetadata.bind(this, profile, accessToken),
52
+ set: this.setRoleConnectionMetadata.bind(this, profile, accessToken),
53
+ },
54
+ complexResolver: this._oauth2._request,
55
+ profile: () => profile,
56
+ resolver: async (key, api) => {
57
+ return new Promise(async (resolve, reject) => {
58
+ try {
59
+ const data = await this.resolveApi(api, accessToken);
60
+ profile[key] = data;
61
+ resolve(profile);
62
+ } catch (err) {
63
+ reject(err);
64
+ }
65
+ });
66
+ },
67
+ resolverCallbackBased: async (key, api, done) => {
68
+ try {
69
+ const data = await this.resolveApi(api, accessToken);
70
+ profile[key] = data;
71
+ done(null, profile);
72
+ } catch (err) {
73
+ done(err, null);
74
+ }
75
+ },
76
+ };
77
+ profile.avatarUrl = profile.avatar
78
+ ? `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}`
79
+ : undefined;
80
+ done(null, { profile, consumable });
81
+ } catch (e) {
82
+ done(e, null);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Retrieves the connections associated with the user's Discord account.
88
+ * @param {Object} profile - The user's profile.
89
+ * @param {string} accessToken - The access token for the user.
90
+ * @throws Will throw an error if the required scope is not included.
91
+ */
92
+ async getConnection(profile, accessToken, done) {
93
+ if (!this.options.scope || !this.options.scope.includes("connections")) {
94
+ throw new Error("Missing Scope, 'connections'");
95
+ }
96
+
97
+ try {
98
+ const connections = await this.resolveApi(
99
+ "users/@me/connections",
100
+ accessToken
101
+ );
102
+ profile.connections = connections;
103
+ if (done) done(null, profile);
104
+ } catch (e) {
105
+ if (done) return done(e, null);
106
+ return e;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Retrieves the guilds associated with the user's Discord account.
112
+ * @param {Object} profile - The user's profile.
113
+ * @param {string} accessToken - The access token for the user.
114
+ * @throws Will throw an error if the required scope is not included.
115
+ */
116
+ async getGuilds(profile, accessToken, done) {
117
+ if (!this.options.scope || !this.options.scope.includes("guilds")) {
118
+ throw new Error("Missing Scope, 'guilds'");
119
+ }
120
+
121
+ try {
122
+ const guilds = await this.resolveApi("users/@me/guilds", accessToken);
123
+ profile.guilds = guilds;
124
+ if (done) done(null, profile);
125
+ } catch (e) {
126
+ if (done) return done(e, null);
127
+ return e;
128
+ }
129
+ }
130
+
131
+ async getMember(profile, accessToken, guild_id, done) {
132
+ if (
133
+ !this.options.scope ||
134
+ !this.options.scope.includes("guilds.members.read")
135
+ ) {
136
+ throw new Error("Missing Scope, 'guilds.members.read'");
137
+ }
138
+ if (!profile.member) {
139
+ profile.member = {};
140
+ }
141
+
142
+ try {
143
+ const member = await this.resolveApi(
144
+ `users/@me/guilds/${guild_id}/member`,
145
+ accessToken
146
+ );
147
+ profile.member[guild_id] = member;
148
+ if (done) done(null, profile);
149
+ } catch (e) {
150
+ if (JSON.parse(e.data)?.code == 10004) {
151
+ profile.member[guild_id] = null;
152
+ e = null;
153
+ } else {
154
+ if (done) return done(e, profile);
155
+ return e;
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Adds a user to a guild with the specified roles and nickname.
162
+ * @param {Object} profile - The user's profile.
163
+ * @param {string} accessToken - The access token for the user.
164
+ * @param {string} botToken - The bot token for guild authorization.
165
+ * @param {string} serverId - The ID of the server to join.
166
+ * @param {string} nick - The nickname to assign to the user.
167
+ * @param {string[]} roles - The roles to assign to the user.
168
+ * @param {Function} done - Callback for handling the result.
169
+ */
170
+ async guildJoiner(
171
+ profile,
172
+ accessToken,
173
+ botToken,
174
+ serverId,
175
+ nick,
176
+ roles,
177
+ done
178
+ ) {
179
+ if (!this.options.scope || !this.options.scope.includes("guilds.join")) {
180
+ done(new Error("Missing Scope, 'guilds.join'"));
181
+ return;
182
+ }
183
+
184
+ const body = {
185
+ access_token: accessToken,
186
+ nick,
187
+ roles,
188
+ };
189
+
190
+ try {
191
+ const res = await new Promise((resolve, reject) => {
192
+ this._oauth2._request(
193
+ "PUT",
194
+ `${API_BASE}guilds/${serverId}/members/${profile.id}`,
195
+ {
196
+ Authorization: `Bot ${botToken}`,
197
+ "content-type": "application/json",
198
+ },
199
+ JSON.stringify(body),
200
+ null,
201
+ (err, result, response) => {
202
+ if (err) {
203
+ reject(err);
204
+ } else {
205
+ resolve(response);
206
+ }
207
+ }
208
+ );
209
+ });
210
+ if (res.statusCode === 201 || res.statusCode === 204) {
211
+ done(null, null);
212
+ } else {
213
+ done(null, res.statusCode);
214
+ }
215
+ } catch (error) {
216
+ done(error);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Retrieves the linked role metadata associated with the user's Discord account.
222
+ * @param {string} profile - The user's profile.
223
+ * @param {string} accessToken - The access token for the user.
224
+ * @returns {Promise<Object>} The resolved data.
225
+ * @throws Will throw an error for request or parsing issues.
226
+ */
227
+ async getRoleConnectionMetadata(profile, accessToken, done) {
228
+ if (
229
+ !this.options.scope ||
230
+ !this.options.scope.includes("role_connections.write")
231
+ ) {
232
+ throw new Error("Missing Scope, 'role_connections.write'");
233
+ }
234
+
235
+ if (!profile.linkedRole) {
236
+ profile.linkedRole = {};
237
+ }
238
+ try {
239
+ const metadata = await this.resolveApi(
240
+ `users/@me/applications/${this.options.clientID}/role-connection`,
241
+ accessToken
242
+ );
243
+ profile.linkedRole.get = metadata;
244
+ if (done) return done(null, profile);
245
+ } catch (e) {
246
+ if (done) return done(e, null);
247
+ return e;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * updated the linked role metadata associated with the user's Discord account.
253
+ * @param {string} profile - The user's profile.
254
+ * @param {string} accessToken - The access token for the user.
255
+ * @param {string} platform_name - The vanity name of the platform a bot has connected (max 50 characters)
256
+ * @param {string} platform_username - The username on the platform a bot has connected (max 100 characters)
257
+ * @typedef {Object} metadata
258
+ * @property {string} property1 - Description for property1.
259
+ * @property {number} property2 - Description for property2.
260
+ * @property {boolean} property3 - Description for property3.
261
+ * @returns {Promise<Object>} The resolved data.
262
+ * @throws Will throw an error for request or parsing issues.
263
+ */
264
+ async setRoleConnectionMetadata(
265
+ profile,
266
+ accessToken,
267
+ platform_name,
268
+ platform_username,
269
+ metadata,
270
+ done
271
+ ) {
272
+ if (
273
+ !this.options.scope ||
274
+ !this.options.scope.includes("role_connections.write")
275
+ ) {
276
+ throw new Error("Missing Scope, 'role_connections.write'");
277
+ }
278
+ if (!profile.linkedRole) {
279
+ profile.linkedRole = {};
280
+ }
281
+ try {
282
+ const role = await new Promise((resolve, reject) => {
283
+ this._oauth2._request(
284
+ "PUT",
285
+ `${API_BASE}/users/@me/applications/${this.options.clientID}/role-connection`,
286
+ {
287
+ Authorization: `Bearer ${accessToken}`,
288
+ "content-type": "application/json",
289
+ },
290
+ JSON.stringify({
291
+ platform_name,
292
+ platform_username,
293
+ metadata,
294
+ }),
295
+ null,
296
+ (err, result, response) => {
297
+ if (err) {
298
+ reject(err);
299
+ } else {
300
+ resolve(JSON.parse(result));
301
+ }
302
+ }
303
+ );
304
+ });
305
+ profile.linkedRole.set = role;
306
+ if (done) return done(null, profile);
307
+ } catch (e) {
308
+ if (e instanceof SyntaxError) {
309
+ reject(new Error("Failed to parse the user profile."));
310
+ }
311
+ if (done) return done(e, null);
312
+ return e;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Resolves an API endpoint and parses the response.
318
+ * @param {string} api - The API endpoint to resolve.
319
+ * @param {string} accessToken - The access token for the user.
320
+ * @returns {Promise<Object>} The resolved data.
321
+ * @throws Will throw an error for request or parsing issues.
322
+ */
323
+ async resolveApi(api, accessToken) {
324
+ return new Promise(async (res, rej) => {
325
+ try {
326
+ const result = await new Promise((resolve, reject) => {
327
+ this._oauth2.get(`${API_BASE}${api}`, accessToken, (err, result) => {
328
+ if (err) {
329
+ reject(err);
330
+ } else {
331
+ resolve(result);
332
+ }
333
+ });
334
+ });
335
+ res(JSON.parse(result));
336
+ } catch (err) {
337
+ if (err instanceof SyntaxError) {
338
+ reject(new Error("Failed to parse the user profile."));
339
+ }
340
+ // throw new InternalOAuthError("Failed to resolve API", err);
341
+ rej(err);
342
+ }
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Authenticate the request.
348
+ * @param {Object} req - The request object.
349
+ * @param {Object} options - Authentication options.
350
+ */
351
+ authenticate = function (req, options) {
352
+ options = options || {};
353
+ var self = this;
354
+
355
+ if (req.query && req.query.error) {
356
+ if (req.query.error == "access_denied") {
357
+ return this.fail({ message: req.query.error_description });
358
+ } else {
359
+ return this.error(
360
+ new AuthorizationError(
361
+ req.query.error_description,
362
+ req.query.error,
363
+ req.query.error_uri
364
+ )
365
+ );
366
+ }
367
+ }
368
+
369
+ var callbackURL = options.callbackURL || this._callbackURL;
370
+ if (callbackURL) {
371
+ var parsed = url.parse(callbackURL);
372
+ if (!parsed.protocol) {
373
+ // The callback URL is relative, resolve a fully qualified URL from the
374
+ // URL of the originating request.
375
+ callbackURL = url.resolve(
376
+ utils.originalURL(req, { proxy: this._trustProxy }),
377
+ callbackURL
378
+ );
379
+ }
380
+ }
381
+
382
+ var meta = {
383
+ authorizationURL: this._oauth2._authorizeUrl,
384
+ tokenURL: this._oauth2._accessTokenUrl,
385
+ clientID: this._oauth2._clientId,
386
+ callbackURL: callbackURL,
387
+ };
388
+
389
+ if ((req.query && req.query.code) || (req.body && req.body.code)) {
390
+ function loaded(err, ok, state) {
391
+ if (err) {
392
+ return self.error(err);
393
+ }
394
+ if (!ok) {
395
+ return self.fail(state, 403);
396
+ }
397
+
398
+ var code = (req.query && req.query.code) || (req.body && req.body.code);
399
+
400
+ var params = self.tokenParams(options);
401
+ params.grant_type = "authorization_code";
402
+ if (callbackURL) {
403
+ params.redirect_uri = callbackURL;
404
+ }
405
+ if (typeof ok == "string") {
406
+ // PKCE
407
+ params.code_verifier = ok;
408
+ }
409
+
410
+ self._oauth2.getOAuthAccessToken(
411
+ code,
412
+ params,
413
+ function (err, accessToken, refreshToken, params) {
414
+ if (err) {
415
+ return self.error(
416
+ self._createOAuthError("Failed to obtain access token", err)
417
+ );
418
+ }
419
+ if (!accessToken) {
420
+ return self.error(new Error("Failed to obtain access token"));
421
+ }
422
+
423
+ self._loadUserProfile(
424
+ accessToken,
425
+ function (err, { profile, consumable }) {
426
+ if (err) {
427
+ return self.error(err);
428
+ }
429
+
430
+ function verified(err, user, info) {
431
+ if (err) {
432
+ return self.error(err);
433
+ }
434
+ if (!user) {
435
+ return self.fail(info);
436
+ }
437
+
438
+ info = info || {};
439
+ if (state) {
440
+ info.state = state;
441
+ }
442
+ self.success(user, info);
443
+ }
444
+
445
+ try {
446
+ if (self._passReqToCallback) {
447
+ var arity = self._verify.length;
448
+ if (arity == 7) {
449
+ self._verify(
450
+ req,
451
+ accessToken,
452
+ refreshToken,
453
+ params,
454
+ profile,
455
+ verified,
456
+ consumable
457
+ );
458
+ } else {
459
+ // arity == 6
460
+ self._verify(
461
+ req,
462
+ accessToken,
463
+ refreshToken,
464
+ profile,
465
+ verified,
466
+ consumable
467
+ );
468
+ }
469
+ } else {
470
+ var arity = self._verify.length;
471
+ if (arity == 6) {
472
+ self._verify(
473
+ accessToken,
474
+ refreshToken,
475
+ params,
476
+ profile,
477
+ verified,
478
+ consumable
479
+ );
480
+ } else {
481
+ // arity == 5
482
+ self._verify(
483
+ accessToken,
484
+ refreshToken,
485
+ profile,
486
+ verified,
487
+ consumable
488
+ );
489
+ }
490
+ }
491
+ } catch (ex) {
492
+ return self.error(ex);
493
+ }
494
+ }
495
+ );
496
+ }
497
+ );
498
+ }
499
+
500
+ var state =
501
+ (req.query && req.query.state) || (req.body && req.body.state);
502
+ try {
503
+ var arity = this._stateStore.verify.length;
504
+ if (arity == 4) {
505
+ this._stateStore.verify(req, state, meta, loaded);
506
+ } else {
507
+ // arity == 3
508
+ this._stateStore.verify(req, state, loaded);
509
+ }
510
+ } catch (ex) {
511
+ return this.error(ex);
512
+ }
513
+ } else {
514
+ var params = this.authorizationParams(options);
515
+ params.response_type = "code";
516
+ if (callbackURL) {
517
+ params.redirect_uri = callbackURL;
518
+ }
519
+ var scope = options.scope || this._scope;
520
+ if (scope) {
521
+ if (Array.isArray(scope)) {
522
+ scope = scope.join(this._scopeSeparator);
523
+ }
524
+ params.scope = scope;
525
+ }
526
+ var verifier, challenge;
527
+
528
+ if (this._pkceMethod) {
529
+ verifier = base64url(crypto.pseudoRandomBytes(32));
530
+ switch (this._pkceMethod) {
531
+ case "plain":
532
+ challenge = verifier;
533
+ break;
534
+ case "S256":
535
+ challenge = base64url(
536
+ crypto.createHash("sha256").update(verifier).digest()
537
+ );
538
+ break;
539
+ default:
540
+ return this.error(
541
+ new Error(
542
+ "Unsupported code verifier transformation method: " +
543
+ this._pkceMethod
544
+ )
545
+ );
546
+ }
547
+ params.code_challenge = challenge;
548
+ params.code_challenge_method = this._pkceMethod;
549
+ }
550
+
551
+ var state = options.state;
552
+ if (state && typeof state == "string") {
553
+ // NOTE: In passport-oauth2@1.5.0 and earlier, `state` could be passed as
554
+ // an object. However, it would result in an empty string being
555
+ // serialized as the value of the query parameter by `url.format()`,
556
+ // effectively ignoring the option. This implies that `state` was
557
+ // only functional when passed as a string value.
558
+ //
559
+ // This fact is taken advantage of here to fall into the `else`
560
+ // branch below when `state` is passed as an object. In that case
561
+ // the state will be automatically managed and persisted by the
562
+ // state store.
563
+ params.state = state;
564
+
565
+ var parsed = url.parse(this._oauth2._authorizeUrl, true);
566
+ utils.merge(parsed.query, params);
567
+ parsed.query["client_id"] = this._oauth2._clientId;
568
+ delete parsed.search;
569
+ var location = url.format(parsed);
570
+ this.redirect(location);
571
+ } else {
572
+ function stored(err, state) {
573
+ if (err) {
574
+ return self.error(err);
575
+ }
576
+
577
+ if (state) {
578
+ params.state = state;
579
+ }
580
+ var parsed = url.parse(self._oauth2._authorizeUrl, true);
581
+ utils.merge(parsed.query, params);
582
+ parsed.query["client_id"] = self._oauth2._clientId;
583
+ delete parsed.search;
584
+ var location = url.format(parsed);
585
+ self.redirect(location);
586
+ }
587
+
588
+ try {
589
+ var arity = this._stateStore.store.length;
590
+ if (arity == 5) {
591
+ this._stateStore.store(req, verifier, state, meta, stored);
592
+ } else if (arity == 4) {
593
+ this._stateStore.store(req, state, meta, stored);
594
+ } else if (arity == 3) {
595
+ this._stateStore.store(req, meta, stored);
596
+ } else {
597
+ // arity == 2
598
+ this._stateStore.store(req, stored);
599
+ }
600
+ } catch (ex) {
601
+ return this.error(ex);
602
+ }
603
+ }
604
+ }
605
+ };
606
+ }
607
+
608
+ module.exports = Strategy;
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "dependencies": {
3
+ "passport-oauth2": "^1.8.0"
4
+ },
5
+ "name": "discord-strategy",
6
+ "description": "Passport strategy for Discord OAuth2",
7
+ "version": "2.2.0",
8
+ "main": "index.js",
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/bjn7/discord-strategy.git"
15
+ },
16
+ "keywords": [
17
+ "discord strategy",
18
+ "discord oauth2",
19
+ "discord auth",
20
+ "passport strategy",
21
+ "passport discord",
22
+ "discord passport"
23
+ ],
24
+ "author": "bjn7",
25
+ "license": "MIT",
26
+ "bugs": {
27
+ "url": "https://github.com/bjn7/discord-strategy/issues"
28
+ },
29
+ "homepage": "https://github.com/bjn7/discord-strategy#readme",
30
+ "devDependencies": {
31
+ "@types/passport-oauth2": "^1.4.17",
32
+ "typescript": "^5.7.2"
33
+ }
34
+ }