-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathm_nickserv_oauth.cpp
More file actions
258 lines (219 loc) · 9.76 KB
/
Copy pathm_nickserv_oauth.cpp
File metadata and controls
258 lines (219 loc) · 9.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/*
* m_nickserv_oauth.cpp
* 2026 Jean "reverse" Chevronnet
*
* Module for Anope IRC Services v2.1
* Adds IDENTIFYOAUTH command to NickServ for JWT-based authentication
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License.
*/
/// BEGIN CMAKE
/// find_package(OpenSSL REQUIRED)
/// target_link_libraries(${SO} PUBLIC OpenSSL::Crypto OpenSSL::SSL)
/// END CMAKE
#include "module.h"
#include <jwt-cpp/jwt.h>
#include <openssl/evp.h>
static Module *me;
class CommandNSIdentifyOAuth final : public Command
{
public:
Anope::string jwt_secret;
Anope::string jwt_issuer;
CommandNSIdentifyOAuth(Module *creator)
: Command(creator, "nickserv/identifyoauth", 2, 2)
{
this->SetDesc(_("Identify to services using an OAuth/JWT token"));
this->SetSyntax(_("\037nickname\037 \037token\037"));
this->AllowUnregistered(true);
}
void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) override
{
const Anope::string &username = params[0];
const Anope::string &token = params[1];
User *u = source.GetUser();
BotInfo *bi = Config->GetClient("NickServ");
if (!u || !bi)
return;
Log(LOG_COMMAND, source, this) << "to authenticate as " << username << " using OAuth token";
// Find the account
NickAlias *na = NickAlias::Find(username);
if (!na)
{
source.Reply(_("Account \002%s\002 is not registered."), username.c_str());
Log(me, "oauth") << u->GetMask() << " tried to IDENTIFYOAUTH as \002" << username << "\002 but account does not exist";
u->BadPassword();
return;
}
NickCore *nc = na->nc;
if (!nc)
{
source.Reply(_("Account \002%s\002 is not registered."), username.c_str());
u->BadPassword();
return;
}
// Check if account is suspended
if (nc->HasExt("NS_SUSPENDED"))
{
source.Reply(_("Your account has been suspended."));
Log(me, "oauth") << u->GetMask() << " tried to IDENTIFYOAUTH as suspended account \002" << username << "\002";
u->BadPassword();
return;
}
// Validate JWT token
try
{
std::string token_str(token.c_str());
auto decoded = jwt::decode(token_str);
// Debug logging
Log(me, "oauth") << "Token decoded - Algorithm: " << decoded.get_algorithm();
if (decoded.has_issuer())
Log(me, "oauth") << "Token issuer: " << decoded.get_issuer();
if (decoded.has_payload_claim("sub"))
Log(me, "oauth") << "Token sub: " << decoded.get_payload_claim("sub").as_string();
// Verify token signature and issuer if configured
if (!this->jwt_secret.empty())
{
std::string secret_str(this->jwt_secret.c_str());
std::string expected_issuer(this->jwt_issuer.c_str());
Log(me, "oauth") << "Verifying with secret length: " << secret_str.length()
<< " expected issuer: " << expected_issuer;
auto verifier = jwt::verify()
.allow_algorithm(jwt::algorithm::hs256{secret_str});
// Only verify issuer if the token has one AND we have an expected issuer
if (!expected_issuer.empty() && decoded.has_issuer())
verifier.with_issuer(expected_issuer);
verifier.verify(decoded);
}
// Extract username from token - check multiple possible claims
std::string token_username;
if (decoded.has_payload_claim("sub"))
token_username = decoded.get_payload_claim("sub").as_string();
else if (decoded.has_payload_claim("name"))
token_username = decoded.get_payload_claim("name").as_string();
else if (decoded.has_payload_claim("username"))
token_username = decoded.get_payload_claim("username").as_string();
// If no username in token, we need to look up by user_id
if (token_username.empty() && decoded.has_payload_claim("user_id"))
{
Log(me, "oauth") << "Token contains user_id but no username claim - cannot verify identity match";
}
// Verify token username matches requested username
if (!token_username.empty())
{
Anope::string token_user_anope(token_username.c_str());
if (!token_user_anope.equals_ci(username))
{
source.Reply(_("Token username does not match requested account."));
Log(me, "oauth") << u->GetMask() << " tried to IDENTIFYOAUTH as \002" << username
<< "\002 but token is for \002" << token_username << "\002";
u->BadPassword();
return;
}
}
// Check token expiration
if (decoded.has_expires_at())
{
auto exp_time = decoded.get_expires_at();
auto now = std::chrono::system_clock::now();
if (exp_time < now)
{
source.Reply(_("OAuth token has expired."));
Log(me, "oauth") << u->GetMask() << " tried to IDENTIFYOAUTH with expired token for \002" << username << "\002";
u->BadPassword();
return;
}
}
// Token is valid - identify the user
u->Identify(na);
source.Reply(_("Password accepted - you are now recognized."));
Log(me, "oauth") << u->GetMask() << " successfully identified to account \002" << nc->display << "\002 using OAuth";
// If the user's current nickname matches the account, update display
if (u->nick.equals_ci(nc->display))
{
Configuration::Block modconf = Config->GetModule("ns_identify");
if (modconf.Get<bool>("modeonid", "yes"))
{
// Set correct modes in channels
for (const auto &[_, cc] : u->chans)
{
Channel *c = cc->chan;
if (c)
c->SetCorrectModes(u, true);
}
}
// Apply configured user modes on identify
const Anope::string &modesonid = modconf.Get<const Anope::string>("modesonid");
if (!modesonid.empty())
u->SetModes(bi, modesonid);
}
FOREACH_MOD(OnNickIdentify, (u));
}
catch (const jwt::error::token_verification_exception &e)
{
source.Reply(_("Invalid or malformed OAuth token."));
Log(me, "oauth") << u->GetMask() << " failed JWT verification for \002" << username
<< "\002: " << e.what();
u->BadPassword();
}
catch (const std::exception &e)
{
source.Reply(_("OAuth authentication failed: Invalid token."));
Log(me, "oauth") << u->GetMask() << " IDENTIFYOAUTH error for \002" << username
<< "\002: " << e.what();
u->BadPassword();
}
}
bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
{
source.Reply(" ");
source.Reply(_("Authenticate to services using a JWT OAuth token obtained from\n"
"the web interface. This command is typically used automatically\n"
"by web chat clients and should not be used manually."));
source.Reply(" ");
source.Reply(_("Syntax: \002IDENTIFYOAUTH \037nickname\037 \037token\037\002"));
source.Reply(" ");
source.Reply(_("Example:"));
source.Reply(_(" /msg NickServ IDENTIFYOAUTH YourNick eyJhbGc..."));
return true;
}
void OnServHelp(CommandSource &source, HelpWrapper &help) override
{
source.Reply(_(" IDENTIFYOAUTH Identify using an OAuth token"));
}
void OnSyntaxError(CommandSource &source, const Anope::string &subcommand) override
{
source.Reply(_("Syntax: \002IDENTIFYOAUTH \037nickname\037 \037token\037\002"));
source.Reply(_("Type \002/msg %s HELP IDENTIFYOAUTH\002 for more information."), source.service->nick.c_str());
}
};
class ModuleNSIdentifyOAuth final : public Module
{
private:
CommandNSIdentifyOAuth commandnsidentifyoauth;
public:
ModuleNSIdentifyOAuth(const Anope::string &modname, const Anope::string &creator)
: Module(modname, creator, EXTRA | VENDOR)
, commandnsidentifyoauth(this)
{
me = this;
}
void OnReload(Configuration::Conf &conf) override
{
Configuration::Block &config = conf.GetModule(this);
// Read JWT configuration from module config
Anope::string jwt_secret = config.Get<const Anope::string>("jwt_secret", "");
Anope::string jwt_issuer = config.Get<const Anope::string>("jwt_issuer", "");
// Update command with new config
commandnsidentifyoauth.jwt_secret = jwt_secret;
commandnsidentifyoauth.jwt_issuer = jwt_issuer;
if (jwt_secret.empty())
{
Log(this) << "Warning: jwt_secret not configured - JWT signature verification disabled";
}
Log(this) << "IDENTIFYOAUTH command loaded (JWT verification: "
<< (jwt_secret.empty() ? "disabled" : "enabled") << ")";
}
};
MODULE_INIT(ModuleNSIdentifyOAuth)