Back to: ASP.NET Core Web API Tutorials
Client Application Two using ASP.NET Core MVC:
In this article, I will discuss how to Implement Client Application Two using the ASP.NET Core MVC Project. Please read our previous article on implementing Client Application One using ASP.NET Core MVC. This is the Fourth or Last Application of our SSO Implementation.
Client Application Two using ASP.NET Core MVC:
We will implement the same functionality as the Client Application One, such as Register, Login, and Logout, but with SSO Integration. We will implement SSO Authentication as follows:
- Create the SSO Link on the UI: When the user clicks on the SSO link, Client Application Two will generate an SSO token.
- Generate the SSO Token: Client Application Two will generate the SSO token by calling an API on the Authentication Server.
- Redirect to Client Application Two: After generating the SSO token, redirect the user to Client Application One, passing the SSO token in the request as part of the Query String.
Creating a New ASP.NET Core MVC Project:
Let us proceed and implement this step by step. Open Visual Studio and create a new ASP.NET Core MVC project named ClientApplicationTwo.
Modifying appSettings.json file:
In the appSettings.json file, we will store the base URL of the Authentication and Resource Server. So, please modify the appSettings.json file as follows. Please make sure to change the URL on which your application is running.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "AuthenticationServer": { "BaseUrl": "https://localhost:7084" //Please replace withe URL of your Authentication Server }, "ResourceServer": { "BaseUrl": "https://localhost:7257" //Please replace withe URL of your Resource Server }, "ClientApplicationOne": { "BaseUrl": "https://localhost:7020" //Please replace withe URL of Client Application } }
Creating Models
Create a class file named AuthenticationModels.cs within the Models folder, and then copy and paste the following code. The following are the models we will use to perform different operations.
using System.ComponentModel.DataAnnotations; namespace ClientApplicationTwo.Models { public class RegisterViewModel { [Required] public string Username { get; set; } [Required] public string Email { get; set; } [Required] public string Password { get; set; } } public class LoginViewModel { [Required] public string Username { get; set; } [Required] public string Password { get; set; } } }
Response Models
Create a new file named ResponseModels.cs in the Models folder. These classes represent the responses returned by the Authentication Server and Resource Server.
namespace ClientApplicationTwo.Models { // Represents the login response that contains the JWT token. public class LoginResponseModel { public string Token { get; set; } } // Represents the response after SSO token validation, including a new access token and user details. public class ValidateSSOResponseModel { public string Token { get; set; } public UserDetailsModel UserDetails { get; set; } } public class UserDetailsModel { public string UserId { get; set; } public string Username { get; set; } public string Email { get; set; } } // Represents responses from the Resource Server endpoints (public/protected data). public class DataResponseModel { public string Message { get; set; } } }
AuthenticationService.cs
The following service will interact with the Authentication Server’s RESTful API to handle user Registration, Login, and SSO token creation. So, create a class file named AuthenticationService.cs within the Models folder and copy and paste the following code.
using System.Net.Http.Headers; using System.Text; using System.Text.Json; namespace ClientApplicationTwo.Models { public class AuthenticationService { private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly string _authServerUrl; public AuthenticationService(IHttpClientFactory httpClientFactory, IConfiguration configuration) { _httpClientFactory = httpClientFactory; _configuration = configuration; _authServerUrl = _configuration["AuthenticationServer:BaseUrl"] ?? "https://localhost:7088"; } public async Task<HttpResponseMessage> RegisterUserAsync(RegisterViewModel model) { var client = _httpClientFactory.CreateClient(); var url = $"{_authServerUrl}/api/Authentication/Register"; var jsonContent = JsonSerializer.Serialize(model); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); return await client.PostAsync(url, content); } public async Task<HttpResponseMessage> LoginUserAsync(LoginViewModel model) { var client = _httpClientFactory.CreateClient(); var url = $"{_authServerUrl}/api/Authentication/Login"; var jsonContent = JsonSerializer.Serialize(model); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); return await client.PostAsync(url, content); } public async Task<HttpResponseMessage> GenerateSSOTokenAsync(string jwtToken) { var client = _httpClientFactory.CreateClient(); var url = $"{_authServerUrl}/api/Authentication/GenerateSSOToken"; var request = new HttpRequestMessage(HttpMethod.Post, url); // Add the JWT token in the Authorization header. request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken); // For this endpoint, no additional JSON payload is needed. return await client.SendAsync(request); } } }
Configure Session and Register Authentication Service in the Program.cs
Please ensure session management is configured in the Program.cs file. Also, add the AuthenticationService to the built-in dependency injection container. So, modify the Program.cs class file as follows:
using ClientApplicationTwo.Models; namespace ClientApplicationTwo { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); // Add distributed memory cache and session services. builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); // Session timeout period. options.Cookie.HttpOnly = true; // Prevents client-side scripts from accessing the cookie. options.Cookie.IsEssential = true; // Marks the cookie as essential. }); // Register HttpClient and AuthenticationService. builder.Services.AddHttpClient(); builder.Services.AddScoped<AuthenticationService>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseSession(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run(); } } }
Creating Account Controller:
Next, create a new Empty MVC Controller named AccountController within the Controllers folder and copy and paste the following code. This controller will manage user registration, login, logout, and SSO token generation and redirection.
using ClientApplicationTwo.Models; using Microsoft.AspNetCore.Mvc; using System.Text.Json; namespace ClientApplicationTwo.Controllers { public class AccountController : Controller { private readonly AuthenticationService _authService; private readonly IConfiguration _configuration; public AccountController(AuthenticationService authService, IConfiguration configuration) { _authService = authService; _configuration = configuration; } // GET: /Account/Register [HttpGet] public IActionResult Register() { return View(); } // POST: /Account/Register [HttpPost] public async Task<IActionResult> Register(RegisterViewModel model) { if (!ModelState.IsValid) { return View(model); } var response = await _authService.RegisterUserAsync(model); if (response.IsSuccessStatusCode) { return RedirectToAction("Login"); } ModelState.AddModelError(string.Empty, "Registration Failed."); return View(); } // GET: /Account/Login [HttpGet] public IActionResult Login() { return View(); } // POST: /Account/Login [HttpPost] public async Task<IActionResult> Login(LoginViewModel model) { if (!ModelState.IsValid) { return View(model); } var response = await _authService.LoginUserAsync(model); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); try { // Deserialize into LoginResponseModel. var loginResponse = JsonSerializer.Deserialize<LoginResponseModel>(responseContent); if (loginResponse != null && !string.IsNullOrEmpty(loginResponse.Token)) { // Store the JWT token and username in session. HttpContext.Session.SetString("JWT", loginResponse.Token); HttpContext.Session.SetString("Username", model.Username); return RedirectToAction("Index", "Home"); } else { ModelState.AddModelError(string.Empty, "Token not found in the response."); return View(); } } catch (Exception ex) { Console.WriteLine("Error parsing response: " + ex.Message); ModelState.AddModelError(string.Empty, "Failed to parse the response."); return View(); } } ModelState.AddModelError(string.Empty, "Login failed."); return View(); } // GET: /Account/GenerateSSOToken // This action calls the Authentication Server to generate an SSO token and then redirects to Client Application One. [HttpGet] public async Task<IActionResult> GenerateSSOToken() { // Ensure the user is logged in. var jwtToken = HttpContext.Session.GetString("JWT"); if (string.IsNullOrEmpty(jwtToken)) { return RedirectToAction("Login"); } var response = await _authService.GenerateSSOTokenAsync(jwtToken); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); try { // Deserialize the SSO response. We expect a JSON with an "SSOToken" property. var ssoResponse = JsonSerializer.Deserialize<Dictionary<string, string>>(responseContent); if (ssoResponse != null && ssoResponse.ContainsKey("SSOToken")) { var ssoToken = ssoResponse["SSOToken"]; // Get the base URL for Client Application One from configuration. var clientAppOneUrl = _configuration["ClientApplicationOne:BaseUrl"]; // Redirect the user to Client Application One with the SSO token as a query parameter. return Redirect($"{clientAppOneUrl}/validate-sso?ssoToken={ssoToken}"); } else { ViewBag.Error = "SSO token not found in the response."; return View("Error"); } } catch (Exception ex) { Console.WriteLine("Error parsing SSO response: " + ex.Message); ViewBag.Error = "Failed to parse the SSO response."; return View("Error"); } } ViewBag.Error = "Failed to generate SSO token."; return View("Error"); } // POST: /Account/Logout [HttpPost] public IActionResult Logout() { HttpContext.Session.Remove("JWT"); HttpContext.Session.Remove("Username"); return RedirectToAction("Index", "Home"); } } }
Creating the Views:
Let us proceed and implement the Registration and Login Views similar to Client Application One.
Register View (Views/Account/Register.cshtml)
Create a view named Register.cshtml and copy and paste the following code.
@model ClientApplicationTwo.Models.RegisterViewModel @{ ViewData["Title"] = "Register"; } <h1 class="text-center">@ViewData["Title"]</h1> <div class="container"> <div class="row justify-content-center"> <div class="col-md-6"> <form asp-action="Register" asp-controller="Account" method="post" class="needs-validation"> <div class="form-group mb-3"> <label asp-for="Username" class="form-label"></label> <input asp-for="Username" class="form-control" placeholder="Enter your username" required /> <span asp-validation-for="Username" class="text-danger"></span> </div> <div class="form-group mb-3"> <label asp-for="Email" class="form-label"></label> <input asp-for="Email" class="form-control" placeholder="Enter your email" type="email" required /> <span asp-validation-for="Email" class="text-danger"></span> </div> <div class="form-group mb-3"> <label asp-for="Password" class="form-label"></label> <input asp-for="Password" class="form-control" placeholder="Enter your password" type="password" required /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="d-grid"> <button type="submit" class="btn btn-primary btn-block">Register</button> </div> </form> </div> </div> </div>
Login View (Views/Account/Login.cshtml)
Create a view named Login.cshtml and copy and paste the following code.
@model ClientApplicationTwo.Models.LoginViewModel @{ ViewData["Title"] = "Login"; } <h1 class="text-center">@ViewData["Title"]</h1> <div class="container"> <div class="row justify-content-center"> <div class="col-md-6"> <form asp-action="Login" method="post" class="needs-validation" novalidate> <div class="form-group mb-3"> <label asp-for="Username" class="form-label"></label> <input asp-for="Username" class="form-control" placeholder="Enter your username" required /> <span asp-validation-for="Username" class="text-danger"></span> </div> <div class="form-group mb-3"> <label asp-for="Password" class="form-label"></label> <input asp-for="Password" class="form-control" placeholder="Enter your password" type="password" required /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="d-grid"> <button type="submit" class="btn btn-primary btn-block">Login</button> </div> </form> </div> </div> </div>
Modifying the Layout View:
Next, modify the Layout view as follows. Here, we are adding the SSO Redirect Link, which will only be visible when the user is logged in.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - ClientApplicationTwo</title> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> </head> <body> <header> <nav class="navbar navbar-expand-sm navbar-light bg-white border-bottom box-shadow mb-3"> <div class="container-fluid"> <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">ClientApplicationTwo</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse"> <span class="navbar-toggler-icon"></span> </button> <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> <ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a> </li> @if (Context.Session.GetString("Username") != null) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="GenerateSSOToken">SSO Redirect</a> </li> } </ul> <ul class="navbar-nav"> @if (Context.Session.GetString("Username") != null) { <li class="nav-item"> <span class="nav-link text-dark">Hello, @Context.Session.GetString("Username")</span> </li> <li class="nav-item"> <form asp-area="" asp-controller="Account" asp-action="Logout" method="post" class="form-inline"> <button type="submit" class="nav-link btn btn-link text-dark">Logout</button> </form> </li> } else { <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Login">Login</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Register">Register</a> </li> } </ul> </div> </div> </nav> </header> <div class="container"> <main role="main" class="pb-3"> @RenderBody() </main> </div> <footer class="border-top footer text-muted"> <div class="container"> © 2025 - ClientApplicationTwo - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </div> </footer> <script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> @await RenderSectionAsync("Scripts", required: false) </body> </html>
Now, run all the applications and test the functionalities, and everything should work as expected. Once you log in, the SSO Redirect Link will be visible. Once you click on the SSO Redirect Link, you will be redirected to the Client Application and automatically see that you will be logged in to the Client Application Once.
That’s it. We have successfully implemented the SSO Authentication, and I hope you enjoy these articles. Please provide your valuable feedback about SSO Authentication Implementation using ASP.NET Core Web API and MVC. Here, instead of using ASP.NET Core MVC for the Client Application, you can use any Client-Side Technologies such as Angular, React, etc. However, the overall functionalities and behavior will remain the same.