how Play sends CSRF token? - playframework-2.6

When my application retrieves index.html and other js and css files from the server, I do not see an csrf token in headers or cookies. How does play sends csrf token?
My UI is an Angular application being served from play. From the documents, I read about csrf token that
This token gets placed either in the query string or body of every form submitted, and also gets placed in the user’s session
The documentation also says that
To ensure that a CSRF token is available to be rendered in forms, and sent back to the client, the global filter will generate a new token for allGETrequests that accept HTML, if a token isn’t already available in the incoming request. - But I don't see this token in response to my initial GET request.
As my UI (and thus form) is not a play UI, I cannot use play's annotation to put csrf token in the form. I would like that when the homepage is delivered, play sends the csrf token which Angular application can store and use later.
Following are the headers I see on browser's developer console.
Response headers
Content-Length 1421
Content-Type text/html; charset=UTF-8
Date Sun, 11 Mar 2018 21:23:52 GMT
Referrer-Policy origin-when-cross-origin, strict-origin-when-cross-origin
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-Permitted-Cross-Domain-Policies master-only
X-XSS-Protection 1; mode=block
Request headers (600 B)
Accept text/html,application/xhtml+xm…plication/xml;q=0.9,*/*;q=0.8
Accept-Encoding gzip, deflate
Accept-Language en-US,en;q=0.5
Connection keep-alive
Cookie PLAY_SESSION=eyJhbGciOiJIUzI1N…AR2uh5KwKBhqKxQQT1wWPWC2yPyCM
Host localhost:9000
Upgrade-Insecure-Requests 1
User-Agent Mozilla/5.0 (Windows NT 10.0; …) Gecko/20100101 Firefox/58.0
The Action in play which servers the homepage is
def index =
Action { implicit request =>
val Token(name, value) = CSRF.getToken.get
println(s"Token name ${name}, value ${value}")
Ok(views.html.index("Your new application is ready."))
}
I can see (print) the token name and value but I am not sure if it is being sent in the Ok response.

This is a partial answer. The 3 csrf configurations of interest in play are token, cookie and header names
if none of the token, cookie and header of csrf properties are configured then the default values are csrfToken for token name), nothing gets configured for cookie and Csrf-Token for header
When token name is configured then play seem to send a PLAY_SESSION cookie. Eg token.name = "CJCsrfToken". In this case, the name of the token is CJCsrfToken instead of csrfToken. However, I couldn't find how csrfToken gets sent and how to retrieve it in the client. I have an Angular5 client and I couldn't get it to pass csrf when only token.name was configured in play.
If cookie name is configured, Play will store the csrf token in a cookie with the given name, instead of in the session. I suppose we should configure either token.name or cookie.name. Eg cookie.name = "CJCsrfCookie" means you should see a cookie with name CJCsrfCookie
Now if only cookie.name is configured but no header name is configured then Play expects that requests from client will contain the csrf token in header Csrf-Token (the default header name)
The code in Angular to accept the cookie and return header was
HttpClientXsrfModule.withOptions({ cookieName: 'CJCsrfCookie', headerName: 'Csrf-Token' }),
If you do not want to use default header name, configure the new name in header.name. This would be the name of the header to accept CSRF tokens from.
eg header.name = "CJCsrfHeader"
The code in Angular to accept the cookie and return header was
HttpClientXsrfModule.withOptions({ cookieName: 'CJCsrfCookie', headerName: 'CJCsrfHeader' }),
Note that for the Angular part, the url has to be relative. See this angular4 httpclient csrf does not send x-xsrf-token has

Related

Do browsers block POST requests if POST isn’t in the Access-Control-Allow-Methods value of the preflight OPTIONS response?

I think I understand CORS pretty well, but I'm still a bit puzzled about the browser's behavior when it comes to the preflight requests.
Let's say the browser issues this preflight request:
OPTIONS http://myserver.local:7000/api/order/4 HTTP/1.1
Host: myserver.local:7000
Connection: keep-alive
Accept: */*
Access-Control-Request-Method: POST
Access-Control-Request-Headers: x-my-custom-header
Origin: http://localhost:5000
Sec-Fetch-Mode: cors
Referer: http://localhost:5000/
and my API returns:
HTTP/1.1 204 No Content
Date: Wed, 09 Mar 2022 12:52:50 GMT
Server: Kestrel
Access-Control-Allow-Headers: x-my-custom-header
Access-Control-Allow-Methods: PUT,DELETE
Access-Control-Allow-Origin: http://localhost:5000
Vary: Origin
Note that the server allows methods PUT and DELETE in the response to the preflight request, but not POST, which is the method of the actual CORS request.
Should the browser not block this request due to the mismatch between the actual request's method and the methods listed in the Access-Control-Allow-Methods header? Or is it enough that the server respond with a 20x status code for the browser to accept the preflight and then send the actual request?
My assumptions was that the browser would compare the allow-methods and block the request if the requested method did no match... Am I missing something?
TL;DR
No, the browser doesn't require the server to explicitly allow the POST method, because the latter, as a so-called CORS-safelisted method, gets a free pass.
More details
What the spec says
The answer, as always, lies in the Fetch standard (section 4.8), which specifies how CORS works:
Let methods be the result of extracting header list values given Access-Control-Allow-Methods and response’s header list.
And further down:
If request’s method is not in methods, request’s method is not a CORS-safelisted method, and request’s credentials mode is "include" or methods does not contain *, then return a network error.
(my emphasis)
What is a CORS-safelisted method? The term is defined in section 2.2.1:
A CORS-safelisted method is a method that is GET, HEAD, or POST.
Interpretation
If the method of the CORS request is one of GET, HEAD, or POST, the browser doesn't require the server to explicitly list that method in the Access-Control-Allow-Methods header for CORS preflight to succeed.
Experiment
I've found Jake Archibald's CORS playground useful for testing my (mis)understanding of CORS. Running this particular instance in your browser may convince you that the POST method doesn't need to be explicitly allowed for CORS preflight to succeed.

Paw not finding access_token from OAuth proxy

I have a use-case where I need to spoof a white-listed Redirect URL locally when performing OAuth 2 authentication.
I'm running a very basic web-server coupled with a hosts file entry for the domain I'm spoofing. I'm able to correctly negotiate my tokens and return them to Paw, but Paw isn't picking up my access_token or refresh_token, it simply displays the raw response:
Here's my server code (with placeholders for sensitive data):
var http = require('http'),
request = require('request');
var PORT = 6109;
var server = http.createServer(function(req, res) {
var code = req.url.split('?')[1].split('=')[2];
request({
url: 'https://<access token URL>/oauth2/token?code=' + code,
method: 'POST',
form: {
'client_id': <client_id>,
'client_secret': <client_secret>,
'grant_type': 'authorization_code',
'redirect_uri': <spoofed redirect URL>
}
}, function(err, response, data) {
data = JSON.parse(data);
res.writeHead(200, {'Content-Type': 'application/json'});
res.write(JSON.stringify(data.result));
// I also tried this with the same end-result
// res.writeHead(200);
// res.write('access_token=' + data.result.access_token + '&token_type=' + data.result.token_type + '&refresh_token=' + data.result.refresh_token);
res.end();
});
});
server.listen(PORT, function() {
console.log('Server listening on port %d', PORT);
});
What am I missing? Why isn't Paw finding my tokens?
Here's my configuration for reference:
Some other noteworthy points:
The OAuth provider is non-standard and flubs quite a few things from the spec (my proxy exists in part to patch up the non-standard bits)
The domain for the Redirect URL is real, but the URL does not resolve (this is a part of the reason for the local hosts entry)
I'm not showing this part of the flow, but I am correctly completing the authorization step prior to being given the code value
I think you're probably confused between the Authorization URL and Access Token URL. When you're in Authorization Code grant type for OAuth 2, you're expected to have a user confirmation step in a web page (the Authorization URL).
Which makes me guess that instead, you're expecting instead to use the Password Grant or Client Credentials? Otherwise, if you want to use Authorization URL, you'll need to specify a webpage at the Authorization URL.
Note: I've tried your Node.js script in Paw using the two last grants I mentioned (Password Grant & Client Credentials), and it works nicely.
Update: Following the comments below, I understand more what you are doing. The Authorization Request should (if successful) return a 302 redirect response to the Redirect URL page, and append a code URL query param to it. It seems like you're returning a JSON response with the code instead, so Paw isn't catching it.
According to the OAuth 2.0 spec (RFC 6749), section *4.1.2. Authorization Response*, if granted, the code should be passed as a URL query param (i.e. a ?key=value param in the URL) to the Redirect URL when doing the redirection.
If the resource owner grants the access request, the authorization
server issues an authorization code and delivers it to the client by
adding the following parameters to the query component of the
redirection URI using the "application/x-www-form-urlencoded" format
Quoting the example from the spec, here's how the response of the Authorization Request should look like if it's a success (code is granted):
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz
I saw that the Redirect URL contains "my Spoofed Uri".
When we need to use authorization code flow, we provide the authorization code and redirect Uri.
When the URI you are providing does not match the URI saved for the client in Identity server, you will not be able to get the token as the URI does not match with the client authorization code.
For example : Consider client identity in the Identity server be:
Auth Code: "xyx"
Redirect Uri: "www.mylocalhost.com\xyz"
And in your example the combination you are providing is:
Auth Code: "xyx"
Redirect Uri: "<my spoofed uri>"
As these 2 wont match there will be no token received.
I believe if you use the correct URI that is registered with the client in the Identity server, you will be able to receive the token.

Cache-Control: no-cache in request header response does not replace previously cached response until page reload in Safari

I am using AngularJS in Safari and in a hybrid iOS application.
I have an HTTP response which is being cached for 10 minutes using the following headers. This response gets cached. We will call this Response A in Step 1.
Cache-Control:public, max-age=600
This gets cached correctly, and after 10 minutes, a new request will be made.
However, I have logic which will cause the client side to ignore the cache due to certain events which would change the response if requested again. When these events occur, I made a new HTTP request with the following request headers to bypass the cache and get a new response. These headers are the only differences between the requests. This is Step 2.
Cache-Control:no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: {Current date time}
This response (Response B) is the same as Response A, except that it has different information in the body. The response headers are identical, other than the Date response header. Making this request successfully bypasses the cache and retrieves the new resource from the server and all is well.
However, this new response does not replace the previous response in the cache. For example, if I navigated to a different page (which does not cause a page reload since my app is a SPA using AngularJS), and then navigated back to the page that makes the same request that was made in Step 1, it gets Response A instead of Response B. I expect Response B to be returned instead.
This is only a problem in Safari. Chrome and IE work correctly in that any future requests will return Response B.
However in Safari, if I do a page reload (⌘ + R), it will ignore the cache completely, and make a brand new request to the same resource. If I hit enter in the Safari URL bar, it does not ignore the cache, but still uses Response A.
How do I get Safari to behave like other browsers and replace cached responses when making new requests which bypass the cache?
To summarize:
Make a request without setting Cache-Control headers in reqeust. Get response with Cache-Control headers called Response A.
Make new request with explicit Cache-Control, Pragma, and Expires request headers which bypass request, get updated Response B.
Navigate away and then back, makes a new request just like in step 1. I expect to get Response B, but get Response A instead. Response B in Step 2 does not replace Response A, even though response headers are mostly identical (except Date header) and the body is different.

localhost cookies not set

I'm trying to set a cookie in my application.
Here's the code that sets the cookie:
public HttpResponseMessage LogIn(UserLoginVM user)
{
// Do login stuff
var cookie = new CookieHeaderValue("STUPID-COOKIE", "12345");
cookie.Domain = Request.RequestUri.Host;
cookie.Path = "/";
cookie.HttpOnly = true;
// Get user's profile
HttpResponseMessage res = Request.CreateResponse<UserProfileVM>(HttpStatusCode.OK, profile);
res.Headers.AddCookies(new CookieHeaderValue[] { cookie });
return res;
}
The response from the server is the following:
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
Set-Cookie: STUPID-COOKIE=12345; domain=localhost; path=/; httponly
Access-Control-Allow-Origin: *
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?QzpcUFJPSkVDVFNcU2Ftc2tpcC5TZXJ2aWNlV2ViTmV3XFNhbXNraXAuQXV0aEFQSVxTYW1za2lwLkF1dGhBUElcbG9naW4=?=
X-Powered-By: ASP.NET
Date: Wed, 18 Feb 2015 11:58:07 GMT
Content-Length: 8019
Notice the following header:
Set-Cookie: STUPID-COOKIE=12345; domain=localhost; path=/; httponly
However, when I go under "Cookies" in "Resources" tab in Chrome, nothing is set. Also when I send a request to the server no cookie is in the headers.
Here's the code that reads the cookie:
CookieHeaderValue cookie = Request.Headers.GetCookies("STUPID-COOKIE").FirstOrDefault();
cookie variable is always null.
My application is running on http://localhost:53998 and the authentication service is running on http://localhost:60858
My Chrome version is 40.0.2214.111.
Here's a GIF demonstrating the problem:
http://i.imgur.com/q7lkXBz.gif
Edit: This seems to be non-specific to Chrome. This doesn't work on FireFox (v35) either. GIF: http://i.imgur.com/ZewnEtc.gif
I ran into this issue today and Gaui's answer was quite useful to me, bearing in mind ideally you do not want to open up your server to CORS requests from any site. I'm in my dev environment and my server and client are on different origins since they are on different ports on localhost. Also I'm using .net core 3.0
My issue was caused by my server not sending cookies to my client side as my CORS settings were blocking the sending of cookie to my domain this was evident by the server not using the header Access-Control-Allow-Credentials: true. To resolve this I configured my server in Startup.cs to allow requests from my client side to allow credentials (A credential is a cookie's authorization headers or TLS client certificates).
var allowedOrigins = new[] {"localhost:3000"}; // Ideally comes from appsettings
app.UseCors(builder =>
builder.WithOrigins(allowedOrigins).AllowCredentials().AllowAnyMethod().AllowAnyHeader().Build());
For the cookie options; I found that the you do not have to set Domain if you do not want to, Secure works even when the site is not using https.
Google chrome now supports cookies on localhost, I believe it didn't used to as a lot of older SO posts have users who faced that issue.
On the client side, you need to configure it to accept cookies as well, as in Gaui's answer above. I was using fetch, and so had to add the option:
credentials: 'include'
Which tells fetch to retrieve cookies across domains. See the docs here
I highly suspect that localhost is not a valid domain name so Chrome rejects it. If you simply remove 'domain=localhost' from the Set-Cookie then it will work and Chrome will assign the domain to localhost for you.
I would personally create a local domain name like "test.dev" and add it to your Windows hosts file, 127.0.0.1 test.dev
I finally managed to solve this.
In the response from the API I had to add Access-Control-Allow-Credentials: true headers, or by adding the following in the WebApiConfig class:
public static void Register(HttpConfiguration config)
{
var cors = new EnableCorsAttribute("*", "*", "*");
cors.SupportsCredentials = true;
config.EnableCors(cors);
}
Then I had to enable it on the client-side, by setting the withCredentials property in the XMLHTTPRequest object to true. In AngularJS app config function you can do the following:
$httpProvider.defaults.withCredentials = true;
Also, for Chrome, I had to omit the Domain (or set it as null).
Hope this helps others struggling with this.

Why is action result cached?

I have an action that generates a password reset link and emails it to the user
public ActionResult SendResetPasswordEmail(string userName)
{
var webUser = LoadUser(userName);
if (webUser != null)
{
var token = WebSecurity.GeneratePasswordResetToken(webUser.UserName);
emailSender.SendPasswordResetEmail(webUser, token, resetAction);
return new HttpStatusCodeResult(HttpStatusCode.OK);
}
return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "No user found with username: " + userName);
}
The first time I call the action from the browser, I get an HTTP 200 response (and hit my breakpoint in the action).
The second time I call the action from the browser, I get an HTTP 304 response indicating that the content is unchanged.
There are no [OutputCache] attributes anywhere in the source file (not on the class or the action).
What is causing the web server to decide that the content is unchanged and return the HTTP 304?
I'm aware of a work-around
https://stackoverflow.com/a/18620970/141172
I'm interested in understanding the root cause for the HTTP 304 response.
Update
Headers on first request:
Request Headers
Request GET /Companies/SendResetPasswordEmail/?userName=ej HTTP/1.1
X-Requested-With XMLHttpRequest
Accept */*
Referer http://local:6797/Companies
Accept-Language en-US
Accept-Encoding gzip, deflate
User-Agent Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)
Host localhost:6797
DNT 1
Connection Keep-Alive
Cookie __RequestVerificationToken=sNOBS6qz32LtnJpLWgHHELhaE44DfIVE1LSMUgjzHjcwsvxlUFa4lOSyA5QeB8keLXYL08Psjg29CRI7W73uHLJy6A81; .ASPXAUTH=DAF8AF47E955F723EE9438866BE1B4BFBF91BA01912EF087824F03581DBCA05A4AECA01373FAF40DF0C4D5C17F17DEFA2F85C1B702988B7E0F750BFE19566FC711C7D6BD81D8F0B0ABD68AF5B3D9BA032286361F; ASP.NET_SessionId=5e2gcvkc2p3rji25z5emyqzd; HelixPlugins1.0=IEPlugin1.0
Response Headers
Response HTTP/1.1 200 OK
Server ASP.NET Development Server/11.0.0.0
Date Thu, 03 Apr 2014 23:29:02 GMT
Cache-Control private, s-maxage=0
Content-Length 0
Connection Close
NOTE: I changed localhost to local in the above because StackOverflow does not allow links containing localhost to be posted :-)
The browser is Internet Explorer 10.
IE caches ajax responses by default, you need to explicitly tell it not to do any ajax caching by setting your ajax object's cache property to false.
Browsers such as Chrome automatically append a random token to your request to make it unique.

Resources