62

For cookie based authentication, my server sends Set-Cookie to my Angular application. However, the application doesn't send the value back in further requests. Following is my code.

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
  withCredentials: true //this is required so that Angular returns the Cookies received from the server. The server sends cookies in Set-Cookie header. Without this, Angular will ignore the Set-Cookie header
};

public getUserProfile(){
    console.log('contacting server at '+this.API_URL +this.GET_USER_PROFILE_URL+"with httpOptions "+httpOptions);
    return this.http.get(this.GET_USER_PROFILE_URL,httpOptions )
      .map(response=>{
        console.log('response from backend service',response);
        let result= <ServerResponse>response; 
        console.log("result is "+result.result+' with additional information '+result.additionalInformation)
        return result;
      })
      .catch(this.handleError);
  }

The server sends the cookie as follows in 200OK of my code (not shown here)

Set-Cookie: id=...

The next message however hasn't got the id in the cookie, thus the server returns 401. If I manually add the Cookie using browser's debug tools, then I get 200OK. Thus I am certain it is the absence of the id value in the cookie which is causing the issue.

What am I doing wrong? Do I need to explicitly store the cookie received in Set-Cookie and explicitly add it in further requests?

Update - At the time when the SPA is initially loaded, the server sends Set-Cookie header with some other cookie's information related to CSRF. I notice that that cookie is still sent by the application. Could it be that Angular honors the first Set-Cookie header but ignores the subsequent ones?

I have added couple of pics to explain what I mean

During signing, the client sends a cookie related to CSRF. I dont think it is required as the client also sends CSRF Header but for some reason it does. The server responds with Set-Cookie with id in it

enter image description here

Then when I ask for profile, the client again sends the CSRF cookie but not the id cookie

enter image description here

mruanova
  • 6,351
  • 6
  • 37
  • 55
Manu Chadha
  • 15,555
  • 19
  • 91
  • 184

6 Answers6

88

Finally, I was able to find the issue. The journey was more satisfying than the result so let me break it down into the steps I took to solve my problem.

In summary, This wasn't an issue with Angular. The cookie I was sending had secureCookie flag on. As I was testing my application without https, it seems that the angular application was not using (or getting access to) the Set-Cookie header received in 200 OK.

My initial code to send the sign in request to the server and handling its response was

return this.http.post(this.SIGNIN_USER_URL, body, httpOptions)
  .map(response => {
    console.log('response from backend service', response);
    let result= <ServerResponse>response; 
    console.log('result is ' + result.result + ' with additional information '+result.additionalInformation)
    return result;
  })
  .catch(this.handleError);

I wasn't using observe: 'response' option which meant that the response would only contain body, not headers. I changed the code to following so that I could see which headers are being received.

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
     
  withCredentials: true, 
  observe: 'response' as 'response'
};  
    
public signinUser(user: UserSigninInfo): any {
  console.log('contacting server at ' + this.API_URL + this.SIGNIN_USER_URL + " with user data " + user + " with httpOptions " + httpOptions.withCredentials + "," + httpOptions.headers ); 
    
  let signinInfo = new UserSignin(user);
  let body = JSON.stringify(signinInfo);
  return this.http.post(this.SIGNIN_USER_URL, body, httpOptions).catch(this.handleError);
}

The above code was being called as follows. I change that to get the headers in the response

return this.bs.signinUser(user).subscribe((res: HttpResponse<any>) => {console.log('response from server:', res);
  console.log('response headers', res.headers.keys())
});

I also created an intercepter to print the incoming and outgoing messages (copied from SO)

import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from "@angular/common/http";
import {Injectable} from "@angular/core";
import {Observable} from "rxjs/Observable";
import 'rxjs/add/operator/do';
    
@Injectable()
export class CustomInterceptor implements HttpInterceptor {
    
  constructor() {}
    
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    
    console.log("outgoing request",request);
    request = request.clone({ withCredentials: true });
      console.log("new outgoing request",request);
    
      return next
        .handle(request)
        .do((ev: HttpEvent<any>) => {
          console.log("got an event",ev)
          if (ev instanceof HttpResponse) {
            console.log('event of type response', ev);
          }
      });
  }
}

When I started debugging, I noticed that though the server was sending 10 headers, only 9 were getting printed

headers from server

enter image description here

Print message on console (Set-Cookie was missing! The header my application needed to get authentication cookie)

0: "Content-Length"
​
1: "Content-Security-Policy"
​
2: "Content-Type"
​
3: "Date"
​
4: "Referrer-Policy"
​
5: "X-Content-Type-Options"
​
6: "X-Frame-Options"
​
7: "X-Permitted-Cross-Domain-Policies"
​
8: "X-XSS-Protection"
​
length: 9

This gave me a direction that the application is not seeing Set-Cookie header. I thought I'll be able to resolve it by adding CORS policy in play framework exposedHeaders = ["Set-Cookie"] but that didnt work. Later I looked closer at the cookie and noticed secureCookie setting

Set-Cookie: id=...Secure; HTTPOnly

This made me think that maybe my cookie settings are wrong for my environment (localhost, no HTTPS). I changed the cookie settings in Silhoutte

val config =  CookieAuthenticatorSettings(secureCookie=false)

And it worked!

Though I'll make the above code work for secureCookie and this wasn't an issue with Angular, I hope that some folks might find the approach helpful

Throvn
  • 795
  • 7
  • 19
Manu Chadha
  • 15,555
  • 19
  • 91
  • 184
  • 1
    A note that would have helped me - I misunderstood the options object and glossed over how withCredentials was added here and other places. It is not a header, and shouldn't be added as a header. It's a separate option, next to the header, and is just used by Angular. It shouldn't be being sent to the backend or anything. – HammerN'Songs Jan 03 '19 at 19:57
  • 5
    For anyone looking for an answer - this is the right one. For me it was simply, after a few long days, add to the interceptor - withCredentials: true – Artipixel Feb 20 '19 at 14:29
  • 4
    If you removed the Secure flag to get it work in local development, how does it work in production? – ILikeFood Mar 14 '19 at 20:05
  • Thanks. I just turned secure flag off for local development. – Gowthaman Dec 17 '19 at 17:53
  • This solution helped to solve my problem partially, +1 for the detailing. Thanks – Sanoop Surendran Oct 30 '20 at 18:09
  • Thanks so much, you saved me a lot of time and research honestly – shahnshah Jan 27 '22 at 12:15
15

"Set-Cookie" with a flag "HttpOnly" means you can not read the cookie from the client-side.

Moamen
  • 440
  • 3
  • 10
2

i assume you using nodejs and express-session for manage session then in express-session httpOnly are by default enabled so you have to change httpOnly for console sever sent cookie

const app = require('express')();
var session = require('express-session');
var cookieParser = require('cookie-parser');

app.use(session( { secret:'hello world',
store:SessionStore,
resave:false,
cookie:{
secure:false,
httpOnly:false // by default it's boolean value true }
}));

in the below image httpOnly are true, so you can't be console this cookie. enter image description here

in the below image httpOnly are false , so you can console this cookie.enter image description here

Piyush Jain
  • 313
  • 3
  • 10
1

we need to check one more option when getting set-cookies header,

set-cookie: SESSIONID=60B2E91C53B976B444144063; Path=/dev/api/abc; HttpOnly

Check for Path attribute value also. This should be the same as your API starting context path like below

https://www.example.com/dev/api/abc/v1/users/123

or use below value when not sure about context path

Path=/;
sharad jain
  • 1,471
  • 13
  • 9
1

There could be multiple reasons that cookies are not saved and thereafter they are not being auto-sent by the browser.

  1. Key Parameters: For cookies to be saved and work few important parameters present in cookies needs to be observed like -

Path: look at the Path in the cookie's value which ensures it will only be saved when the request URL path matches this path value.

Secure: look at the Secure in the cookie's value if present then it ensures cookies will only be saved in https environment.

HttpOnly: look at the HttpOnly in the cookie's value if present then it forbids JavaScript from accessing the cookie, for example, through the Document.cookie property. Note that a cookie that has been created with HttpOnly will still be sent with JavaScript-initiated requests, for example, when calling XMLHttpRequest.send() or fetch(). This mitigates attacks against cross-site scripting.

Apart from above 3 which are crucial others keys should also be considered refer here for all available options https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie

  1. Cross Origin Issue: Second crucial point to work on is that cookies will only be saved when the client and server are in the same origin. So if you are working in localhost and trying to access a remote URL then try to set up proxy for your localhost to remote URL. To setup proxy in angular refer here https://angular.io/guide/build#proxying-to-a-backend-server
  2. Enable CORS (Cross-Origin Resource Sharing): If you are making a cross-origin request (i.e., the Angular client is running on a different domain/port than the server), ensure that the server is properly configured to allow cross-origin requests and includes the Access-Control-Allow-Credentials header with the value true. This is required for the browser to allow the Set-Cookie header in the response. When Access-Control-Allow-Credentials header accepted by server then only withCredentials option could be passed in angular HTTP client.
  3. Set withCredentials property: When making the HTTP request in Angular, make sure to set the withCredentials property to true. This property allows the browser to include cookies in cross-origin requests but Access-Control-Allow-Credentials header should be accepted by server.

5. Check Angular HTTP client configuration: Verify that your Angular HTTP client is not modifying or filtering out the Set-Cookie header. By default, the Angular HTTP client should include all headers in the response, including Set-Cookie. However, you can also configure the client to filter specific headers using the responseType property or interceptors.

Hope these options will help you or somebody else to resolve the issue. Thanks!

Happy Coding :-)

Aman Kumar Gupta
  • 2,640
  • 20
  • 18
0

NOTE - REFER THIS ANSWER IF YOU ARE USING ZUUL IN PROJECT

Trying all possible solutions (referring articles and n number of stackoverflow questions) and banging head for 2 days, finally, I found this solution. Thanks to Zuul docs - Cookies and Sensitive Headers.

I was facing the same issue - from API response, set-Cookie response header was coming where as calling same api from Angular code, set-cookie was getting skipped or ignored.

Use 'sensativeHeaders' property inside zuul configuration in your application.yml or properties file. Refer below code snippet

zuul:
   routes:
      users:
      path: /myusers/**
      sensitiveHeaders:
      url: https://downstream

I have not set any extra headers or properties like 'withCredentials' in interceptor.

Morez
  • 2,048
  • 5
  • 36
  • 49