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
Client Application Two will implement similar core functionalities as Client Application One, such as user Registration, Login, and Logout, but with an additional focus on Single Sign-On (SSO) integration to enable seamless authentication between multiple client apps.
How SSO is implemented:
- SSO Link on the UI: Client Application Two will provide a user interface link (or button) labeled as SSO (visit the Client Application One link). When a logged-in user clicks this link, the application will initiate the SSO process.
- Generating the SSO Token: Upon clicking, Client Application Two will request an SSO token from the Authentication Server by calling a specific API endpoint. This token uniquely represents the user’s authenticated session.
- Redirecting to Client Application One: Once the SSO token is successfully generated, Client Application Two will redirect the user to Client Application One, passing the token securely in the query string. Client Application One will then validate this token and log the user in without requiring manual credentials again.
This flow enables users to switch between applications smoothly with a shared authentication session.
Creating a New ASP.NET Core MVC Project:
Open Visual Studio and create a new ASP.NET Core MVC project named ClientApplicationTwo. This will serve as the foundation for implementing the features mentioned above.
Modifying appSettings.json file:
Instead of hardcoding URLs throughout the app, store the base URLs for the Authentication Server, Resource Server, and Client Application One in the configuration file appSettings.json. This promotes flexibility and an easier environment management.
{ “Logging”: { “LogLevel”: { “Default”: “Information”, “Microsoft.AspNetCore”: “Warning” } }, “AllowedHosts”: “*”, “AuthenticationServer”: { “BaseUrl”: “https://localhost:7084” //Please replace with URL of your Authentication Server }, “ResourceServer”: { “BaseUrl”: “https://localhost:7257” //Please replace with URL of your Resource Server }, “ClientApplicationOne”: { “BaseUrl”: “https://localhost:7020” //Please replace with URL of Client Application } }
Creating Models
Create models representing the data structures for user registration, login, and server responses. For simplicity, we are including the Login feature here. You can also add the registration feature. So, create a class file named LoginViewModel.cs within the Models folder and then copy and paste the following code:
using System.ComponentModel.DataAnnotations; namespace ClientApplicationTwo.Models { public class LoginViewModel { [Required(ErrorMessage = "Username is Required")] public string Username { get; set; } = null!; [Required(ErrorMessage = "Password is Required")] [DataType(DataType.Password)] public string Password { get; set; } = null!; } }
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. I am creating all the classes in this class file. But you should create a separate class file for each class.
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; } = null!; public string Username { get; set; } = null!; public string Email { get; set; } = null!; } // Represents responses from the Resource Server endpoints (public/protected data). public class DataResponseModel { public string? Message { get; set; } } }
Creating User Authentication Service
Create a service class to centralize all calls to the Authentication Server REST APIs for registration, login, and SSO token generation. Create a class file named UserAuthenticationService.cs within the Models folder and copy and paste the following code into it.
using System.Net.Http.Headers; using System.Text; using System.Text.Json; namespace ClientApplicationTwo.Models { public class UserAuthenticationService { private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly string _authServerUrl; public UserService(IHttpClientFactory httpClientFactory, IConfiguration configuration) { _httpClientFactory = httpClientFactory; _configuration = configuration; _authServerUrl = _configuration["AuthenticationServer:BaseUrl"] ?? "https://localhost:7088"; } 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 Services in the Program.cs
Please 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 UserService. builder.Services.AddHttpClient(); builder.Services.AddScoped<UserAuthenticationService>(); 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 then copy and paste the following code. This is the controller that will manage the user registration, login, logout, and SSO Token generation and redirection.
using ClientApplicationTwo.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using System.Text.Json; namespace ClientApplicationTwo.Controllers { public class AccountController : Controller { private readonly UserAuthenticationService _userAuthenticationService; private readonly IConfiguration _configuration; public AccountController(UserAuthenticationService userAuthenticationService, IConfiguration configuration) { _userAuthenticationService = userAuthenticationService; _configuration = configuration; } // 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 _userAuthenticationService.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 User 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 _userAuthenticationService.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. I am only implementing the Login view.
Login View (Views/Account/Login.cshtml)
Create a view named Login.cshtml within the Views/Account folder, and then 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> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
Modifying the Layout View:
Next, modify the Layout view as follows. Here, we are adding the SSO Redirect Link, which will be only 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" /> <title>@ViewData["Title"] - ClientApplicationTwo</title> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" /> <link href="~/css/site.css" rel="stylesheet" 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> <!-- Load jQuery globally: required for validation --> <script src="~/lib/jquery/dist/jquery.min.js"></script> <!-- Load Bootstrap JS bundle --> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <!-- Render page-specific scripts (like jquery validation) --> @RenderSection("Scripts", required: false) </body> </html>
Now, run all the applications and test their functionalities; 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 One, and you will see that you are automatically logged in to the Client Application.
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.