Access EF DbContext within Google OAuth flow - asp.net-core

I have a working Google authentication flow in ASP.NET Core 2.1.
I would like to add some authorization by checking the user's email address against a database when they sign in. How can I access the Entity Framework DbContext here?
public void ConfigureServices(IServiceCollection services)
{
services
.AddDbContext<Database>(options => options.UseSqlServer(Configuration["SqlServer"]));
services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.LoginPath = "/Auth/SignIn";
o.LogoutPath = "/Auth/SignOut";
o.ExpireTimeSpan = TimeSpan.FromDays(90);
})
.AddGoogle(o =>
{
o.ClientId = Configuration["Auth:Google:ClientId"];
o.ClientSecret = Configuration["Auth:Google:ClientSecret"];
o.Events = new OAuthEvents()
{
OnTicketReceived = async context =>
{
var email = context.Principal.Identity.GetEmail().ToLowerInvariant();
if (/* User not in database */) // <-- How can I access the EF DbContext here?
{
context.Response.Redirect("/Auth/Unauthorised");
context.HandleResponse();
}
}
};
});
services.AddMvc();
/* ... */
}

You can access your HttpContext from context and you can access the IServiceProvider from there.
context.HttpContext.RequestServices.GetService<Database>()

Related

EF Core to call database based on parameter in API

I have an API developed in .NET Core with EF Core. I have to serve multiple clients with different data(but the same schema). This is a school application, where every school want to keep their data separately due to competition etc. So we have a database for each school. Now my challenge is, based on some parameters, I want to change the connection string of my dbContext object.
for e.g., if I call api/students/1 it should get all the students from school 1 and so on. I am not sure whether there is a better method to do it in the configure services itself. But I should be able to pass SchoolId from my client application
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<SchoolDataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("APIConnectionString")));
services.AddScoped<IUnitOfWorkLearn, UnitOfWorkLearn>();
}
11 May 2021
namespace LearnNew
{
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)
{
//Comenting to implement Mr Brownes Solution
//services.AddDbContext<SchoolDataContext>(options =>
// options.UseSqlServer(
// Configuration.GetConnectionString("APIConnectionString")));
services.AddScoped<IUnitOfWorkLearn, UnitOfWorkLearn>();
services.AddControllers();
services.AddHttpContextAccessor();
services.AddDbContext<SchoolDataContext>((sp, options) =>
{
var requestContext = sp.GetRequiredService<HttpContext>();
var constr = GetConnectionStringFromRequestContext(requestContext);
options.UseSqlServer(constr, o => o.UseRelationalNulls());
});
ConfigureSharedKernelServices(services);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "LearnNew", Version = "v1" });
});
}
private string GetConnectionStringFromRequestContext(HttpContext requestContext)
{
//Trying to implement Mr Brownes Solution
var host = requestContext.Request.Host;
// Since I don't know how to get the connection string, I want to
//debug the host variable and see the possible way to get details of
//the host. Below line is temporary until the right method is identified
return Configuration.GetConnectionString("APIConnectionString");
}
private void ConfigureSharedKernelServices(IServiceCollection services)
{
ServiceProvider serviceProvider = services.BuildServiceProvider();
SchoolDataContext appDbContext = serviceProvider.GetService<SchoolDataContext>();
services.RegisterSharedKernel(appDbContext);
}
// 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.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "LearnNew v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
You can access the HttpContext when configuring the DbContext like this:
services.AddControllers();
services.AddHttpContextAccessor();
services.AddDbContext<SchoolDataContext>((sp, options) =>
{
var requestContext = sp.GetRequiredService<IHttpContextAccessor>().HttpContext;
var constr = GetConnectionStringFromRequestContext(requestContext);
options.UseSqlServer(constr, o => o.UseRelationalNulls());
});
This code:
var requestContext = sp.GetRequiredService<IHttpContextAccessor>().HttpContext; var constr = GetConnectionStringFromRequestContext(requestContext);
options.UseSqlServer(constr, o => o.UseRelationalNulls());
will run for every request, configuring the connection string based on details from the HttpRequestContext.
If you need to use your DbContext on startup, don't resolve it through DI. Just configure a connection like this:
var ob = new DbContextOptionsBuilder<SchoolDataContext>();
var constr = "...";
ob.UseSqlServer(constr);
using (var db = new Db(ob.Options))
{
db.Database.EnsureCreated();
}
But in production you would normally create all your tenant databases ahead-of-time.

ServiceStack ServiceClient stores wrong cookies after authentication

i have a strange problem with Servicestack Authentication.
I've developed an Asp .Net Core web app (.net core 3.1) in which is implemented a servicestack authentication with credentials auth provider. Everything work correctly if i authenticate with any browsers.
Instead if i try to authenticate from external application with JsonServiceClient pointing to servicestack /auth/{provider} api i've this problem:
authentication goes well but the JsonServiceClient object stores a SessionId in cookies (s-id/s-pid) different from the SessionId of AuthenticateResponse. Here my example.
Authenticate request = new Authenticate()
{
provider = "credentials",
UserName = username,
Password = password,
RememberMe = true
};
var client = new JsonServiceClient(webappUrl);
AuthenticateResponse response = await client.PostAsync(request);
var cookies = client.GetCookieValues();
If i check values in cookies variable i see that there are s-id and s-pid completely different from the sessionId of the response.
The other strange thing is that if i repeat the authentication a second time under those lines of code, now the s-pid cookie is equal to sessionId of response!
Why??
In the startup of web app i have these lines of code:
public new void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => options.EnableEndpointRouting = false);
// Per accedere all'httpcontext della request
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Per accedere alla request context della request
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
// Registro il json di configurazione (innietta l'appSettings)
services.AddSingleton(Configuration);
// Filters
services.AddSingleton<ModulePermissionFilter>();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
... other lines of code
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IBackgroundJobClient backgroundJobs)
{
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseServiceStack(new AppHost
{
AppSettings = new NetCoreAppSettings(Configuration)
});
}
public class AppHost : AppHostBase
{
public AppHost() : base("webapp", typeof(BaseServices).Assembly) { }
// Configure your AppHost with the necessary configuration and dependencies your App needs
public override void Configure(Container container)
{
SetConfig(new HostConfig
{
UseCamelCase = false,
WriteErrorsToResponse = true,
ReturnsInnerException = true,
AllowNonHttpOnlyCookies = false,
DebugMode = AppSettings.Get(nameof(HostConfig.DebugMode), HostingEnvironment.IsDevelopment()),
// Restrict cookies to domain level in order to support PflowV2
RestrictAllCookiesToDomain = !string.IsNullOrEmpty(AppSettings.Get("RestrictAllCookiesToDomain", "")) && AppSettings.Get("RestrictAllCookiesToDomain", "").ToLower() != "localhost" ? AppSettings.Get("RestrictAllCookiesToDomain", "") : null
});
// Create DBFactory for cache
var defaultConnection = appHost.AppSettings.Get<string>("ConnectionStrings:Webapp");
var dbFactory = new OrmLiteConnectionFactory(defaultConnection, SqlServerDialect.Provider);
// Register ormlite sql session and cache
appHost.Register<IDbConnectionFactory>(dbFactory);
appHost.RegisterAs<OrmLiteCacheClient, ICacheClient>();
appHost.Resolve<ICacheClient>().InitSchema();
appHost.Register<ISessionFactory>(new SessionFactory(appHost.Resolve<ICacheClient>()));
//Tell ServiceStack you want to persist User Auth Info in SQL Server
appHost.Register<IAuthRepository>(new OrmLiteAuthRepository(dbFactory));
appHost.Resolve<IAuthRepository>().InitSchema();
var sessionMinute = appHost.AppSettings.Get("SessionTimeoutMinute", 15);
// Adding custom usersession and custom auth provider
Plugins.Add(new AuthFeature(() => new CustomUserSession(), new IAuthProvider[] { new CustomCredentialsAuthProvider(), new ApiKeyAuthProvider() })
{
HtmlRedirect = "/Account/Login", // Redirect to login if session is expired
IncludeAssignRoleServices = false,
SessionExpiry = TimeSpan.FromHours(sessionMinute),
});
Plugins.Add(new SessionFeature());
}
}

Cannot replace default JSON contract resolver in ASP.NET Core 3

After creating basic Web API project based on .NET Core 3.0 framework, all API responses were coming in camel case. I installed SwashBuckle Swagger + built-in JSON serializer from System.Text.Json, specifically, to display enums as strings, everything worked as before. Then, I decided to switch to NSwag + NewtonSoftJson, because of some limitations of built-in serializer with dynamic and expando objects. Now, all API responses are displayed in PascalCase and I cannot change neither naming policy, nor even create custom contract resolver.
Example
https://forums.asp.net/t/2138758.aspx?Configure+SerializerSettings+ContractResolver
Question
I suspect that maybe some package overrides contract resolver behind the scene. How to make sure that API service uses ONLY custom contract resolver that I assign at startup and ignores all other similar settings?
Custom JSON contract resolver:
public class CustomContractResolver : IContractResolver
{
private readonly IHttpContextAccessor _context;
private readonly IContractResolver _contract;
private readonly IContractResolver _camelCaseContract;
public CustomContractResolver(IHttpContextAccessor context)
{
_context = context;
_contract = new DefaultContractResolver();
_camelCaseContract = new CamelCasePropertyNamesContractResolver();
}
// When API endpoint is hit, this method is NOT triggered
public JsonContract ResolveContract(Type value)
{
return _camelCaseContract.ResolveContract(value);
}
}
Controller:
[ApiController]
public class RecordsController : ControllerBase
{
[HttpGet]
[Route("services/records")]
[ProducesResponseType(typeof(ResponseModel<RecordEntity>), 200)]
public async Task<IActionResult> Records([FromQuery] QueryModel queryModel)
{
var response = new ResponseModel<RecordEntity>();
return Content(JsonConvert.SerializeObject(response), "application/json"); // NewtonSoft serializer
}
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services
.AddCors(o => o.AddDefaultPolicy(builder => builder
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()));
services
.AddControllers(o => o.RespectBrowserAcceptHeader = true)
/*
.AddJsonOptions(o =>
{
o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
o.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
})
*/
.AddNewtonsoftJson(o =>
{
o.UseCamelCasing(true);
o.SerializerSettings.Converters.Add(new StringEnumConverter());
//o.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver { NamingStrategy = new CamelCaseNamingStrategy() };
o.SerializerSettings.ContractResolver = new CustomContractResolver(new HttpContextAccessor());
});
services.AddOpenApiDocument(o => // NSwag
{
o.PostProcess = document =>
{
document.Info.Version = "v1";
document.Info.Title = "Demo API";
};
});
DataConnection.DefaultSettings = new ConnectionManager(DatabaseOptionManager.Instance); // LINQ to DB
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors(o => o.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(o => o.MapControllers());
app.UseOpenApi(); // NSwag
app.UseSwaggerUi3(o => o.Path = "/v2/docs");
app.UseReDoc(o => o.Path = "/v1/docs");
}
Still don't understand why custom contract resolver is not triggered by API endpoint, but found a combination that works for me to switch API to camel case. Feel free to explain why it works this way.
services.AddControllers(o => o.RespectBrowserAcceptHeader = true)
// Options for System.Text.Json don't affect anything, can be uncommented or removed
//.AddJsonOptions(o =>
//{
// o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
// o.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
// o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
//})
.AddNewtonsoftJson(o =>
{
o.UseCamelCasing(true);
o.SerializerSettings.Converters.Add(new StringEnumConverter());
// This option below breaks global settings, so had to comment it
//o.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver
//{
// NamingStrategy = new CamelCaseNamingStrategy()
//};
});
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
Idea was taken from this article.
NewtonSoft allows to set global serialization settings disregarding MVC, Web API, and other frameworks.

How to do Azure AD groups based authorization?

net core web api application. I have configured swagger for my web api app. I am doing authentication and authorization from swagger and I do not have webapp or SPA. Now I want to do authorization based on groups. When I saw JWT token I saw hasgroups: true rather than group ids. This is changed If more than 5 groups are associated with user. Please correct me If my understanding is wrong. So I have now hasgroups: true. So to get groups I need to call graph api. Once I get groups from graph API I need to create policies. This is my understanding and please correct me If I am on wrong track. Now I have my below web api app.
Startup.cs
public Startup(IConfiguration configuration)
{
Configuration = configuration;
azureActiveDirectoryOptions = Configuration.GetSection("AzureAd").Get<AzureActiveDirectoryOptions>();
swaggerUIOptions = Configuration.GetSection("Swagger").Get<SwaggerUIOptions>();
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services
.AddAuthentication(o =>
{
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.Authority = azureActiveDirectoryOptions.Authority;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidAudiences = new List<string>
{
azureActiveDirectoryOptions.AppIdUri,
azureActiveDirectoryOptions.ClientId
},
};
});
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = swaggerUIOptions.AuthorizationUrl,
TokenUrl = swaggerUIOptions.TokenUrl
});
c.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", new[] { "readAccess", "writeAccess" } }
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.OAuthClientId(swaggerUIOptions.ClientId);
c.OAuthClientSecret(swaggerUIOptions.ClientSecret);
c.OAuthRealm(azureActiveDirectoryOptions.ClientId);
c.OAuthAppName("Swagger");
c.OAuthAdditionalQueryStringParams(new { resource = azureActiveDirectoryOptions.ClientId });
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseAuthentication();
app.UseMvc();
}
}
I have API as below.
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private IHttpContextAccessor _httpContextAccessor;
public ValuesController(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
// GET api/values
[HttpGet]
public ActionResult<string> Get()
{
string owner = (User.FindFirst(ClaimTypes.Name))?.Value;
var accessToken = _httpContextAccessor.HttpContext.Request.Headers["Authorization"];
return owner;
}
}
Now After log in I can hit to API. Now I want to have something like Authorize(admin/user) based on the groups I want to control authorization. Now I am having trouble, where I should call graph api and get groups. Can some one help me to understand this? Any help would be appreciated. Thanks
Which protol and which flow you are using. ?
Yes , implict flow has the limit for groups claim . To use Microsoft Graph to get current user's groups , you can try below ways :
Use the on-behalf-of grant to acquire an access token that allows the API to call MS Graph as the user , here is code sample .
Use client credentials flow to acquire Microsoft Graph's access token in web api, this flow uses application's permission with no user context . Code sample here is for Azure AD V1.0 using ADAL . And here is code sample for Azure AD V2.0 using MSAL .

How to authorize SignalR Core Hub method with JWT

I am using JWT authentication in my ASP.NET Core 2.0 application with OpenIddict.
I am following idea in this thread and calling AuthorizeWithJWT method after SignalR handshake. But now, I do not know what should I set in AuthorizeWithJWT method so I can use [Authorize(Roles="Admin")] for example.
I tried with setting context user, but it is readonly:
public class BaseHub : Hub
{
public async Task AuthorizeWithJWT(string AccessToken)
{
//get user claims from AccesToken
this.Context.User = user; //error User is read only
}
}
And using authorize attribute:
public class VarDesignImportHub : BaseHub
{
[Authorize(Roles = "Admin")]
public async Task Import(string ConnectionString)
{
}
}
I strongly encourage you to continue doing authentication at the handshake level instead of going with a custom and non-standard solution you'd implement at the SignalR level.
Assuming you're using the validation handler, you can force it to retrieve the access token from the query string:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication()
.AddOAuthValidation(options =>
{
options.Events.OnRetrieveToken = context =>
{
context.Token = context.Request.Query["access_token"];
return Task.CompletedTask;
};
});
}
Or OnMessageReceived if you want to use JWTBearer:
services.AddAuthentication()
.AddJwtBearer(o =>
{
o.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
if (context.Request.Path.ToString().StartsWith("/HUB/"))
context.Token = context.Request.Query["access_token"];
return Task.CompletedTask;
},
};
});
No other change should be required.