Docker: The quickest way to stand up FusionAuth. (There are other ways).
While this sample application doesn't have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.
In that case, the system might look like this before FusionAuth is introduced.
Request flow during login before FusionAuth
The login flow will look like this after FusionAuth is introduced.
Request flow during login after FusionAuth
In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.
In this section, you’ll get FusionAuth up and running and use .NET to create a new application.
First off, grab the code from the repository and change into that directory.
git clone https://github.com/FusionAuth/fusionauth-quickstart-dotnet-web.git
cd fusionauth-quickstart-dotnet-web
You'll find a Docker Compose file (docker-compose.yml
) and an environment variables configuration file (.env
) in the root directory of the repo.
Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.
docker compose up -d
Here you are using a bootstrapping feature of FusionAuth called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json
file and configure FusionAuth to your specified state.
If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing docker compose down -v
, then re-run docker compose up -d
.
FusionAuth will be initially configured with these settings:
e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
.super-secret-secret-that-should-be-regenerated-for-production
.richard@example.com
and the password is password
.admin@example.com
and the password is password
.http://localhost:9011/
.You can log in to the FusionAuth admin UI and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.
If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app. The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View
icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the OAuth configuration
section.
The .env
file contains passwords. In a real application, always add this file to your .gitignore
file and never commit secrets to version control.
Now you are going to create a .NET application using C#. While this section builds a simple .NET web application, you can use the same configuration to integrate your existing .NET application with FusionAuth.
You’ll use Razor Pages and .NET 7. This application will display common information to all users. There will also be a secured area, only available to an authenticated user. The full source code is available if you want to download it and take a look.
If you simply want to run the application , there is a completed version in the ‘complete-application’ directory. You can use the following commands to get it up and running if you do not want to create your own.
cd complete-application
dotnet run
Then view the website at the following location http://localhost:5000
.
To get started building your own application, open your command-line shell (i.e. Terminal on a Mac or Powershell on Windows) and navigate to the directory of your choice. Then create a new web application using the dotnet
CLI and go to that directory:
dotnet new webapp -o your-application
cd your-application
Creating a new application in Visual Studio will work as well. You will use the command line here for the simplicity of the example.
Once the application has been created, you can start up the web application using the following command in.
dotnet run
The output should look similar to the following.
Building...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5146
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: /Users/Shared/your-application
The application will be accessible in your browser at http://localhost:{PortNumber}
where the port number will be displayed in your shell window, i.e http://localhost:5146
.
Press Control+C in the terminal window to stop the application from running.
It’s always smart to leverage existing libraries as they are likely to be more secure and better handle edge cases. You’re going to add an OpenIdConnect
library to the application. In your terminal window, make sure you’re in the root directory for your application and run the command below to add the library.
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
This application will contain two pages that require authorization. The Account
page will display the user’s balance and the MakeChange
page will make change for the user. To protect these pages you can use the Authorize filter attribute in the page model.
Create Account.cshtml
and Account.cshtml.cs
in the Pages
directory. To protect the “Account” page you can use the Authorize filter attribute in the page model.
Copy the following code into Account.cshtml.cs
:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
namespace Pages;
[Authorize]
public class AccountsModel : PageModel
{
private readonly ILogger<PrivacyModel> _logger;
public AccountsModel(ILogger<PrivacyModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
Copy the following code into Account.cshtml
:
@page
@model AccountsModel
@{
ViewData["Title"] = "Account";
}
<div style="flex: 1;">
<!-- Application page -->
<div class="column-container">
<div class="app-container">
<h3>Your balance</h3>
<div class="balance">$100.00</div>
</div>
</div>
</div>
Create MakeChange.cshtml
and MakeChange.cshtml.cs
in the Pages
directory. You will use the same Authorize filter attribute to secure the page.
Copy the following code into MakeChange.cshtml.cs
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Pages
{
[Authorize]
public class MakeChangeModel : PageModel
{
public string Message { get; private set; } = "";
public bool Error { get; private set; } = false;
public string Amount { get; set; } = "0.00";
public void OnGet()
{
}
public void OnPost(string amount)
{
MakeChange(amount);
}
public void MakeChange(string amount)
{
try
{
Amount = amount;
decimal remainingamount = Convert.ToDecimal(amount);
Message = "We can make change for";
var coins = new[] { // ordered
new { name = "quarters", nominal = 0.25m },
new { name = "dimes", nominal = 0.10m },
new { name = "nickels", nominal = 0.05m },
new { name = "pennies", nominal = 0.01m }
};
foreach (var coin in coins)
{
int count = (int)(remainingamount / coin.nominal);
remainingamount -= count * coin.nominal;
Message += $" {count} {coin.name}";
}
Message += "!";
}
catch (Exception ex)
{
Message = @$"There was a problem converting the amount submitted. {ex.Message}";
}
}
}
}
Copy the following code into MakeChange.cshtml
:
@page
@model Pages.MakeChangeModel
@{
}
<div style="flex: 1;">
<div class="column-container">
<div class="app-container change-container">
<h3>We Make Change</h3>
<div class="change-message">
@Model.Message
</div>
<form method="post">
<div class="h-row">
<div class="change-label">Amount in USD: $</div>
<input class="change-input" name="amount" value="@Model.Amount" />
<input class="change-submit" type="submit" value="Make Change" />
</div>
</form>
</div>
</div>
</div>
You will need a way to allow the user to logout of the application as well. You’ll build the logout functionality in the Logout
page. This will:
FusionAuth will then destroy its session and redirect the user back to the app’s configured logout URL.
Add the following file to the Pages
directory and call it Logout.cshtml.cs
:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Pages
{
public class LogoutModel : PageModel
{
private readonly ILogger<LogoutModel> _logger;
private readonly IConfiguration _configuration;
public LogoutModel(ILogger<LogoutModel> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
public IActionResult OnGet()
{
SignOut("cookie", "oidc");
var host = _configuration["FusionAuthSettings:Authority"];
var cookieName = _configuration["FusionAuthSettings:CookieName"];
var clientId = _configuration["FusionAuthSettings:ClientId"];
var url = host + "/oauth2/logout?client_id=" + clientId;
Response.Cookies.Delete(cookieName);
return Redirect(url);
}
}
}
OnGet
is the important method. Here you sign out using a method of the authentication library, delete the JWT cookie, and send the user to the FusionAuth OAuth logout endpoint.
Now add Logout.cshtml
to the Pages
directory. No content is necessary. Just declare the page and model as shown below.
@page
@model LogoutModel
@{
}
You will need to do a little plumbing to ensure things like having the correct settings and configuring the cookies properly to work over HTTP for the sample application.
Replace the text in the appsettings.json
file with the text below. The important part here is that we are adding the FusionAuthSettings
section so that the code above will run with the correct configuration settings. Authority
is just the location of the user identity server, in this case, FusionAuth. Urls
ensures the application runs on the specified port, 5000.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Urls": "http://localhost:5000",
"FusionAuthSettings": {
"Authority": "http://localhost:9011",
"CookieName": "mycookie",
"ClientId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
"ClientSecret": "super-secret-secret-that-should-be-regenerated-for-production"
}
}
Replace the text in the Program.cs
file. Since you are using unsecured cookies, you need to allow this in the configuration. Safari and Chrome require different settings and that is reflected in the code below.
var builder = WebApplication.CreateBuilder(args);
var startup = new Startup(builder.Configuration);
startup.ConfigureServices(builder.Services);
var app = builder.Build();
// Add this before any other middleware that might write cookies
app.UseCookiePolicy();
app.UseCookiePolicy(new CookiePolicyOptions
{
Secure = CookieSecurePolicy.Always,
//safari does not work if this is not set here and chrome works as well
MinimumSameSitePolicy = SameSiteMode.Unspecified
});
startup.Configure(app, builder.Environment);
app.Run();
Add the Startup.cs
file in the root directory of the application. This sets the correct port and manages unsecured cookies for the browsers. The CheckSameSite
and DisallowsSameSiteNone
functions help manage the cookie settings for the different browsers.
using System;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Logging;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddRazorPages();
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("cookie", options =>
{
options.Cookie.Name = Configuration["FusionAuthSettings:CookieName"];
options.Cookie.SameSite = SameSiteMode.None;
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = Configuration["FusionAuthSettings:Authority"];
options.ClientId = Configuration["FusionAuthSettings:ClientId"];
options.ClientSecret = Configuration["FusionAuthSettings:ClientSecret"];
options.ResponseType = "code";
options.RequireHttpsMetadata = false;
options.Scope.Add("email");
});
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
options.OnAppendCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
options.OnDeleteCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/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.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); });
IdentityModelEventSource.ShowPII = true;
}
private void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
if (DisallowsSameSiteNone(userAgent))
{
options.SameSite = SameSiteMode.Unspecified;
options.Secure = false;
}
}
// Read comments in https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
public bool DisallowsSameSiteNone(string userAgent)
{
// Check if a null or empty string has been passed in, since this
// will cause further interrogation of the useragent to fail.
if (String.IsNullOrWhiteSpace(userAgent))
return false;
// Cover all iOS based browsers here. This includes:
// - Safari on iOS 12 for iPhone, iPod Touch, iPad
// - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
// - Chrome on iOS 12 for iPhone, iPod Touch, iPad
// All of which are broken by SameSite=None, because they use the iOS networking
// stack.
if (userAgent.Contains("CPU iPhone OS 12") ||
userAgent.Contains("iPad; CPU OS 12"))
{
return true;
}
// Cover Mac OS X based browsers that use the Mac OS networking stack.
// This includes:
// - Safari on Mac OS X.
// This does not include:
// - Chrome on Mac OS X
// Because they do not use the Mac OS networking stack.
if (userAgent.Contains("Macintosh; Intel Mac OS X") &&
userAgent.Contains("Version/") && userAgent.Contains("Safari"))
{
return true;
}
// Cover Chrome 50-69, because some versions are broken by SameSite=None,
// and none in this range require it.
// Note: this covers some pre-Chromium Edge versions,
// but pre-Chromium Edge does not require SameSite=None.
if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
{
return true;
}
return false;
}
}
There are a few settings applied to simplify the sample application. In a production application, do not allow cookies over http
or keep the ClientSecret
in a .json file for production applications. Get the ClientSecret
from an environmental variable that has been set on a production machine or some other secure way.
This application will display different menu items for authenticated users and non authenticated users. To add this type of navigation update /Pages/Shared/_Layout.cshtml
with the following:
<html>
<head>
<meta charset="utf-8" />
<title>FusionAuth OpenID .Net Web Example</title>
<link rel="stylesheet" href="~/css/changebank.css">
</head>
<body>
<div id="page-container">
@if (User.Identity != null && User.Identity.IsAuthenticated == true)
{
<div id="page-header">
<div id="logo-header">
<img src="https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg" />
<div class="h-row">
<p class="header-email">@User.Claims.Where(x => x.Type == "email").First().Value</p>
<a class="button-lg" href="/Logout">Logout</a>
</div>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link" " href="/makechange">Make Change</a>
<a class="menu-link" href="/account">Account</a>
</div>
</div>
}
else
{
<div id="page-header">
<div id="logo-header">
<img src="https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg" />
<a class="button-lg" href="/Account">Login</a>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link">About</a>
<a class="menu-link">Services</a>
<a class="menu-link">Products</a>
<a class="menu-link" style="text-decoration-line: underline;">Home</a>
</div>
</div>
}
@RenderBody()
</div>
</body>
</html>
Update the default page.
Copy the following code into Pages/Index.cshtml.cs
:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Pages;
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
Copy the following code into Index.cshtml
and notice the logic to show a different view to users that have not logged in.
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div style="flex: 1;">
<div class="column-container">
<div class="content-container">
<div style="margin-bottom: 100px;">
<h1>Welcome to Changebank</h1>
@if (User.Identity != null && User.Identity.IsAuthenticated == true)
{
<p>To get started, please select from the menu.</p>
}
else
{
<p>To get started, <a href="/Account">log in or create a new account</a>.</p>
}
</div>
</div>
<div style="flex: 0;">
<img src="~/img/money.jpg" style="max-width: 800px;"/>
</div>
</div>
</div>
To give the pages the look and feel of an application, apply the following styling and images.
Add the changebank.css
file in the wwwroot/css
directory of the application.
h1 {
color: #096324;
}
h3 {
color: #096324;
margin-top: 20px;
margin-bottom: 40px;
}
a {
color: #096324;
}
p {
font-size: 18px;
}
.header-email {
color: #096324;
margin-right: 20px;
}
.fine-print {
font-size: 16px;
}
body {
font-family: sans-serif;
padding: 0px;
margin: 0px;
}
.h-row {
display: flex;
align-items: center;
}
#page-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
#page-header {
flex: 0;
display: flex;
flex-direction: column;
}
#logo-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
.menu-bar {
display: flex;
flex-direction: row-reverse;
align-items: center;
height: 35px;
padding: 15px 50px 15px 30px;
background-color: #096324;
font-size: 20px;
}
.menu-link {
font-weight: 600;
color: #FFFFFF;
margin-left: 40px;
}
.menu-link {
font-weight: 600;
color: #FFFFFF;
margin-left: 40px;
}
.inactive {
text-decoration-line: none;
}
.button-lg {
width: 150px;
height: 30px;
background-color: #096324;
color: #FFFFFF;
font-size: 16px;
font-weight: 700;
border-radius: 10px;
text-align: center;
padding-top: 10px;
text-decoration-line: none;
}
.column-container {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.content-container {
flex: 1;
display: flex;
flex-direction: column;
padding: 60px 20px 20px 40px;
}
.balance {
font-size: 50px;
font-weight: 800;
}
.change-label {
font-size: 20px;
margin-right: 5px;
}
.change-input {
font-size: 20px;
height: 40px;
text-align: end;
padding-right: 10px;
}
.change-submit {
font-size: 15px;
height: 40px;
margin-left: 15px;
border-radius: 5px;
}
.change-message {
font-size: 20px;
margin-bottom: 15px;
}
.error-message {
font-size: 20px;
color: #FF0000;
margin-bottom: 15px;
}
.app-container {
flex: 0;
min-width: 440px;
display: flex;
flex-direction: column;
margin-top: 40px;
margin-left: 80px;
}
.change-container {
flex: 1;
}
Copy the money image for the Index
page. Run the following command in a terminal window. Make sure you are in the root directory for your application.
curl --create-dirs --output wwwroot/img/money.jpg https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/app/assets/images/money.jpg
Next we need to do a little clean up and make sure all the namespaces work together in the application.
Replace the text in the Pages/_ViewImports.cshtml
file with the following code.
@namespace Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Replace the text in the Pages/Error.cshtml.cs
file with the following code.
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Pages;
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
private readonly ILogger<ErrorModel> _logger;
public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
Replace the text in the Pages/Privacy.cshtml.cs
file with the following code.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Pages;
public class PrivacyModel : PageModel
{
private readonly ILogger<PrivacyModel> _logger;
public PrivacyModel(ILogger<PrivacyModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
You can start up the .NET 7 application using this command in a terminal window from the root directory of your application:
dotnet run
You can now open up an incognito window and visit the app at http://localhost:5000. Log in with the user account you created when setting up FusionAuth, and you’ll see the email of the user next to a logout button.
This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you're going to want to do.
FusionAuth gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:
If you run into an issue with cookies on Safari or other browsers, you might need to run the .NET application under SSL.
You can follow this guide to install development SSL certificates for your .NET environment.
Alternatively, you can run the project using Visual Studio, which will run the project using SSL.
If you do this, make sure to update the Authorized Redirect URL
to reflect the https
protocol. Also note that the project will probably run on a different port when using SSL, so you must update that as well. To do so, log into the administrative user interface, navigate to Applications
, then click the Edit
button on your application and navigate to the OAuth
tab. You can have more than one URL.
If you change the name of the application from ‘your-application’ to some other name, you will need to make sure the correct namespace is used in all of your code.
There are multiple ways of deploying an application, but publishing ensures your deployment process is repeatable. You can use the RID catalog to build different versions of this application for different platforms.
If you have trouble running the application, please verify all the files are updated as instructed. The directory for the your-application
tree should look like this:
└── your-application
├── appsettings.Development.json
├── appsettings.json
├── bin/
├── ...
├── obj/
├── ...
├── Pages/
│ ├── _ViewImports.cshtml
│ ├── _ViewStart.cshtml
| |── Account.cshtml
| |── Accuont.cshtml.cs
│ ├── Error.cshtml
│ ├── Error.cshtml.cs
│ ├── Index.cshtml
│ ├── Index.cshtml.cs
│ ├── Logout.cshtml
│ ├── Logout.cshtml.cs
| |── MakeChange.cshtml
| |── MakeChange.cshtml.cs
│ ├── Privacy.cshtml
│ ├── Privacy.cshtml.cs
│ └── Shared/
│ ├── _Layout.cshtml
│ ├── _Layout.cshtml.css
│ └── _ValidateScriptsPartial.cshtml
├── Program.cs
├── Properties/
├── ...
├── Startup.cs
└── wwwroot/
├── ...
The full code is available here.