I am using the github example active-directory-b2c-dotnet-webapp-and-webapi.
I configured my own b2c tenant and got the MVC sample to work successfully.
However, I need this to work with a WebForms app. I created a new Webforms app, checked that the references were pointing to the same versions of the dlls as the working sample and adjusted the code to work in WebForms.
Everything works up until the time that I issue the Authentication.Challenge and process the AuthorizationCodeReceivedNotification in the OnAuthorizationCodeReceived function.
The Session object in notification.OwinContext.Environment["System.Web.HttpContextBase"] is null whereas at this point in the MVC sample the session object in notification.OwinContext.Environment["System.Web.HttpContextBase"].Session is a System.Web.HttpSessionStateWrapper with a valid SessionId.
In WebForms, HttpContext.GetOwinContext throws an error as not being available, so I then tried HttpContext.Current.GetOwinContext which returned an HttpContext object instead of a HttpContextBase object. So I finally used
if (!Request.IsAuthenticated)
{
HttpContextBase context = new HttpContextWrapper(HttpContext.Current);
context.GetOwinContext().Authentication.Challenge();
return;
}
instead of the following which is used in the MVC sample
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge();
return;
}
At this point in the code, in both MVC and WebForms the Authentication object has a valid Session object in its OwinContext.Environment prior to the Challenge being issued. The problem is that whilst in the MVC version the resulting notification includes the session object, the WebForms version of the notification has a null object.
The problem finally surfaces when getting the TokenCache by calling the MSALSessionCache which uses the httpContext.Session object (MVC works; Webforms throws Null reference exception)
I am aware that MVC and Webforms treat the session differently but cannot work out how to solve this problem.
EDIT
Whilst this worked it did not address the problem regarding the session id. I added
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = AuthenticationFailed,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
MessageReceived = OnMessageReceived,
SecurityTokenReceived=OnSecurityTokenReceived,
SecurityTokenValidated= OnSecurityTokenValidated
},
to your code with the appropriate stubs for these functions. MessageReceived was invoked as was SecurityTokenReceived and Validated. However, AuthorizationCodeReceived was never invoked. When the first three were invoked, I examined the notification.OwinContext.Environment["System.Web.HttpContextBase"] and in all cases the Session object is null - which is what my problem was to start with.
EDIT (includes answer)
Thanks to #Ramakrishna the solution is to add A RequireAspNetSession helper function at the beginning of ConfigureAuth. The revised code snippet for his sample is as follows:
public void ConfigureAuth(IAppBuilder app)
{
RequireAspNetSession(app);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// Configure OpenID Connect middleware for each policy
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignUpPolicyId));
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(ProfilePolicyId));
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignInPolicyId));
}
public static void RequireAspNetSession(IAppBuilder app)
{
app.Use((context, next) =>
{
var httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
httpContext.SetSessionStateBehavior(SessionStateBehavior.Required);
return next();
});
// To make sure the above `Use` is in the correct position:
app.UseStageMarker(PipelineStage.MapHandler);
}
you will need to add the following references:
using System.Web.SessionState;
using Microsoft.Owin.Extensions;
if (!Request.IsAuthenticated)
{
HttpContext.Current.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" }, Startup.SignUpPolicyId);
}
Try this in webforms.
EDIT
Sample Application hosted at https://github.com/Zen3InfoSolutions/B2CSamples/tree/master/B2C-WebForms
Zero configuration required to run and verify the policies.
Related
Background
I've been following the documentation for using IdentityServer4 with single-page-applications on ASP.NET-Core 3.1 and as such created a project via the dotnet new react -au Individual command.
This creates a project which uses the Microsoft.AspNetCore.ApiAuthorization.IdentityServer NuGet package.
So far it's been really great and it got token-based authentication for my ReactJS application working without any pain!
From my ReactJS application, I can access the user information populated by the oidc-client npm package such as the username.
Also, calls to my Web APIs with the [Authorize] attribute work as expected: only calls with a valid JWT access token in the request header have access to the API.
Problem
I'm now trying to access basic user information (specifically username) from within a GraphQL mutation resolver via an injected IHttpContextAccessor but the only user information I can find are the following claims under IHttpContextAccessor.HttpContext.User:
nbf: 1600012246
exp: 1600015846
iss: https://localhost:44348
aud: MySite.HostAPI
client_id: MySite
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: (actual user GUID here)
auth_time: 1600012235
http://schemas.microsoft.com/identity/claims/identityprovider: local
scope: openid
scope: profile
scope: MySite.HostAPI
http://schemas.microsoft.com/claims/authnmethodsreferences: pwd
The same issue happens for Web API controllers as well.
Details
MySite is the namespace of my solution and is also what I have defined as a client in my appsettings.json file:
{
"IdentityServer": {
"Clients": {
"MySite": {
"Profile": "IdentityServerSPA"
}
}
}
}
My web application project's name is MySite.Host so MySite.HostAPI the name of the resource and scope that are automatically generated by calling AuthenticationBuilder.AddIdentityServerJwt().
... this method registers an <<ApplicationName>>API API resource with IdentityServer with a default scope of <<ApplicationName>>API and configures the JWT Bearer token middleware to validate tokens issued by IdentityServer for the app.
Research
According to a few answers on Stack Overflow, adding IdentityResources.Profile() resource via IIdentityServerBuilder.AddInMemoryIdentityResources() should do the trick but it looks like it's already available via the claims I posted above (scope: profile).
I nevertheless tried it but the result is that the authentication flow becomes broken: the redirect to the login page does not work.
All of the answers I've found also make a reference to a Config class like in this demo file which holds configurations that are mainly fed to IIdentityServerBuild.AddInMemory...() methods.
However, it seems that Microsoft.AspNetCore.ApiAuthorization.IdentityServer does most of this in its implementation and instead offers extendable builders to use.
From the IdentityServer documentation, I don't believe I need to add a Client because the access token already exists. The client ReactJS application uses the access_token from oidc-client to make authorised calls to my Web APIs.
It also doesn't appear like I need to add a Resource or Scope for the username information because I believe these already exist and are named profile. More to this point is that the documentation for "IdentityServerSPA" client profile states that:
The set of scopes includes the openid, profile, and every scope defined for the APIs in the app.
I also looked at implementing IProfileService because according to the documentation this is where additional claims are populated. The default implementation is currently being used to populate the claims that are being requested by the ProfileDataRequestContext.RequestedClaimTypes object and this mechanism already works because this is how the ReactJS client code receives them. This means that when I'm trying to get the user claims from ASP.NET-Core Identity, it's not properly populating ProfileDataRequestContext.RequestedClaimTypes or perhaps not even calling IProfileServices.GetProfileDataAsync at all.
Question
Considering that my project uses Microsoft.AspNetCore.ApiAuthorization.IdentityServer, how can I view the username from my ASP.NET-Core C# code, preferably with IHttpContextAccessor?
What you need to do is to extend the default claims requested by IdentityServer with your custom ones. Unfortunately, since you're using the minimalistic IdentityServer implementation by Microsoft, the correct way of making the client request the claims isn't easy to find. However, assuming you have only one application (as per the template), you could say that the client always wants some custom claims.
Very important first step:
Given your custom IProfileService called, say, CustomProfileService, after these lines:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
you have to get rid of the implementation used in the scaffolded template, and use your own:
services.RemoveAll<IProfileService>();
services.AddScoped<IProfileService, CustomProfileService>();
Next, the actual implementation of the custom IProfileService isn't really hard if you start from Microsoft's version:
public class CustomProfileService : IdentityServer4.AspNetIdentity.ProfileService<ApplicationUser>
{
public CustomProfileService(UserManager<ApplicationUser> userManager,
IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory) : base(userManager, claimsFactory)
{
}
public CustomProfileService(UserManager<ApplicationUser> userManager,
IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory,
ILogger<ProfileService<ApplicationUser>> logger) : base(userManager, claimsFactory, logger)
{
}
public override async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
string sub = context.Subject?.GetSubjectId();
if (sub == null)
{
throw new Exception("No sub claim present");
}
var user = await UserManager.FindByIdAsync(sub);
if (user == null)
{
Logger?.LogWarning("No user found matching subject Id: {0}", sub);
return;
}
var claimsPrincipal = await ClaimsFactory.CreateAsync(user);
if (claimsPrincipal == null)
{
throw new Exception("ClaimsFactory failed to create a principal");
}
context.AddRequestedClaims(claimsPrincipal.Claims);
}
}
With those two steps in place, you can start tweaking CustomProfileService's GetProfileDataAsync according to your needs. Notice that ASP.NET Core Identity by default already has the email and the username (you can see these in the claimsPrincipal variable) claims, so it's a matter of "requesting" them:
// ....
// also notice that the default client in the template does not request any claim type,
// so you could just override if you want
context.RequestedClaimTypes = context.RequestedClaimTypes.Union(new[] { "email" }).ToList();
context.AddRequestedClaims(claimsPrincipal.Claims);
And if you want to add custom data, for example, the users first and last name:
// ....
context.RequestedClaimTypes = context.RequestedClaimTypes.Union(new[] { "first_name", "last_name" }).ToList();
context.AddRequestedClaims(claimsPrincipal.Claims);
context.AddRequestedClaims(new[]
{
new Claim("first_name", user.FirstName),
new Claim("last_name", user.LastName),
});
User information can be retrieved via the scoped UserManager<ApplicationUser> service which is set up by the project template. The users's claims contains "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" (ClaimTypes.NameIdentifier) whose value is the user identifier. UserManager<>.FindByIdAsync() can then be used to retrieve the ApplicationUser associated with the user and which contains additional user information.
Note that this contacts the user store each time it's invoked. A better solution would be to have the extra user information in the claims.
First, explicitly add the IHttpContextAccessor service if you haven't already by calling services.AddHttpContextAccessor();
From within an arbitrary singleton service:
public class MyService
{
public MyService(
IHttpContextAccessor httpContextAccessor,
IServiceProvider serviceProvider
)
{
var nameIdentifier = httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
using (var scope = serviceProvider.CreateScope())
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var user = await userManager.FindByIdAsync(nameIdentifier);
// Can access user.UserName.
}
}
}
UserManager<ApplicationUser> can be accessed directly within Razor pages and Controllers because these are already scoped.
I am using hangfire and for authorization purposes, I am trying to access session value in Authorize([NotNull] DashboardContext context) method which is null. Please note that this problem has encountered after I have incorporated SignalR in my application.
If I remove the "app.MapSignalR();" line from startup.cs, I am able to access session successfully
public void Configuration(IAppBuilder app)
{
...
app.MapSignalR(); //if I remove this line, session is accessible
...
}
public class MyRestrictiveAuthorizationFilter : IDashboardAuthorizationFilter{
public bool Authorize([NotNull] DashboardContext context)
{
HttpSessionStateBase session = ((System.Web.HttpContextWrapper)(owinEnvironment["System.Web.HttpContextBase"])).Session; //Which is null
session = HttpContext.Current.Session; //Also Null
}
}
Please note that session value is null after I introduced " app.MapSignalR()" in Configuration method in startup.cs
According to ASP.Net Core Docs, getting null value of session is normal behaviour.
Session isn't supported in SignalR apps because a SignalR Hub may execute independent of an HTTP context. For example, this can occur when a long polling request is held open by a hub beyond the lifetime of the request's HTTP context.
Also
SignalR and session state
SignalR apps should not use session state to store information. SignalR apps can store per connection state in Context.Items in the hub.
Docs
I'm trying to extend IPrincipal in .NET Core with Windows Authentication.
In a previous project (using .NET Framework 4.6.1) on Application_Start() I added the code below to extend IPrincipal:
protected void WindowsAuthentication_OnAuthenticate(object sender, WindowsAuthenticationEventArgs e)
{
if (e.Identity != null && e.Identity.IsAuthenticated)
{
NgUser opPrincipal = CustomStaticMethod.GetUserAD();
HttpContext.Current.User = opPrincipal;
}
}
And this is my custom class
public class NgUser : IPrincipal
{
// code removed for abbr ...
}
Then every time in a controller casting HttpContext.Current.User as CustomPrincipal I'd have access to the custom properties without having to use claims or using static extensions or storing an object in session.
Now in .NET Core I've seen you can customize claims transformation and I've also read this and they basically extended IPrincipal.
I'd prefer to extend IPrincipal with my custom class, register it in Startup.cs and be able to access it in my controllers.
Surely this is doable, question is how?
I hope this is clear and some one can help me.
Many thanks
It's very doable. The easiest way is to add a piece of middleware, which behaves similar to OnAuthenticate, to the Configure() pipeline in .NET Core. Here you'll swap out the IPrincipal. Note, this only works if the web app is set to run with Windows Authentication only in IIS/IIS Express. Anonymous authentication adds extra overhead.
If you have this simple Win auth setup, in your Startup.cs put this near the top of your Configure(IApplicationBuilder app) method:
// Because IIS automatically captures the user login, by the time the app is touched
// in any request, the context (ctx) User is already present.
app.Use(async (ctx, next) =>
{
if (ctx.User?.Identity?.IsAuthenticated == true)
{
NgUser opPrincipal = CustomStaticMethod.GetUserAD();
ctx.User = opPrincipal;
}
// be sure to continue the rest of the pipeline!
await next.Invoke();
});
```
In an OWIN MVC application, I am trying to preserve the BootstrapContext.SecurityToken between requests to allow for creating an ActAs token for federation.
By default the Cookie serialization tries to preserve the BootstrapContext.Token (string form) and ignores the BootstrapContext.SecurityToken (decoded token), this sort of makes sense, avoiding the exposure of the decoded token?
I have therefore tried to use the CookieAuthenticationOptions.SessionStore to preserve the whole BootstrapContext server side. This (sort of) works but with the problem as described below!
Background:
I have lifted the AspNetAuthSessionStore implementation from the sandbox code in the Katana source repository [http://katanaproject.codeplex.com/] which functions as expected saving and restoring the ticket to the HttpContext.Session.
I also lifted the following extension code:
public static class AspNetSessionExtensions {
public static IAppBuilder RequireAspNetSession(this IAppBuilder app) {
app.Use((context, next) => {
// Depending on the handler the request gets mapped to, session might not be enabled. Force it on.
HttpContextBase httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
httpContext.SetSessionStateBehavior(SessionStateBehavior.Required);
return next();
});
// SetSessionStateBehavior must be called before AcquireState
app.UseStageMarker(PipelineStage.MapHandler);
return app;
}
public static IAppBuilder UseAspNetAuthSession(this IAppBuilder app) {
return app.UseAspNetAuthSession(new CookieAuthenticationOptions());
}
public static IAppBuilder UseAspNetAuthSession(this IAppBuilder app, CookieAuthenticationOptions options) {
app.RequireAspNetSession();
options.SessionStore = new AspNetAuthSessionStore();
app.UseCookieAuthentication(options, PipelineStage.PreHandlerExecute);
return app;
}
}
and use the registration functions in my OWIN Startup.
Problem:
In my MVC 4 target action I check the following:
var ctxtuser = HttpContext.User as ClaimsPrincipal;
var claimsPrincipal = Thread.CurrentPrincipal as ClaimsPrincipal;
With the straight cookie implementation (no session store):
app.UseCookieAuthentication(new CookieAuthenticationOptions() {});
the two principals are the same and are the expected value (but without the BootstrapContext content!).
If I simply try to add the Session store to this implementation:
app.UseCookieAuthentication(new CookieAuthenticationOptions() {
SessionStore = new AspNetAuthSessionStore()
});
it fails due to there being no session to store into! Presumably because we aren't allowed a session at Authenticate stage.
If I use the registration from the extension above:
app.UseAspNetAuthSession(new CookieAuthenticationOptions() {
});
version then the code functions almost as expected except that the Thread.CurrentPrincipal is not equal to the HttpContext.User by the time it hits my action. The context user is my claims principle but the Thread.CurrentPrincipal is set to an annonymous WindowsPrincipal.
I have debugged through the code and the claims principal is being applied to both the thread and the context in:
Microsoft.Owin.Host.SystemWeb.OwinCallContext.SetServerUser
But somehow the Thread version gets set back to the annonymous Windows user before hitting my action??
I assume this has something to do with the specific pipeling stage setting in
app.UseCookieAuthentication(options, PipelineStage.PreHandlerExecute);
as opposed to the default value (PipelineStage.Authenticate) used when I request
app.UseCookieAuthentication(new CookieAuthenticationOptions()...
I am wondering if the default code setting the principal at the Authenticate stage makes it 'Stick' as the one ASP.Net wants to use, waiting until PreHandlerExecute is too late?
But can't see how to fix this.
What am I doing wrong?
I have problem with creating a session in application hosted using Owin. I have tried using RedisSession, but I didn't know how to configure it so it gave me an error.
I was looking for a solution for some time, tried different things and finally decided to ask here for help.
Scenario:
I'm logging in the application using HTTP POST request,
User login and password should be stored in session,
For each next GET/POST request which need previous login session is
empty (login and password are null).
Object HTTPContext is empty.
I'm using Ninject for dependency injections.
I tried something like that: Can OWIN middleware use the http session?
Does anybody have an idea how to store login data in Owin session?
Below is Owin configuration file, included in it things are from link posted above.
[assembly: OwinStartup(typeof(Service.Startup))]
namespace Service
{
public class Startup
{
public void Configuration(IAppBuilder appBuilder)
{
var config = new HttpConfiguration();
config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{action}/{id}", new { id = RouteParameter.Optional }
);
appBuilder.RequireAspNetSession();
appBuilder.UseNinjectMiddleware(CreateKernel).UseNinjectWebApi(config);
}
public static StandardKernel CreateKernel()
{
var kernel = new StandardKernel(new Module());
return kernel;
}
}
public static class AspNetSessionExtensions
{
public static IAppBuilder RequireAspNetSession(this IAppBuilder app)
{
app.Use((context, next) =>
{
// Depending on the handler the request gets mapped to, session might not be enabled. Force it on.
HttpContextBase httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
httpContext.SetSessionStateBehavior(SessionStateBehavior.Required);
return next();
});
// SetSessionStateBehavior must be called before AcquireState
app.UseStageMarker(PipelineStage.MapHandler);
return app;
}
}
}
I've had some struggling with sessions as well.
Here is the solution, which works for me:
1) Add NuGet Microsoft.AspNetCore.Session
2) Call .AddSession on your IServiceCollection. Note, it might need configuration.
In my case it is:
3) Use your session. Keep in mind, that if there are no values set to a session, on each request SessionID is different.
So you'd have to add some value to a session. This is how it would stay the same across multiple requests.
And here is my session pinning middleware:
Hope it helps.
For anyone wanting to do this with ASP.net Framework, instead of Core, read on.
First, make sure Owin.Extensions is installed.
Next, add this code to your Owin Startup.cs before the middleware that you want to use session.
app.Use((context, next) =>
{
var httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
httpContext.SetSessionStateBehavior(SessionStateBehavior.Required);
return next();
}).UseStageMarker(PipelineStage.MapHandler);
By default, OWIN Middleware run at the last event (PipelineStage.PreHandlerExecute) which is too late for accessing session state.
.UseStageMarker tells OWIN where in the execution pipeline to execute the function passed to .Use().
To use sessions, you need to execute middleware that runs after the session has been initialized by the ASP.net runtime. This middleware must be run in the PostAquireState phase, like so:
.Use((context, next) =>
{
HttpContext.Current.Session["mykey"] = "myvalue";
return next();
}).UseStageMarker(PipelineStage.PostAcquireState);