Coverage for gws-app/gws/plugin/qfieldcloud/auth.py: 98%
40 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
1"""Token authorization method for QField clients."""
3import datetime
4import re
6import gws
7import gws.base.auth
8import gws.lib.datetimex as dtx
11@gws.ext.config.authMethod('qfieldcloud')
12class Config(gws.base.auth.method.Config):
13 """Token authentication for QField clients."""
16class Result(gws.Data):
17 """Result of a successful authentication."""
19 sess: gws.AuthSession
20 """Session of the user."""
21 user: gws.User
22 """Authenticated user."""
23 token: str
24 """Session token, sent by QField in the ``Authorization`` header."""
25 expiresAt: datetime.datetime
26 """Expiry time of the session."""
29@gws.ext.object.authMethod('qfieldcloud')
30class Object(gws.base.auth.method.Object):
31 """QField Cloud authorization method.
33 Token authorization for QField clients. The ``qfieldcloud`` action creates
34 sessions with this method on login and accepts only tokens of its sessions.
35 """
37 def configure(self):
38 self.uid = 'gws.plugin.qfieldcloud.auth'
40 def authenticate_from_credentials(self, credentials: gws.Data, req: gws.WebRequester) -> Result:
41 """Authenticate a user by username and password and create a session.
43 Args:
44 credentials: Data with ``username`` and ``password``.
45 req: Web request.
47 Returns:
48 Authentication result with the new session and its expiry time.
50 Raises:
51 ``gws.AuthenticationError``: If the credentials are invalid.
52 """
53 am = self.root.app.authMgr
54 user = am.authenticate(self, credentials, req)
55 if not user:
56 raise gws.AuthenticationError('invalid username or password')
58 sess = am.sessionMgr.create(self, user)
59 return Result(
60 sess=sess,
61 user=user,
62 token=sess.uid,
63 expiresAt=dtx.add(seconds=am.sessionMgr.lifeTime),
64 )
66 def authenticate_from_token(self, req: gws.WebRequester) -> Result:
67 """Authenticate a request by the ``Authorization: Token ...`` header and touch the session.
69 Args:
70 req: Web request.
72 Returns:
73 Authentication result with the existing session and its new expiry time.
75 Raises:
76 ``gws.AuthenticationError``: If the header is missing, or the token is invalid or belongs to another method.
77 ``gws.ForbiddenError``: If the method cannot be used in this context, e.g. without a secure connection.
78 """
79 h = req.header('Authorization', '')
80 m = re.match(r'^Token (.+)$', h)
81 if not m:
82 raise gws.AuthenticationError('qfieldcloud_auth: missing or invalid Authorization header')
84 token = m.group(1)
85 am = self.root.app.authMgr
86 if not am.can_use_method(req, self):
87 raise gws.ForbiddenError('qfieldcloud_auth: insecure_context')
89 sess = am.sessionMgr.get(token)
90 if not sess:
91 raise gws.AuthenticationError('qfieldcloud_auth: invalid or expired token')
92 if not sess.method or sess.method.uid != self.uid:
93 raise gws.AuthenticationError(f'qfieldcloud_auth: wrong method {sess.method=}')
95 am.sessionMgr.touch(sess)
96 gws.log.debug(f'qfieldcloud_auth: ok: {token=} {sess.user.uid=} {sess.user.loginName=}')
98 return Result(
99 sess=sess,
100 user=sess.user,
101 token=sess.uid,
102 expiresAt=dtx.add(seconds=am.sessionMgr.lifeTime),
103 )