Skip to content
Scott Kirkland edited this page Sep 19, 2022 · 4 revisions

Welcome to the finjector wiki!

New project setup

Detailing the steps to create a brand new .net6 react w/ typescript SPA, authenticated via CAS/OIDC.

We'd like to use the SpaProxy approach from the template, but with custom, simplified auth. And we'll start the CRA template from scratch, instead of what's defaulted, for simplicity and to get the most up-to-date code.

1. Create the project structure

From the root of your project:

dotnet new react -n Finjector.Web
dotnet new sln
dotnet sln add Finjector.Web/Finjector.Web.csproj

2. Brand new CRA

The default template comes with old versions of react, router, and has plenty of stuff we don't want. So let's dump it and create our own.

rm -rf ClientApp

and then create a new typescript based client app:

npx create-react-app finjector-client --template typescript

and then rename the resulting directory to ClientApp just to make everything easy.

3. Proxying and HTTP

New we have a new CRA server that'll run on port 3000. We want (a) to have it run w/ a dev SSL cert, and most importantly (b) proxy non-client routes to our backend. To do this, we'll need to adjust a few ports and introduce back in a few helper files we deleted previously.

We need to copy aspnetcore-react.js, aspnetcore-https.js, and src/setupProxy.js from the dotnet spa-templates repo. In the setupProxy file, remove the #if section.

We also want the .env.development and .env files.

For .env.development just use these values:

PORT=3000
HTTPS=true

We'll also need to install the http-proxy-middleware npm package since setupProxy uses that. See the package.json in the templates repo for the proper version.

Now, in your package.json file, add the following to the scripts section:

 "prestart": "node aspnetcore-https && node aspnetcore-react",

4. Ports

We want it to appear we are always using our client address, https://localhost:3000. Anything not intended for our client (hopefully just API routes and auth) will go through the proxy and be transparently delivered to our .net backend.

Find your *.csproj file and ensure it has

<SpaProxyServerUrl>https://localhost:3000</SpaProxyServerUrl>

The proxy should automatically find your backend port, so providing this front-end port should be the only thing you need to set manually.

At this point, you can run the app and it'll launch open your .net process landing page, which should soon swap over to https://localhost:3000/ once you front-end is ready (can take a little while if you've changed npm packages).

5. Authorization

Now we'd like to add simple auth w/ CAS using OIDC. This will work mostly like before, with server-handled auth producing a client aspnet auth cookie. But since the proxy is loaded up first, our client app will need to determine if auth is required, and if so it'll need to redirect to a specific "Auth Initiation" endpoint. After the auth is complete and a cookie is set, we simply redirect back to the original destination and future API requests from the client will pass the auth cookie.

5a: Setup OIDC provider

We'll need to install the OIDC provider

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

and then set OIDC up in the same way as other apps, by add AddOpenIdConnect() in the services and app.UseAuthorization(); after useRouting(). See Program.cs in this repo for an example.

5b: Setup controller to handle auth

We'll need to use httpcontext, so AddHttpContextAccessor() in your services.

Create AccountController.cs and make one method to force anyone that visits it to login

public class AccountController : Controller
{
    private readonly IHttpContextAccessor httpContextAccessor;

    public AccountController(IHttpContextAccessor _httpContextAccessor)
    {
        this.httpContextAccessor = _httpContextAccessor;
    }

    [Authorize] // trigger authentication
    public IActionResult Login(string? returnUrl)
    {
        // attempt to add custom cookie
        this.httpContextAccessor.HttpContext?.Response.Cookies.Append("ASPNET_AUTH_SESSION", "true", new CookieOptions { 
            HttpOnly = false, // need client to read it
            Secure = true,
            SameSite = SameSiteMode.Lax,
            Expires = DateTime.Now.AddYears(1) // same expiration as auth cookie
        });

        // redirect to return url if it exists, otherwise /
        return Redirect(returnUrl ?? "/");
    }
}

The idea is we have a client-readable cookie to indicate if the user has signed in yet. Another option would be to just try to call some user-info endpoint and if you get 403 then we can go login. This way was simple.

5c: Set React to handle auth

To keep this super simple, we are just going to check for this auth cookie on app load. If it's not there, redirect them to login. If someone fakes the cookie they won't have the proper aspnet auth cookies and all API calls will fail, so no security issue there. We could send additional info (like username) in this cookie, but probably better to just use a regular fetch() call and get that info later if needed.

So for now, in App.tsx right inside the main App() function:

  const cookies = document.cookie;

  if (cookies.split(';').some(c => c === 'ASPNET_AUTH_SESSION=true')) {
    // we have a valid cookie, so we can render the app
  } else {
    // we don't have a valid cookie, so we need to redirect to the login page
    window.location.href = '/account/login';
  }

6. Celebrate 🎉

And that should do it! CRA is free to do whatever it wants w/o the dotnet backend knowing -- no more custom UseReactDevelopmentServer() or script injection code or websocket constraints. This should make updating any client code easier but mostly makes it more future-proof in case they deprecate the SpaServices stuff.

Downside is that the proxy stuff is a little complex behind the scenes, and also once we build/deploy a prod environment the proxy is gone. This hopefully should be a transparent difference, but it's still a difference.