There seems to be a bug in the way DRF renders hyper-linked URL's in the Self describing API. DRF is translating my url signature for S3 (django-storage) from %2B to '+' when being displayed. However format=json does not have this translation issue.
For example:
This is the Serializer:
class CatalogueSerializer(serializers.HyperlinkedModelSerializer):
image = HyperlinkedImageField()
class Meta:
model = CatalogueItem
fields = ('url', 'name', 'image')
HyperlinkedImageField:
class HyperlinkedImageField(serializers.ImageField):
def to_native(self, value):
request = self.context.get('request', None)
if value:
url = request.build_absolute_uri(value.url)
else:
url = 'null'
return url
Value of the URL is correct and the signature is right. However when DRF renders the URL it changes the Signature from this....
Good
Signature=lMG4NLl51IHeXWCU%2B2GPBN1vU30%3D&Expires=1404604768
to this:
Bad
Signature=lMG4NLl51IHeXWCU+2GPBN1vU30=&Expires=1404604768
Where the only difference is the translation of %2B to '+'.
I have tried to get around the following in my serializer:
def transform_image(self, obj, value):
return urllib.quote(value, safe="%/:=&?~#+!$,;'#()*[]")
But, no matter, it always does the translation of %2B to '+'.
Is there a work around?
You could wrap whatever string you want in "slugify": https://docs.djangoproject.com/en/dev/ref/utils/#django.utils.text.slugify
That makes sure that the string is always "URL safe"
Related
I'm working at the moment on an implementation of webauthn on a project. The main point is to give the possibility to user to use FaceId or fingerprint scan on their mobile on the website.
I tried the djoser version of webauthn but I wanted to give the possibility to user that already have an account so I took the implementation of webauthn of djoser and I updated it to make it working with already created account.
I can ask for the signup request of a webauthn token and create the webauthn token with the front (Angular) where I use #simplewebauthn/browser ("#simplewebauthn/browser": "^6.3.0-alpha.1") . Everything is working fine there.
I use the latest version of djoser by pulling git and the version of webauthn is 0.4.7 linked to djoser.
djoser #git+https://github.com/sunscrapers/djoser.git#abdf622f95dfa2c6278c4bd6d50dfe69559d90c0
webauthn==0.4.7
But when I send back to the backend the result of the registration, I have an error:
Authentication rejected. Error: Invalid signature received..
Here's the SignUpView:
permission_classes = (AllowAny,)
def post(self, request, ukey):
co = get_object_or_404(CredentialOptions, ukey=ukey)
webauthn_registration_response = WebAuthnRegistrationResponse(
rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
origin=settings.DJOSER["WEBAUTHN"]["ORIGIN"],
registration_response=request.data,
challenge=co.challenge,
none_attestation_permitted=True,
)
try:
webauthn_credential = webauthn_registration_response.verify()
except RegistrationRejectedException as e:
return Response(
{api_settings.NON_FIELD_ERRORS_KEY: format(e)},
status=status.HTTP_400_BAD_REQUEST,
)
user = User.objects.get(username=request.data["username"])
user_serializer = CustomUserSerializer(user)
co.challenge = ""
co.user = user
co.sign_count = webauthn_credential.sign_count
co.credential_id = webauthn_credential.credential_id.decode()
co.public_key = webauthn_credential.public_key.decode()
co.save()
return Response(user_serializer.data, status=status.HTTP_201_CREATED)
And I based my work on https://github.com/sunscrapers/djoser/blob/abdf622f95dfa2c6278c4bd6d50dfe69559d90c0/djoser/webauthn/views.py#L53
Here's also the SignUpRequesrtView where I edited some little things to make it working the way I want:
class SignupRequestView(APIView):
permission_classes = (AllowAny,)
def post(self, request):
CredentialOptions.objects.filter(username=request.data["username"]).delete()
serializer = WebauthnSignupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
co = serializer.save()
credential_registration_dict = WebAuthnMakeCredentialOptions(
challenge=co.challenge,
rp_name=settings.DJOSER["WEBAUTHN"]["RP_NAME"],
rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
user_id=co.ukey,
username=co.username,
display_name=co.display_name,
icon_url="",
)
return Response(credential_registration_dict.registration_dict)
And I also updated the WebAuthnSignupSerializer to retrieve an check if there's an account with the username given and if yes, create the CredentialOptions:
class WebauthnSignupSerializer(serializers.ModelSerializer):
class Meta:
model = CredentialOptions
fields = ("username", "display_name")
def create(self, validated_data):
validated_data.update(
{
"challenge": create_challenge(
length=settings.DJOSER["WEBAUTHN"]["CHALLENGE_LENGTH"]
),
"ukey": create_ukey(length=settings.DJOSER["WEBAUTHN"]["UKEY_LENGTH"]),
}
)
return super().create(validated_data)
def validate_username(self, username):
if User.objects.filter(username=username).exists():
return username
else:
raise serializers.ValidationError(f"User {username} does not exist.")```
EDIT: TL;DR;
#simplewebauthn/browser encodes the signature as base64url while duo-labs/py_webauthn expects a hex encoded signature.
Well, it's not really an answer, but rather a little "assistance".
You can check whether the signature is valid using this little tool (at the bottom of the page): https://webauthn.passwordless.id/demos/playground.html
At least, using that, you will know if your data is correct or if something was stored wrongly. There are so many conversions from bytes to base64url and back that it's not always easy to track. Perhaps it is a data format/convertion issue? Like not converting to bytes, or accidentally double encoding as base64url.
Lastly, the stored public key has a different format depending on the algorithm. Either "raw" or "ASN.1" wrapped, in case you have a problem with the key itself.
Good luck!
EDIT:
While delving a bit into the source code of sunscrapers/djoser, I noticed something quite odd. While all data is encoded as base64, it appears the signature is hex encoded instead, see their test app
That seems to be because it uses duo-labs/py_webauthn as dependency which expects a hex encoded signature. On the other hand, the #simplewebauthn/browser lib encodes it into base64url, like all other data.
The verify() method expects a RegistrationResponse object as an argument, but you're passing it the entire request data. You need to extract the registrationResponse field from the request data and pass that to the verify() method instead.
Change this:
webauthn_registration_response = WebAuthnRegistrationResponse(
rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
origin=settings.DJOSER["WEBAUTHN"]["ORIGIN"],
registration_response=request.data, # <-- update this line
challenge=co.challenge,
none_attestation_permitted=True,)
To this:
webauthn_registration_response = WebAuthnRegistrationResponse(
rp_id=settings.DJOSER["WEBAUTHN"]["RP_ID"],
origin=settings.DJOSER["WEBAUTHN"]["ORIGIN"],
registration_response=request.data['registrationResponse'], # <-- update this line
challenge=co.challenge,
none_attestation_permitted=True,)
I have a model whose Primary Key is a TextField. This is a minimalistic reproduction of my issue:
Model:
class Filename(models.Model):
path = models.TextField(primary_key=True)
Serializer:
class FilenameSerializer(ModelSerializer):
class Meta:
model = Filename
fields = '__all__'
View:
class FilenameViewSet(ModelViewSet):
queryset = Filename.objects.all()
serializer_class = FilenameSerializer
I'm using a DefaultRouter for URLs. Here is the problem: If I sent
{"path":"test"} with a POST /filename/ I can perfectly retrieve my object with GET /filename/test/ as you would expect. However, if I POST /filename/ something like {"path":"c:\\test"} I would expect either GET /filename/c%3A%5Ctest/ or GET /filename/c%3A%5C%5Ctest/ to be the proper way to get it, but none of those works. Does anybody knows what's going on?
Update: The webserver logs show Not Found: /filename/c:\test so it's being decoded properly at some moment. Maybe some URL regex issue?
The issue was not with any URL encoded in general, but with the dot (%2E) in particular. The DefaultRouter() does not match the dots by default. This behavior can be modified setting the lookup_value_regex attribute in the ViewSet.
class FilenameViewSet(ModelViewSet):
queryset = Filename.objects.all()
serializer_class = FilenameSerializer
lookup_value_regex = '[^/]+'
hey you have to define the lookupfield , you can overide the get_object change string format to a norma text
class FilenameViewSet(ModelViewSet):
queryset = Filename.objects.all()
serializer_class = FilenameSerializer
lookup_field = 'path'
def get_object(self):
try:
from urllib import unquote
except ImportError:
from urllib.parse import unquote
path = unquote(kwarhs['path'))
return self.get_queryset.get(path=path)
if im getting your question correctly . you problem is with json format .
On adding escape character \ before special characters would resolve your issue.
see this link below:
How to escape special characters in building a JSON string?
Hard facts:
I am using Django 2.0 with python 3.6, if it makes any difference.
What I am trying to achieve is a link to a list of objects that belong to a summary.
I have a ManyToOne relationship in my models.py.
class Summary(models.model):
type=models.CharField
class Object(models.Model):
summary= models.ForeignKey(Summary, on_delete=models.CASCADE)
in urls.py
object_list= views.ObjectListViewSet.as_view({
'get': 'list'
})
urlpatterns = format_suffix_patterns([
url(r'^summary/(?P<pk>[^/.]+)/objects/$', object_list, name='summary-objects')
])
and now the idea was to give a user the possibility to click the an url in the browsable API and getting all objects.
So, I tried to write a MethodField in serializers.py. I am not able to get any reasonable URL here, the only solution would be to hardcode it.
class SummarySerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="app:summary-detail")
objects= serializers.SerializerMethodField('get_obj_url')
def get_obj_url(self, obj):
pass
class Meta:
model = Summary
Is this possible?
Is it necessary to write a MethodField?
If yes, how do I get the url I need?
Actually, reverse, as suggested in the comments, does the trick.
The solution is:
def get_obj_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse('api-root')) + 'summary/{id}/objects'.format(
id=obj.id)
EDIT:Typo
If I define a django-rest-framework view framework as:
class ProblemViewSet(viewsets.ModelViewSet):
queryset = Problem.objects.all()
serializer_class = ProblemSerializer
#detail_route(methods=['post'], permission_classes=(permissions.IsAuthenticated,))
def submit(self, request, *args, **kwargs):
# my code here
return Response(...)
When I look at the CoreAPI schema that is defined, I find this:
problems: {
create(title, text, value, category, hint)
destroy(pk)
list([page])
partial_update(pk, [title], [text], [value], [category], [hint])
retrieve(pk)
submit(pk)
update(pk, title, text, value, category, hint)
}
I'd like the submit API endpoint to take an additional parameter, called answer, but so far I haven't figured out how to add such a custom parameter. I know I can just pass in the POST array, but that seems inelegant and un-RESTful. Any ideas?
I don't understand your question, but I think that you want to add in the input the filed answer to django-rest form or raw, you can do this adding in the serializer ProblemSerializer the next:
from rest_framework import serializers
...
class CustomerSerializer(serializers.ModelSerializer):
answer = serializers.SerializerMethodField()
class Meta:
model = Problem
fields = ['answer', 'my-normal-params',...]
def get_answer(self, problem):
if hasattr(problem, 'answer'):
request = self.context['request']
return answer
In get_answer method you will display in the json the value that you want, or you can return None (null) if you want,
If that is not your problem, please say me, I'll help you.
I am trying to write something elegant where I am not relying on Request object in my code. All the examples are using:
(r'^hello/(?P.*)$', 'foobar.views.hello')
but it doesn't seem like you can post to a URL like that very easily with a form. Is there a way to make that URL respond to ..../hello?name=smith
Absolutely. If your url is mapped to a function, in this case foobar.views.hello, then that function might look like this for a GET request:
def hello(request):
if request.method == "GET":
name_detail = request.GET.get("name", None)
if name_detail:
# got details
else:
# error handling if required.
Data in encoded forms, i.e. POST parameters, is available if you HTTP POST from request.POST.
You can also construct these yourself if you want, say, query parameters on a POST request. Just do this:
PARAMS = dict()
raw_qs = request.META.get('QUERY_STRING', '') # this will be the raw query string
if raw_qs:
for line in raw_qs.split("&"):
key,arg = line.split("=")
PARAMS[key] = arg
And likewise for form-encoded parameters in non POST requests, do this:
FORM_PARAMS = QueryDict(request.raw_post_data)
However, if you're trying to use forms with Django, you should definitely look at django.forms. The whole forms library will just generally make your life easier; I've never written a html form by hand using Django because this part of Django takes all the work out of it. As a quick summary, you do this:
forms.py:
class FooForm(forms.Form):
name = fields.CharField(max_length=200)
# other properties
or even this:
class FooForm(forms.ModelForm):
class Meta:
model = model_name
Then in your request, you can pass a form out to the template:
def pagewithforminit(request):
myform = FooForm()
return render_to_response('sometemplate.html', {'nameintemplate': myform},
context_instance=RequestContext(request))
And in the view that receives it:
def pagepostingto(request):
myform = FooForm(request.POST)
if myform.is_valid(): # check the fields for you:
# do something with results. if a model form, this:
myform.save()
# creates a model for you.
See also model forms. In short, I strongly recommend django.forms.
You can't catch GET parameters in a URL pattern. As you can see in django.core.handlers.base.BaseHandler.get_response, only the part of the URL that ends up in request.path_info is used to resolve an URL:
callback, callback_args, callback_kwargs = resolver.resolve(
request.path_info)
request.path_info does not contain the GET parameters. For handling those, see Ninefingers answer.