Im working on an ASP.NET core app and I need to implements a chat with SignalR.
Im using Identity with jwt to manage login/permissions and this works fine.
I followed this docs for implementing authentication in SignalR hub but it doesn't work, my hub functions can be reached even with the [Authorize] attribute on the hub.
This is my code for configuring the service
services
.AddAuthorization()
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = jwtSettings.Issuer,
ValidAudience = jwtSettings.Issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret)),
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/chat")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
And this is the code I have for my hub
[Authorize]
public class Chat : Hub<IChatClientMethods>
{
private readonly ProjectHubContext _context;
private readonly UserManager<User> _userManager;
public Chat(ProjectHubContext context, UserManager<User> userManager) : base()
{
_context = context;
_userManager = userManager;
}
// This method is executed even with the [Authorize] attribute
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
Console.WriteLine("Connected");
var user = await _userManager.GetUserAsync(Context.User); //Always null
}
}
And finnaly the hub mapping
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<Chat>("/chat");
});
Why can I call hub functions from client when I provide a random test token ?
I found out why my [Authorize] tag didn't work.
I used a wrong using.
I used using MiNET.Plugins.Attributes;
instead of using Microsoft.AspNetCore.Authorization;
I am building my own package in order to abstract the user from some stuffs.
My original Startup.cs looked like
services.AddAuthentication(options =>
{
// Store the session to cookies
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// OpenId authentication
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(o =>
{
// URL of the Keycloak server
o.Authority = Configuration["Jwt:Authority"];
// Client configured in the Keycloak
o.ClientId = Configuration["Jwt:ClientId"];
...
}
I want to extract those Add* methods in my own package like this:
public static class MyExtension
{
public static IServiceCollection AddMyAuthentication(this IServiceCollection services)
{
// Add ASP.NET Core Options libraries - needed so we can use IOptions<SVOSOptions>
services.AddOptions();
services.AddAuthentication(options =>
{
// Store the session to cookies
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// OpenId authentication
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(o =>
{
// URL of the Keycloak server
o.Authority = * MyOptions.Authority *;
// Client configured in the Keycloak
o.ClientId = * MyOptions.ClientId *
...
});
return services;
}
public static IServiceCollection AddMyAuthentication(this IServiceCollection services, Action<MyOptions> configure)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configure != null)
{
services.Configure(configure);
}
return services.AddMyAuthentication();
}
}
Of course I want to replace all the Configuration[] calls within my class with some options which I want to send from my new Startup.cs like this:
services.AddMyAuthentication(options => {
options.Authority = Configuration["Jwt:Authority"];
options.ClientId = Configuration["Jwt:ClientId"];
})
I'm also doing this because I want to hardcode that the user doesn't need to know. I hope that this is the right way to do something like this.
If you're just using the resulting settings during the service registration and you don't have a need to register those options into the service provider for later injection, you can probably just pass them as a strongly typed POCO into your static extension, which will add some clarity as to the required incoming parameters.
public class MyAuthenticationOptions
{
public string Authority { get; set; }
public string ClientId { get; set; }
}
and then use Microsoft.Extensions.Configuration.Binder to bind to an instance:
// Replace the quoted section with your config section
services.AddMyAuthentication(Configuration.GetSection("MyAuthenticationOptions").Get<MyAuthenticationOptions>();
And finally update your extension method to accept the object:
public static IServiceCollectionAddMyAuthentication(this IServiceCollection services, MyAuthenticationOptions options); //...
I would like to update the DB after a user logged in to my app (using fb) and I am not sure how to use the DbContext within startup.cs.
startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<mysiteContext>(options =>
options.UseSqlServer(_configurationRoot.GetConnectionString("DefaultConnection")));
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddFacebook(options =>
{
options.AppId = "********";
options.AppSecret = "*********";
options.Events.OnCreatingTicket = context =>
{
var userFbId = context.User.Value<string>("id");
string userProfileImageUrl = $"https://graph.facebook.com/{userFbId}/picture?type=large";
//TODO: Save to DB infromation about the user and update last login date.
//This is where I am having the issue.
UserRepository userRepo = new UserRepository();
//Example how to add information to the claim.
var surname = context.User.Value<string>("last_name");
context.Identity.AddClaim(new Claim(ClaimTypes.Surname, surname));
return Task.FromResult(0);
};
})
.AddCookie();
And my UserRepository.cs:
public class UserRepository
{
private readonly mysiteContext _myDbContext;
private readonly short _languageTypeId;
public UserRepository(mysiteContext ctx)
{
_myDbContext = ctx;
_languageTypeId = Language.GetLanguageTypeId();
}
}
How can I pass mysiteContext to the UserRepository class?
You can do as follows:
services.AddScoped<UserRepository>(); // <-- Register UserRepository here
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddFacebook(options =>
{
options.AppId = "********";
options.AppSecret = "*********";
options.Events.OnCreatingTicket = context =>
{
........
ServiceProvider serviceProvider = services.BuildServiceProvider();
var userRepository = serviceProvider.GetService<UserRepository>();
// Do whatever you want to do with userRepository here.
.........
return Task.FromResult(0);
};
})
Alternatively you can also get UserRepository instance from context as follows:
var userRepository = context.HttpContext.RequestServices.GetService<UserRepository>();
I had to use my Dapper service inside 'ConfigureServices' section, where 'context' is not available. The easiest way for me was to get the service via 'BuildServiceProvider':
IDapper _dapper = services.BuildServiceProvider().GetService<IDapper>();
I'm trying to build a custom AuthenticationHandler in ASP.Net Core 2. Following up topic like ASP.NET Core 2.0 authentication middleware and Why is Asp.Net Core Authentication Scheme mandatory, I've created the specific classes. The registering happens like this:
services.AddAuthentication(
options =>
{
options.DefaultScheme = Constants.NoOpSchema;
options.DefaultAuthenticateScheme = Constants.NoOpSchema;
options.DefaultChallengeScheme = Constants.NoOpSchema;
options.DefaultSignInScheme = Constants.NoOpSchema;
options.DefaultSignOutScheme = Constants.NoOpSchema;
options.DefaultForbidScheme = Constants.NoOpSchema;
}
).AddScheme<CustomAuthOptions, CustomAuthHandler>(Constants.NoOpSchema, "Custom Auth", o => { });
Everything works, if the specific controllers set the Scheme explicitely:
[Authorize(AuthenticationSchemes= Constants.NoOpSchema)]
[Route("api/[controller]")]
public class IndividualsController : Controller
But I would like to not have to set the Schema, since it should get added dynamically. As soon as I remove the Scheme Property, like this:
[Authorize]
[Route("api/[controller]")]
public class IndividualsController : Controller
It doesn't work anymore.
I would have hoped, that setting the DefaultScheme Properties does this job. Interesting enough, I didn't find any specific discussion about this topic.
Am I doing something wrong here or is my expected outcome wrong?
Edit: Thanks for the questions, it helped me a lot. It seems like mapping the DefaultScheme is using by the Authentication Middleware, which I only used, when the CustomAuthHandler was not in place. Therefore I had to add the AuthenticationMiddleware always.
Edit2: Unfortunately, it still doesn't work. To enhance a bit my question: I'm adding the middleware as usual:
app.UseAuthentication();
app.UseMvc();
Now I get into my Handler, which is looking like this:
public class NoOpAuthHandler : AuthenticationHandler<NoOpAuthOptions>
{
public const string NoOpSchema = "NoOp";
public NoOpAuthHandler(IOptionsMonitor<NoOpAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync() => Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(Context.User, NoOpSchema)));
}
But even if I always return success, I get an 401. I think I have to dig deeper and kindahow set some Claims, but unfortunately, the Handlers from Microsoft are quite hard to analyze, since they contain a lot of Code.
ASP.NET Core 2 Answer
You have to set a default authorization policy tied to your authentication scheme:
services.AddAuthorization(options => {
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(Constants.NoOpSchema)
.RequireAuthenticatedUser()
.Build();
});
ASP.NET Core 3 Answer
In ASP.NET Core 3 things apparently changed a bit, so you would want to create an extension method to add your authentication handler:
public static class NoOpAuthExtensions
{
public static AuthenticationBuilder AddNoOpAuth(this AuthenticationBuilder builder)
=> builder.AddNoOpAuth(NoOpAuthHandler.NoOpSchema, _ => { });
public static AuthenticationBuilder AddNoOpAuth(this AuthenticationBuilder builder, Action<NoOpAuthOptions> configureOptions)
=> builder.AddNoOpAuth(NoOpAuthHandler.NoOpSchema, configureOptions);
public static AuthenticationBuilder AddNoOpAuth(this AuthenticationBuilder builder, string authenticationScheme, Action<NoOpAuthOptions> configureOptions)
=> builder.AddNoOpAuth(authenticationScheme, null, configureOptions);
public static AuthenticationBuilder AddNoOpAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<NoOpAuthOptions> configureOptions)
{
return builder.AddScheme<NoOpAuthOptions, NoOpAuthHandler>(authenticationScheme, displayName, configureOptions);
}
}
And use it in your ConfigureServices method like this:
services
.AddAuthentication(NoOpAuthHandler.NoOpSchema)
.AddNoOpAuth();
Make sure that you have Authentication middleware in your pipeline and place it before MVC.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
///
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
///
}
UPDATE
try using this code in HandleAuthenticateAsync method.
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
List<Claim> claims = new List<Claim>();
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, Scheme.Name);
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
AuthenticationTicket authenticationTicket = new AuthenticationTicket(claimsPrincipal, Scheme.Name);
return AuthenticateResult.Success(authenticationTicket);
}
I suffer this for hours, in my case problem was default AuthenticationMiddleware class.
More specifically;
If you set ClaimsPrincipal within custom middleware in your request pipeline like below;
HttpContext.context.User = new ClaimsPrincipal(identity);
this will override your default auth config settings,
What I did to solve; remove custom middleware and add app.UseAuthentication(); in Configure section in Startup.cs, and Authorize attribute checks whatever set in config section as default;
Here is mine;
services.AddAuthentication(x =>
{
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateLifetime = true,
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
};
});
in .NET Core 3.1, I had to manually add [Authorize(AuthenticationSchemes = "Bearer")] instead of a plain [Authorize] to endpoints in order for authentication to function as I'd expect. The following configuration change solved the issue:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Bearer";
})
try this.
services.AddAuthentication(
options =>
{
options.DefaultAuthenticateScheme = "Cookie"
options.DefaultChallengeScheme = Constants.NoOpSchema;
options.DefaultSignInScheme = "Cookie";
}
)
.AddCookie("Cookie")
.AddScheme<CustomAuthOptions, CustomAuthHandler>(Constants.NoOpSchema, "Custom Auth", o => { });
I may get claims using DI in asp.core1, as in the below
public class UserService : IUserService
{
private IHttpContextAccessor _httpContext;
public UserService(IHttpContextAccessor httpContext)
{
_httpContext = httpContext;
}
}
In case with In asp.core 2, _httpContext.User.Identity.IsAuthenticated has the value false, and doesn't contain claims.
My startup.cs looks like this:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
// ...
};
});
services.AddCors();
services.AddMvc();
services.AddTransient<IUserService, UserService>();
services.AddTransient<IHttpContextAccessor, HttpContextAccessor();
}