]> git.ipfire.org Git - ipfire.org.git/blame - src/web/auth.py
accounts: Drop StopForumSpam
[ipfire.org.git] / src / web / auth.py
CommitLineData
08df6527
MT
1#!/usr/bin/python
2
3import logging
4import tornado.web
2fe1d960 5import urllib.parse
08df6527 6
124a8404 7from . import base
08df6527 8
da24ac0a 9class AuthenticationMixin(object):
d8a15b2e 10 def login(self, account):
08df6527 11 # User has logged in, create a session
906e1e6a
MT
12 session_id, session_expires = self.backend.accounts.create_session(
13 account, self.request.host)
08df6527
MT
14
15 # Check if a new session was created
16 if not session_id:
17 raise tornado.web.HTTPError(500, "Could not create session")
18
19 # Send session cookie to the client
20 self.set_cookie("session_id", session_id,
abd1d7aa 21 domain=self.request.host, expires=session_expires, secure=True)
08df6527
MT
22
23 def logout(self):
24 session_id = self.get_cookie("session_id")
25 if not session_id:
26 return
27
906e1e6a 28 success = self.backend.accounts.destroy_session(session_id, self.request.host)
08df6527
MT
29 if success:
30 self.clear_cookie("session_id")
31
32
08df6527
MT
33class LoginHandler(AuthenticationMixin, base.BaseHandler):
34 def get(self):
35 next = self.get_argument("next", None)
36
a76ac21e
MT
37 self.render("auth/login.html", next=next,
38 incorrect=False, username=None)
08df6527 39
53a15fe0 40 @base.ratelimit(minutes=15, requests=10)
08df6527
MT
41 def post(self):
42 username = self.get_argument("username")
43 password = self.get_argument("password")
a76ac21e 44 next = self.get_argument("next", "/")
08df6527 45
328a7710
MT
46 # Find user
47 account = self.backend.accounts.auth(username, password)
48 if not account:
a76ac21e
MT
49 logging.error("Unknown user or invalid password: %s" % username)
50
51 # Set status to 401
52 self.set_status(401)
53
54 # Render login page again
55 return self.render("auth/login.html",
56 incorrect=True, username=username, next=next,
57 )
328a7710
MT
58
59 # Create session
08df6527 60 with self.db.transaction():
328a7710 61 self.login(account)
08df6527 62
a76ac21e
MT
63 # Redirect the user
64 return self.redirect(next)
08df6527
MT
65
66
67class LogoutHandler(AuthenticationMixin, base.BaseHandler):
68 def get(self):
69 with self.db.transaction():
70 self.logout()
71
72 # Get back to the start page
73 self.redirect("/")
9b8ff27d
MT
74
75
da24ac0a 76class RegisterHandler(base.BaseHandler):
f32dd17f 77 def get(self):
d521c9df
MT
78 # Redirect logged in users away
79 if self.current_user:
80 self.redirect("/")
73f2f29a 81 return
d521c9df 82
f32dd17f
MT
83 self.render("auth/register.html")
84
53a15fe0 85 @base.ratelimit(minutes=15, requests=5)
9fdf4fb7 86 async def post(self):
f32dd17f
MT
87 uid = self.get_argument("uid")
88 email = self.get_argument("email")
89
90 first_name = self.get_argument("first_name")
91 last_name = self.get_argument("last_name")
92
93 # Register account
f2ba8a1f
MT
94 try:
95 with self.db.transaction():
96 self.backend.accounts.register(uid, email,
757372cd
MT
97 first_name=first_name, last_name=last_name,
98 country_code=self.current_country_code)
f2ba8a1f 99 except ValueError as e:
09c67399 100 raise tornado.web.HTTPError(400, "%s" % e) from e
f32dd17f
MT
101
102 self.render("auth/register-success.html")
103
104
d8a15b2e
MT
105class ActivateHandler(AuthenticationMixin, base.BaseHandler):
106 def get(self, uid, activation_code):
b4d72c76 107 self.render("auth/activate.html")
d8a15b2e
MT
108
109 def post(self, uid, activation_code):
b4d72c76
MT
110 password1 = self.get_argument("password1")
111 password2 = self.get_argument("password2")
d8a15b2e 112
b4d72c76
MT
113 if not password1 == password2:
114 raise tornado.web.HTTPError(400, "Passwords do not match")
d8a15b2e 115
b4d72c76
MT
116 with self.db.transaction():
117 account = self.backend.accounts.activate(uid, activation_code)
118 if not account:
119 raise tornado.web.HTTPError(400, "Account not found: %s" % uid)
d8a15b2e 120
b4d72c76
MT
121 # Set the new password
122 account.passwd(password1)
d8a15b2e 123
b4d72c76
MT
124 # Create session
125 self.login(account)
d8a15b2e 126
b00cc400
MT
127 # Redirect to success page
128 self.render("auth/activated.html", account=account)
689effd0
MT
129
130
da24ac0a 131class PasswordResetInitiationHandler(base.BaseHandler):
c7594d58
MT
132 def get(self):
133 username = self.get_argument("username", None)
134
135 self.render("auth/password-reset-initiation.html", username=username)
136
53a15fe0 137 @base.ratelimit(minutes=15, requests=10)
c7594d58
MT
138 def post(self):
139 username = self.get_argument("username")
140
141 # Fetch account and submit password reset
142 account = self.backend.accounts.get_by_uid(username)
143 if account:
144 with self.db.transaction():
391ede9e 145 account.request_password_reset()
c7594d58
MT
146
147 self.render("auth/password-reset-successful.html")
148
149
391ede9e
MT
150class PasswordResetHandler(AuthenticationMixin, base.BaseHandler):
151 def get(self, uid, reset_code):
152 account = self.backend.accounts.get_by_uid(uid)
153 if not account:
154 raise tornado.web.HTTPError(404, "Could not find account: %s" % uid)
155
156 self.render("auth/password-reset.html", account=account)
157
158 def post(self, uid, reset_code):
159 account = self.backend.accounts.get_by_uid(uid)
160 if not account:
161 raise tornado.web.HTTPError(404, "Could not find account: %s" % uid)
162
163 password1 = self.get_argument("password1")
164 password2 = self.get_argument("password2")
165
166 if not password1 == password2:
167 raise tornado.web.HTTPError(400, "Passwords do not match")
168
169 # Try to perform password reset
170 with self.db.transaction():
171 account.reset_password(reset_code, password1)
172
173 # Login the user straight away after reset was successful
174 self.login(account)
175
176 # Redirect back to /
177 self.redirect("/")
178
179
da24ac0a 180class SSODiscourse(base.BaseHandler):
2fe1d960
MT
181 @base.ratelimit(minutes=24*60, requests=100)
182 @tornado.web.authenticated
183 def get(self):
184 # Fetch Discourse's parameters
185 sso = self.get_argument("sso")
186 sig = self.get_argument("sig")
187
188 # Decode payload
189 try:
190 params = self.accounts.decode_discourse_payload(sso, sig)
191
192 # Raise bad request if the signature is invalid
193 except ValueError:
194 raise tornado.web.HTTPError(400)
195
196 # Redirect back if user is already logged in
197 args = {
198 "nonce" : params.get("nonce"),
199 "external_id" : self.current_user.uid,
200
201 # Pass email address
202 "email" : self.current_user.email,
203 "require_activation" : "false",
204
205 # More details about the user
206 "username" : self.current_user.uid,
207 "name" : "%s" % self.current_user,
208 "bio" : self.current_user.description or "",
209
210 # Avatar
211 "avatar_url" : self.current_user.avatar_url(absolute=True),
212 "avatar_force_update" : "true",
213
214 # Send a welcome message
215 "suppress_welcome_message" : "false",
216
217 # Group memberships
218 "admin" : "true" if self.current_user.is_admin() else "false",
219 "moderator" : "true" if self.current_user.is_moderator() else "false",
220 }
221
222 # Format payload and sign it
223 payload = self.accounts.encode_discourse_payload(**args)
224 signature = self.accounts.sign_discourse_payload(payload)
225
226 qs = urllib.parse.urlencode({
227 "sso" : payload,
228 "sig" : signature,
229 })
230
231 # Redirect user
232 self.redirect("%s?%s" % (params.get("return_sso_url"), qs))
233
234
689effd0 235class APICheckUID(base.APIHandler):
66181c96 236 @base.ratelimit(minutes=1, requests=100)
689effd0
MT
237 def get(self):
238 uid = self.get_argument("uid")
239 result = None
240
241 if not uid:
242 result = "empty"
243
244 # Check if the username is syntactically valid
245 elif not self.backend.accounts.uid_is_valid(uid):
246 result = "invalid"
247
248 # Check if the username is already taken
249 elif self.backend.accounts.uid_exists(uid):
250 result = "taken"
251
252 # Username seems to be okay
253 self.finish({ "result" : result or "ok" })
66181c96
MT
254
255
256class APICheckEmail(base.APIHandler):
257 @base.ratelimit(minutes=1, requests=100)
258 def get(self):
259 email = self.get_argument("email")
260 result = None
261
262 if not email:
263 result = "empty"
264
3095c017
MT
265 elif not self.backend.accounts.mail_is_valid(email):
266 result = "invalid"
267
66181c96
MT
268 # Check if this email address is blacklisted
269 elif self.backend.accounts.mail_is_blacklisted(email):
3095c017 270 result = "blacklisted"
66181c96
MT
271
272 # Check if this email address is already useed
273 elif self.backend.accounts.get_by_mail(email):
274 result = "taken"
275
276 self.finish({ "result" : result or "ok" })