Play Framework 2.6 CSRF and Session - scala

I got strange issue. I'm implementing cart functionality on my website and I use session to store cart positions. I have a POST action to add new position to cart, and I have CSRF filter enabled to secure website. I call it with ajax on a product page, so first call is okay, but second says Unauthorized and in logs there are [CSRF] Check failed because no token found in headers for /cart. But it has. I call it with:
$("form").submit(function(e){
e.preventDefault();
$.ajax({
url: '/cart',
method: 'POST',
data: getCartPosition(),
beforeSend: function(xhr){xhr.setRequestHeader('Csrf-Token', $('input[name=csrfToken]').val());},
success: function (data, textStatus) {
alert('Added!');
},
error: function (error) {
alert('Error!');
}
});
});
and I put CSRF token in template somewhere:
#CSRF.formField
and it's in request:
I have enabled this in config
play.filters.csrf.bypassCorsTrustedOrigins=true
play.filters.hosts {
# Allow requests to example.com, its subdomains, and localhost:9000
allowed = ["localhost:9000", "localhost:4200"]
}
But what is strange that it seems it puts csrfToken in session, because after failed request I have session like this
Session(Map(cart -> {"positions":
[{"trackId":1},{"trackId":24},{"trackId":20}]},
username -> user,
token -> 0639d0b0-e7c8-4e82-9aad-2a43044e72db,
csrfToken -> e705413843ea96a6491a0e9e800ba36a712c4f70-1506542471068-0baeef7535eb9c889fb6fed2))
Idk why it's there, my add2cart action looks like:
private def cartAction(addToCartForm: Form[CartPosition], action: (Cart, CartPosition) => Cart)(implicit request: UserRequest[Any]) = {
addToCartForm.fold(
_ => BadRequest("Error!"),
position => {
getCart match {
case Some(cart) => Ok("Ok").withSession("cart" -> Json.toJson(action(cart, position)).toString(), "username" -> request.session.get("username").getOrElse(""), "token" -> request.session.get("token").getOrElse(""))
case _ => Ok("Ok, no").withSession("cart" -> Json.toJson(action(Cart(Seq.empty), position)).toString())
}
}
)
}
def addToCart() = guestAction { implicit request =>
cartAction(addToCartForm.bindFromRequest, addCartPos)
}
and addCartPos just adds position to json

I've got same issue with Play 2.7.3.
In my case, the form is generated by the Twirl with the csrf token and because I'm using ajax to submit the form, I've copied the csrf token from the rendered form and pass it to the ajax header as writen in the Play's documentation.
The form can be submitted multiple times so I need to updated the token. Therefore I'm passing through ajax response new csrf token taken in the controller from play.filters.csrf.CSRF.getToken.get.value.
But unfortunatelly, the second submission failed as cutoffurmind mentioned.
And the fix is as described by Knut Arne Vedaa to add new token to the session. I did it by the withSession method.
So the controller response is looking like this:
Ok(Json.obj(
"status" -> (user != None),
"notif" -> "Success login",
"data" -> Map(
"adminUrl" -> "www.something ...",
"csrf" -> play.filters.csrf.CSRF.getToken.get.value
)
)).withSession(
"uid" -> user.getOrElse(User()).id.toString,
"csrfToken" -> play.filters.csrf.CSRF.getToken.get.value
)
It doesn't look like an issue as Play Framework doesn't have session data kept on the server, so it is logical that the token has to be updated in the client site after the ajax request. The main issue is that it is not mentioned in the documentation (in the CSRF ajax section), what could be handy as people simply doesn't read the doc from A to Z in expected order.

In my case the solution was to set the play.filters.csrf.cookie.name config option to a value other than null:
play.filters.csrf.cookie.name = csrf_token

Related

Flask JWT Extended- Different locations for access_token (header) & refresh_token(httpOnly cookie)

How to configure flask app with flask-jwt-extended for which we need something like below.
AccessToken/Bearer must sent as a Header (and not cookie)
RefreshToken must sent as httpOnlyCookie for /api/refreshtoken path only
How to set two different token one in header and one in cookie? We are able to set either both as cookie or both as a header.
Any help?
Thanks
Raxit
I wanted to do the same while building a React + Flask single page application after days of headache trying to understand authorization and authentication as I am a beginner.
Anyways, I managed to do it this way:
In Flask, config:
app.config['JWT_TOKEN_LOCATION'] = ['headers', 'cookies']
app.config['JWT_REFRESH_COOKIE_PATH'] = '/auth/refresh'
And what I return in my login function:
resp = jsonify({'access_token': access_token})
set_refresh_cookies(resp, refresh_token)
return resp, 200
And in my refresh function:
# Refresh access token
#app.route('/auth/refresh', methods=['POST'])
#jwt_refresh_token_required
def refresh():
user = get_jwt_identity()
resp = {
'access_token': create_access_token(
identity={
'username': user['username'],
'role': user['role']
},
expires_delta=timedelta(seconds=600),
user_claims=user['role']
)
}
return jsonify(resp), 200
And on the front side, I collect the JSON access_token and set it in memory and use withCredentials to send the refresh_token with my API calls.
axios.defaults.withCredentials = true;
axios.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
more precisely:
.then(({ data: { access_token } }) => {
axiosHttp.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
return jwt_decode(access_token);
})
then I use the data from my decoded access_token in a React Context Component to authenticate access to pages depending on roles.
logout is simply setting to null my context and calling the api to unset the refresh cookie
#app.route('/auth/logout', methods=['DELETE'])
#jwt_required
def logout():
resp = jsonify({"msg": "Successfully logged out"})
unset_jwt_cookies(resp)
return resp, 200
it's quite simple in the end but it took me quite a while to figure out!

Disable redirect in fetch request using React native

I'm trying to crawl a web using React Native which has no API. It's written in PHP.
To log an user, a POST request must be sent. The response returns a cookie with a PHPSessid cookie which I must capture to use in subsequent requests.
I would like to capture the cookie value, buy the POST response is a 302 and the redirection is followed automatically, so I can't see the cookie. In node I was able to do it with redirect:manual, but it does not work in react native.
The cookie is sent automatically in subsequent requests, buy I'm trying to manage cookies by hand with react-native-cookie and I'd like to know if it's possible.
Do you know a way to stop the redirection?
I've been checking the code and what I did was the following:
Clear all cookies
Launch an empty login request
Capture the PHPSessID coookie
Launch a login request with that PHPSessID
After that, the subsequent fetch requests would have automatically a PHPSessID cookie with a valid logged in user, so we can use the site with simple fetchs
Here is some code, but the important thing is that you do a first empty login request, capture the PHPSessid and launch the real login request with that PHPSessid.
This would be the main function:
import Cookie from 'react-native-cookie';
// I think this is used only to clear the cookies
function login(user, pass){
// clear all cookies for all domains
// We need to start withouth authorization token
Cookie.clear();
const makeLoginRequest = (sessid) =>
makeLoginRequestForUserAndPass(user,pass,sessid);
return makeInitialRequest()
.then(getSessionIDFromResponse)
.then(makeLoginRequest)
.then(checkIfLoggedAndGetSessionID);
}
The initial request is a request to the login script. Note that I used GET because it worked with my site, perhaps an empty post would be necessary:
function makeInitialRequest() {
const INIT_PATH = '/index.php?r=site/login';
const INIT_URL = site + INIT_PATH;
const request = new Request(INIT_URL, options....);
return fetch(request);
}
We have the session ID in the response. I used a simple regex to extract it. Note that we are not logged in; PHP has created a session and that's what we have here:
function getSessionIDFromResponse(response) {
return getPHPSessIdFromCookie(response.headers.get('set-cookie'));
}
function getPHPSessIdFromCookie(header) {
const regex = /PHPSESSID=(\w*)/;
const match = regex.exec(header);
return match ? match[1] : '';
}
Now the login request. Note that I can't stop redirection here, but I't have to do it because we can have PHPSessid later. Redirection must be set to manual in POST request:
function makeLoginRequestForUserAndPass(user, pass, sessid) {
const request = buildLoginRequest(user, pass, sessid);
return fetch(request);
}
// This is where we build the real login request
function buildLoginRequest(user, pass, sessid) {
const LOGIN_PATH = '/index.php?r=site/login';
const LOGIN_URL = site + LOGIN_PATH;
const fields = [
{name: 'LoginForm[username]', value: user},
{name: 'LoginForm[password]', value: pass},
etc...
];
const data = translateFieldsToURLEncodedData(fields);
const headers = {
'Content-type': 'application/x-www-form-urlencoded',
Cookie: `PHPSESSID=${sessid}`, // HERE is where you put the data
};
const options = { method: 'POST',
headers: headers,
mode: 'cors',
cache: 'default',
agent: proxy,
body: data,
redirect: 'manual' // VERY IMPORTANT: if you don't do it, the cookie is lost
};
return new Request(LOGIN_URL, options);
}
// Simple utility function
function translateFieldsToURLEncodedData(fields){
let pairs = fields.map( (field) => {
return encodeURIComponent(field.name) + '=' + encodeURIComponent(field.value);
});
return pairs.join('&');
}
This is the last part. To see if I was logged in I checked if the response had text belonging to login error's page. I also got the PHPSessid (I think it changed after login, not sure, it was a year ago) but I don't know if I used it, I believe it was included automatically in subsequent requests. I think this part could be simplified an improved:
function checkIfLoggedAndGetSessionID(response) {
return (
checkIfLoggedOK(response)
.then(() => getSessionIDFromResponse(response))
);
}
function checkIfLoggedOK(response){
return getTextFromResponse(response)
.then(throwErrorIfNotLogedOk);
}
function getTextFromResponse(response) {
return response.text();
}
function throwErrorIfNotLogedOk(page) {
if(isErrorPage(page)) throw new Error("Login failed");
}
function isErrorPage(text) {
const ERROR_MESSAGE = 'Something that appears in login failed page of your site';
let n = text.search(ERROR_MESSAGE);
return n !== -1;
}
Hope this can be useful.

angular 2 login with spring security

im trying to integrate spring security with a custom angular 2 login, that is a specific endpoint of my app is protected with spring security, trying to access it will redirect to /login that is handled in angular 2. as things stands now i have no clue as to how to perform the login and grant access to the backend API once logged.
i am configuring spring security as follows:
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors().and()
.authorizeRequests()
.antMatchers("/api/someEndpoint/**")
.hasRole(ADMIN_ROLE).and().formLogin()
.loginPage("/login").and().logout();
}
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
as I had the default login everything worked fine, but I have found myself unable to create a working angular 2 login integration.
I tried the following code in angular 2 to no avail:
login(loginDetails:Object) {
console.log(loginDetails)
const headers = new Headers({ 'Content-Type': 'application/json' });
const options = new RequestOptions({ headers: headers });
const body = JSON.stringify(loginDetails);
console.log(headers);
console.log(body);
return this.http.post(this.loginUrl, body, options)
}
as far as I know spring security defaults for username and password variable names are "username" and "password", which i am sure are being passed in the request body so when passing some invalid user data like {"username":"admin", "password" : "pass"}I should be redirected to /login?error or something, and when successfully authenticated I should be redirected to /welcome and stay authenticated
I have the user and pass defined in my db and my custom userDetailsService checks against it
any answers, comments or questions are welcome
Once you're working with an API you've to use the HTTP Basic authentication.
It's also required to use HTTPS to prevent the main-in-middle attack.
To implement HTTP Basic with Angular the login service would look like this:
login (loginDetails: any): Observable<LoginResponse> { // custom class, may be empty for now
let headers = new Headers({
'Authorization': 'Basic ' + btoa(loginDetails.login + ':' + loginDetails.pass),
'X-Requested-With': 'XMLHttpRequest' // to suppress 401 browser popup
});
let options = new RequestOptions({
headers: headers
});
return this.http.post(this.loginUrl, {}, options)
.catch(e => this.handleError(e)); // handle 401 error - bad credentials
}
... then you subscribe this in the caller component:
loginNow() {
this
.loginService
.login(this.loginDetails)
.subscribe(next => {
this.router.navigateByUrl("/"); // login succeed
}, error => {
this.error = "Bad credentials"; // or extract smth from <error> object
});
}
Then you can use the loginNow() method inside component templates like (click)="loginNow().
As soon as the server will accept an authorization, JSESSIONID will be stored in your browser automatically because of Spring Security features and you won't be forced to send the credentials each time you access private resources.
Your login server method may look like this:
#PreAuthorize("hasRole('USER')")
#PostMapping("/login")
public ResponseEntity login() {
return new ResponseEntity<>(HttpStatus.OK);
}
... it would reject with 401 UNAUTHORIZED when the authorization fails or accept with 200 SUCCESS when it's not.
How to setup a server in the proper way there's a number of Spring Security demo projects present: https://github.com/spring-guides/tut-spring-security-and-angular-js
Your spring security config needs to look like this
http!!
.cors().and()
.csrf().disable()
.authorizeRequests()
.requestMatchers(object: RequestMatcher {
override fun matches(request: HttpServletRequest?): Boolean {
return CorsUtils.isCorsRequest(request)
}
}).permitAll()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin().permitAll()
I had a similar issue, but I had to override the successlogout handler as mentioned here.

Silhouette and mobile application

I've used as example play-silhouette-angular-seed.
Authorization via Satellizer works fine.
When I try to authorize via iOs app I got next error:
com.mohiva.play.silhouette.impl.exceptions.UnexpectedResponseException:
[Silhouette][facebook] Cannot build OAuth2Info because of invalid response format:
List((/access_token,List(ValidationError(List(error.path.missing),WrappedArray()))))
I got an error 400 in this function from OAuth2Provider.scala :
protected def getAccessToken(code: String)(implicit request: RequestHeader): Future[OAuth2Info] = {
httpLayer.url(settings.accessTokenURL).withHeaders(headers: _*).post(Map(
ClientID -> Seq(settings.clientID),
ClientSecret -> Seq(settings.clientSecret),
GrantType -> Seq(AuthorizationCode),
Code -> Seq(code),
RedirectURI -> Seq(resolveCallbackURL(settings.redirectURL))) ++ settings.accessTokenParams.mapValues(Seq(_))).flatMap { response =>
logger.debug("[Silhouette][%s] Access token response: [%s]".format(id, response.body))
Future.from(buildInfo(response))
}
}
This error has been risen because Satellizer for authentication via Facebook send to server an 'authentication code' and Silhouette server use this code to get Facebook 'access token' and create user.
Facebook iOs SDK, instead, obtained 'Access token' and I've tried to send it to server in Json in field 'code' like 'Satellizer.
To resolve this issue I send an 'access token' in Json field named 'access_token' and use next code to authenticate mobile application:
class MobileSocialAuthController #Inject() (
val messagesApi: MessagesApi,
userService: UserService,
authInfoRepository: AuthInfoRepository,
socialProviderRegistry: SocialProviderRegistry,
val env: Environment[User, JWTAuthenticator])
extends Silhouette[User, JWTAuthenticator]
{
def authenticate(provider: String) = UserAwareAction.async(parse.json) {
implicit request =>
provider match {
case "facebook" =>
request.body.asOpt[OAuth2Info] match {
case Some(authInfo) =>
(socialProviderRegistry.get[FacebookProvider](provider) match {
case Some(p: FacebookProvider) =>
for {
profile <-p.retrieveProfile(authInfo)
user <- userService.save(profile)
authInfo <- authInfoRepository.save(profile.loginInfo, authInfo)
authenticator <- env.authenticatorService.create(profile.loginInfo)
token <- env.authenticatorService.init(authenticator)
} yield {
env.eventBus.publish(LoginEvent(user, request, request2Messages))
Ok(Json.obj("token" -> token))
}
case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider"))
}).recover {
case e: ProviderException =>
logger.error("Unexpected provider error", e)
Unauthorized(Json.obj("message" -> Messages("could.not.authenticate")))
}
case _ =>
Future(BadRequest(Json.obj(
"message" -> "Bad OAuth2 json.")))
}
case _ =>
Future(BadRequest(Json.obj(
"message" -> "You can use only Facebook account for authentication.")))
}
}
}
As a result, I have a token which I use in ios application to obtain resources.
This happens when the OAuth2Provider gets a response it can't parse, which is, any non-success response. So there can be many reasons for this error, for instance the authorization code is invalid or expired, or you haven't configured the redirect_uri properly (check your Facebook app configuration on the Facebook dev site to set the redirect_uri).
Silhouette does log the response it gets from Facebook which should help you debug what the actual issue is, the log line to look for is in the snippet you provided:
logger.debug("[Silhouette][%s] Access token response:...
So check your logs, there you should see the response from Facebook, likely with an error indicating why they couldn't give you an access_token.

Scala Play Templates: Reverse routing with HTTP POST

when reverse routing to a GET route, i simply do
Hello
this means theres a GET route to the sayHello function. What if it was POST and needed an attached payload? is the POST implicit? how is the payload data attached?
The HTTP method for the reverse router is derived from your route configuration file.
Here is an example from a route configuration where I have two different requests with the same URL but different HTTP methods pointing to different methods:
GET /login controllers.Application.login
POST /login controllers.Application.authenticate
The login() method in the Application controller simply streams out the HTML form:
def login = Action { implicit request =>
Ok(html.loginForm(loginForm))
}
the authenticate() method however binds the request to a form allowing further processing:
def authenticate = Action { implicit request =>
loginForm.bindFromRequest.fold(
formWithErrors => BadRequest(html.loginForm(formWithErrors)),
user => {
// OTHER CODE HERE
Redirect(routes.Blog.createPost).withSession("user" -> user)
}
)
}
This second method requires a form definition in the controller:
val loginForm = Form(
tuple(
"username" -> text,
"password" -> text
) verifying ("Invalid username or password", result => result match {
case (username, password) => Account.authenticate(username, password).isDefined
})
)
So depending which method you put to the reverse router in your view, that is the HTTP method which will be used for the request.