CSRF exempt on REST View - django

I am serving an API which will be accessible with a small sensor sending a POST request with data. This sensor has a limited software, and I want to disable the CSRF protection on my API view.
So I've added the decorator:
url(
regex=r'^beacons/$',
view=csrf_exempt(ScanListCreateAPIView.as_view()),
name='beacons'
),
Unfortunately, when I perform a POST with my sensor, I still get a 403 error:
<h1>Forbidden <span>(403)</span></h1>
<p>CSRF verification failed. Request aborted.</p>
<p>You are seeing this message because this HTTPS site requires a 'Referer
header' to be sent by your Web browser, but none was sent
. This header is
required for security reasons, to ensure that your browser is not being
hijacked by third parties.</p>
<p>If you have configured your browser to disable 'Referer' headers, please
re-enable them, at least for this site, or for HTTPS connections, or for
'same-origin' requests.</p>
I've try to add a "Referer: " null header in my POST request, but I still have a 403 response, mentionning that CSRF failed.
My request is:
POST /api/beacons HTTP/1.1
Host: vincent.pythonanywhere.com
Content-Type: application/json
Accept: */*
User-Agent: Mozilla/4.0 (compatible; esp8266 Lua; Windows NT 5.1)
Content-Length: 597
{"beacon":"aaa"," ...
The same request passed throught curl is working ok, with a 201 response.

Here is the solution to diable CSRF:
1- As DRF does its own csrf with SessionAuth, you have to specify in the view:
authentication_classes = (BasicAuthentication,)
2- Then I don't know exacly why, but view=csrf_exempt(ScanListCreateAPIView.as_view()), in urls doesn't work. Instead, use the braces mixin:
from braces.views import LoginRequiredMixin, CsrfExemptMixin
class ScanListCreateAPIView(ListCreateAPIView, CsrfExemptMixin):
authentication_classes = (BasicAuthentication,)

Related

cookies are not being sent during post request (flask-cors)

i have credentials in cookies that must be sent over a post request, but they are not present.
however, they are present in get requests
here is my flask cors config
CORS(
app,
resources={"/*": {"origins": ["http://localhost:3000", "http://localhost:1002"]}},
allow_headers="*",
supports_credentials=True,
expose_headers=[
"tokens",
"Set-Cookie",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials",
],
)
here is the response from preflight (options)
As you can tell I have allow credentials true, and my allow-origin is explicitly set to an origin
again, all get requests succeed, while all post requests fail with a 401 where the cookies are nowhere to be found.

Django rest framework + axios: Invalid preflight http method on first request

We have a rather strange situation with a react based frontend using axios to talk to our Django rest framework based backend.
When logging in a preflight OPTIONS request is sent by axios (as expected) but the first time the backend receives it, it seems to be malformed and thus results in a 401.
However if I the retry, or even replay the exact same request using the browsers dev tool, the backend accepts the OPTIONS request and everything works as expected. We can consistently reproduce this.
The Django development server log looks like this:
First request
[23/Jan/2019 15:43:42] "{}OPTIONS /backend/api/auth/login/ HTTP/1.1" 401
Subsequent Retry
[23/Jan/2019 15:43:52] "OPTIONS /backend/api/auth/login/ HTTP/1.1" 200 0
[23/Jan/2019 15:43:52] "POST /backend/api/auth/login/ HTTP/1.1" 200 76
So as you can see, curly braces are added in the request method, which means that the request is not considered to be an OPTIONS request, and therefore a 401 is returned.
The django view
class LoginView(KnoxLoginView):
authentication_classes = [BasicAuthentication]
# urls.py (extract)
path('auth/login/', LoginView.as_view(), name='knox_login'),
The Axios request
axios.post('/backend/api/auth/login/', {}, {
headers: {
'Authorization': 'Basic ' + window.btoa(creds.username + ":" + creds.password)
}
}).then((response) => {
dispatch({type: 'SIGNIN_SUCCESS', response})
}).catch((err) => {
dispatch({type: 'SIGNIN_ERROR', err})
})
Some package version info
Django==2.1.4
django-cors-headers==2.4.0
django-debug-toolbar==1.11
django-extensions==2.1.4
django-rest-knox==3.6.0
django-rest-passwordreset==0.9.7
djangorestframework==3.9.0
axios#0.18.0

Cognito logout does not work as documented

I have a Cognito user pool configured with a SAML identity provider (ADFS) and I'm able to sign it as a federated user (AD) but sign out does not work.
Following the documentation, I make a GET request to
https://my-domain.auth.us-west-2.amazoncognito.com/logout?client_id=63...ng&logout_uri=http:%2F%2Fyahoo.com (using some public logout uri), from my client (an AngularJS 1.x app), and I get back a 302 with a Location header like
https://my-domain.auth.us-west-2.amazoncognito.com/login?client_id=63...ng&logout_uri=http:%2F%2Fyahoo.com
(In fact there I see 2 requests like the above).
When I log back in (thru ADFS) it does not prompt for my AD credentials, i.e. seems that I'm not logged out.
My user pool is configured as described here (see step 7), where the Enable IdP sign out flow is checked, which is supposed to log the user out from ADFS as well.
Any suggestions?
Thanks.
General
-------
Request URL: https://my-domain.auth.us-west-2.amazoncognito.com/logout?client_id=63...ng&logout_uri=http:%2F%2Fyahoo.com
Request Method: GET
Status Code: 302
Remote Address: 54.69.30.36:443
Referrer Policy: no-referrer-when-downgrade
Response Headers
----------------
cache-control: private
content-length: 0
date: Fri, 20 Apr 2018 21:31:12 GMT
expires: Thu, 01 Jan 1970 00:00:00 UTC
location: https://my-domain.auth.us-west-2.amazoncognito.com/login?client_id=63...ng&logout_uri=http:%2F%2Fyahoo.com
server: Server
set-cookie: XSRF-TOKEN=...; Path=/; Secure; HttpOnly
set-cookie: XSRF-TOKEN=""; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Path=/; Secure; HttpOnly
status: 302
strict-transport-security: max-age=31536000 ; includeSubDomains
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
Request Headers
---------------
:authority: my-domain.auth.us-west-2.amazoncognito.com
:method: GET
:path: /logout?client_id=63...ng&logout_uri=http:%2F%2Fyahoo.com
:scheme: https
accept: application/json, text/plain, */*
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
authorization: Bearer eyJra...
cache-control: no-cache
origin: https://localhost:8443
pragma: no-cache
referer: https://localhost:8443/logout
user-agent: Mozilla/5.0...
This redirect happens whenever logout_uri parameter doesn't match exactly what's listed among Sign out URL(s) in AWS Cognito User Pools App client settings configuration.
Cognito allows logout with either logout_uri or with the same arguments as login (i.e. redirect_uri and response_type) to log out and take the user back to the login screen. It seems that whenever logout_uri is invalid, it assume the re-login flow, does this redirect, and then reports an error about missing login arguments.
As for SAML, I don't know, but guessing that it doesn't work because there was actually an error, just not properly reported.
The /logout endpoint signs the user out.It only supports HTTPS GET. It is working
Sample Requests - Logout and Redirect Back to Client
It clears out the existing session and redirects back to the client. Both parameters are required.
GET https://<YOUR DOMAIN NAME>/logout?
client_id=xxxxxxxxxxxx&
logout_uri=com.myclientapp://myclient/logout
Also make sure that Logout URL is same as SIGNOUT URL in AWS Cognito APP too.
for more information, refer AWS LOGOUT Endpoint
Finally I was able to fix this issue. I found the actual cause of the issue from the event viewer of my windows server 2012 R2. It says the following details about the failed sign out flow.
The SAML Single Logout request does not correspond to the logged-in session participant.
Requestor: urn:amazon:cognito:sp:userpoolid
Request name identifier: Format: urn:oasis:names:tc:SAML:2.0:nameid-format:persistent, NameQualifier: SPNameQualifier: , SPProvidedId:
Logged-in session participants:
Count: 1, [Issuer: urn:amazon:cognito:sp:userpoolid, NameID: (Format: , NameQualifier: SPNameQualifier: , SPProvidedId: )]
User Action
Verify that the claim provider trust or the relying party trust configuration is up to date. If the name identifier in the request is different from the name identifier in the session only by NameQualifier or SPNameQualifier, check and correct the name identifier policy issuance rule using the AD FS 2.0 Management snap-in.
The Error clearly says that the name identifier in the request is different from the name identifier in the session only by NameQualifier. I have corrected this error in the claim issuance tab of relying party trusts by adding the rule as below. The below rule replace the user#myadfsdomain to simply user when issuing the claim.
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"]
=> issue(Type = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", Issuer = c.Issuer, OriginalIssuer = c.OriginalIssuer, Value = RegExReplace(c.Value, "(?i)^myadfsdomain*\\", ""), ValueType = c.ValueType, Properties["http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format"] = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent");
Besides this i have forgot to check in the enable signout flow in the cognito configuration which caused the problem. Logout started working seamlessly for me.
From documentation here
https://docs.aws.amazon.com/cognito/latest/developerguide/logout-endpoint.html
If you want
And your login url is like
"https://xxxx.auth.eu-west-1.amazoncognito.com/login?client_id=1234&response_type=token&scope=aws.cognito.signin.user.admin+email+openid+phone+profile&redirect_uri=http://localhost:3000/"
Then your logout url is like
"https://xxxx.auth.eu-west-1.amazoncognito.com/logout?client_id=1234&logout_uri=http://localhost:3000/";
Note the difference
login > login? and redirect_uri
logout > logout? and logout_uri
Those redirect / logout uri must match what you have configured inside Cognito.
If you don't do it right, you may get strange errors like
error=unauthorized_client
or
Required String parameter 'response_type' is not present
and who knows what else. :o)
My solution below is based on -
https://github.com/nextauthjs/next-auth/discussions/3938#discussioncomment-2165155
I am able to solve the issue with few changes-
in signout button upon clicking i will call handleSignOut
function handleSignOut() {
const clientId = 'paste your AWS COGNITO CLIENT ID';
const domain = 'paste your AWS COGNITO DOMAIN';
window.location.href = `${domain}/logout?client_id=${clientId}&logout_uri=http://localhost:3000/logout`
}
In Aws cognito-> app integration -> app client settings -> signout url
keep the following url-
http://localhost:3000/logout
in pages create a seperate page called logout and keep following code-
import { useEffect } from "react"
import { signOut } from 'next-auth/react'
export default function SignoutPage() {
useEffect(() => {
setTimeout(() => {
signOut({ callbackUrl: window.location.origin });
}, 1000);
}, []);
return (
<>
<div className="loader">
Loading..
</div>
<style jsx>{`
.loader{
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform: -webkit-translate(-50%, -50%);
transform: -moz-translate(-50%, -50%);
transform: -ms-translate(-50%, -50%);
}
`}</style>
</>
)
}
click signout it will log you out
#RequestMapping(value = "/logout", method = RequestMethod.GET)
public void logout(HttpServletRequest request, HttpServletResponse response) {
LOGGER.info("Logout controller");
try {
Cookie awsCookie = new Cookie("AWSELBAuthSessionCookie-0", "deleted");
awsCookie.setMaxAge(-1);
awsCookie.setPath("/");
response.addCookie(awsCookie);
Properties appProperties = new Properties();
applicationPropertyConfigurer.loadProperties(appProperties);
response.sendRedirect(appProperties.getProperty("logout.url"));
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", -1);
request.getSession().invalidate();
} catch (IOException e) {
LOGGER.error("Exception in redirecting to logout.url URL", e);
}
}
//https:/domain_name.auth.us-west-x.amazoncognito.com/logout?response_type=code&client_id=&redirect_uri=redirect_uri_should_be_present_in_cognito_user_pool_Callback URL&state=STATE&scope=openid

why is XMLHttpRequest.withCredentials necessary even for same site Ajax requests

I am trying to implement an authentication service deployed in a different HTTP server from the one serving my login page.
The following diagram depicts my setup:
On step #1 my browser makes an HTTP GET request to fetch the login page. This is provided as the HTTP response in #2. My browser renders the login page and when I click the login button I send an HTTP POST to a different server (on the same localhost). This is done in #3. The authentication server checks the login details and sends a response that sets the cookie in #4.
The ajax POST request in #3 is made using jQuery:
$.post('http://127.0.0.1:8080/auth-server/some/path/',
{username: 'foo', password: 'foo'},
someCallback);
The authentication service's response (assuming authentication was successful) has the following header:
HTTP/1.1 200
Set-Cookie: session-id=v3876gc9jf22krlun57j6ellaq;Version=1
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: origin, content-type, accept, authorization
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, HEAD
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 21 Nov 2016 16:17:08 GMT
So the cookie (session-id) is present in the HTTP response in step #4.
Now if the user tries to login again I would like the authentication service to detect that. To test that scenario I press the login button again in order to repeat the post in #3. I would have expected the second request to contain the cookie. However the second time I press the login button, the request header in the post sent out in #3 does not contain the cookie.
What I have discovered is that for the post in #3 to contain the cookie, I have to do it like this:
$.ajax({
type: 'post',
url: 'http://127.0.0.1:8080/auth-server/some/path',
crossDomain: true,
dataType: 'text',
xhrFields: {
withCredentials: true
},
data: {
username : 'foo',
password : 'foo',
},
success: someCallback
});
Why would that be necessary? MDN states that this is only required for cross-site requests. This SO post also uses xhrFields but only in relation to a cross-domain scenario. I understand that my case is not cross-domain as both the page that serves the script is on localhost, and the page to where the ajax request is sent is on the same host. I also understand that cookie domains are not port specific. Moreover, since my cookie did not explicitly specify a domain, then the effective domain is that of the request meaning 127.0.0.1 which is identical the second time I send the POST request (#3). Finally, the HTTP reponse on #4 already includes Access-Control-Allow-Origin: * which means that the resource can be accessed by any domain in a cross-site manner.
So why did I have to use the xhrFields: {withCredentials: true} to make this work?
What I understand is that setting Access-Control-Allow-Origin: * is simply enabling cross-site requests but that in order for cookies to be sent then the xhrFields: {withCredentials: true} should be used regardless (as explained in MDN section on requests with credentials). Moreover, I understand that the request is indeed cross-site since the port number is important when deciding whether a request is cross-site or not. Whether the domain of a cookie includes ports is irrelevant. Is this understanding correct?
update
I think this is explained very clearly in this answer, so maybe this question should be deleted.
All parts of the origin must match the host(ajax target) for it to be considered same-origin. The 3 part of the origin https://sales.company.com:9443 for example includes:
protocol/scheme (https - won't match http)
hostname (sales.company.com - won't match subdomain.sales.company.com)
port (9443 - won't match 443)
see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy

Any idea why I can't POST to this Django REST API?

I'm currently trying to get a POST request using multipart/form-data running to the Django REST framework. I've successfully run through some test requests via the interactive API screens, which work fine. I've then tried to convert these over to using a non-Session based auth strategy, and I've consistently got errors. The requests I've sent are of the form:
POST /api/logs/ HTTP/1.1
Host: host:8080
Connection: keep-alive
Content-Length: 258
Accept: application/json
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTOhRsMbL8ak9EMQB
Authorization: Token -token-
------WebKitFormBoundaryx6ThtBDZxZNUCkKl
Content-Disposition: form-data; name="SubmittedAt"
2014-01-23T10:39:00
------WebKitFormBoundaryx6ThtBDZxZNUCkKl
Content-Disposition: form-data; name="Device"
CheeseDevice
------WebKitFormBoundaryx6ThtBDZxZNUCkKl--
Sadly, the result has been (for all the requests I've run):
{"Device": ["This field is required."], "SubmittedAt": ["This field is required."], "LogFile": ["This field is required."]}
Interestingly, I've been able to send chunks of JSON through to the endpoint, and they're accepted as expected, eg:
POST /api/logs/ HTTP/1.1
Content-Type: application/json
Host: host:8080
Connection: keep-alive
Content-Length: 35
Accept: application/json
Authorization: Token -token-
{
"Device": "CheeseDevice"
}
Returns:
{"SubmittedAt": ["This field is required."], "LogFile": ["This field is required."]}
As expected - it actually accepts the Device argument and only raises errors on the missing items. I'd switch to using JSON, but sadly cannot upload files with it...
Thanks in advance for any help!
Edit:
Further investigation (ie: writing a view method that returns the request data shows that request.DATA isn't getting populated, for some reason. Method I'm using to debug follows:
def test_create(self, request, pk=None):
return Response(request.DATA)
Edit 2:
Even further investigation (and dropping code chunks into the framework for debugging) indicates that the requests are getting caught up in _perform_form_overloading and never hitting the MultiPartParser. Not sure why this is occurring but I'll try and trace it further.
After delving down every level I could find...
Looks like the problem stems from the line endings - ie: the libs and request senders I've been using send the content through with "\n" (LF) endings, while the HTTP spec requires "\r\n" endings (CR,LF)
This hinges on the following code in the Django core, within http/multipartparser.py - in parse_boundary_stream:
header_end = chunk.find(b'\r\n\r\n')
For dev purposes (and because it's going to be way easier to patch at the Django end than in the clients...) I've switched the above line to:
header_end = chunk.replace("\r\n","\n").find(b'\n\n')
This updated code follows the recommendations in Section 19.3 of the HTTP/1.1 spec regarding Tolerant Applications and accepting LF instead of just CRLF - I'll try and get around to seeing if this is suitable for inclusion in the Django core.
Edit:
For reference, the patch is up on GitHub: https://github.com/tr00st/django/commit/9cf6075c113dd27e3743626ab0e18c6616488bd9
This could be due to malformed multipart post data.
Also possible that you don't have MultiPartParser installed, but I don't think that'll be it as you'd normally expect to see a 415 Unsupported Media Type response in that case.