I am trying to authenticate the user in my WASM Blazor app using google's OIDC.
I have managed to retrieve the token by following this article: https://learn.microsoft.com/en-gb/aspnet/core/blazor/security/webassembly/standalone-with-authentication-library?view=aspnetcore-3.1&tabs=visual-studio
I am trying to retrieve the AccessToken to pass it to the SignalR hub using the injected instance of IAccessTokenProvider when building an instance of HubConnection:
public RemoteCombatListener(ITokenCache tokenCache)
{
_connection = new HubConnectionBuilder()
.WithUrl("https://localhost:44364/combat", opts => {
opts.AccessTokenProvider = tokenCache.GetToken;
})
.Build();
}
Here is the implementation of my TokenCache:
public class TokenCache : ITokenCache
{
private readonly IAccessTokenProvider _tokenProvider;
private readonly NavigationManager _navManager;
public string CachedToken { get; private set; }
public TokenCache(IAccessTokenProvider tokenProvider, NavigationManager navManager)
{
_tokenProvider = tokenProvider;
_navManager = navManager;
}
public async Task<string> GetToken()
{
if (string.IsNullOrEmpty(CachedToken))
{
var requestedToken = await _tokenProvider.RequestAccessToken();
if (requestedToken.TryGetToken(out var accessToken))
{
CachedToken = accessToken.Value;
}
else
{
throw new AccessTokenNotAvailableException(_navManager, requestedToken, Enumerable.Empty<string>());
}
}
return CachedToken;
}
}
The problem I am facing right now is that when calling the _tokenProvider.RequestAccessToken() method, I get the following exception:
An exception occurred executing JS interop: The JSON value could not be converted to System.DateTimeOffset. Path: $.token.expires | LineNumber: 0 | BytePositionInLine: 80.. See InnerException for more details.
I am unable to figure out what is wrong with my setup as debugging stopped working for me randomly and the only option I have is Console.Log debugging.
It turns out that default configuration for the Oidc doesn't request access_token, only id_token. Had to add the following:
builder.Services.AddOidcAuthentication(options => {
// Rest of configs ...
options.ProviderOptions.ResponseType = "id_token token";
});
Related
I have a Blazor server project with some API controllers in same project.
In my Program.cs I have this code :
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
..
app.UseAuthentication();
app.UseAuthorization();
When I call my API from my blazor component I got 404 not found response.
If I comment out app.UseAuthentication() and app.UseAuthorization my component can call my API and it works.
I'm a newbie on auth and API and don't know where to start.
My API has no [Auth] tags in it. I can reach the API with Swagger without problems.
My code in component (it works without "UseAuth" but not when it's activate):
string filnamn = WebUtility.UrlEncode(fil.Namn);
string reqUri = $"delete/{filnamn}";
Http.BaseAddress = new Uri("https://localhost:7285/");
Http.DefaultRequestHeaders.Accept.Clear();
HttpResponseMessage response = await Http.DeleteAsync(reqUri);
My API controller :
[ApiController]
public class UploadController : ControllerBase
{
private readonly string grundPath = #"G:\Testfolder";
private readonly string ulPath = "Upload";
[HttpDelete("delete/{filename}")]
public IActionResult Delete(string filename)
{
try
{
var filePath = Path.Combine(grundPath, ulPath, filename);
if (System.IO.File.Exists(filePath))
{
System.IO.File.Delete(filePath);
return StatusCode(200);
}
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
return StatusCode(500);
}
Do you see some obvious wrong/missing part I do or could give me some direction on what I should google for?
I've got a working EFCore, .NET5, Blazor WASM application.
I call await host.MigrateDatabase(); in my Program.Main() to have my database always up-to-date.
public static async Task<IHost> MigrateDatabase(this IHost host)
{
using var scope = host.Services.CreateScope();
try
{
// Get the needed context factory using DI:
var contextFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();
// Create the context from the factory:
await using var context = contextFactory.CreateDbContext();
// Migrate the database:
await context.Database.MigrateAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
return host;
}
In my AppDbContext I've overridden SaveChangesAsync() to add and update CreatedOn en UpdatedOn.
I mentioned this in DbContext.SaveChanges overrides behaves unexpected before.
I also want to fill CreatedBy and UpdatedBy with the userId.
I have an IdentityOptions class to hold the user data:
public class IdentityOptions
{
public string UserId => User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
public ClaimsPrincipal User { get; set; }
}
I've registered this class in StartUp like this:
services.AddScoped(sp =>
{
var context = sp.GetService<IHttpContextAccessor>()?.HttpContext;
var identityOptions = new IdentityOptions();
if (context?.User.Identity != null && context.User.Identity.IsAuthenticated)
{
identityOptions.User = context.User;
}
return identityOptions;
});
I inject this IdentityOptions class into several other services, without any problem.
But when I inject it in my AppDbContext:
public AppDbContext(DbContextOptions<AppDbContext> options, IdentityOptions identityOptions)
: base(options)
{
...
}
I get an error in MigrateDatabase():
"Cannot resolve scoped service 'IdentityOptions' from root provider."
I've been trying numerous options I found googling but can't find a solution that works for me.
Please advice.
Update:
services.AddDbContextFactory<AppDbContext>(
options => options.UseSqlServer(Configuration.GetConnectionString("DbConnection"),
b => b.MigrationsAssembly("DataAccess"))
#if DEBUG
.LogTo(Console.WriteLine, new [] {RelationalEventId.CommandExecuted})
.EnableSensitiveDataLogging()
#endif
);
Thanks to the great help of #IvanStoev (again), I found the answer.
Adding lifetime: ServiceLifetime.Scoped to AddDbContextFactory in Startup solved my problem.
Now I can use my IdentityOptions class in SaveChanges and automatically update my Created* and Updated* properties.
I did a lot of Razor pages the past year, and a couple of weeks ago I started to transform all to a ViewModel for my Blazor Server App.
Now I thought it's time to make a new Blazor WebAssembly App.
But I struggle to build a POC with a ViewModel, based on the WeatherForecast example.
But whatever I do, I have errors. And so far I did not find a a good basic example.
Unhandled exception rendering component: Unable to resolve service for type 'fm2.Client.Models.IFetchDataModel' while attempting to activate 'fm2.Client.ViewModels.FetchDataViewModel'.
System.InvalidOperationException: Unable to resolve service for type 'fm2.Client.Models.IFetchDataModel' while attempting to activate 'fm2.Client.ViewModels.FetchDataViewModel'.
Example: https://github.com/rmoergeli/fm2
namespace fm2.Client.ViewModels
{
public interface IFetchDataViewModel
{
WeatherForecast[] WeatherForecasts { get; set; }
Task RetrieveForecastsAsync();
Task OnInitializedAsync();
}
public class FetchDataViewModel : IFetchDataViewModel
{
private WeatherForecast[] _weatherForecasts;
private IFetchDataModel _fetchDataModel;
public WeatherForecast[] WeatherForecasts
{
get => _weatherForecasts;
set => _weatherForecasts = value;
}
public FetchDataViewModel(IFetchDataModel fetchDataModel)
{
Console.WriteLine("FetchDataViewModel Constructor Executing");
_fetchDataModel = fetchDataModel;
}
public async Task RetrieveForecastsAsync()
{
_weatherForecasts = await _fetchDataModel.RetrieveForecastsAsync();
Console.WriteLine("FetchDataViewModel Forecasts Retrieved");
}
public async Task OnInitializedAsync()
{
_weatherForecasts = await _fetchDataModel.RetrieveForecastsAsync();
}
}
}
namespace fm2.Client
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<IFetchDataViewModel, FetchDataViewModel>();
await builder.Build().RunAsync();
}
}
}
Additional note:
Here how I did it previously for Blazor Server App: https://github.com/rmoergeli/fm2_server
Here I try the same for the Blazor WebAssembly App:
https://github.com/rmoergeli/fm2_wasm (Constructor is not initialized).
This POC is different comapred to the first link at the top. Here I tried to just do the same like I did for the Blazor Server App.
I pulled the latest code from Github. It looks like the wrong api was getting called.
When I changed from this:
WeatherForecast[] _weatherForecast = await _http.GetFromJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
to this:
WeatherForecast[] _weatherForecast = await _http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
in WeatherViewModel.cs
I could get the weather data to be displayed.
Question
How can I implement Basic Authentication with Custom Membership in an ASP.NET Core web application?
Notes
In MVC 5 I was using the instructions in this article which requires adding a module in the WebConfig.
I am still deploying my new MVC Coreapplication on IIS but this approach seems not working.
I also do not want to use the IIS built in support for Basic authentication, since it uses Windows credentials.
ASP.NET Security will not include Basic Authentication middleware due to its potential insecurity and performance problems.
If you require Basic Authentication middleware for testing purposes, then please look at https://github.com/blowdart/idunno.Authentication
ASP.NET Core 2.0 introduced breaking changes to Authentication and Identity.
On 1.x auth providers were configured via Middleware (as the accepted answer's implementation).
On 2.0 it's based on services.
Details on MS doc:
https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x
I've written a Basic Authentication implementation for ASP.NET Core 2.0 and publish to NuGet:
https://github.com/bruno-garcia/Bazinga.AspNetCore.Authentication.Basic
I'm disappointed by the ASP.NET Core authentication middleware design. As a framework it should simplify and led to greater productivity which isn't the case here.
Anyway, a simple yet secure approach is based on the Authorization filters e.g. IAsyncAuthorizationFilter. Note that an authorization filter will be executed after the other middlewares, when MVC picks a certain controller action and moves to filter processing. But within filters, authorization filters are executed first (details).
I was just going to comment on Clays comment to Hector's answer but didn't like Hectors example throwing exceptions and not having any challenge mechanism, so here is a working example.
Keep in mind:
Basic authentication without HTTPS in production is extremely bad. Make sure your HTTPS settings are hardened (e.g. disable all SSL and TLS < 1.2 etc.)
Today, most usage of basic authentication is when exposing an API that's protected by an API key (see Stripe.NET, Mailchimp etc). Makes for curl friendly APIs that are as secure as the HTTPS settings on the server.
With that in mind, don't buy into any of the FUD around basic authentication. Skipping something as basic as basic authentication is high on opinion and low on substance. You can see the frustration around this design in the comments here.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace BasicAuthFilterDemo
{
public class BasicAuthenticationFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
public string Realm { get; set; }
public const string AuthTypeName = "Basic ";
private const string _authHeaderName = "Authorization";
public BasicAuthenticationFilterAttribute(string realm = null)
{
Realm = realm;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
try
{
var request = context?.HttpContext?.Request;
var authHeader = request.Headers.Keys.Contains(_authHeaderName) ? request.Headers[_authHeaderName].First() : null;
string encodedAuth = (authHeader != null && authHeader.StartsWith(AuthTypeName)) ? authHeader.Substring(AuthTypeName.Length).Trim() : null;
if (string.IsNullOrEmpty(encodedAuth))
{
context.Result = new BasicAuthChallengeResult(Realm);
return;
}
var (username, password) = DecodeUserIdAndPassword(encodedAuth);
// Authenticate credentials against database
var db = (ApplicationDbContext)context.HttpContext.RequestServices.GetService(typeof(ApplicationDbContext));
var userManager = (UserManager<User>)context.HttpContext.RequestServices.GetService(typeof(UserManager<User>));
var founduser = await db.Users.Where(u => u.Email == username).FirstOrDefaultAsync();
if (!await userManager.CheckPasswordAsync(founduser, password))
{
// writing to the Result property aborts rest of the pipeline
// see https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.0#cancellation-and-short-circuiting
context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
}
// Populate user: adjust claims as needed
var claims = new[] { new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, AuthTypeName) };
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthTypeName));
context.HttpContext.User = principal;
}
catch
{
// log and reject
context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
}
}
private static (string userid, string password) DecodeUserIdAndPassword(string encodedAuth)
{
var userpass = Encoding.UTF8.GetString(Convert.FromBase64String(encodedAuth));
var separator = userpass.IndexOf(':');
if (separator == -1)
return (null, null);
return (userpass.Substring(0, separator), userpass.Substring(separator + 1));
}
}
}
And these are the supporting classes
public class StatusCodeOnlyResult : ActionResult
{
protected int StatusCode;
public StatusCodeOnlyResult(int statusCode)
{
StatusCode = statusCode;
}
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
return base.ExecuteResultAsync(context);
}
}
public class BasicAuthChallengeResult : StatusCodeOnlyResult
{
private string _realm;
public BasicAuthChallengeResult(string realm = "") : base(StatusCodes.Status401Unauthorized)
{
_realm = realm;
}
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
context.HttpContext.Response.Headers.Add("WWW-Authenticate", $"{BasicAuthenticationFilterAttribute.AuthTypeName} Realm=\"{_realm}\"");
return base.ExecuteResultAsync(context);
}
}
We implemented Digest security for an internal service by using an ActionFilter:
public class DigestAuthenticationFilterAttribute : ActionFilterAttribute
{
private const string AUTH_HEADER_NAME = "Authorization";
private const string AUTH_METHOD_NAME = "Digest ";
private AuthenticationSettings _settings;
public DigestAuthenticationFilterAttribute(IOptions<AuthenticationSettings> settings)
{
_settings = settings.Value;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
ValidateSecureChannel(context?.HttpContext?.Request);
ValidateAuthenticationHeaders(context?.HttpContext?.Request);
base.OnActionExecuting(context);
}
private void ValidateSecureChannel(HttpRequest request)
{
if (_settings.RequireSSL && !request.IsHttps)
{
throw new AuthenticationException("This service must be called using HTTPS");
}
}
private void ValidateAuthenticationHeaders(HttpRequest request)
{
string authHeader = GetRequestAuthorizationHeaderValue(request);
string digest = (authHeader != null && authHeader.StartsWith(AUTH_METHOD_NAME)) ? authHeader.Substring(AUTH_METHOD_NAME.Length) : null;
if (string.IsNullOrEmpty(digest))
{
throw new AuthenticationException("You must send your credentials using Authorization header");
}
if (digest != CalculateSHA1($"{_settings.UserName}:{_settings.Password}"))
{
throw new AuthenticationException("Invalid credentials");
}
}
private string GetRequestAuthorizationHeaderValue(HttpRequest request)
{
return request.Headers.Keys.Contains(AUTH_HEADER_NAME) ? request.Headers[AUTH_HEADER_NAME].First() : null;
}
public static string CalculateSHA1(string text)
{
var sha1 = System.Security.Cryptography.SHA1.Create();
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(text));
return Convert.ToBase64String(hash);
}
}
Afterwards you can annotate the controllers or methods you want to be accessed with Digest security:
[Route("api/xxxx")]
[ServiceFilter(typeof(DigestAuthenticationFilterAttribute))]
public class MyController : Controller
{
[HttpGet]
public string Get()
{
return "HELLO";
}
}
To implement Basic security, simply change the DigestAuthenticationFilterAttribute to not use SHA1 but direct Base64 decoding of the Authorization header.
Super-Simple Basic Authentication in .NET Core:
1. Add this utility method:
static System.Text.Encoding ISO_8859_1_ENCODING = System.Text.Encoding.GetEncoding("ISO-8859-1");
public static (string, string) GetUsernameAndPasswordFromAuthorizeHeader(string authorizeHeader)
{
if (authorizeHeader == null || !authorizeHeader.Contains("Basic "))
return (null, null);
string encodedUsernamePassword = authorizeHeader.Substring("Basic ".Length).Trim();
string usernamePassword = ISO_8859_1_ENCODING.GetString(Convert.FromBase64String(encodedUsernamePassword));
string username = usernamePassword.Split(':')[0];
string password = usernamePassword.Split(':')[1];
return (username, password);
}
2. Update controller action to get username and password from Authorization header:
public async Task<IActionResult> Index([FromHeader]string Authorization)
{
(string username, string password) = GetUsernameAndPasswordFromAuthorizeHeader(Authorization);
// Now use username and password with whatever authentication process you want
return View();
}
Example
This example demonstrates using this with ASP.NET Core Identity.
public class HomeController : Controller
{
private readonly UserManager<IdentityUser> _userManager;
public HomeController(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
[AllowAnonymous]
public async Task<IActionResult> MyApiEndpoint([FromHeader]string Authorization)
{
(string username, string password) = GetUsernameAndPasswordFromAuthorizeHeader(Authorization);
IdentityUser user = await _userManager.FindByNameAsync(username);
bool successfulAuthentication = await _userManager.CheckPasswordAsync(user, password);
if (successfulAuthentication)
return Ok();
else
return Unauthorized();
}
}
I am trying to write my own authentication, so I inherited CredentialsAuthProvider and have overridden the Authenticate method. Auth is working fine, also when i call another service i can see all data that i saved in the session.
The Problem is: When i try add the Authenticate attribute and call it from a client, it goes and throws an Unauthorized exception, even if i want to use Requered Role.
Auth service is:
public class CustomCredentialsAuthProvider : CredentialsAuthProvider
{
public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
{
return true;
}
public override object Authenticate(IServiceBase authService, IAuthSession session, Auth request)
{
session.FirstName = "Name";
//...
session.Authenticate = true;
session.UserName = request.UserName;
session.Roles = new List<string>;
session.Roles.Add("admin")
//....
authService.SaveSession(session, SessionExpiry);
// Return custom object
return new UserAuthResponse { SessionId = session.Id ......};
}
AppHost is:
public override void Configure(Container container)
{
Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] {
new CustomCredentialsAuthProvider()
}));
Plugins.Add(new RegistrationFeature());
container.Register<ICacheClient>(new MemoryCacheClient());
var userRep = new InMemoryAuthRepository();
container.Register<IUserAuthRepository>(userRep);
}
and test service:
[Authenticate]
public class TestService : Service {
public object Any(UserRequest request) {
return new UserResponse{Name = request.Name};
}
}
It is not real code, so sorry for syntax mistake!))))
But the idea is the same! Help me please what is wrong, why I got Unauthorized exception when i call Test service??????????
When I had this issue, I had to create a custom authenticate attribute [CustomAuthenticate] with guidance from this gist -> https://gist.github.com/joeriks/4518393
In the AuthenticateIfBasicAuth method, I set provider to use MyAuthProvider.Name instead of BasicAuthProvider.Name
Then,
[CustomAuthenticate]
public class TestService : Service {
public object Any(UserRequest request) {
return new UserResponse{Name = request.Name};
}
}
Also see: http://joeriks.com/2013/01/12/cors-basicauth-on-servicestack-with-custom-authentication/