Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c87378a
feat: Add .NET 8.0 target, populate example Duo credentials, enhance …
tcotidiane33 Feb 11, 2026
577a9e9
Update README with images and credentials
tcotidiane33 Feb 11, 2026
1710279
feat: Implement database-backed user authentication with EF Core and …
tcotidiane33 Feb 11, 2026
3ff80b0
feat: Introduce Versus Bank and IT Experts branding assets and update…
tcotidiane33 Feb 12, 2026
ef61fd5
feat: Apply IT Experts & Versus Bank branding with Duo 2FA integration
tcotidiane33 Feb 12, 2026
e58fa6e
feat: Add branding assets and example HTML pages to the DuoUniversal …
tcotidiane33 Feb 12, 2026
9cd071d
feat: Add seed data for five new users.
tcotidiane33 Feb 12, 2026
cb619a1
feat: Add error logging for missing session state in callback and con…
tcotidiane33 Feb 12, 2026
90534a7
feat: Add published build artifacts for the DuoUniversal example appl…
tcotidiane33 Feb 12, 2026
031823e
feat: Implement strongly-typed Duo configuration using `IOptions` and…
tcotidiane33 Feb 12, 2026
a5aa58a
feat: Add `using Microsoft.Extensions.Configuration` directive.
tcotidiane33 Feb 12, 2026
7d89551
feat: Refactor .env loading to set environment variables for Duo conf…
tcotidiane33 Feb 12, 2026
143d88d
Move `StringExtensions` class inside `Program` class.
tcotidiane33 Feb 12, 2026
c992aa1
feat: Replace manual .env file loading with `ConfigureAppConfiguratio…
tcotidiane33 Feb 12, 2026
76263f6
fix: Enhance session cookie compatibility for Duo redirects and refin…
tcotidiane33 Feb 12, 2026
790e257
feat: Persist data protection keys to the file system, update the exa…
tcotidiane33 Feb 12, 2026
e19b282
feat: Add ForwardedHeaders middleware and a new Data Protection key f…
tcotidiane33 Feb 12, 2026
e0e3396
feat: Add a watermark and demo badge to the callback page, incorporat…
tcotidiane33 Feb 12, 2026
8a553b6
feat: Enhance login page with Duo health status indicators and fallba…
tcotidiane33 Mar 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Duo Universal C# Configuration
# Rename this file to .env on your server

# Duo credentials from Admin Panel
DUO_CLIENT_ID=DIJAYTMEB36S4FIOXI7F
DUO_CLIENT_SECRET=Pp9UJnNA7CQ10wlP2CLyWTHW8n9KH6Xjefa1KQJu
DUO_API_HOST=api-8905b780.duosecurity.com
# Utilisation de l'IP publique directe (Duo nécessite HTTPS)
DUO_REDIRECT_URI=https://localhost:5001/duo_callback

# ASP.NET Core settings
ASPNETCORE_ENVIRONMENT=Production
# IMPORTANT: Utilisez 0.0.0.0 pour être accessible via votre IP publique
ASPNETCORE_URLS=http://0.0.0.0:5000;https://0.0.0.0:5001
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.vscode
*/bin
*/obj
*.db
*.db-shm
*.db-wal
.env
.env.example
28 changes: 28 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy solution and projects for restore
COPY ["duo_universal_csharp.sln", "./"]
COPY ["DuoUniversal/DuoUniversal.csproj", "DuoUniversal/"]
COPY ["DuoUniversal.Example/DuoUniversal.Example.csproj", "DuoUniversal.Example/"]
COPY ["DuoUniversal.Tests/DuoUniversal.Tests.csproj", "DuoUniversal.Tests/"]

RUN dotnet restore

# Copy everything else
COPY . .

# Build and publish
WORKDIR "/src/DuoUniversal.Example"
RUN dotnet build "DuoUniversal.Example.csproj" -c Release -o /app/build
RUN dotnet publish "DuoUniversal.Example.csproj" -c Release -o /app/publish /p:UseAppHost=false

# Final stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "DuoUniversal.Example.dll"]
Binary file added DuoUniversal.Example/.DS_Store
Binary file not shown.
11 changes: 11 additions & 0 deletions DuoUniversal.Example/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.EntityFrameworkCore;

namespace DuoUniversal.Example.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

public DbSet<User> Users { get; set; }
}
}
10 changes: 10 additions & 0 deletions DuoUniversal.Example/Data/DuoConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace DuoUniversal.Example.Data
{
public class DuoConfig
{
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string ApiHost { get; set; }
public string RedirectUri { get; set; }
}
}
45 changes: 45 additions & 0 deletions DuoUniversal.Example/Data/SeedData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Linq;

namespace DuoUniversal.Example.Data
{
public static class SeedData
{
public static void Initialize(AppDbContext context)
{
context.Database.EnsureCreated();

if (!context.Users.Any(u => u.Username == "duouser"))
{
context.Users.Add(new User
{
Username = "duouser",
Password = "password123"
});
}

if (!context.Users.Any(u => u.Username == "Desktop_versus"))
{
context.Users.Add(new User
{
Username = "Desktop_versus",
Password = "password123"
});
}

for (int i = 1; i <= 15; i++)
{
string username = $"user{i}";
if (!context.Users.Any(u => u.Username == username))
{
context.Users.Add(new User
{
Username = username,
Password = "password123"
});
}
}

context.SaveChanges();
}
}
}
15 changes: 15 additions & 0 deletions DuoUniversal.Example/Data/User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;

namespace DuoUniversal.Example.Data
{
public class User
{
public int Id { get; set; }

[Required]
public string Username { get; set; }

[Required]
public string Password { get; set; } // Storing plain text/simple hash for demo purposes
}
}
10 changes: 9 additions & 1 deletion DuoUniversal.Example/DuoUniversal.Example.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@
<ProjectReference Include="..\DuoUniversal\DuoUniversal.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net6.0</TargetFrameworks>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

</Project>
171 changes: 159 additions & 12 deletions DuoUniversal.Example/Pages/Callback.cshtml
Original file line number Diff line number Diff line change
@@ -1,17 +1,164 @@
@* SPDX-FileCopyrightText: 2022 Cisco Systems, Inc. and/or its affiliates *@
@* SPDX-License-Identifier: BSD-3-Clause *@

@page "/duo_callback"
@model CallbackModel
@model DuoUniversal.Example.Pages.CallbackModel
@{
Layout = null;
}

<div class="content">
<div class="logo">
<a href="/"><img src=~/images/logo.png></a>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authentification Réussie - Versus Bank</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', Arial, sans-serif;
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: linear-gradient(135deg, #a98946 0%, #8b7238 100%);
padding: 25px 20px;
text-align: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.header img {
max-width: 280px;
height: auto;
}
.content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.success-card {
background: white;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
max-width: 800px;
width: 100%;
padding: 50px 40px;
text-align: center;
}
.success-card h1 {
color: #a98946;
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 20px;
}
.success-image {
margin: 30px 0;
}
.success-image img {
max-width: 100%;
height: auto;
max-height: 400px;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0,0,0,0.08);
}
.message {
color: #666;
font-size: 1.1rem;
line-height: 1.6;
margin-top: 25px;
}
.footer {
background: #2c2c2c;
color: #ccc;
padding: 25px 20px;
text-align: center;
font-size: 0.95rem;
}
.footer strong {
color: #a98946;
}
.watermark {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(-25deg);
opacity: 0.12;
pointer-events: none;
z-index: 0;
text-align: center;
width: 100%;
}
.watermark img {
max-width: 600px;
width: 80%;
height: auto;
}
.watermark-text {
font-size: 8rem;
font-weight: 900;
color: #2c2c2c;
display: block;
margin-top: -20px;
}
.demo-badge {
margin-top: 30px;
padding: 15px;
border-top: 1px solid #eee;
font-size: 0.9rem;
color: #888;
}
.demo-badge strong {
color: #a98946;
}
@@media (max-width: 768px) {
.success-card {
padding: 30px 20px;
}
.success-card h1 {
font-size: 1.8rem;
}
.header img {
max-width: 200px;
}
}
</style>
</head>
<body>
<div class="header">
<img src="/images/logo-versus-bank.png" alt="Versus Bank Logo">
</div>
<div class="auth-resp">
<h3><b>Auth Response:</b></h3>

<div class="watermark">
<img src="/images/Blue-Logo-H.png" alt="ITEXPERTS4AFRICA Logo">
<span class="watermark-text">DÉMO</span>
</div>
<div class="success">
<pre class="language.json"><code class="language-json">@Model.AuthResponse</code></pre>

<div class="content">
<div class="success-card">
<h1>Bienvenue, @Model.DuoToken?.Username!</h1>

<div class="success-image">
<img src="/images/popo-up-VERSUS-NET-PRO-1.png" alt="Authentification Réussie">
</div>

<p class="message">
Votre authentification à deux facteurs a été validée avec succès.<br>
Vous pouvez maintenant fermer cette fenêtre et retourner à votre application.
</p>

<div class="demo-badge">
<img src="/images/Blue-Logo-H.png" alt="ITEXPERTS4AFRICA Logo" style="max-height: 40px; display: block; margin: 0 auto 10px; opacity: 0.8;">
Démonstration réalisée par <strong>ITEXPERTS4AFRICA</strong> pour <strong>VERSUS BANK</strong>
</div>
</div>
</div>

<div class="footer">
&copy; @DateTime.Now.Year <strong>Versus Bank</strong> - Notre expertise fait la différence
</div>
</div>
</body>
</html>
8 changes: 7 additions & 1 deletion DuoUniversal.Example/Pages/Callback.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace DuoUniversal.Example.Pages
{
Expand All @@ -19,12 +20,15 @@ namespace DuoUniversal.Example.Pages
public class CallbackModel : PageModel
{
private readonly IDuoClientProvider _duoClientProvider;
private readonly Microsoft.Extensions.Logging.ILogger<CallbackModel> _logger;

public string AuthResponse { get; set; }
public IdToken DuoToken { get; set; }

public CallbackModel(IDuoClientProvider duoClientProvider)
public CallbackModel(IDuoClientProvider duoClientProvider, Microsoft.Extensions.Logging.ILogger<CallbackModel> logger)
{
_duoClientProvider = duoClientProvider;
_logger = logger;
}

public async Task<IActionResult> OnGet(string state, string code)
Expand All @@ -50,6 +54,7 @@ public async Task<IActionResult> OnGet(string state, string code)
// If either is missing, something is wrong.
if (string.IsNullOrEmpty(sessionState) || string.IsNullOrEmpty(sessionUsername))
{
_logger.LogError("Session state or username missing. State: {state}, Code: {code}, Request: {url}", state, code, Request.Path + Request.QueryString);
throw new DuoException("State or username were missing from your session");
}

Expand All @@ -70,6 +75,7 @@ public async Task<IActionResult> OnGet(string state, string code)
WriteIndented = true
};
AuthResponse = JsonSerializer.Serialize(token, options);
DuoToken = token;
return Page();
}
}
Expand Down
Loading