Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
bin
obj
*.user
App_Data/*.json

# User secrets (lokalne dane uwierzytelniania)
appsettings.Development.json
appsettings.Production.json
appsettings.*.json

# Produkcyjne dane użytkowników
App_Data/*.json
5 changes: 5 additions & 0 deletions App_Data/users.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
{
"username": "twoj-email@outlook.com"
}
]
54 changes: 25 additions & 29 deletions Controllers/AccountController.cs
Original file line number Diff line number Diff line change
@@ -1,58 +1,54 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Passwords.Models;
using Passwords.Services;

namespace Passwords.Controllers;

public class AccountController : Controller
{
private readonly JsonDataStore _store;

public AccountController(JsonDataStore store)
{
_store = store;
}

[HttpGet]
public IActionResult Login()
[AllowAnonymous]
public IActionResult Login(string? error = null)
{
if (IsLoggedIn)
if (User.Identity?.IsAuthenticated == true)
{
return RedirectToAction("Index", "Entries");
}

return View(new LoginViewModel());
return View(new LoginViewModel { Error = error });
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Login(LoginViewModel model)
[AllowAnonymous]
public IActionResult MicrosoftLogin(string? returnUrl = null)
{
if (string.IsNullOrWhiteSpace(model.Username) || string.IsNullOrWhiteSpace(model.Password))
{
ViewData["Error"] = "Username and password are required.";
return View(model);
}
var redirectUrl = Url.IsLocalUrl(returnUrl)
? returnUrl
: Url.Action("Index", "Entries") ?? "/";

if (!_store.ValidateUser(model.Username, model.Password))
var properties = new AuthenticationProperties
{
ViewData["Error"] = "Invalid username or password.";
return View(model);
}

HttpContext.Session.SetString("username", model.Username);
_store.LogLogin(model.Username);
RedirectUri = redirectUrl
};

return RedirectToAction("Index", "Entries");
return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme);
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Logout()
{
HttpContext.Session.Clear();
return RedirectToAction("Login");
}
var properties = new AuthenticationProperties
{
RedirectUri = Url.Action("Login", "Account")
};

private bool IsLoggedIn => !string.IsNullOrEmpty(HttpContext.Session.GetString("username"));
return SignOut(properties,
CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
}
}
38 changes: 3 additions & 35 deletions Controllers/EntriesController.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Passwords.Models;
using Passwords.Services;

namespace Passwords.Controllers;

[Authorize]
public class EntriesController : Controller
{
private readonly JsonDataStore _store;
Expand All @@ -16,11 +18,6 @@ public EntriesController(JsonDataStore store)
[HttpGet]
public IActionResult Index()
{
if (!IsLoggedIn)
{
return RedirectToLogin();
}

var currentUser = CurrentUser;
var entries = _store.GetEntries()
.Where(e => IsVisibleToUser(e, currentUser))
Expand All @@ -33,11 +30,6 @@ public IActionResult Index()
[HttpGet]
public IActionResult Details(int id)
{
if (!IsLoggedIn)
{
return RedirectToLogin();
}

var entry = _store.GetEntry(id);
if (entry == null)
{
Expand All @@ -51,23 +43,13 @@ public IActionResult Details(int id)
[HttpGet]
public IActionResult Create()
{
if (!IsLoggedIn)
{
return RedirectToLogin();
}

return View(new EntryCreateViewModel());
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(EntryCreateViewModel model)
{
if (!IsLoggedIn)
{
return RedirectToLogin();
}

if (string.IsNullOrWhiteSpace(model.Title))
{
ModelState.AddModelError(nameof(model.Title), "Title is required.");
Expand All @@ -85,11 +67,6 @@ public IActionResult Create(EntryCreateViewModel model)
[HttpGet]
public IActionResult Edit(int id)
{
if (!IsLoggedIn)
{
return RedirectToLogin();
}

var entry = _store.GetEntry(id);
if (entry == null)
{
Expand All @@ -111,11 +88,6 @@ public IActionResult Edit(int id)
[ValidateAntiForgeryToken]
public IActionResult Edit(EntryEditViewModel model)
{
if (!IsLoggedIn)
{
return RedirectToLogin();
}

if (string.IsNullOrWhiteSpace(model.Title))
{
ModelState.AddModelError(nameof(model.Title), "Title is required.");
Expand All @@ -136,11 +108,7 @@ public IActionResult Edit(EntryEditViewModel model)
return RedirectToAction("Details", new { id = model.Id });
}

private string CurrentUser => HttpContext.Session.GetString("username") ?? "";

private bool IsLoggedIn => !string.IsNullOrEmpty(CurrentUser);

private IActionResult RedirectToLogin() => RedirectToAction("Login", "Account");
private string CurrentUser => UserIdentifier.GetUserIdentifier(User) ?? "Unknown";

private static bool IsVisibleToUser(Entry entry, string currentUser)
{
Expand Down
3 changes: 1 addition & 2 deletions Models/LoginViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ namespace Passwords.Models;

public class LoginViewModel
{
public string Username { get; set; } = "";
public string Password { get; set; } = "";
public string? Error { get; set; }
}
1 change: 0 additions & 1 deletion Models/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ namespace Passwords.Models;
public class User
{
public string Username { get; set; } = "";
public string Password { get; set; } = "";
}
8 changes: 7 additions & 1 deletion Passwords.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-SecretsManager-$(MSBuildProjectName)</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="2.15.2" />
</ItemGroup>

<ItemGroup>
<Content Update="App_Data\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand Down
64 changes: 59 additions & 5 deletions Program.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,71 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Tokens;
using Passwords.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<JsonDataStore>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSession(options =>
builder.Services.AddAuthentication(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
})
.AddOpenIdConnect(options =>
{
var authSection = builder.Configuration.GetSection("Authentication:Microsoft");
options.Authority = "https://login.microsoftonline.com/common/v2.0";
options.ClientId = authSection["ClientId"] ?? string.Empty;
options.ClientSecret = authSection["ClientSecret"] ?? string.Empty;
options.CallbackPath = authSection["CallbackPath"] ?? "/signin-oidc";
options.ResponseType = "code";
options.SaveTokens = false;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "preferred_username"
};
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context =>
{
var store = context.HttpContext.RequestServices.GetRequiredService<JsonDataStore>();
var identifier = UserIdentifier.GetUserIdentifier(context.Principal)?.Trim();
if (string.IsNullOrWhiteSpace(identifier) || !store.IsAllowedUser(identifier))
{
context.Fail("User is not allowed.");
return Task.CompletedTask;
}

store.LogLogin(identifier);
return Task.CompletedTask;
},
OnRemoteFailure = context =>
{
context.HandleResponse();
var message = context.Failure?.Message?.Contains("not allowed", StringComparison.OrdinalIgnoreCase) == true
? "Your Microsoft account is not authorized for this app."
: "Microsoft sign-in failed.";
var encoded = Uri.EscapeDataString(message);
context.Response.Redirect($"/Account/Login?error={encoded}");
return Task.CompletedTask;
}
};
});

builder.Services.AddAuthorization();

var app = builder.Build();

app.Use(async (context, next) =>
Expand All @@ -29,7 +82,8 @@

app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
name: "default",
Expand Down
Loading