DEV Community

Masui Masanori
Masui Masanori

Posted on • Edited on

[PostgreSQL][Entity Framework Core] Try ASP.NET Core Identity with React

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 
Enter fullscreen mode Exit fullscreen mode

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(); ... 
Enter fullscreen mode Exit fullscreen mode

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"); } } 
Enter fullscreen mode Exit fullscreen mode

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(); ... 
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode

[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> } 
Enter fullscreen mode Exit fullscreen mode

[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 
Enter fullscreen mode Exit fullscreen mode

[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> } 
Enter fullscreen mode Exit fullscreen mode

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(); ... 
Enter fullscreen mode Exit fullscreen mode

[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(); } } 
Enter fullscreen mode Exit fullscreen mode

[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" }; }; ... 
Enter fullscreen mode Exit fullscreen mode

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; }; 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)