I have a Blazor WebAssembly (WASM) app that authenticates users using Okta. After they successfully log in via Okta, I want to authorize the user by calling an API that I wrote to retrieve that users roles and other general user info that we store. This call to get user info must also include the access token retrieved from the Okta log in.
The authentication piece with Okta works fine.
I'm not sure how to correctly call our API to get user info/roles as part of the login process, so that the roles can be added as claims BEFORE being redirected to any other page.
The Okta log in piece is set up using the RemoteAuthenticatorView and added in Program.Main as:
builder.Services.AddOidcAuthentication(options =>
{
options.ProviderOptions.Authority = builder.Configuration.GetValue<string>("Okta:Authority");
options.ProviderOptions.ClientId = builder.Configuration.GetValue<string>("Okta:ClientId");
options.ProviderOptions.ResponseType = "code";
});
What I've tried so far:
Using the OnLogInSucceeded event callback of the RemoteAuthenticatorView. This doesn't work because the user will be redirected to the page they tried to access before the api call completes. Therefore if the page has any Authorize(Roles="Admin") type of restrictions on it, those roles haven't been populated yet.
Using a factory that inherits from AccountClaimsPrincipalFactory. This seems like the correct way, however I'm getting runtime errors anytime I inject certain classes or services into my factory. I think I've narrowed it down to being an issue with an injected service using the IHttpClientFactory. Here's my factory code:
public class ClaimsPrincipalFactory : AccountClaimsPrincipalFactory
{
private IUserService userService { get; set; }
public ClaimsPrincipalFactory(
IAccessTokenProviderAccessor accessor,
IUserService userService
)
: base(accessor)
{
this.userService = userService;
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
RemoteUserAccount account, RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);
var userInfo = await userService.UserInfo();
var identity = user.Identity as ClaimsIdentity;
if (userInfo != null)
{
foreach (var role in userInfo.UserRoles)
{
identity.AddClaim(new Claim(ClaimsIdentity.DefaultRoleClaimType, role.ApplicationRole.Name));
}
}
return user;
}
}
Here is the constructor of my UserService:
public UserService(IHttpClientFactory clientFactory)
{
http = clientFactory.CreateClient("BlazorClient.ServerApi");
}
The CreateClient line causes this runtime error:
crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: ValueFactory attempted to access the Value property of this instance.
System.InvalidOperationException: ValueFactory attempted to access the Value property of this instance.
at System.Lazy`1[[Microsoft.Extensions.Http.ActiveHandlerTrackingEntry, Microsoft.Extensions.Http, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]].ViaFactory(LazyThreadSafetyMode mode)
at System.Lazy`1[[Microsoft.Extensions.Http.ActiveHandlerTrackingEntry, Microsoft.Extensions.Http, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]].ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
Here is how the httpFactory is set up in my Program file:
builder.Services
.AddHttpClient("BlazorClient.ServerApi", client => client.BaseAddress = new Uri(builder.Configuration.GetValue<string>("ServerApi:BaseAddress")))
.AddHttpMessageHandler<CorsRequestAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("BlazorClient.ServerApi"));
Here is how the Factory is added in Program:
builder.Services.AddApiAuthorization()
.AddAccountClaimsPrincipalFactory<ClaimsPrincipalFactory>();
What is the correct way of doing this? I've been stuck on this issue for literally days and it doesn't seem like it should be this hard (and so hard to find documented info on it).
I was strugling with the same issue and based on your code snippet I might solved it.
What I did is to pass a HttpClientFactory to the generator of the CustomUserFactory, then in the CreateUser func I can create my userService with this factory.
Hope it's an ok solution and helps you as well.
public class CustomUserFactory : AccountClaimsPrincipalFactory<CustomUserAccount>
{
private IUserService _userService { get; set; }
private IHttpClientFactory _httpClientFactory { get; set; }
public CustomUserFactory(IAccessTokenProviderAccessor accessor, IHttpClientFactory httpClientFactory)
: base(accessor)
{
_httpClientFactory = httpClientFactory;
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(CustomUserAccount account, RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);
_userService = new UserService(_httpClientFactory);
...
Related
I am using Azure B2C in a Razor Pages web app on dotnet core 3.1 and I want to log information to my database whenever a user is added or changed. I also want to confirm a user is in the database when they sign in, and add them if they are not. Right now I'm just trying to get the "new user" running. I don't know how to get logging and configuration into my static class given the event has a specific signature.
public UserRepository(ILogger<UserRepository> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
I have been following this article and I have read this post and have implemented a static class with:
public async static Task<Task> OnTicketReceivedCallback(TicketReceivedContext context)
{
//OnTicketReceived
//Check if user just completed signup flow
List<Claim> claims = context.Principal.Claims.ToList();
bool isNewUser = claims.FirstOrDefault(x => x.Type == "newUser") == null ? false : true;
//If so, do what needs to be done
if (isNewUser)
{
//This is where I am getting the error and don't know how to push the logger and configuration through the stack
UserRepository repo = new UserRepository();
}
return Task.CompletedTask;
}
Being called from the startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C");
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
options.Events.OnTicketReceived = B2CExtensions.OnTicketReceivedCallback
);
services.AddRazorPages()
.AddMicrosoftIdentityUI();
}
Make OnTicketReceivedCallback class non static and register it as a singleton. That way you can pass an ILogger trough the constructor.
I am using a postgres Database through a library called Marten with a .NET app, I have a custom IUserLoginStore which manages retrieving the user and its roles. This seems to be working correctly but I am have an issue with setting up authorization.
I am using authentication through google and it is working fine:
var info = await _signInManager.GetExternalLoginInfoAsync();
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
This action throws an access denied issue:
[HttpPost()]
[Authorize(Roles = "Admin")]
public JsonResult SubmitArticle([FromBody] ArticleInputModel input) {...}
I have dug into the Authorization code and the problem seems to be with the default ClaimsPrincipal code:
public virtual bool IsInRole(string role)
{
return false;
}
Should I implement my own version of ClaimsPrinciple and override the IsInRole, and if I do how do I get this back into the app?
private static void ConfigureSecurity(IServiceCollection services)
{
services.AddIdentity<User, Role>()
.AddUserValidator<UserValidator>()
.AddUserStore<MartenUserStore>()
.AddRoleStore<MartenRoleStore>()
.AddDefaultTokenProviders();
}
Alright figured it out after a large amount of digging, In my case the MartenRoleStore was implementing IUserLoginStore it also needed to implement IUserRoleStore which has GetRolesAsync and IsInRoleAsync. (This is very important it has to be the exact same class you used for .AddUserStore<>();)
This is the code that I found that caused the issue:
https://github.com/aspnet/Identity/blob/master/src/Microsoft.AspNetCore.Identity/UserManager.cs#L258
This is what makes it work:
https://github.com/aspnet/Identity/blob/master/src/Microsoft.AspNetCore.Identity/UserClaimsPrincipalFactory.cs#L96
I am using the Cookie Middleware to authenticate the user. I have been following this official tutorial.
Inside my Startup class, an excerpt from my Configure method looks like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// ...
// Cookie-based Authentication
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Events = new CustomCookieAuthenticationEvents(app),
});
// ...
}
The CustomCookieAuthenticationEvents class is defined as follows:
public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
private IApplicationBuilder _app;
private IMyService _myService = null;
private IMyService MyService
{
get
{
if(_myService != null)
{
return _myService;
} else
{
return _myService = (IMyService) _app.ApplicationServices.GetService(typeof(IMyService));
}
}
}
public CustomCookieAuthenticationEvents(IApplicationBuilder app)
{
_app = app;
}
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
string sessionToken = context.Principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
LogonSession response = null;
var response = await MyService.CheckSession(sessionToken);
if (response == null)
{
context.RejectPrincipal();
await context.HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
}
Since the dependency injection is not available at Startup.Configure (the services are not even registered at that point), I made a bit of a workaround:
Pass IApplicationBuilder service to the CustomCookieAuthenticationEvents class
Fetch IMyService upon first request inside a read-only property (singleton pattern)
tl;dr
My solution works, but it's ugly. There is no dependency injection involved, as it is not possible at that time.
The essence of the problem is that I must instantiate CustomCookieAuthenticationEvents. As far as I have read the source code, there is no way around this, because the UseCookieAuthentication throws an exception if I omit the options parameter.
Any suggestion how can one make my current solution nicer?
Startup.ConfigureServices() is called before Startup.Configure() (see https://learn.microsoft.com/en-us/aspnet/core/fundamentals/startup for more information). So Dependency Injection is available at that time ;)
As a consequence, you can resolve your dependence in your configure method like this:
app.ApplicationServices.GetRequiredService<CustomCookieAuthenticationEvents>()
You should be really careful when you resolve services inside middleware. Your current approach (and the one suggested by #arnaudauroux) can result in difficulties when you use/need/require scoped services (i.e. usage of DbContext).
Resolving via app.ApplicationServices results in static (singleton) services, when the service is registered as scoped (transient are resolved per call, so they are not affected). It would be better to resolve your service during the request from HttpContext inside ValidatePrincipal method.
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
string sessionToken = context.Principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
LogonSession response = null;
var myService = context.HttpContext.RequestServices.GetService<IMyService >();
var response = await myService.CheckSession(sessionToken);
if (response == null)
{
context.RejectPrincipal();
await context.HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
With this approach you don't need to pass any dependencies inside your CustomCookieAuthenticationEvents class at all. HttpContext.RequiredServices is made specifically for such classes (any other can be solved via constructor injection, but not middleware and http context related pipeline, as there is no other otherway to correctly resolve scoped services in middlewares - Middleware instance is static and only instantiated once per request)
This way you won't have lifetime issues with your scoped services.
When you resolve transient services, they will be disposed at the end of request. Whereas transient services resolved via app.ApplicationServices will be resolved at some point in future after the request is finished and when garbage collection triggers (means: your resources will be freed at the earliest possible moment, which is when the request ends).
I am building a fairly small sized mvc4 application. I want to use the simple membership provided as in time I can it being useful for the social stuff.
I want to be able to take advantage of the webapi within the project as I am building an ios application and would like for it to be able to use the same registration/account details. I have added an API area to the site.
I am using ninject for my injection so I have created a repository and have taken the default membership registration component and put it into this.
My Account controller and the apicontroller use the same repository.
public RegisterModel RegisterLocalUser(RegisterModel model)
{
try
{
WebSecurity.CreateUserAndAccount(model.UserName, model.Password,
new
{
Mobile = model.Mobile,
FirstName = model.FirstName,
LastName = model.LastName,
Email = model.Email,
});
Creating a user using the site works fine however when I try to use the api via fiddler to create a user I run into an issue of
{"Message":"An error has occurred.","ExceptionMessage":"You must call the \"WebSecurity.InitializeDatabaseConnection\" method before you call any other method of the \"WebSecurity\" class. This call should be placed in an _AppStart.cshtml file in the root of your site.","ExceptionType":"System.InvalidOperationException","StackTrace":" at WebMatrix.WebData.SimpleMembershipProvider.VerifyInitialized()\r\n at WebMatrix.WebData.WebSecurity.VerifyProvider()\r\n at WebMatrix.WebData.WebSecurity.CreateUserAndAccount(String userName, String password, Object propertyValues, Boolean requireConfirmationToken)\r\n at MySite.Web.Repository.AccountRepository.RegisterLocalUser(RegisterModel model) in c:\#Projects\Site\Site\Site.Consumer.Web\Repository\AccountRepository.cs:line 28\r\n at MySite.Web.Areas.API.Controllers.AccountController.RegisterLocal(RegisterModel data) in c:\#Projects\Site\Site\Site.Consumer.Web\Areas\API\Controllers\AccountController.cs:line 26\r\n at lambda_method(Closure , Object , Object[] )\r\n at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c_DisplayClass13.b_c(Object instance, Object[] methodParameters)\r\n at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments)\r\n at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.<>c_DisplayClass5.b_4()\r\n at System.Threading.Tasks.TaskHelpers.RunSynchronously[TResult](Func`1 func, CancellationToken cancellationToken)"}
Adding a breakpoint within the repository the CreateUserAndAccount is never completed.
I have a custom DependancyResolver needed to allow the api and mvc stuff to work
public class NinjectDependencyScope : IDependencyScope
{
private IResolutionRoot resolver;
internal NinjectDependencyScope(IResolutionRoot resolver)
{
Contract.Assert(resolver != null);
this.resolver = resolver;
}
public void Dispose()
{
IDisposable disposable = resolver as IDisposable;
if (disposable != null)
disposable.Dispose();
resolver = null;
}
public object GetService(Type serviceType)
{
if (resolver == null)
throw new ObjectDisposedException("this", "This scope has already been disposed");
return resolver.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
if (resolver == null)
throw new ObjectDisposedException("this", "This scope has already been disposed");
return resolver.GetAll(serviceType);
}
}
public class NinjectDependencyResolver : NinjectDependencyScope, IDependencyResolver
{
private IKernel kernel;
public NinjectDependencyResolver(IKernel kernel)
: base(kernel)
{
this.kernel = kernel;
}
public IDependencyScope BeginScope()
{
return new NinjectDependencyScope(kernel.BeginBlock());
}
}
Has someone else been able to find a work around to get these components working together?
Thank you
Your error dump suggests you are not using the InitializeSimpleMembershipAttribute for your AccountController.
Either decorate your new account controller with InitializeSimpleMembershipAttribute or move that Websecurity.InitializeDatabaseConnection() to your Global.asax startup code.
Of course, this ignores the dependency issue and I don't have a good answer for that as I don't bother injecting WebSecurity.
WebSecurity is a static class
You'd only need to call InitializeDatabaseConnection once at startup. With the attribute usage it's called each time the controller is accessed.
Which means you must inject an initialized WebSecurity into your repository.
I need to create Unit Tests for an ASP.NET MVC 2.0 web site. The site uses Windows Authentication.
I've been reading up on the necessity to mock the HTTP context for code that deals with the HttpContext. I feel like I'm starting to get a handle on the DI pattern as well. (Give the class an attribute of type IRepository and then pass in a Repository object when you instantiate the controller.)
What I don't understand, however, is the proper way to Mock the Windows Principal object available through User.Identity. Is this part of the HttpContext?
Does any body have a link to an article that demonstrates this (or a recommendation for a book)?
Thanks,
Trey Carroll
I've used IoC to abstract this away with some success. I first defined a class to represent the currently logged in user:
public class CurrentUser
{
public CurrentUser(IIdentity identity)
{
IsAuthenticated = identity.IsAuthenticated;
DisplayName = identity.Name;
var formsIdentity = identity as FormsIdentity;
if (formsIdentity != null)
{
UserID = int.Parse(formsIdentity.Ticket.UserData);
}
}
public string DisplayName { get; private set; }
public bool IsAuthenticated { get; private set; }
public int UserID { get; private set; }
}
It takes an IIdentity in the constructor to set its values. For unit tests, you could add another constructor to allow you bypass the IIdentity dependency.
And then I use Ninject (pick your favorite IoC container, doesn't matter), and created a binding for IIdentity as such:
Bind<IIdentity>().ToMethod(c => HttpContext.Current.User.Identity);
Then, inside of my controller I declare the dependency in the constructor:
CurrentUser _currentUser;
public HomeController(CurrentUser currentUser)
{
_currentUser = currentUser;
}
The IoC container sees that HomeController takes a CurrentUser object, and the CurrentUser constructor takes an IIdentity. It will resolve the dependencies automatically, and voila! Your controller can know who the currently logged on user is. It seems to work pretty well for me with FormsAuthentication. You might be able to adapt this example to Windows Authentication.
I don't know for MVC 2.0, but in newer versions you can mock the ControllerContext:
// create mock principal
var mocks = new MockRepository(MockBehavior.Default);
Mock<IPrincipal> mockPrincipal = mocks.Create<IPrincipal>();
mockPrincipal.SetupGet(p => p.Identity.Name).Returns(userName);
mockPrincipal.Setup(p => p.IsInRole("User")).Returns(true);
// create mock controller context
var mockContext = new Mock<ControllerContext>();
mockContext.SetupGet(p => p.HttpContext.User).Returns(mockPrincipal.Object);
mockContext.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);
// create controller
var controller = new MvcController() { ControllerContext = mock.Object };
see also How to unit-test an MVC controller action which depends on authentification in c#?
Scott Hanselman shows in his blog how to use IPrincipal and ModelBinder to make easier to test the controller by mocking IPrincipal.
Example for mocking username and SID on MVC4.
The username and SID (Windows Authentication) in the following action should be tested:
[Authorize]
public class UserController : Controller
{
public ActionResult Index()
{
// get Username
ViewBag.Username = User.Identity.Name;
// get SID
var lIdentity = HttpContext.User.Identity as WindowsIdentity;
ViewBag.Sid = lIdentity.User.ToString();
return View();
}
}
I use Moq and Visual Studio Test Tools. The test is implemented as follows:
[TestMethod]
public void IndexTest()
{
// Arrange
var myController = new UserController();
var contextMock = new Mock<ControllerContext>();
var httpContextMock = new Mock<HttpContextBase>();
var lWindowsIdentity = new WindowsIdentity("Administrator");
httpContextMock.Setup(x => x.User).Returns(new WindowsPrincipal(lWindowsIdentity));
contextMock.Setup(ctx => ctx.HttpContext).Returns(httpContextMock.Object);
myController.ControllerContext = contextMock.Object;
// Act
var lResult = myController.Index() as ViewResult;
// Assert
Assert.IsTrue(lResult.ViewBag.Username == "Administrator");
Assert.IsTrue(lResult.ViewBag.Sid == "Any SID Pattern");
}
I've changed dev environment global.asax and Web.Config for use FormsAuth for force a specific user. The username uses the same WindowsAuth format. See:
public override void Init()
{
base.Init();
this.PostAuthenticateRequest +=
new EventHandler(MvcApplication_PostAuthenticateRequest);
}
void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e)
{
FormsAuthentication.SetAuthCookie("Domain\\login", true);
}
The Windows or Forms Auth shares the same login patterns.
The application will work with both Windows authentication and Form authentication.
To mock WindowsIdentity you can do the following:
var mockedPrincipal = new Mock<WindowsPrincipal>(WindowsIdentity.GetCurrent());
mockedPrincipal.SetupGet(x => x.Identity.IsAuthenticated).Returns(true);
mockedPrincipal.SetupGet(x => x.Identity.Name).Returns("Domain\\User1");
mockedPrincipal.Setup(x => x.IsInRole("Domain\\Group1")).Returns(true);
mockedPrincipal.Setup(x => x.IsInRole("Domain\\Group2")).Returns(false);
then use mockedPrincipal.Object to get the actual WindowsIdentity