discord-strategy 2.1.4
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 +2 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/index.js +416 -0
- package/package.json +34 -0
package/.gitattributes
ADDED
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,195 @@
|
|
|
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
|
+
## Strategy Options
|
|
79
|
+
|
|
80
|
+
- **`clientID`**: Your Discord application's Client ID.
|
|
81
|
+
- **`clientSecret`**: Your Discord application's Client Secret.
|
|
82
|
+
- **`callbackURL`**: The URL to which Discord will redirect after authorization.
|
|
83
|
+
- **`scope`**: An array of scopes specifying the level of access (default: `["identify", "email"]`).
|
|
84
|
+
|
|
85
|
+
## Consumable Functions
|
|
86
|
+
|
|
87
|
+
With the v2.0 patch, all utility functions are now encapsulated in a new `consume` parameter.
|
|
88
|
+
|
|
89
|
+
List of Consumable Functions
|
|
90
|
+
|
|
91
|
+
- **`guilds(callback?)`**: Fetches the user's connections. Requires the `connections` scope.
|
|
92
|
+
|
|
93
|
+
- **`connections(callback?)`**: Fetches the guilds the user is part of. Requires the `guilds` scope.
|
|
94
|
+
|
|
95
|
+
- **`guildJoiner(botToken: string, serverId: string, nickname: string, roles: string[], callback)`**: join the specified guild.
|
|
96
|
+
|
|
97
|
+
- **`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.
|
|
98
|
+
|
|
99
|
+
- **`resolver(key, api)`**: Fetches data from a specified API endpoint and stores it under the given key in the profile.
|
|
100
|
+
|
|
101
|
+
- **`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.
|
|
102
|
+
|
|
103
|
+
- **`consume.profile()`**: Returns the updated user profile.
|
|
104
|
+
|
|
105
|
+
### Example Usage
|
|
106
|
+
|
|
107
|
+
## Concurrent Data Fetching
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
async function verify(accessToken, refreshToken, profile, done, consume) {
|
|
111
|
+
try {
|
|
112
|
+
await Promise.all([consume.connections(), consume.guilds()]);
|
|
113
|
+
profile = consume.profile();
|
|
114
|
+
console.log("[Asynchronous] Authentication successful!", profile);
|
|
115
|
+
done(null, profile);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
done(err, null);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Callback based Data Fetching (Not Recommended // extreme-slow)**
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
async function verify(accessToken, refreshToken, profile, done, consume) {
|
|
126
|
+
consume.connections((err) => {
|
|
127
|
+
if (err) return done(err, false);
|
|
128
|
+
consume.guilds((err) => {
|
|
129
|
+
if (err) return done(err, false);
|
|
130
|
+
console.log("[Synchronous] Authentication successful!", profile);
|
|
131
|
+
done(null, profile);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Resolver Functions
|
|
138
|
+
|
|
139
|
+
## Basic Get Resolver
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
async function verify(accessToken, refreshToken, profile, done, consume) {
|
|
143
|
+
try {
|
|
144
|
+
await consume.resolver("guilds", "users/@me/guilds");
|
|
145
|
+
profile = consume.profile();
|
|
146
|
+
done(null, profile);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
done(err, null);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Basic Information Only
|
|
154
|
+
|
|
155
|
+
For scenarios where only basic user information is needed:
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
function verify(accessToken, refreshToken, profile, done) {
|
|
159
|
+
console.log("Fetched", profile);
|
|
160
|
+
return done(null, profile);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Refresh Tokens and Additional Handling
|
|
165
|
+
|
|
166
|
+
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.
|
|
167
|
+
|
|
168
|
+
## Changelog
|
|
169
|
+
|
|
170
|
+
### v2.1 Patch
|
|
171
|
+
|
|
172
|
+
- 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
|
|
173
|
+
|
|
174
|
+
- Resolver function now rejects the promise instead of throwing an error.
|
|
175
|
+
|
|
176
|
+
### v2.0.1 Patch
|
|
177
|
+
|
|
178
|
+
- Fixed typo and doc error
|
|
179
|
+
|
|
180
|
+
### v2.0 Patch
|
|
181
|
+
|
|
182
|
+
- Switched to an asynchronous approach `async/await` for non-blocking operations.
|
|
183
|
+
- Significantly improved performance compared to the previous version.
|
|
184
|
+
- Introduced the `consume` parameter to encapsulate utility functions.
|
|
185
|
+
- Removed the clean function, as the profile object is no longer need to be sanitize, replacement `consume.profile()` now returns a latest profile object.
|
|
186
|
+
- Added support for both asynchronous and callback-based resolvers.
|
|
187
|
+
|
|
188
|
+
### v1.1 Patch
|
|
189
|
+
|
|
190
|
+
- No longer required to pass the access token to the consumable functions.
|
|
191
|
+
- Added two new consumable functions: `complexResolver()` and `guildJoiner()`.
|
|
192
|
+
|
|
193
|
+
### v1.0.1 Patch
|
|
194
|
+
|
|
195
|
+
- Bound the cleaner function to the `clean` property of the profile (`profile.clean()`).
|
package/index.js
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
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 = options.authorizationURL || "https://discord.com/api/oauth2/authorize";
|
|
21
|
+
options.tokenURL = options.tokenURL || "https://discord.com/api/oauth2/token";
|
|
22
|
+
options.scopeSeparator = options.scopeSeparator || " ";
|
|
23
|
+
options.scope = options.scope || [
|
|
24
|
+
"identify",
|
|
25
|
+
"email",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
if (!options.callbackURL) throw new Error("Missing callbackURL property");
|
|
29
|
+
if (!options.clientID) throw new Error("Missing clientID property");
|
|
30
|
+
if (!options.clientSecret) throw new Error("Missing clientSecret property");
|
|
31
|
+
super(options, verify);
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.verify = verify;
|
|
34
|
+
this.name = "discord";
|
|
35
|
+
this._oauth2.useAuthorizationHeaderforGET(true);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fetches the user profile from Discord using the provided access token.
|
|
40
|
+
* @param {string} accessToken - The access token for the user.
|
|
41
|
+
* @param {Function} done - Callback to handle the user profile.
|
|
42
|
+
*/
|
|
43
|
+
async userProfile(accessToken, done) {
|
|
44
|
+
try {
|
|
45
|
+
const profile = await this.resolveApi("users/@me", accessToken);
|
|
46
|
+
const consumable = {
|
|
47
|
+
guilds: this.getGuilds.bind(this, profile, accessToken),
|
|
48
|
+
guildJoiner: this.guildJoiner.bind(this, profile, accessToken),
|
|
49
|
+
connections: this.getConnection.bind(this, profile, accessToken),
|
|
50
|
+
member: this.getMember.bind(this, profile, accessToken),
|
|
51
|
+
complexResolver: this._oauth2._request,
|
|
52
|
+
profile: () => profile,
|
|
53
|
+
resolver: async (key, api) => {
|
|
54
|
+
return new Promise(async (resolve, reject) => {
|
|
55
|
+
try {
|
|
56
|
+
const data = await this.resolveApi(api, accessToken);
|
|
57
|
+
profile[key] = data;
|
|
58
|
+
resolve(profile);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
reject(err);
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
},
|
|
64
|
+
resolverCallbackBased: async (key, api, done) => {
|
|
65
|
+
try {
|
|
66
|
+
const data = await this.resolveApi(api, accessToken);
|
|
67
|
+
profile[key] = data;
|
|
68
|
+
done(null, profile);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
done(err, null);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
profile.avatarUrl = profile.avatar
|
|
75
|
+
? `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}`
|
|
76
|
+
: undefined;
|
|
77
|
+
done(null, { profile, consumable });
|
|
78
|
+
} catch (e) {
|
|
79
|
+
done(e, null);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Retrieves the connections associated with the user's Discord account.
|
|
85
|
+
* @param {Object} profile - The user's profile.
|
|
86
|
+
* @param {string} accessToken - The access token for the user.
|
|
87
|
+
* @throws Will throw an error if the required scope is not included.
|
|
88
|
+
*/
|
|
89
|
+
async getConnection(profile, accessToken, done) {
|
|
90
|
+
if (!this.options.scope || !this.options.scope.includes("connections")) {
|
|
91
|
+
throw new Error("Missing Scope, 'connections'");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const connections = await this.resolveApi("users/@me/connections", accessToken);
|
|
96
|
+
profile.connections = connections;
|
|
97
|
+
if (done) done(null, profile)
|
|
98
|
+
} catch (e) {
|
|
99
|
+
if (done) return done(e, null)
|
|
100
|
+
return e;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Retrieves the guilds associated with the user's Discord account.
|
|
106
|
+
* @param {Object} profile - The user's profile.
|
|
107
|
+
* @param {string} accessToken - The access token for the user.
|
|
108
|
+
* @throws Will throw an error if the required scope is not included.
|
|
109
|
+
*/
|
|
110
|
+
async getGuilds(profile, accessToken, done) {
|
|
111
|
+
if (!this.options.scope || !this.options.scope.includes("guilds")) {
|
|
112
|
+
throw new Error("Missing Scope, 'guilds'");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const guilds = await this.resolveApi("users/@me/guilds", accessToken);
|
|
117
|
+
profile.guilds = guilds;
|
|
118
|
+
if (done) done(null, profile)
|
|
119
|
+
} catch (e) {
|
|
120
|
+
if (done) return done(e, null)
|
|
121
|
+
return e;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async getMember(profile, accessToken, guild_id, done) {
|
|
126
|
+
if (!this.options.scope || !this.options.scope.includes("guilds.members.read")) {
|
|
127
|
+
throw new Error("Missing Scope, 'guilds.members.read'");
|
|
128
|
+
}
|
|
129
|
+
if (!profile.member) {
|
|
130
|
+
profile.member = {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const member = await this.resolveApi(`users/@me/guilds/${guild_id}/member`, accessToken);
|
|
135
|
+
profile.member[guild_id] = member;
|
|
136
|
+
if (done) done(null, profile)
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (JSON.parse(e.data)?.code == 10004) {
|
|
139
|
+
profile.member[guild_id] = null
|
|
140
|
+
e = null
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
if (done) return done(e, profile)
|
|
144
|
+
return e;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Adds a user to a guild with the specified roles and nickname.
|
|
152
|
+
* @param {Object} profile - The user's profile.
|
|
153
|
+
* @param {string} accessToken - The access token for the user.
|
|
154
|
+
* @param {string} botToken - The bot token for guild authorization.
|
|
155
|
+
* @param {string} serverId - The ID of the server to join.
|
|
156
|
+
* @param {string} nick - The nickname to assign to the user.
|
|
157
|
+
* @param {string[]} roles - The roles to assign to the user.
|
|
158
|
+
* @param {Function} done - Callback for handling the result.
|
|
159
|
+
*/
|
|
160
|
+
async guildJoiner(profile, accessToken, botToken, serverId, nick, roles, done) {
|
|
161
|
+
if (!this.options.scope || !this.options.scope.includes("guilds.join")) {
|
|
162
|
+
done(new Error("Missing Scope, 'guilds.join'"));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const body = {
|
|
167
|
+
access_token: accessToken,
|
|
168
|
+
nick,
|
|
169
|
+
roles,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const res = await new Promise((resolve, reject) => {
|
|
174
|
+
this._oauth2._request(
|
|
175
|
+
"PUT",
|
|
176
|
+
`${API_BASE}guilds/${serverId}/members/${profile.id}`,
|
|
177
|
+
{
|
|
178
|
+
Authorization: `Bot ${botToken}`,
|
|
179
|
+
"content-type": "application/json",
|
|
180
|
+
},
|
|
181
|
+
JSON.stringify(body),
|
|
182
|
+
null,
|
|
183
|
+
(err, result, response) => {
|
|
184
|
+
if (err) {
|
|
185
|
+
reject(err);
|
|
186
|
+
} else {
|
|
187
|
+
resolve(response);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
if (res.statusCode === 201 || res.statusCode === 204) {
|
|
193
|
+
done(null, null);
|
|
194
|
+
} else {
|
|
195
|
+
done(new Error(`Unexpected status code: ${res.statusCode}`));
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
done(error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Resolves an API endpoint and parses the response.
|
|
204
|
+
* @param {string} api - The API endpoint to resolve.
|
|
205
|
+
* @param {string} accessToken - The access token for the user.
|
|
206
|
+
* @returns {Promise<Object>} The resolved data.
|
|
207
|
+
* @throws Will throw an error for request or parsing issues.
|
|
208
|
+
*/
|
|
209
|
+
async resolveApi(api, accessToken) {
|
|
210
|
+
return new Promise(async (res, rej) => {
|
|
211
|
+
try {
|
|
212
|
+
const result = await new Promise((resolve, reject) => {
|
|
213
|
+
this._oauth2.get(`${API_BASE}${api}`, accessToken, (err, result) => {
|
|
214
|
+
if (err) {
|
|
215
|
+
reject(err);
|
|
216
|
+
} else {
|
|
217
|
+
resolve(result);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
res(JSON.parse(result))
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (err instanceof SyntaxError) {
|
|
224
|
+
reject(new Error("Failed to parse the user profile."));
|
|
225
|
+
}
|
|
226
|
+
// throw new InternalOAuthError("Failed to resolve API", err);
|
|
227
|
+
rej(err)
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Authenticate the request.
|
|
234
|
+
* @param {Object} req - The request object.
|
|
235
|
+
* @param {Object} options - Authentication options.
|
|
236
|
+
*/
|
|
237
|
+
authenticate = function (req, options) {
|
|
238
|
+
options = options || {};
|
|
239
|
+
var self = this;
|
|
240
|
+
|
|
241
|
+
if (req.query && req.query.error) {
|
|
242
|
+
if (req.query.error == 'access_denied') {
|
|
243
|
+
return this.fail({ message: req.query.error_description });
|
|
244
|
+
} else {
|
|
245
|
+
return this.error(new AuthorizationError(req.query.error_description, req.query.error, req.query.error_uri));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
var callbackURL = options.callbackURL || this._callbackURL;
|
|
250
|
+
if (callbackURL) {
|
|
251
|
+
var parsed = url.parse(callbackURL);
|
|
252
|
+
if (!parsed.protocol) {
|
|
253
|
+
// The callback URL is relative, resolve a fully qualified URL from the
|
|
254
|
+
// URL of the originating request.
|
|
255
|
+
callbackURL = url.resolve(utils.originalURL(req, { proxy: this._trustProxy }), callbackURL);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var meta = {
|
|
260
|
+
authorizationURL: this._oauth2._authorizeUrl,
|
|
261
|
+
tokenURL: this._oauth2._accessTokenUrl,
|
|
262
|
+
clientID: this._oauth2._clientId,
|
|
263
|
+
callbackURL: callbackURL
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if ((req.query && req.query.code) || (req.body && req.body.code)) {
|
|
267
|
+
function loaded(err, ok, state) {
|
|
268
|
+
if (err) { return self.error(err); }
|
|
269
|
+
if (!ok) {
|
|
270
|
+
return self.fail(state, 403);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
var code = (req.query && req.query.code) || (req.body && req.body.code);
|
|
274
|
+
|
|
275
|
+
var params = self.tokenParams(options);
|
|
276
|
+
params.grant_type = 'authorization_code';
|
|
277
|
+
if (callbackURL) { params.redirect_uri = callbackURL; }
|
|
278
|
+
if (typeof ok == 'string') { // PKCE
|
|
279
|
+
params.code_verifier = ok;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
self._oauth2.getOAuthAccessToken(code, params,
|
|
283
|
+
function (err, accessToken, refreshToken, params) {
|
|
284
|
+
if (err) { return self.error(self._createOAuthError('Failed to obtain access token', err)); }
|
|
285
|
+
if (!accessToken) { return self.error(new Error('Failed to obtain access token')); }
|
|
286
|
+
|
|
287
|
+
self._loadUserProfile(accessToken, function (err, {
|
|
288
|
+
profile,
|
|
289
|
+
consumable,
|
|
290
|
+
}) {
|
|
291
|
+
if (err) { return self.error(err); }
|
|
292
|
+
|
|
293
|
+
function verified(err, user, info) {
|
|
294
|
+
if (err) { return self.error(err); }
|
|
295
|
+
if (!user) { return self.fail(info); }
|
|
296
|
+
|
|
297
|
+
info = info || {};
|
|
298
|
+
if (state) { info.state = state; }
|
|
299
|
+
self.success(user, info);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
if (self._passReqToCallback) {
|
|
304
|
+
var arity = self._verify.length;
|
|
305
|
+
if (arity == 7) {
|
|
306
|
+
self._verify(req, accessToken, refreshToken, params, profile, verified, consumable);
|
|
307
|
+
} else { // arity == 6
|
|
308
|
+
self._verify(req, accessToken, refreshToken, profile, verified, consumable);
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
var arity = self._verify.length;
|
|
312
|
+
if (arity == 6) {
|
|
313
|
+
self._verify(accessToken, refreshToken, params, profile, verified, consumable);
|
|
314
|
+
} else { // arity == 5
|
|
315
|
+
self._verify(accessToken, refreshToken, profile, verified, consumable);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch (ex) {
|
|
319
|
+
return self.error(ex);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
var state = (req.query && req.query.state) || (req.body && req.body.state);
|
|
327
|
+
try {
|
|
328
|
+
var arity = this._stateStore.verify.length;
|
|
329
|
+
if (arity == 4) {
|
|
330
|
+
this._stateStore.verify(req, state, meta, loaded);
|
|
331
|
+
} else { // arity == 3
|
|
332
|
+
this._stateStore.verify(req, state, loaded);
|
|
333
|
+
}
|
|
334
|
+
} catch (ex) {
|
|
335
|
+
return this.error(ex);
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
var params = this.authorizationParams(options);
|
|
339
|
+
params.response_type = 'code';
|
|
340
|
+
if (callbackURL) { params.redirect_uri = callbackURL; }
|
|
341
|
+
var scope = options.scope || this._scope;
|
|
342
|
+
if (scope) {
|
|
343
|
+
if (Array.isArray(scope)) { scope = scope.join(this._scopeSeparator); }
|
|
344
|
+
params.scope = scope;
|
|
345
|
+
}
|
|
346
|
+
var verifier, challenge;
|
|
347
|
+
|
|
348
|
+
if (this._pkceMethod) {
|
|
349
|
+
verifier = base64url(crypto.pseudoRandomBytes(32))
|
|
350
|
+
switch (this._pkceMethod) {
|
|
351
|
+
case 'plain':
|
|
352
|
+
challenge = verifier;
|
|
353
|
+
break;
|
|
354
|
+
case 'S256':
|
|
355
|
+
challenge = base64url(crypto.createHash('sha256').update(verifier).digest());
|
|
356
|
+
break;
|
|
357
|
+
default:
|
|
358
|
+
return this.error(new Error('Unsupported code verifier transformation method: ' + this._pkceMethod));
|
|
359
|
+
}
|
|
360
|
+
params.code_challenge = challenge;
|
|
361
|
+
params.code_challenge_method = this._pkceMethod;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
var state = options.state;
|
|
365
|
+
if (state && typeof state == 'string') {
|
|
366
|
+
// NOTE: In passport-oauth2@1.5.0 and earlier, `state` could be passed as
|
|
367
|
+
// an object. However, it would result in an empty string being
|
|
368
|
+
// serialized as the value of the query parameter by `url.format()`,
|
|
369
|
+
// effectively ignoring the option. This implies that `state` was
|
|
370
|
+
// only functional when passed as a string value.
|
|
371
|
+
//
|
|
372
|
+
// This fact is taken advantage of here to fall into the `else`
|
|
373
|
+
// branch below when `state` is passed as an object. In that case
|
|
374
|
+
// the state will be automatically managed and persisted by the
|
|
375
|
+
// state store.
|
|
376
|
+
params.state = state;
|
|
377
|
+
|
|
378
|
+
var parsed = url.parse(this._oauth2._authorizeUrl, true);
|
|
379
|
+
utils.merge(parsed.query, params);
|
|
380
|
+
parsed.query['client_id'] = this._oauth2._clientId;
|
|
381
|
+
delete parsed.search;
|
|
382
|
+
var location = url.format(parsed);
|
|
383
|
+
this.redirect(location);
|
|
384
|
+
} else {
|
|
385
|
+
function stored(err, state) {
|
|
386
|
+
if (err) { return self.error(err); }
|
|
387
|
+
|
|
388
|
+
if (state) { params.state = state; }
|
|
389
|
+
var parsed = url.parse(self._oauth2._authorizeUrl, true);
|
|
390
|
+
utils.merge(parsed.query, params);
|
|
391
|
+
parsed.query['client_id'] = self._oauth2._clientId;
|
|
392
|
+
delete parsed.search;
|
|
393
|
+
var location = url.format(parsed);
|
|
394
|
+
self.redirect(location);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
var arity = this._stateStore.store.length;
|
|
399
|
+
if (arity == 5) {
|
|
400
|
+
this._stateStore.store(req, verifier, state, meta, stored);
|
|
401
|
+
} else if (arity == 4) {
|
|
402
|
+
this._stateStore.store(req, state, meta, stored);
|
|
403
|
+
} else if (arity == 3) {
|
|
404
|
+
this._stateStore.store(req, meta, stored);
|
|
405
|
+
} else { // arity == 2
|
|
406
|
+
this._stateStore.store(req, stored);
|
|
407
|
+
}
|
|
408
|
+
} catch (ex) {
|
|
409
|
+
return this.error(ex);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
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.1.4",
|
|
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
|
+
}
|