6
6
7
7
from django .conf import settings
8
8
from django .contrib .auth import get_user_model
9
- from django .http import HttpRequest , HttpResponse
10
- from django .http .response import JsonResponse
9
+ from django .contrib .auth .tokens import default_token_generator
10
+ from django .contrib .sites .shortcuts import get_current_site
11
+ from django .http import HttpRequest
12
+ from django .http .response import HttpResponseBase , JsonResponse
11
13
from django .utils import timezone
12
14
from fastapi_jwt_auth import AuthJWT
13
15
from fastapi_jwt_auth .exceptions import JWTDecodeError
14
16
from ninja import Router , Schema , errors
15
17
from ninja .security import HttpBearer
16
18
from ninja .security .apikey import APIKeyBase
17
19
18
- from . import schemas
20
+ from . import schemas , utils
19
21
20
22
logger = logging .getLogger (__name__ )
21
23
@@ -36,94 +38,129 @@ def get_config():
36
38
class RefreshTokenCookieAuth (APIKeyBase , ABC ):
37
39
"""Check is refresh token exists in the Cookies"""
38
40
39
- # We don't inherit APIKeyCookie
41
+ # We don't inherit APIKeyCookie to skip CSRF check for now
40
42
openapi_in : str = "cookie"
41
43
param_name = "refresh-token"
42
44
43
45
def _get_key (self , request : HttpRequest ) -> Optional [str ]:
44
46
return request .COOKIES .get (self .param_name )
45
47
46
48
def authenticate (self , request : HttpRequest , key : Optional [str ]) -> Optional [Any ]:
47
- user , decoded_token = JWTLogin ().authenticate (key )
48
- if user :
49
- request .user = user
50
- else :
49
+ user , decoded_token = Authenticator ().authenticate_by_token (key )
50
+ if not user :
51
51
return None
52
+
53
+ request .user = user
52
54
return decoded_token
53
55
54
56
55
57
class JWTAuthBearer (HttpBearer ):
56
58
"""Verify JWT token"""
57
59
58
- def authenticate (self , request , token ):
59
- user , decoded_token = JWTLogin ().authenticate (token , is_active = None )
60
- if user :
61
- request .user = user
62
- return decoded_token
63
-
64
-
65
- class JWTAuthUserBearer (HttpBearer ):
66
- """Verify JWT token and make sure the user is active"""
60
+ def __init__ (self , inactive_user_raise_403 : bool = True ) -> None :
61
+ super ().__init__ ()
62
+ self .inactive_user_raise_403 = inactive_user_raise_403
67
63
68
64
def authenticate (self , request , token ):
69
- user , decoded_token = JWTLogin ().authenticate (token )
70
- if user :
71
- request .user = user
65
+ user , decoded_token = Authenticator ().authenticate_by_token (token )
66
+ if user and not user .is_active and self .inactive_user_raise_403 :
67
+ raise errors .HttpError (403 , "Please verify your email." )
68
+ elif not user :
69
+ return None
70
+
71
+ request .user = user
72
72
return decoded_token
73
73
74
74
75
75
@router .post ("/token" , response = schemas .JWTToken )
76
76
def create_jwt_token (request , payload : schemas .JWTTokenCreation ):
77
- try :
78
- user = User .objects .get (username = payload .username , is_active = True )
79
- if not user .check_password (payload .password ):
80
- raise User .DoesNotExist ()
77
+ authenticator = Authenticator ()
81
78
82
- access_token , refresh_token = JWTLogin ().login (user )
83
- resp = JsonResponse (schemas .JWTToken (access = access_token ).dict ())
84
- return JWTLogin .set_refresh_cookie (resp , refresh_token )
79
+ try :
80
+ user = authenticator .authenticate (payload .username , payload .password )
81
+ if not user .is_active :
82
+ raise errors .HttpError (403 , "Please verify your email." )
83
+ access_token , refresh_token = authenticator .login (user )
84
+ kwargs = schemas .JWTToken (access = access_token ).dict ()
85
+ return authenticator .generate_http_response (
86
+ request , JsonResponse , refresh_token , resp_kwargs = kwargs
87
+ )
85
88
except User .DoesNotExist :
86
89
raise errors .HttpError (400 , "Invalid username or password" )
87
90
88
91
89
92
@router .post ("/token/refresh" , auth = RefreshTokenCookieAuth (), response = schemas .JWTToken )
90
93
def refresh_jwt_token (request ):
91
- access_token , refresh_token = JWTLogin ().login (request .user )
94
+ authenticator = Authenticator ()
95
+ access_token , refresh_token = authenticator .login (request .user )
92
96
93
97
# workaround to set Cookie in the response, see: https://github.com/vitalik/django-ninja/issues/117
94
- resp = JsonResponse (schemas .JWTToken (access = access_token ).dict ())
95
- return JWTLogin .set_refresh_cookie (resp , refresh_token )
98
+ return authenticator .generate_http_response (
99
+ request ,
100
+ JsonResponse ,
101
+ refresh_token ,
102
+ resp_kwargs = schemas .JWTToken (access = access_token ).dict (),
103
+ )
104
+
105
+
106
+ @router .get ("/verify-email" , response = schemas .JWTToken )
107
+ def verify_email (request , uid : str , token : str ):
108
+ authenticator = Authenticator ()
109
+ try :
110
+ access_token , refresh_token = authenticator .verify_email (uid , token )
111
+ kwargs = schemas .JWTToken (access = access_token ).dict ()
112
+ return authenticator .generate_http_response (
113
+ request , JsonResponse , refresh_token , resp_kwargs = kwargs
114
+ )
115
+ except Exception as e :
116
+ logger .warning ("Invalid user or token: %s=%s, err: %s" , uid , token , e )
117
+ raise errors .HttpError (401 , "Invalid user or token" )
96
118
97
119
98
- class JWTLogin :
120
+ class Authenticator :
99
121
def __init__ (self ):
100
122
self .auth = AuthJWT ()
101
123
102
124
def login (
103
125
self ,
104
126
user ,
105
- user_claims : typing .Optional [typing .Dict ] = None ,
106
127
) -> typing .Tuple [str , str ]:
107
128
user .last_login = timezone .now ()
108
129
user .save (update_fields = ["last_login" ])
109
130
110
- if user_claims is None :
111
- user_claims = {}
112
-
131
+ user_id = utils .encode_id (user .id )
113
132
return self .auth .create_access_token (
114
- subject = user .username , user_claims = user_claims
115
- ), self .auth .create_refresh_token (subject = user .username )
133
+ subject = user_id
134
+ ), self .auth .create_refresh_token (subject = user_id )
135
+
136
+ def verify_email (self , encoded_id : str , token : str ) -> typing .Tuple [str , str ]:
137
+ user = User .objects .get (id = utils .decode_id (encoded_id ))
138
+ if not default_token_generator .check_token (user , token ):
139
+ raise ValueError ("Invalid or expired token" )
140
+ user .is_active = True
141
+ user .save ()
142
+ return self .login (user )
143
+
144
+ def authenticate (self , username : str , password : str ) -> User :
145
+ user = User .objects .get (username = username )
146
+ if not user .check_password (password ):
147
+ raise User .DoesNotExist ()
148
+ return user
116
149
117
- def authenticate (self , token , is_active : typing .Optional [bool ] = None ):
150
+ def authenticate_by_token (
151
+ self , token : str , is_active : typing .Optional [bool ] = None
152
+ ) -> typing .Tuple [typing .Optional [User ], typing .Optional [str ]]:
118
153
user = None
119
154
decoded_token = None
155
+
120
156
kwargs = {}
121
- if is_active is not None :
157
+ if isinstance ( is_active , bool ) :
122
158
kwargs ["is_active" ] = is_active
123
159
124
160
try :
125
161
decoded_token = self .auth .get_raw_jwt (token )
126
- user = User .objects .get (username = decoded_token ["sub" ], ** kwargs )
162
+ user_id = utils .decode_id (decoded_token ["sub" ])
163
+ user = User .objects .get (id = user_id , ** kwargs )
127
164
except User .DoesNotExist :
128
165
logger .warning ("User doesn't exist: %s" , decoded_token )
129
166
except JWTDecodeError as e :
@@ -133,13 +170,27 @@ def authenticate(self, token, is_active: typing.Optional[bool] = None):
133
170
134
171
return user , decoded_token
135
172
136
- @classmethod
137
- def set_refresh_cookie (cls , resp : HttpResponse , refresh_token : str ) -> HttpResponse :
173
+ @staticmethod
174
+ def generate_http_response (
175
+ request ,
176
+ resp_cls : typing .Type [HttpResponseBase ],
177
+ refresh_token : str ,
178
+ resp_kwargs : typing .Dict = None ,
179
+ unpack_resp_kwargs : bool = False ,
180
+ ) -> HttpResponseBase :
181
+ site = get_current_site (request )
182
+ if resp_kwargs is None :
183
+ resp_kwargs = {}
184
+
185
+ if unpack_resp_kwargs :
186
+ resp = resp_cls (** resp_kwargs )
187
+ else :
188
+ resp = resp_cls (resp_kwargs )
138
189
resp .set_cookie (
139
190
RefreshTokenCookieAuth .param_name ,
140
191
refresh_token ,
141
192
httponly = True ,
142
- domain = settings . SHARED_TW_SETTINGS [ "DOMAIN" ],
193
+ domain = site . domain . split ( ":" , 1 )[ 0 ], # remove port
143
194
samesite = "Strict" ,
144
195
max_age = timedelta (days = 7 ).total_seconds (),
145
196
)
0 commit comments