Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Starts an application.
5"""
6import hashlib
7from starlette.responses import RedirectResponse
8from itsdangerous import URLSafeTimedSerializer
9import ujson
12class AuthentificationAnswers:
13 """
14 Defines answers for an application with authentification.
15 It stores a cookie with only the user alias.
16 The method `authentify_user <mathenjeu.apps.common.auth_app.AuthentificationAnswers.authentify_user>`_
17 must be overwritten. The method
18 `page_context <mathenjeu.apps.qcm.acm_app.ACMApp.page_context>`_
19 returns additional information to add before applying any template.
20 """
22 def __init__(self, app,
23 login_page="login.html",
24 notauth_page="notauthorized.html",
25 auth_page="authorized.html",
26 redirect_logout="/", max_age=14 * 24 * 60 * 60,
27 cookie_key=None, cookie_name="mathenjeu",
28 cookie_domain="127.0.0.1", cookie_path="/",
29 secure=False, page_context=None, userpwd=None):
30 """
31 @param app :epkg:`starlette` application
32 @param login_page name of the login page
33 @param notauth_page page displayed when a user is not authorized
34 @param auth_page page displayed when a user is authorized
35 @param redirect_logout a not authorized used is redirected to this page
36 @param max_age cookie's duration in seconds
37 @param cookie_key to encrypt information in the cookie (cannot be None)
38 @param cookie_name name of the session cookie
39 @param cookie_domain cookie is valid for this path only
40 @param cookie_path path of the cookie once storeds
41 @param secure use secured connection for cookies
42 @param page_context to retrieve additional context
43 before rendering the pages (as a function
44 which returns a dictionary)
45 @param userpwd users are authentified with any alias but a common password
46 """
47 if cookie_key is None:
48 raise ValueError("cookie_key cannot be None")
49 self.app = app
50 self.login_page = login_page
51 self.notauth_page = notauth_page
52 self.auth_page = auth_page
53 self.redirect_logout = redirect_logout
54 self.cookie_name = cookie_name
55 self.cookie_domain = cookie_domain
56 self.cookie_path = cookie_path
57 self.cookie_key = cookie_key
58 self.max_age = max_age
59 self.secure = secure
60 self.signer = URLSafeTimedSerializer(self.cookie_key)
61 self.userpwd = userpwd
62 self.hashed_userpwd = None if userpwd is None else self.hash_pwd(
63 userpwd)
64 self._get_page_context = page_context
65 app._get_session = self.get_session
66 for method in ['log_event', 'log_any']:
67 if hasattr(self, method):
68 setattr(app, '_' + method, getattr(self, method))
70 async def login(self, request):
71 """
72 Login page. If paramater *returnto* is specified in the url,
73 the user will go to this page after being logged.
74 """
75 ps = request.query_params
76 context = {'request': request, 'returnto': ps.get('returnto', '/')}
77 context.update(self._get_page_context())
78 return self.templates.TemplateResponse(self.login_page, context) # pylint: disable=E1101
80 def hash_pwd(self, pwd):
81 """
82 Hashes a password.
84 @param pwd password
85 @return hashed password in hexadecimal format
86 """
87 m = hashlib.sha256()
88 m.update(pwd.encode("utf-8"))
89 return m.hexdigest()
91 async def authenticate(self, request):
92 """
93 Authentification.
95 @param request request
96 @return response
97 """
98 try:
99 fo = await request.form()
100 except Exception as e:
101 raise RuntimeError( # pylint: disable=W0707
102 "Unable to read login and password due to '{0}'".format(e))
103 if 'alias' not in fo:
104 return self.is_allowed(
105 alias=None, pwd=None, request=request)
107 ps = request.query_params
108 loge = getattr(self, 'logevent', None)
109 if loge:
110 loge("authenticate", request, session={}, # pylint: disable=E1102
111 alias=fo['alias'])
112 res = self.is_allowed(alias=fo['alias'], pwd=fo['pwd'],
113 request=request)
114 if res is not None:
115 return res
116 data = dict(alias=fo['alias'], hashpwd=self.hash_pwd(fo['pwd']))
117 returnto = ps.get('returnto', '/')
118 context = {'request': request,
119 'alias': fo['alias'], 'returnto': returnto}
120 context.update(self._get_page_context())
121 response = self.templates.TemplateResponse( # pylint: disable=E1101
122 'authorized.html', context)
123 self.save_session(response, data)
124 return response
125 # response = RedirectResponse(url=returnto)
126 # return response
128 async def logout(self, request):
129 """
130 Logout page.
131 """
132 response = RedirectResponse(url=self.redirect_logout)
133 response.delete_cookie(self.cookie_name, domain=self.cookie_domain,
134 path=self.cookie_path)
135 return response
137 def save_session(self, response, data):
138 """
139 Saves the session to the response in a secure cookie.
141 @param response response
142 @param data data
143 """
144 data = ujson.dumps(data) # pylint: disable=E1101
145 signed_data = self.signer.dumps([data]) # pylint: disable=E1101
146 response.set_cookie(self.cookie_name, signed_data,
147 max_age=self.max_age,
148 httponly=True, domain=self.cookie_domain,
149 path=self.cookie_path, secure=self.secure)
151 def get_session(self, request, notnone=False):
152 """
153 Retrieves the session.
155 @param request request
156 @param notnone None or empty dictionary
157 @return session
158 """
159 cook = request.cookies.get(self.cookie_name)
160 if cook is not None:
161 unsigned = self.signer.loads(cook)
162 data = unsigned[0]
163 jsdata = ujson.loads(data) # pylint: disable=E1101
164 # We check the hashed password is still good.
165 hashpwd = jsdata.get('hashpwd', '')
166 if not self.authentify_user(jsdata.get('alias', ''), hashpwd, False):
167 # We cancel the authentification.
168 return {}
169 return jsdata
170 return {} if notnone else None
172 def is_allowed(self, alias, pwd, request):
173 """
174 Checks that a user is allowed. Returns None if it is allowed,
175 otherwise an page with an error message.
177 @param alias alias or iser
178 @param pwd password
179 @param request received request
180 @return None if allowed, *HTMLResponse* otherwise
181 """
182 if not self.authentify_user(alias, pwd):
183 context = {'request': request, 'alias': alias}
184 context.update(self._get_page_context())
185 return self.templates.TemplateResponse('notauthorized.html', context) # pylint: disable=E1101
186 return None
188 def authentify_user(self, alias, pwd, hash_before=True):
189 """
190 Overwrites this method to allow or reject users.
192 @param alias alias or user
193 @param pwd password
194 @param hash_before hashes the password before comparing, otherwise,
195 the function assumes it is already hashed
196 @return boolean
198 The current behavior is to allow anybody if the alias is longer
199 than 3 characters.
200 """
201 if alias is None or len(alias.strip()) <= 3:
202 return False
203 if self.hashed_userpwd is None:
204 return True
205 if hash_before:
206 hashed_pwd = self.hash_pwd(pwd)
207 return hashed_pwd == self.hashed_userpwd
208 return pwd == self.hashed_userpwd