-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Welcome to the finjector wiki!
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.
From the root of your project:
dotnet new react -n Finjector.Web
dotnet new sln
dotnet sln add Finjector.Web/Finjector.Web.csproj
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.
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",
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).
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.
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.
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.
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';
}
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.