DEV Community

Masui Masanori
Masui Masanori

Posted on • Edited on

1

[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

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay