question

lesponce avatar image
0 Votes"
lesponce asked ZhiLv-MSFT edited

Dependency Injection Using IdentityUser

I got a razor page, I got the following code that I'd like to translate to C# code.

 @inject SignInManager<IdentityUser> SignInManager
 @inject UserManager<IdentityUser> UserManager

How do I translate to C# code? Should I have it coded in the startup class and set it up like a service? If so, how do I code this?

dotnet-csharpdotnet-aspnet-core-mvcdotnet-aspnet-core-security
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

cooldadtx avatar image
0 Votes"
cooldadtx answered

What do you mean by "translate to C# code"? If you're using Razor Pages then those instances will be passed to your page automatically.

If you need to register those types then you won't be doing it directly in most cases. If you're using Identity then you would have added the identity middleware to your app during startup. That code would register the types. Something like this, depending upon your provider.

services.AddDefaultIdentity<IdentityUser>(options => {
});


This is covered here.

5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Bruce-SqlWork avatar image
0 Votes"
Bruce-SqlWork answered Bruce-SqlWork edited

DI is done via the class constructor. the c# would be:

 public MyClass 
 { 
      private SignInManager<IdentityUser> SignInManager; 
      private UserManager<IdentityUser> UserManager; 
     
      public MyClass(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager) 
      { 
            this.SignInManager = signInManager; 
            this.UserManager = userManager; 
      } 
 } 

in razor pages, you don't define the constructor, so the @inject directive is used instead. the razor code generator will create the constructor and private variables.

5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

lesponce avatar image
0 Votes"
lesponce answered cooldadtx commented

Thanks for your responses. It's giving me an idea. I'm trying to use a controller that has these entry parms in the constructor:

UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager,
IConfiguration configuration

I tried to use @Bruce-SqlWork 's recommendation, but the values are null.

How do I get userManager and signInManager populated so I can call a method that resides in a Controller?

· 6
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

It could be the configuration. Please share the relevant code when asking for help on the forum. We have no idea where the mistakes are in your code.

0 Votes 0 ·

A controller with razor pages? That sort of defeats the purpose of razor pages if you're using a controller. However assuming you have configured your startup to use controllers then you'd just pass the parameters to the controller's constructor. But, again, that doesn't make sense for razor pages.

public class MyController : Controller
{
   //Injected by DI automatically for an MVC project
   public MyController ( UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signinManager )
   {
   }
}


Note that as discussed in the link I gave you need to ensure you're calling UseAuthentication and (if needed) UseAuthorization as part of your startup and that needs to happen after routing but before the endpoints are configured otherwise things don't work properly.

//Configure logic
app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

//Other middleware
0 Votes 0 ·

Thanks for your response. The thing is that the working example that I got it's using an Index.cshtml injecting the two references below and an asp-action call to a method.

@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

I don't want to use and Index razor page to call the call the controller from a button click. I'd like to call the code in the controller from the Startup class. If that's the case, I'm assuming that I need to create a middleware class to be able to call that controller. How can I accomplish this?

0 Votes 0 ·

What?

Are you building a regular MVC app or an app using Razor Pages?

You cannot call a controller from the startup of your app. App startup is run before any HTTP request processing begins. Controllers run in the context of an HTTP request and make no sense outside of an HTTP request. Why do you need to call a controller from Startup?

0 Votes 0 ·
Show more comments
lesponce avatar image
0 Votes"
lesponce answered cooldadtx commented

Thanks @cooldadtx

The app should display a third party page that is related to single sign on. So the controller should trigger that third party page. After the authentication happens I will need to handle the response, but for now.....in order to complete the fist step to get that page displayed, I need to be able to make a code reference that will trigger the controller.

A working example is calling the controller that I need from a razor page. In my case, I don't want the razor page. Please see code example below. I will list the following:
1. Index.cshtml file that I don't want, but it's working from the example.
2. The controller that I need.
3. The startup class.

 @page
 @model IndexModel
 @{
     ViewData["Title"] = "Home";
 }
    
 @using Microsoft.AspNetCore.Identity
    
 @inject SignInManager<IdentityUser> SignInManager
 @inject UserManager<IdentityUser> UserManager
    
 <div class="jumbotron">
     <h1 class="display-4">Example Service Provider</h1>
     <p class="lead">
         This example demonstrates using the ComponentSpace SAML v2.0 library to enable SAML single sign-on
         as the service provider.
     </p>
     @if (!SignInManager.IsSignedIn(User))
     {
         <p><a asp-area="" asp-controller="Saml" asp-action="InitiateSingleSignOn" class="btn btn-primary btn-lg">SSO to the Identity Provider</a></p>
     }
 </div>



// CONTROLLER:

using ComponentSpace.Saml2;
using ComponentSpace.Saml2.Metadata.Export;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using System;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using System.Xml;

namespace ExampleServiceProvider.Controllers
{
[Route("[controller]/[action]")]
public class SamlController : Controller
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly ISamlServiceProvider _samlServiceProvider;
private readonly IConfigurationToMetadata _configurationToMetadata;
private readonly IConfiguration _configuration;

     public SamlController(
         UserManager<IdentityUser> userManager,
         SignInManager<IdentityUser> signInManager,
         ISamlServiceProvider samlServiceProvider,
         IConfigurationToMetadata configurationToMetadata,
         IConfiguration configuration)
     {
         _userManager = userManager;
         _signInManager = signInManager;
         _samlServiceProvider = samlServiceProvider;
         _configurationToMetadata = configurationToMetadata;
         _configuration = configuration;
     }

     public async Task<IActionResult> InitiateSingleSignOn(string returnUrl = null)
     {
         var partnerName = _configuration["PartnerName"];

         // To login automatically at the service provider, 
         // initiate single sign-on to the identity provider (SP-initiated SSO).            
         // The return URL is remembered as SAML relay state.
         await _samlServiceProvider.InitiateSsoAsync(partnerName, returnUrl);

         return new EmptyResult();
     }

     public async Task<IActionResult> InitiateSingleLogout(string returnUrl = null)
     {
         // Request logout at the identity provider.
         await _samlServiceProvider.InitiateSloAsync(relayState: returnUrl);

         return new EmptyResult();
     }

     public async Task<IActionResult> AssertionConsumerService()
     {
         // Receive and process the SAML assertion contained in the SAML response.
         // The SAML response is received either as part of IdP-initiated or SP-initiated SSO.
         var ssoResult = await _samlServiceProvider.ReceiveSsoAsync();

         // Automatically provision the user.
         // If the user doesn't exist locally then create the user.
         // Automatic provisioning is an optional step.
         var user = await _userManager.FindByNameAsync(ssoResult.UserID);

         if (user == null)
         {
             user = new IdentityUser { UserName = ssoResult.UserID, Email = ssoResult.UserID };

             var result = await _userManager.CreateAsync(user);

             if (!result.Succeeded)
             {
                 throw new Exception($"The user {ssoResult.UserID} couldn't be created - {result}");
             }

             // For demonstration purposes, create some additional claims.
             if (ssoResult.Attributes != null)
             {
                 var samlAttribute = ssoResult.Attributes.SingleOrDefault(a => a.Name == ClaimTypes.Email);

                 if (samlAttribute != null)
                 {
                     await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, samlAttribute.ToString()));
                 }

                 samlAttribute = ssoResult.Attributes.SingleOrDefault(a => a.Name == ClaimTypes.GivenName);

                 if (samlAttribute != null)
                 {
                     await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.GivenName, samlAttribute.ToString()));
                 }

                 samlAttribute = ssoResult.Attributes.SingleOrDefault(a => a.Name == ClaimTypes.Surname);

                 if (samlAttribute != null)
                 {
                     await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Surname, samlAttribute.ToString()));
                 }
             }
         }

         // Automatically login using the asserted identity.
         await _signInManager.SignInAsync(user, isPersistent: false);

         // Redirect to the target URL if specified.
         if (!string.IsNullOrEmpty(ssoResult.RelayState))
         {
             return LocalRedirect(ssoResult.RelayState);
         }

         return RedirectToPage("/Index");
     }

     public async Task<IActionResult> SingleLogoutService()
     {
         // Receive the single logout request or response.
         // If a request is received then single logout is being initiated by the identity provider.
         // If a response is received then this is in response to single logout having been initiated by the service provider.
         var sloResult = await _samlServiceProvider.ReceiveSloAsync();

         if (sloResult.IsResponse)
         {
             // SP-initiated SLO has completed.
             if (!string.IsNullOrEmpty(sloResult.RelayState))
             {
                 return LocalRedirect(sloResult.RelayState);
             }

             return RedirectToPage("/Index");
         }
         else
         {
             // Logout locally.
             await _signInManager.SignOutAsync();

             // Respond to the IdP-initiated SLO request indicating successful logout.
             await _samlServiceProvider.SendSloAsync();
         }

         return new EmptyResult();
     }

     public async Task<IActionResult> ArtifactResolutionService()
     {
         // Resolve the HTTP artifact.
         // This is only required if supporting the HTTP-Artifact binding.
         await _samlServiceProvider.ResolveArtifactAsync();

         return new EmptyResult();
     }

     public async Task<IActionResult> ExportMetadata()
     {
         var entityDescriptor = await _configurationToMetadata.ExportAsync();
         var xmlElement = entityDescriptor.ToXml();

         Response.ContentType = "text/xml";
         Response.Headers.Add("Content-Disposition", "attachment; filename=\"metadata.xml\"");

         var xmlWriterSettings = new XmlWriterSettings()
         {
             Async = true,
             Encoding = Encoding.UTF8,
             Indent = true,
             OmitXmlDeclaration = true
         };

         using (var xmlWriter = XmlWriter.Create(Response.Body, xmlWriterSettings))
         {
             xmlElement.WriteTo(xmlWriter);
             await xmlWriter.FlushAsync();
         }

         return new EmptyResult();
     }
 }

}






// STARTUP:

using ComponentSpace.Saml2.Configuration;
using ComponentSpace.Saml2.Configuration.Resolver;
using ComponentSpace.Saml2.Exceptions;
using ExampleServiceProvider.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shared;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ExampleServiceProvider
{
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)
     {
         services.AddDbContext<ApplicationDbContext>(options =>
             options.UseSqlServer(
                 Configuration.GetConnectionString("DefaultConnection")));
         services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = false)
             .AddEntityFrameworkStores<ApplicationDbContext>();

         services.AddRazorPages();

         services.Configure<CookiePolicyOptions>(options =>
         {
             // SameSiteMode.None is required to support SAML SSO.
             options.MinimumSameSitePolicy = SameSiteMode.None;

             // Some older browsers don't support SameSiteMode.None.
             options.OnAppendCookie = cookieContext => SameSite.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
             options.OnDeleteCookie = cookieContext => SameSite.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
         });

         services.ConfigureApplicationCookie(options =>
         {
             // Use a unique identity cookie name rather than sharing the cookie across applications in the domain.
             options.Cookie.Name = "ExampleServiceProvider.Identity";

             // SameSiteMode.None is required to support SAML logout.
             options.Cookie.SameSite = SameSiteMode.None;
         });

         // Add SAML SSO services.
         services.AddSaml(Configuration.GetSection("SAML"));
     }

     // 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();
             app.UseDatabaseErrorPage();
         }
         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.UseHttpsRedirection();
         app.UseStaticFiles();

         app.UseRouting();

         app.UseCookiePolicy();
         app.UseAuthentication();
         app.UseAuthorization();

         app.UseEndpoints(endpoints =>
         {
             endpoints.MapRazorPages();
             endpoints.MapControllers();
         });
     }

     // Demonstrates loading SAML configuration programmatically 
     // rather than through appsettings.json or another JSON configuration file.
     // This is useful if configuration is stored in a custom database, for example.
     // The SAML configuration is registered in ConfigureServices by calling:
     // services.AddSaml(config => ConfigureSaml(config));
     private void ConfigureSaml(SamlConfigurations samlConfigurations)
     {
         samlConfigurations.Configurations = new List<SamlConfiguration>()
         {
             new SamlConfiguration()
             {
                 LocalServiceProviderConfiguration = new LocalServiceProviderConfiguration()
                 {
                     Name = "https://ExampleServiceProvider",
                     Description = "Example Service Provider",
                     AssertionConsumerServiceUrl = "https://localhost:44360/SAML/AssertionConsumerService",
                     SingleLogoutServiceUrl = "https://localhost:44360/SAML/SingleLogoutService",
                     ArtifactResolutionServiceUrl = "https://localhost:44360/SAML/ArtifactResolutionService",
                     LocalCertificates = new List<Certificate>()
                     {
                         new Certificate()
                         {
                             FileName = "certificates/sp.pfx",
                             Password = "password"
                         }
                     }
                 },
                 PartnerIdentityProviderConfigurations = new List<PartnerIdentityProviderConfiguration>()
                 {
                     new PartnerIdentityProviderConfiguration()
                     {
                         Name = "https://ExampleIdentityProvider",
                         Description = "Example Identity Provider",
                         SignAuthnRequest = true,
                         SignLogoutRequest = true,
                         SignLogoutResponse = true,
                         WantLogoutRequestSigned = true,
                         WantLogoutResponseSigned = true,
                         SingleSignOnServiceUrl = "https://localhost:44313/SAML/SingleSignOnService",
                         SingleLogoutServiceUrl = "https://localhost:44313/SAML/SingleLogoutService",
                         ArtifactResolutionServiceUrl = "https://localhost:44313/SAML/ArtifactResolutionService",
                         PartnerCertificates = new List<Certificate>()
                         {
                             new Certificate()
                             {
                                 FileName = "certificates/idp.cer"
                             }
                         }
                     }
                 }
             }
         };
     }
 }

 // Demonstrates loading SAML configuration dynamically using a custom configuration resolver.
 // Hard-coded configuration is returned in this example but more typically configuration would be read from a custom database.
 // The configurationName parameter specifies the SAML configuration in a multi-tenancy application but is not used in this example.
 // The custom configuration resolver is registered in ConfigureServices by calling:
 // services.AddSaml();
 // services.AddTransient<ISamlConfigurationResolver, CustomConfigurationResolver>();
 public class CustomConfigurationResolver : AbstractSamlConfigurationResolver
 {
     public override Task<bool> IsLocalServiceProviderAsync(string configurationName)
     {
         return Task.FromResult(true);
     }

     public override Task<LocalServiceProviderConfiguration> GetLocalServiceProviderConfigurationAsync(string configurationName)
     {
         var localServiceProviderConfiguration = new LocalServiceProviderConfiguration()
         {
             Name = "https://ExampleServiceProvider",
             Description = "Example Service Provider",
             AssertionConsumerServiceUrl = "https://localhost:44360/SAML/AssertionConsumerService",
             SingleLogoutServiceUrl = "https://localhost:44360/SAML/SingleLogoutService",
             ArtifactResolutionServiceUrl = "https://localhost:44360/SAML/ArtifactResolutionService",
             LocalCertificates = new List<Certificate>()
             {
                 new Certificate()
                 {
                     FileName = "certificates/sp.pfx",
                     Password = "password"
                 }
             }
         };

         return Task.FromResult(localServiceProviderConfiguration);
     }

     public override Task<PartnerIdentityProviderConfiguration> GetPartnerIdentityProviderConfigurationAsync(string configurationName, string partnerName)
     {
         if (partnerName != "https://ExampleIdentityProvider")
         {
             throw new SamlConfigurationException($"The partner identity provider {partnerName} is not configured."); 
         }

         var partnerIdentityProviderConfiguration = new PartnerIdentityProviderConfiguration()
         {
             Name = "https://ExampleIdentityProvider",
             Description = "Example Identity Provider",
             SignAuthnRequest = true,
             SignLogoutRequest = true,
             SignLogoutResponse = true,
             WantLogoutRequestSigned = true,
             WantLogoutResponseSigned = true,
             SingleSignOnServiceUrl = "https://localhost:44313/SAML/SingleSignOnService",
             SingleLogoutServiceUrl = "https://localhost:44313/SAML/SingleLogoutService",
             ArtifactResolutionServiceUrl = "https://localhost:44313/SAML/ArtifactResolutionService",
             PartnerCertificates = new List<Certificate>()
             {
                 new Certificate()
                 {
                     FileName = "certificates/idp.cer"
                 }
             }
         };

         return Task.FromResult(partnerIdentityProviderConfiguration);
     }

     public override Task<IList<string>> GetPartnerIdentityProviderNamesAsync(string configurationName)
     {
         IList<string> partnerIdentityProviderNames = new List<string> { "https://ExampleIdentityProvider" };

         return Task.FromResult(partnerIdentityProviderNames);
     }
 }

}






· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

To trigger authentication you shouldn't need to write any controller. If you apply the Authorize attribute to your controller/actions then when that route is hit the runtime will confirm the user is authenticated. If they aren't then it will automatically send a response back to the browser that redirects the user to the login URL you specified as part of your authentication configuration during startup. This is the standard way of handling authentication and doesn't require that you do anything else. Is this not sufficient for your needs?

Looking at just the razor page portion of your code, since you said everything else works, that is just demoing the link to the controller and doesn't actually do anything. From your Angular client side redirect the user to the controller's action directly. It'll be something along the lines of saml/initiatesinglesignon and is generally of the form controller/action. This will cause your controller to execute the action that the razor page was also using. Nothing else in the razor page code does anything.

0 Votes 0 ·
lesponce avatar image
0 Votes"
lesponce answered ZhiLv-MSFT commented

Hi @cooldadtx Thanks so much for your help. Yes, I've done the [Authorize] attribute before. The razor page was provided by the demo that I was using to mimic what I'm trying to accomplish. The redirect using Angular sounds like a good approach. I will give it a try. I'll come back to this. Thanks again.

· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Hi @lesponce,

Whether the problem have been solved or not? If not can you create a simple sample to reproduce the problem, and share it via github or onedrive, so it is easier for us help you solve it.

Best regards,
Dillion

0 Votes 0 ·