Closed LouisDelbosc closed 10 months ago
With some more test I succeed in two things here is latest testing version:
class DeviceSchema(Schema):
class Config:
model = Device
include = ("vendor_uuid", )
vendor_uuid: UUID
class DeviceOutSchema(Schema):
class Config:
model = Device
include = ("vendor_uuid", )
vendor_uuid: UUID
uuid: UUID
class UserOutSchema(Schema):
uuid: UUID
class MyTokenObtainPairOutSchema(Schema):
refresh: str
access: str
user: UserOutSchema
device: DeviceOutSchema
class TokenSessionInputSchema(TokenObtainPairInputSchema):
device: DeviceSchema
def output_schema(self):
out_dict = self.get_response_schema_init_kwargs()
out_dict.update(user=UserOutSchema.from_orm(self._user))
return MyTokenObtainPairOutSchema(**out_dict)
@api_controller('/token', tags=['Auth'])
class MyTokenObtainPairController(TokenObtainPairController):
@route.post(
"/pair", response=MyTokenObtainPairOutSchema, url_name="token_obtain_pair"
)
def obtain_token(self, user_token: TokenSessionInputSchema):
return user_token.output_schema()
With the controller I don't have the typing issues. I add the device
field in the TokenSessionInputSchema
and I see I can access those data with self.dict()
it seems.
Is this the right way to do it ?
If I want to add side-effects, should I do it in the output_schema ? (creating the device if it doesn't exist)
How should I add the User
schema in the TokenSessionInput
to have input data in the shape of {user: {**credentials}, device: {device}}
@LouisDelbosc Sorry I think I am confused here. Correct me if I am wrong,
In my opinion, the best way to create a device when its not existing is in validate_values
method
class TokenSessionInputSchema(TokenObtainPairInputSchema):
device: DeviceSchema
def output_schema(self):
out_dict = self.get_response_schema_init_kwargs()
out_dict.update(user=UserOutSchema.from_orm(self._user))
return MyTokenObtainPairOutSchema(**out_dict)
@classmethod
def validate_values(cls, values: Dict) -> Dict:
update_values = super().validate_values(values)
# read the device data in `update_values` and search for device in database
device_schema = DeviceSchema(**update_values['device'])
device_model = DeviceModel.objects.filter(vendor_uuid=device_schema.vendor_uuid).first()
if not device_model:
# if there is no device then create one here
device = DeviceFactory(user=cls._user)
update_values.update({"device": {"vendor_uuid": device.vendor_uuid}})
return update_values
Nice thank you @eadwinCode, you answer one of my question.
Talking with my team, we'd like to have a passwordless authentication, where we send a code with mail or phone text. Do you we could use the django-ninja-jwt for that ? I saw a blog post using restframework-simple-jwt to achieve it here however instead of putting my token to a user, I'd to link it to a session, a data structure with my user and my device (so a user can be connected with multiples device).
Do you think it's feasible ?
I think if you follow the pattern referenced in the article you shared using NinjaJWT, you will achieve the same result. The article didnt really use restframework-simple-jwt in a deep. So, if you have your user, you can simple call the RefreshToken
from ninja_jwt
and generate a token for the use. An example
from ninja_jwt.token import RefreshToken
class User(AbstractBaseUser, PermissionsMixin):
# Our basic user db model
# This has all the attributes that are set on a user
# as well as methods to work with them
# including this method, which issues a user's tokens
def get_new_tokens(self) -> Dict[str, str]:
blacklist_tokens(self)
refresh = RefreshToken.for_user(self)
return {“refresh”: str(refresh), “access”: str(refresh.access_token)}
About linking it to a session, you might want to create a server session because the client is a mobile phone and not a browser. And ninja-jwt does not offer that solution. But it might be leveraged in one or two actions in the general process
I made some modification for my needs, I create others tokens for partial auth and mobile authentications:
class MobileRefreshToken(RefreshToken):
token_type = "mobile_refresh"
@classmethod
def for_session(cls, session) -> "Token":
session_id = str(session.uuid)
token = cls().for_user(session.user)
token["session_id"] = session_id
return token
I did not create a get_new_tokens()
function on my Session model, maybe I'll do it later.
Finally for the tokens, I did not use the output_schema
, I created the token manually.
# views.py
@router.post("/sessions/", response=SessionOut)
def partial_authentication(request, data: SessionIn):
user, usr_created = User.objects.get_or_create(phone_number=data.phone_number)
device, dev_created = Device.objects.get_or_create(user=user, **data.device.dict())
try:
session = get_alive_session(user, device)
except Session.DoesNotExist:
session = Session.objects.create(
user=user,
device=device,
)
session.get_token() # generate partial_auth_token
return session
class MFAParams(Schema):
mfa_code: str
partial_auth_token: str
class PatchSessionOut(Schema):
id: str
user: UserOut
device: DeviceOut
tokens: typing.Any
@staticmethod
def resolve_tokens(obj):
refresh = MobileRefreshToken.for_session(obj)
return {"refresh": str(refresh), "access": str(refresh.access_token)}
@router.patch("/sessions/", response=PatchSessionOut)
def validate_partial_authentication(request, params: MFAParams):
try:
token = PartialAuthToken(params.partial_auth_token)
token.blacklist()
except TokenError as e:
raise HttpError(401, "Token not valid") from e
try:
session = Session.objects.get(uuid=token.payload["session_id"])
if session.mfa_code == params.mfa_code:
session.status == "confirmed"
session.save()
else:
raise HttpError(400, "Wrong code")
except Session.DoesNotExist:
raise HttpError(404, "Session not found")
return session
I put the token.blacklist()
just after the return in case the mfa_code
is wrong.
Do you think it the right place ? Where should I make sure to blacklist a token ?
Hello, I'd like to obtain the pair of keys but instead of having just
username
andpassword
, I need to have a more complexe structure (because I'm building a mobile app).To better illustrate, here is a simple unit test:
I override the schema
However I got the following error:
{'detail': [{'type': 'missing', 'loc': ['body', 'user_token', 'password'], 'msg': 'Field required'}, {'type': 'missing', 'loc': ['body', 'user_token', 'phone_number'], 'msg': 'Field required'}]}
.In the return of validate_values, he doesn't find the
password
andphone_number
field. So when fixing thevalidate_values
function like followingI got another error:
pydantic_core._pydantic_core.ValidationError: 1 validation error for NinjaResponseSchema response.phone_number
but I did not specifyphone_number
for django-ninja-jwt so I don't know how did he foundphone_number
.Well sorry for this long issue. I tried debugging but I don't know how to configure a custom input (and not only
username
andpassword
) and keep the data inside the classes to have custom behavior (for example creating a device when there is none in the database).Thanks for your help.