Skip to content

XSRF token and SSR #303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
georgiosd opened this issue Jun 13, 2017 · 26 comments
Closed

XSRF token and SSR #303

georgiosd opened this issue Jun 13, 2017 · 26 comments

Comments

@georgiosd
Copy link

Welp! My head hurts!

So you know how angular is supposed to read XSRF-TOKEN cookies and pass them a X-XSRF-TOKEN header when making a HTTP request?

Well, that doesn't seem to work with universal. I tried manually defining the XsrfStrategy in app.module but it complained about cookies.

Any ideas??

@kyse
Copy link

kyse commented Jun 13, 2017

With the following I get the proper headers sent back to the server from angular automatically. Not sure how you're implementing it. But bear in mind using his provided http-transfer wrapper, it will pass null options to http calls if you don't supply them manually, thus preventing any automatic usage of any defined BaseRequestOptions service interceptor you might have defined to update headers.

Startup.cs configuring services:

            services.AddAntiforgery(opts => opts.HeaderName = "X-XSRF-Token");
            services.AddMvcWithDynamicApi<DbEntities>(options =>
                    {
                        options.Filters.AddService(typeof(AntiforgeryCookieResultFilter));
                    });
            services.AddTransient<AntiforgeryCookieResultFilter>();

AntiforgeryCookieResultFilter.cs:

public class AntiforgeryCookieResultFilter : ResultFilterAttribute
    {
        private readonly IAntiforgery _antiforgery;
        public AntiforgeryCookieResultFilter(IAntiforgery antiforgery)
        {
            _antiforgery = antiforgery;
        }

        public override void OnResultExecuting(ResultExecutingContext context)
        {
            if (context.Result is ViewResult)
            {
                var tokens = _antiforgery.GetAndStoreTokens(context.HttpContext);
                context.HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
            }
        }
    }

@georgiosd
Copy link
Author

georgiosd commented Jun 13, 2017 via email

@kyse
Copy link

kyse commented Jun 13, 2017

Per Angular.io Guide Security -> XSRF it takes care of it automatically.

Angular's http has built-in support for the client-side half of this technique in its XSRFStrategy. The default CookieXSRFStrategy is turned on automatically. Before sending an HTTP request, the CookieXSRFStrategy looks for a cookie called XSRF-TOKEN and sets a header named X-XSRF-TOKEN with the value of that cookie.

The server must do its part by setting the initial XSRF-TOKEN cookie and confirming that each subsequent state-modifying request includes a matching XSRF-TOKEN cookie and X-XSRF-TOKEN header.

Double check you're using XSRF-TOKEN as the cookie name, and X-XSRF-TOKEN as the header it looks for.

My guess is, if this works fine on the browser -> api side, and your only experiencing issues with SSR calls to the API, you could add in a BaseRequestOptions to check if the platform is server, and manually add in the X-XSRF-TOKEN header from a transferData value. You'd have to grab the token in the HomeController and inject it into the transfer data and make sure it all gets wired up properly for the server side of things.

I haven't tested with POST, but GET's on SSR doesn't give me any issues regarding XSFR.

@georgiosd
Copy link
Author

My guess is, if this works fine on the browser -> api side, and your only experiencing issues with SSR calls to the API, you could add in a BaseRequestOptions to check if the platform is server, and manually add in the X-XSRF-TOKEN header from a transferData value. You'd have to grab the token in the HomeController and inject it into the transfer data and make sure it all gets wired up properly for the server side of things.

Yes, that's what I thought so too. I understand it conceptually but I'm too new in Angular to understand your steps - would you please give a few more pointers, like where I would do each thing? :)

@kyse
Copy link

kyse commented Jun 14, 2017

Frankly, whether using the http or transfer-http modules should all be including the header automatically no matter which platform is calling them. Can you post your implementation?

For manually intercepting and modifying http headers:

In the modules/transfer-http/ folder create a new file:

transfer-http-options.ts

Create an interceptor for http requests to modify headers.

import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { BaseRequestOptions, RequestOptions } from '@angular/http';
import { REQUEST } from '../../app/shared/constants/request';
import { TRANSFERDATA, TransferData } from '../../app/shared/constants/transferData';

@Injectable()
export class TransferHttpOptions extends BaseRequestOptions {

    constructor(
        @Inject(REQUEST) private request,
        @Inject(PLATFORM_ID) private platformId: Object,
        @Inject(TRANSFERDATA) private transferData: TransferData
    ) {
        super();
        this.headers.append('Authorization', "bearer " + this.transferData.apiToken);

        if (isPlatformServer(PLATFORM_ID)) {
            console.log('Add stuff to http request header for server side only here.');
        }
    }
}

export const requestOptionsProvider = { provide: RequestOptions, useClass: TransferHttpOptions };

transfer-http-module.ts

Add interceptor to module

import { NgModule } from '@angular/core';
import { TransferHttp } from './transfer-http';
import { TransferHttpOptions } from './transfer-http-options';

@NgModule({
    providers: [
        TransferHttp,
        TransferHttpOptions
    ]
})
export class TransferHttpModule {}

transfer-http.ts

Update service to use interceptor. Note: Need to do this because interceptor isn't used automatically when this service passes a null options object to http calls.

// Add import
import { TransferHttpOptions } from './transfer-http-options';

// Update constructor to inject our interceptor.
@Injectable()
export class TransferHttp {

  private isServer = isPlatformServer(this.platformId);

  constructor(
    @Inject(PLATFORM_ID) private platformId,
    private http: Http,
    private transferOptions: TransferHttpOptions,
    protected transferState: TransferState
  ) { }

// Update calls to http to use interceptor if options is null.
  get(url: string, options?: RequestOptionsArgs): Observable<any> {
    return this.getData(url, options, (url: string, options: RequestOptionsArgs) => {
        return this.http.get(url, options || this.transferOptions);
    });
  }

@kyse
Copy link

kyse commented Jun 15, 2017

So after further investigation, it appears there are some issues with the xsfr token. Initial page load, token is generated page is sent to user. User logs in via post, and auth service is updated, and redirected to main page (this is all handled in the SPA). When I initially setup my auth testing with a seperate mvc page/form to handling login/logout, it seemed to work properly, but moving everything inside the SPA to handle presenting the login/logout form/pages, the xsfr token isn't updated once authentication happens. I get an error that the token was generated for a different principal.

This would obviously also happen if the user's rolers change during being logged in and (via api for instance) and the validate principal authentication validator class replace the principal cookie with an updated principal respecting the changed roles, or whatever the validator is setup to detect is out of date.

Basically somethings missing here. Any time the principal changes, the xsfr token needs to be updated on the browser. But this isn't happening with regular http calls. Going to experiment with using transferHttp wrapper calls to see if that properly gets the updated tokens set on the browser.

I would have expected await HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); in addition to setting AspNet.Cookie= null to also null out the antiforgery token. But no dice.

End of the day, until this is resolved, a browser refresh is required to get HMR and any api calls to work after a sign in, or to sign in again after a sign out happens. Because the antiforgery token is generated using the user principal context it seems and when that changes, it barfs.

@kyse
Copy link

kyse commented Jun 16, 2017

Ok, so I reworked my setup a little. Instead of completely handling login/logout responses inside angular based on the response from the login/logout account controller actions, I had the response return a redirect to returnurl or "/" if successfull. This properly gets the xsrf token updated on the browser linked to the logged in identity context.

I noticed an oddity with my JWT bearer token however doing it like this. @MarkPieszak, perhaps you could shed some light here.

As you suggested in another ticket, I'm passing the JWT in TransferData to the angular app so it can properly do server side rendering when API calls are needed. So I noticed on the browser side after the login redirect, the response from the redirected url (in this case "/") gives me the proper updated jwt token in the transferdata script tag.

Initial Login:
Browser receives:

<script>window['TRANSFER_CACHE'] = {"isAuthenticated":false,"userName":null,"apiToken":"unauthenticated_token"};</script>

Check Chrome console:

TRANSFER_CACHE
Object {isAuthenticated: true, userName: "kyse", apiToken: "unauthenticated_token"}

Log in response:
Returns 302, Location: /

Redirect to /
Browser receives:

 <script>window['TRANSFER_CACHE'] = {"isAuthenticated":true,"userName":"kyse","apiToken":"autheneticated_token"};</script>

Check Chrome console after login/redirect reloads page:

TRANSFER_CACHE
Object {isAuthenticated: true, userName: "kyse", apiToken: "unauthenticated_token"}

When I made an api call (hit a SPA page that calls out to the API), I end up with the unauthenticated_token being used.

I checked the console both before and after login and noticed it's not updated, yet it's returned with the redirect html/script content.

using Cache-Control:no-cache, so that's not the issue.

@georgiosd
Copy link
Author

I'm using the anti-forgery package like you've outlined above and then this simple middleware:

public class XsrfTokenMiddleware
    {
        private readonly RequestDelegate next;
        private readonly XsrfTokenProviderOptions options;
        private readonly IAntiforgery antiforgery;

        public XsrfTokenMiddleware(
            RequestDelegate next, 
            IOptions<XsrfTokenProviderOptions> options, 
            IAntiforgery antiforgery)
        {
            this.options = options.Value;
            this.antiforgery = antiforgery;
            this.next = next;
        }

        public Task Invoke(HttpContext context)
        {
            if (options.Path.Contains(context.Request.Path.Value, StringComparer.OrdinalIgnoreCase))
            {
                antiforgery.GetAndStoreTokens(context);
            }

            return next(context);
        }
    }

I have to say I'm really frustrated with angular. Even at version 4 the ecosystem doesn't seem all that mature and there are hurdles all the way. Me and my team at the time adopted AngularJS (v1) when it was at 0.9.x or so and we loved working with it over time.

Not getting the same feeling with ng2/4 and I'm not sure it's because it's new. I'm gonna give React a try.

@MarkPieszak
Copy link
Member

MarkPieszak commented Jun 19, 2017

@georgiosd Do you need Server-side rendering?
The problem is that in general (as with any framework, or even plain JS), when you're trying to make your code universal and run on the server & client, and transfer state/etc, it raises the difficulty a lot.

Have you thought about removing SSRendering, you won't run into nearly as many issues?

Reading through everything now, is the problem adding the correct Headers to Http calls, or transfering them to the client?

Also check out this comment here, see if it's similar to how you're doing things now:
#59 (comment)

@isaacrlevin
Copy link
Contributor

I'm kind of confused by this thread my apologies. Is the issue simply doing JWT auth in Angular against some provider on the backend? I have a simple user table that is a copy of the identity one. I can register, login, and logout in Spa passing a JWT around. I can share my code if someone is interested.

@MarkPieszak
Copy link
Member

Can you post some of it just in case I'm sure others will run into similar issues!
🙏 @isaac2004

@georgiosd
Copy link
Author

georgiosd commented Jun 26, 2017

@MarkPieszak I think that's a little of a moot point (whether I need it right now or not).

I mean, I could live without it but it would need a lot of extra effort to get the site online and kind of defeats the point of switching to ng2 - universal was one of the main selling points for me because it made SEO so much easier.

Anyway - don't mean to be a party pooper. I'm just disappointed that I'm disappointed :)

@isaac2004 it needs to be with cookies!

@MarkPieszak
Copy link
Member

I totally understand, I'm trying to help get edge case scenarios into documentation or this repo (somehow) where possible, but it's tough to get to them all, there's just a million scenarios! 🐰
Don't give up hope, it'll get better with time, running JS on the server and client is definitely no easy task!

@georgiosd
Copy link
Author

I understand, and salute everyone's efforts. React had more time on this so I guess it's reasonable to be a bit ahead.

Perhaps the separation of scenarios as I suggested in another thread could help you with the million combos :)

@kyse
Copy link

kyse commented Jun 26, 2017

Sorry, I did kind of hijack it with JWT. My biggest issue with it is setting up services that need data to use the constructor to grab the jwt token for making api calls, then on the login/logout side to update the jwt token. Doing a transferhttp.get does not update the jwt token in tranferData global variables unless you do a browser refresh.

@isaacrlevin
Copy link
Contributor

@georgiosd have you given this a try with Dot Net Core 2.0? Shawn Wildermuth has a pretty straightforward walk through on doing Cookie Auth. Is the issue you are having on the Angular side?

@naveedahmed1
Copy link

@isaac2004

I can register, login, and logout in Spa passing a JWT around. I can share my code if someone is interested.

Can you please share the code?

@isaacrlevin
Copy link
Contributor

@naveedahmed1 when I get some spare time, I am going to create a branch or PR with end to end Auth with JWT

@naveedahmed1
Copy link

Thank you so much @isaac2004 . A suggestion regarding this repo, the current project has alot of external plugins and other stuff, I understand that its helpful for many people. But I think not everyone is using ng-bootstrap or Signal R etc. Shouldn't we have a repo with a much simplified version, which just shows using ASP.Net Node Services and Angular Universal along with SEO benefit?

@isaacrlevin
Copy link
Contributor

@isaac2004 I think that is the plan more or less. This repo will change a lot over the next few months

@naveedahmed1
Copy link

naveedahmed1 commented Sep 4, 2017

Sounds great, I hope it could be done soon :)

@isaacrlevin
Copy link
Contributor

please take a look at the howto/auth branch for end to end JWT flow. Open a new issue if you have questions

@kyl3t
Copy link

kyl3t commented Sep 18, 2017

@isaac2004 I've had a look at that branch, is there a way to rework this to work with Identity Server Cookie Auth / OIDC?

@isaacrlevin
Copy link
Contributor

@kyl3t pretty sure identity server doesn't have support for Core 2.0 yet

DuendeArchive/IdentityServer4#1173

I can put in simple cookie auth though.

@kyl3t
Copy link

kyl3t commented Sep 18, 2017

@isaac2004 I'm still using the .NET Core 1.1 version for this repo because of that very reason. I've managed to hook up my app as an Identity Server client for the server pages. And the REST API calls work because the cookies are sent back to server. My main issue is that when the Identity timeout expires, it tries to redirect my browser to my identity server (when I do API calls) resulting in a failed 302 due to cross-domain. Any architectural recommendations are welcome.

@isaacrlevin
Copy link
Contributor

@kyl3t hmmmm that is interesting. The auth branch target core 2.0 so most of that code is worthless to you (the server stuff at least). Are you able to catch the identity server exception in anyway? If yes you can log and do whatever. Sorry if I am missing a key piece.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants