Intro
In this time, I will try signing in and signing out with my React application.
I will access PostgreSQL by Entity Framework Core(Npgsql) and create tables by DB migrations First.
My project is as same as the last time I used.
DB migrations
For create "ApplicationUser" table, I create a DbContext class and "ApplicationUser" entity class as same as below post.
But then I try creating a migration file, I get an exception.
An error occurred while accessing the Microsoft.Extensions.Hosting services. Continuing without the application service provider. Error: The entry point exited without ever building an IHost.
Unable to create a 'DbContext' of type 'RuntimeType'. The exception 'Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContextOptions`1[OfficeFileAccessor.OfficeFileAccessorContext]' while attempting to activate 'OfficeFileAccessor.OfficeFileAccessorContext'.' was thrown while attempting to create an instance. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
To avoid this probrem, I have to add "builder.Services.AddRazorPages();" into Program.cs.
[Server-Side] Program.cs
using System.Text;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using Microsoft.IdentityModel.Tokens;
using NLog;
using NLog.Web;
using OfficeFileAccessor;
using OfficeFileAccessor.AppUsers.Repositories;
using OfficeFileAccessor.OfficeFiles;
...
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddDbContext<OfficeFileAccessorContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("OfficeFileAccessor")));
// ---- Add this line ----
builder.Services.AddRazorPages();
...
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
// stop reference loop.
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
builder.Services.AddScoped<IApplicationUsers, ApplicationUsers>();
var app = builder.Build();
...
app.Run();
...
Sign-in from the React application
Server-side
After completing authentication with E-mail address and password, I will set a JWT into Http cookie
to confirm the sign-in status.
[Server-Side] ApplicationUserService.cs
using Microsoft.AspNetCore.Identity;
using OfficeFileAccessor.Apps;
using OfficeFileAccessor.AppUsers.DTO;
using OfficeFileAccessor.AppUsers.Entities;
using OfficeFileAccessor.AppUsers.Repositories;
using OfficeFileAccessor.Web;
namespace OfficeFileAccessor.AppUsers;
public class ApplicationUserService(SignInManager<ApplicationUser> SignIn,
IApplicationUsers Users,
IUserTokens Tokens): IApplicationUserService
{
public async Task<ApplicationResult> SignInAsync(SignInValue value, HttpResponse response)
{
var target = await Users.GetByEmailForSignInAsync(value.Email);
if(target == null)
{
return ApplicationResult.GetFailedResult("Invalid e-mail or password");
}
SignInResult result = await SignIn.PasswordSignInAsync(target, value.Password, false, false);
if(result.Succeeded)
{
// Add generated JWT into HTTP cookie.
response.Cookies.Append("User-Token", Tokens.GenerateToken(target), DefaultCookieOption.Get());
return ApplicationResult.GetSucceededResult();
}
return ApplicationResult.GetFailedResult("Invalid e-mail or password");
}
public async Task SignOutAsync(HttpResponse response)
{
await SignIn.SignOutAsync();
// Remove the cookie value.
response.Cookies.Delete("User-Token");
}
}
When the server-side application receives the client accesses, it will get JWT from Http cookies and set as the HTTP Authorization header.
[Server-Side] Program.cs
...
var builder = WebApplication.CreateBuilder(args);
...
var app = builder.Build();
...
app.Use(async (context, next) =>
{
// Get JWT from the HTTP cookie.
if(context.Request.Cookies.TryGetValue("User-Token", out string? token))
{
// If a token exists, set as HTTP Authorization header.
if(string.IsNullOrEmpty(token) == false)
{
context.Request.Headers.Append("Authorization", $"Bearer {token}");
}
}
await next();
});
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
...
app.Run();
...
Client-Side
To share the sign-in status to every pages, I create a React.Context and define sign-in/sign-out functions.
[Client-Side] AuthenticationContext.tsx
import { createContext, useContext } from "react";
import { AuthenticationType } from "./authenticationType";
export const AuthenticationContext = createContext<AuthenticationType|null>(null);
export const useAuthentication = (): AuthenticationType|null => useContext(AuthenticationContext);
[Client-Side] AuthenticationProvider.tsx
import { ReactNode, useState } from "react";
import { getServerUrl } from "../web/serverUrlGetter";
import { AuthenticationContext } from "./authenticationContext";
import { getCookieValue } from "../web/cookieValues";
import { hasAnyTexts } from "../texts/hasAnyTexts";
export const AuthenticationProvider = ({children}: { children: ReactNode }) => {
// to show or hide the sign-out button.
const [signedIn, setSignedIn] = useState(false);
const signIn = async (email: string, password: string) => {
const res = await fetch(`${getServerUrl()}/api/users/signin`, {
mode: "cors",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password })
});
if(res.ok) {
const result = await res.json();
setSignedIn(result?.succeeded === true);
return result;
}
return {
succeeded: false,
errorMessage: "Something wrong"
};
};
const signOut = async () => {
const res = await fetch(`${getServerUrl()}/api/users/signout`, {
mode: "cors",
method: "GET",
});
if(res.ok) {
setSignedIn(false);
return true;
};
return false;
};
// Access a page that requires authentication to check current sign-in status.
const check = () =>
fetch(`${getServerUrl()}/api/auth`, {
mode: "cors",
method: "GET",
})
.then(res => res.ok);
return <AuthenticationContext.Provider value={{ signedIn, signIn, signOut, check }}>
{children}
</AuthenticationContext.Provider>
}
[Client-Side] App.tsx
import './App.css'
import {
BrowserRouter as Router,
Route,
Routes,
Link
} from "react-router-dom";
import { IndexPage } from './IndexPage';
import { RegisterPage } from './RegisterPage';
import { SigninPage } from './SigninPage';
import { AuthenticationProvider } from './auth/AuthenticationProvider';
import { SignOutButton } from './components/SignoutButton';
function App() {
return (
<>
<AuthenticationProvider>
<Router basename='/officefiles'>
<SignOutButton />
...
<Routes>
<Route path="/pages/signin" element={<SigninPage />} />
<Route path="/" element={<IndexPage />} />
...
</Routes >
</Router>
</AuthenticationProvider>
</>
)
}
export default App
[Client-Side] SigninPage.tsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuthentication } from "./auth/authenticationContext";
export function SigninPage(): JSX.Element {
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const authContext = useAuthentication();
const navigate = useNavigate();
const handleEmailChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
setEmail(event.target.value);
}
const handlePasswordChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
setPassword(event.target.value);
}
const signin = () => {
if(authContext == null) {
console.error("No Auth context");
return;
}
// If success, move to the top page.
authContext.signIn(email, password)
.then(res => {
if(res.succeeded) {
navigate("/");
}
})
.catch(err => console.error(err));
};
return <div>
<h1>Signin</h1>
<input type="text" placeholder="Email" value={email}
onChange={handleEmailChanged}></input>
<input type="password" value={password}
onChange={handlePasswordChanged}></input>
<button onClick={signin}>Signin</button>
</div>
}
CSRF
Because I set JWT into HTTP cookie to store sign-in status, I will add another type of token to prevent CSRF attacks.
When the client open a page, the server-side application set tokens into the HTTP cookie.
After finishing loading the page, the client-side gets the token and puts it into the HTTP request header to sign-in.
[Server-Side] Program.cs
using System.Text;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using Microsoft.IdentityModel.Tokens;
...
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddAntiforgery(options =>
{
// HTTP request header name
options.HeaderName = "X-XSRF-TOKEN";
options.SuppressXFrameOptionsHeader = false;
});
...
var app = builder.Build();
...
var antiforgery = app.Services.GetRequiredService<IAntiforgery>();
app.Use((context, next) =>
{
var requestPath = context.Request.Path.Value;
if (requestPath != null &&
(string.Equals(requestPath, "/", StringComparison.OrdinalIgnoreCase) ||
requestPath.StartsWith("/pages", StringComparison.CurrentCultureIgnoreCase)))
{
// Generate a token and put into the cookie.
AntiforgeryTokenSet tokenSet = antiforgery.GetAndStoreTokens(context);
if(tokenSet.RequestToken != null) {
// To use this token on the client-side, set "HttpOnly=false"
context.Response.Cookies.Append("XSRF-TOKEN", tokenSet.RequestToken,
new CookieOptions {
HttpOnly = false,
SameSite = SameSiteMode.Lax,
});
}
}
return next(context);
});
...
app.Run();
...
[Server-Side] ApplicationUserController.cs
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OfficeFileAccessor.AppUsers.DTO;
namespace OfficeFileAccessor.AppUsers;
// Automatically validates AntiforgeryToken for POST, PUT, etc.
[AutoValidateAntiforgeryToken]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class ApplicationUserController(IAntiforgery Antiforgery, IApplicationUserService Users): Controller
{
[AllowAnonymous]
[HttpPost("/api/users/signin")]
public async Task<IActionResult> ApplicationSignIn([FromBody] SignInValue value)
{
return Json(await Users.SignInAsync(value, Response));
}
...
[HttpGet("/api/auth")]
public IActionResult CheckAuthenticationStatus()
{
AntiforgeryTokenSet tokenSet = Antiforgery.GetAndStoreTokens(HttpContext);
if(tokenSet.RequestToken != null) {
HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokenSet.RequestToken,
new CookieOptions {
HttpOnly = false,
SameSite = SameSiteMode.Lax,
});
}
return Ok();
}
}
[Client-Side] AuthenticationProvider.tsx
import { ReactNode, useState } from "react";
import { getServerUrl } from "../web/serverUrlGetter";
import { AuthenticationContext } from "./authenticationContext";
import { getCookieValue } from "../web/cookieValues";
import { hasAnyTexts } from "../texts/hasAnyTexts";
export const AuthenticationProvider = ({children}: { children: ReactNode }) => {
const [signedIn, setSignedIn] = useState(false);
const signIn = async (email: string, password: string) => {
// Get AntiforgeryToken
const cookieValue = getCookieValue("XSRF-TOKEN");
if(!hasAnyTexts(cookieValue)) {
throw Error("Invalid token");
}
// Set the token into the HTTP request header.
const res = await fetch(`${getServerUrl()}/api/users/signin`, {
mode: "cors",
method: "POST",
headers: {
"Content-Type": "application/json",
"X-XSRF-TOKEN": cookieValue,
},
body: JSON.stringify({ email, password })
});
if(res.ok) {
const result = await res.json();
setSignedIn(result?.succeeded === true);
return result;
}
return {
succeeded: false,
errorMessage: "Something wrong"
};
};
...
cookieValues.ts
export function getCookieValue(name: string): string|null {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
const result = parts.pop()?.split(';')?.shift();;
if(result != null) {
return result;
}
}
return null;
};
- Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core- Microsoft Learn
- ASP.NET Core Security
Author Of article : Masui Masanori Read full article