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
190 changes: 165 additions & 25 deletions Rsk.Samples.OpenIddict.AdminUI/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
@@ -1,55 +1,92 @@
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityExpress.Identity;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Rsk.Samples.OpenIddict.AdminUiIntegration.Data;
using Rsk.Samples.OpenIddict.AdminUiIntegration.Models;
using Rsk.Samples.OpenIddict.AdminUiIntegration.Services;

namespace Rsk.Samples.OpenIddict.AdminUiIntegration.Controllers;

public class AccountController(SignInManager<ApplicationUser> signInManager) : Controller
public class AccountController(
SignInManager<ApplicationUser> signInManager,
IAccountService accountService,
IAuthenticationSchemeProvider schemeProvider,
IUrlHelperFactory urlHelperFactory) : Controller
{
[HttpGet]
public IActionResult Login([FromQuery] string returnUrl)
public async Task<IActionResult> Login([FromQuery] string returnUrl)
{
return View(new LoginInputModel
{
ReturnUrl = returnUrl
});
bool externalLogin = Request.Cookies["Identity.External"] != null;

var vm = !externalLogin ? await accountService.BuildLoginViewModelAsync(returnUrl)
: accountService.BuildLinkLoginViewModel(returnUrl);

return View(vm);
}

/// <summary>
/// Handle postback from username/password login
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
public async Task<IActionResult> Login(LoginViewModel model, string button)
{
if (ModelState.IsValid)
if (button != "login")
{
var user = await signInManager.UserManager.FindByNameAsync(model.Username);
if (user?.UserName == null)
if (Request.Cookies["Identity.External"] != null)
{
ModelState.AddModelError(string.Empty, "Invalid Credentials");
return View(model);
await HttpContext.SignOutAsync("Identity.External");
}

return Redirect(model.ReturnUrl);
}

if (ModelState.IsValid)
{
var user = await signInManager.UserManager.FindByNameAsync(model.Username);

var result = await signInManager.PasswordSignInAsync(user, model.Password, false, false);
if (!result.Succeeded)
if (user != null && await signInManager.UserManager.CheckPasswordAsync(user, model.Password))
{
ModelState.AddModelError(string.Empty, "Invalid Credentials");
return View(model);
}
// only set explicit expiration here if user chooses "remember me".
// otherwise we rely upon expiration configured in cookie middleware.
AuthenticationProperties props = null;
if (model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromDays(30))
};
}

// issue authentication cookie with subject ID and username
await signInManager.SignInAsync(user, props);

await signInManager.SignInAsync(user, false);
// link external login if cookie exists
await LinkIfExternalLogin(user);

// make sure the returnUrl is still valid, and if so redirect back to authorize endpoint or a local page
if (!Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}

var returnUrl = model.ReturnUrl;
if (returnUrl == null || !Url.IsLocalUrl(returnUrl))
{
return Redirect("~/");
}

return Redirect(returnUrl);
ModelState.AddModelError("", "Invalid username or password");
}

ModelState.AddModelError(string.Empty, "Invalid username or password");

// something went wrong, show form with error
var vm = await accountService.BuildLoginViewModelAsync(model);
vm.LinkSetup = Request.Cookies["Identity.External"] != null;
return View(model);
}

Expand All @@ -72,4 +109,107 @@ public async Task<IActionResult> Logout(LogoutInputModel model)
return View("LoggedOut");
}

/// <summary>
/// initiate roundtrip to external authentication provider
/// </summary>
[HttpGet]
public IActionResult ExternalLogin(string provider, string returnUrl)
{
var urlHelper = urlHelperFactory.GetUrlHelper(ControllerContext);
var props = new AuthenticationProperties
{
RedirectUri = urlHelper.Action("ExternalLoginCallback"),
Items =
{
{ "returnUrl", returnUrl }
}
};

// challenge specific authentication middleware
props.Items.Add("scheme", provider);
return Challenge(props, provider);
}

/// <summary>
/// Post processing of external authentication
/// </summary>
[HttpGet]
public async Task<IActionResult> ExternalLoginCallback()
{
// get external identity from external scheme cookie
var result = await HttpContext.AuthenticateAsync("Identity.External");
if (result?.Succeeded != true) throw new Exception("External authentication error");

var externalUser = result.Principal;
var claims = externalUser.Claims.ToList();

// try to determine the unique id of the external user
var userIdClaim = claims.FirstOrDefault(x => x.Type == "sub") ?? claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
if (userIdClaim == null) throw new Exception("Unknown userid");

claims.Remove(userIdClaim);
var userId = userIdClaim.Value;
var provider = result.Properties.Items["scheme"];

var returnUrl = result.Properties.Items["returnUrl"];

if (!Url.IsLocalUrl(returnUrl))
{
returnUrl = "~/";
}

// check if the external user is already provisioned
ApplicationUser user = await signInManager.UserManager.FindByLoginAsync(provider, userId);

if (user == null)
{
return RedirectToAction("Login", new { returnUrl });
}

var additionalClaims = new List<IdentityExpressClaim>();

// if the external system sent a session id claim, copy it over
var sid = claims.FirstOrDefault(x => x.Type == "sid");
if (sid != null) additionalClaims.Add(new IdentityExpressClaim(){ClaimType = "sid", ClaimValue = sid.Value});

// if the external provider issued an id_token, we'll keep it for signout
AuthenticationProperties props = null;
var idToken = result.Properties.GetTokenValue("id_token");
if (idToken != null)
{
props = new AuthenticationProperties();
props.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = idToken } });
}

// issue local authentication cookie for user
await signInManager.SignInAsync(user, props);

// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync("Identity.External");

return Redirect(returnUrl);
}

private async Task<IdentityResult> LinkIfExternalLogin(ApplicationUser localUser)
{
// get external identity from external scheme cookie
var result = await HttpContext.AuthenticateAsync("Identity.External");
if (result?.Succeeded != true) return null;

var externalUser = result.Principal;
var claims = externalUser.Claims.ToList();

// try to determine the unique id of the external user
var userIdClaim = claims.FirstOrDefault(x => x.Type == "sub") ?? claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
if (userIdClaim == null) throw new Exception("Unknown userid");

claims.Remove(userIdClaim);
var userId = userIdClaim.Value;
var providerScheme = result.Properties.Items["scheme"];

var provider = await schemeProvider.GetSchemeAsync(providerScheme);
var outcome = await signInManager.UserManager.AddLoginAsync(localUser, new UserLoginInfo(provider.Name, userId, provider.DisplayName));
await HttpContext.SignOutAsync("Identity.External");
return outcome;
}
}
113 changes: 0 additions & 113 deletions Rsk.Samples.OpenIddict.AdminUI/Controllers/AuthenticationController.cs

This file was deleted.

7 changes: 7 additions & 0 deletions Rsk.Samples.OpenIddict.AdminUI/Models/ExternalProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Rsk.Samples.OpenIddict.AdminUiIntegration.Models;

public class ExternalProvider
{
public string DisplayName { get; set; }
public string AuthenticationScheme { get; set; }
}
2 changes: 1 addition & 1 deletion Rsk.Samples.OpenIddict.AdminUI/Models/LoginInputModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ public class LoginInputModel
public string ReturnUrl { get; set; }

[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
public bool RememberLogin { get; set; }
}
15 changes: 15 additions & 0 deletions Rsk.Samples.OpenIddict.AdminUI/Models/LoginViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Rsk.Samples.OpenIddict.AdminUiIntegration.Models;

public class LoginViewModel : LoginInputModel
{
public bool LinkSetup { get; set; } = false;

public IEnumerable<ExternalProvider> ExternalProviders { get; set; } = new List<ExternalProvider>();
public IEnumerable<ExternalProvider> VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName));

public string ExternalLoginScheme => ExternalProviders?.SingleOrDefault()?.AuthenticationScheme;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
<PackageReference Include="Rsk.Saml.OpenIddict.AspNetCore.Identity" Version="11.0.0" />
<PackageReference Include="Rsk.Saml.OpenIddict.EntityFrameworkCore" Version="11.0.0" />
<PackageReference Include="Rsk.Saml.OpenIddict.Quartz" Version="11.0.0" />
<PackageReference Include="Rsk.DynamicAuthenticationProviders" Version="3.3.0" />
<PackageReference Include="Rsk.DynamicAuthenticationProviders.EntityFramework" Version="3.3.0" />
<PackageReference Include="Rsk.DynamicAuthenticationProviders.Saml" Version="3.3.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading