Search code examples
c#asp.net-coreasp.net-core-mvcsingle-sign-oncas

Creating SSO (single sign on) CAS on ASP.NET Core 6 MVC app


I am trying to create an ASP.NET Core 6 MVC app and want to implement CAS, but cannot find directions. I got the nuget package for GSS.Authentication.CAS and they do not have instructions or explanations of what to do. I got the program.cs file part then I do not know how to set up the controller.

program.cs file:

#pragma warning disable SA1200 // Using directives should be placed correctly
using System.Security.Claims;
using GSS.Authentication.CAS.AspNetCore;
using GSS.Authentication.CAS.Validation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Extensions;

#pragma warning restore SA1200 // Using directives should be placed correctly

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddAuthorization(options =>
{
    // Globally Require Authenticated Users
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Events = new CookieAuthenticationEvents
        {
            OnSigningOut = context =>
            {
                // Single Sign-Out
                var casUrl = new Uri(builder.Configuration["Authentication:CAS:ServerUrlBase"]);
                var links = context.HttpContext.RequestServices.GetRequiredService<LinkGenerator>();
                var serviceUrl = links.GetUriByPage(context.HttpContext, "/Index");
                var redirectUri = UriHelper.BuildAbsolute(
                    casUrl.Scheme,
                    new HostString(casUrl.Host, casUrl.Port),
                    casUrl.LocalPath,
                    "/logout",
                    QueryString.Create("service", serviceUrl!));

                var logoutRedirectContext = new RedirectContext<CookieAuthenticationOptions>(
                    context.HttpContext,
                    context.Scheme,
                    context.Options,
                    context.Properties,
                    redirectUri);
                context.Response.StatusCode = 204; // Prevent RedirectToReturnUrl
                context.Options.Events.RedirectToLogout(logoutRedirectContext);
                return Task.CompletedTask;
            },
        };
    })
    .AddCAS(options =>
    {
        options.CasServerUrlBase = builder.Configuration["Authentication:CAS:ServerUrlBase"];
        var protocolVersion = builder.Configuration.GetValue("Authentication:CAS:ProtocolVersion", 2); // change protocol to match your system
        if (protocolVersion != 3)
        {
            options.ServiceTicketValidator = protocolVersion switch
            {
                1 => new Cas10ServiceTicketValidator(options),
                2 => new Cas20ServiceTicketValidator(options),
                _ => null
            };
        }

        options.Events = new CasEvents
        {
            OnCreatingTicket = context =>
            {
                if (context.Identity == null)
                {
                    return Task.CompletedTask;
                }

                // Map claims from assertion
                var assertion = context.Assertion;
                context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, assertion.PrincipalName));
context.Identity.AddClaim(new Claim(ClaimTypes.Name, assertion.PrincipalName)); //This line allows you to access primary login info as User.identity.Name in cs code
                if (assertion.Attributes.TryGetValue("display_name", out var displayName))
                {
                    context.Identity.AddClaim(new Claim(ClaimTypes.Name, displayName));
                }

                if (assertion.Attributes.TryGetValue("email", out var email))
                {
                    context.Identity.AddClaim(new Claim(ClaimTypes.Email, email));
                }

                return Task.CompletedTask;
            },
            OnRemoteFailure = context =>
            {
                var failure = context.Failure;
                var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<CasEvents>>();
                if (!string.IsNullOrWhiteSpace(failure?.Message))
                {
                    logger.LogError(failure, "{Exception}", failure.Message);
                }

                context.Response.Redirect("/Account/ExternalLoginFailure");
                context.HandleResponse();
                return Task.CompletedTask;
            },
        };
    });

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.UseAuthentication();
app.UseAuthorization();
app.UseCookiePolicy();



app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Solution

  • So I think I was on the right track with the posted program.cs.

    The next step is to create a controller AccountController.cs

    namespace {YourAppName}.Controllers {
        using System;
        using System.Diagnostics;
        using Microsoft.AspNetCore.Authorization;
        using Microsoft.AspNetCore.Mvc;
        using OneRecordIssue6.Models;
    
        [AllowAnonymous]
        public class AccountController : Controller
        {
            IConfiguration configuration;
    
            public AccountController(IConfiguration configuration)
            {
                this.configuration = configuration;
            }
    
            public IActionResult Login()
            {
                string isDev = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
                if (isDev == "Development")
                {
                   // if in developement, allow me to use any user choosen for testing. This shows me how different rules apply
                    var simulatedUser = this.configuration.GetSection("Settings:simulatedUser").Value.ToString();
                    var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, simulatedUser) }, CookieAuthenticationDefaults.AuthenticationScheme);
                    var principal = new ClaimsPrincipal(identity);
                    var authProperties = new AuthenticationProperties { ExpiresUtc = DateTimeOffset.UtcNow.AddSeconds(5), IsPersistent = false, RedirectUri = "/" };
                    this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, authProperties);
                    return this.RedirectToAction("Index", "Home");
                }
                else
                {
                    return this.Challenge(new AuthenticationProperties { RedirectUri = "/" }, "CAS");
                }
            }
    
            public IActionResult ExternalLoginFailureModel()
            {
                this.Response.StatusCode = 500;
                return this.RedirectToAction("Error", "Home");
            }
        } }
    

    Then make sure in a any controller that needs to to be authorized just add the

    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    

    then any page or controller that needs to be authenticated add

    [Authorize]

    (include the square brackets) and this will enforce the auth