I have an ASP.NET Core 5 web app (actually a blazor server app) and another ASP.NET Core 5 Web API, both running on IIS in a domain environment.
I have configured Windows auth so that users can authenticate with the Blazor server app, and this returns the expected domain user's identity from the HttpContext.
If I use the Web API through Swagger to get the users identity from the HttpContext, I also get the expected domain user's identity. However if naively call from the Blazor server app using the HttpClient (given by DI) to the Web API, I get the app pool identity for the user instead.
There are hundreds of posts on this subject and why this happens (i.e.)
https://github.com/dotnet/runtime/issues/17828
.Net Core WindowsIdentity impersonation does not seem to be working
However my question is essentially even if I do manage to get the stars to align and get all the infrastructure config correct is there any way to get the middleware to provide an HttpClient that is already "impersonated" or am I forced to wrap every use of HttpClient something like this:
https://stackoverflow.com/a/66511109/29411
IPrincipal p = _httpContextAccessor.HttpContext.User;
HttpResponseMessage result = null;
if (p.Identity is WindowsIdentity wid)
{
await WindowsIdentity.RunImpersonated(wid.AccessToken, async () =>
{
result = await _client.GetAsync("APIController/Action");
});
}
.Net 5 running Impersonated
(I am running this in Blazor Server)
I have only recently solved this so at present I think everything will need to be wrapped but I'll update if I find anything that solves that.
I have seen a lot of references to using an IHttpConextAccessor and lots of problems with this being null. This article from Microsoft suggests that this shouldn’t be used (Certainly in Blazor)
MS-Docs
Solution:
To get the user to impersonate use the AuthenticationStateProvider and get the user from this and cast to a WindowsIDentity to retrieve the AccessToken.
This works in both a service and a razor component.
Inject the AuthenticationStateProvider and then in your method use the following code:
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
var userToImpersonate = (WindowsIdentity)user.Identity;
await WindowsIdentity.RunImpersonatedAsync(userToImpersonate.AccessToken, async () =>
{
// Your Code in here
}
Related
I have an ASP.NET Core 3.1 Web App calling an ASP.NET Core 3.1 Web API, which in turn accesses an Azure SQL database. Authentication is provided via MSAL (Microsoft Identity Platform) - i.e. using the relatively new Microsoft.Identity.Web and Microsoft.Identity.Web.UI libraries.
The goal is to ensure that the user pulls data from SQL via the API under the context of his/her own login, thus enabling row-level security, access auditing and other good things.
I have succeeded in getting the sign-in process to work for the Web App - and through that it obtains a valid access token to access the API using a scope I created when registering the latter with AD.
When I run both the API and the App locally from Visual Studio everything works as expected - the correct access tokens are provided to the App to access the API, and the API to access SQL - in both cases under the user's (i.e. my) identity.
When I publish the API to App Services on Azure, however, and access it there either from a local version of the Web App or an App-Services hosted version of it, the access token that the API gets to access SQL contains the API's Application Identity (system-assigned managed identity), and not the user's identity. Although I can access SQL as the application, it's not what we need.
The Web App obtains its access token using the GetAccessTokenForUserAsync method of ITokenAcquisition - taking as a parameter the single scope I defined for the API.
The API gets its token (to access SQL) like so:
var token = await new AzureServiceTokenProvider().GetAccessTokenAsync("https://database.windows.net", _tenantId)
...where _tenantId is the tenant ID of the subscription.
I have added the SQL Azure Database "user_impersonation" API permission to the AD registration for the API - but that has not helped. As an aside, for some reason Azure gives the full name of this permission as https://sql.azuresynapse.usgovcloudapi.net/user_impersonation - which is slightly alarming as this is just a UK-based regular Azure account.
I have found a few similar posts to this, but mostly for older versions of the solution set. I'm hoping to avoid having to write my own code to post the token requests - this is supposed to be handled by the MSAL libraries.
Should I somehow be separately requesting a SQL access scope from the Web App after sign-in, or should the API be doing something different to get hold of a SQL access token that identifies the user? Why does it work perfectly when running locally?
It seems like this should be a very common use case (the most common?) but it is barely documented - most documentation I've found refers only to the application identity being used or doesn't tell you what to do for this particular tech stack.
Finally - success! In the end this was the critical piece of documentation: Microsoft identity platform and OAuth 2.0 On-Behalf-Of flow - the key points being:
The App only asks for a token to access the API.
The API then requests a token, on behalf of the user identified via the 1st token, to access SQL.
The key is that - since the API cannot trigger a consent window for the second step - I had to use the Enterprise Applications tab in the Azure portal to pre-grant the permissions for SQL.
So the good news is it does work: maybe it's obvious to some but IMO it took me far too long to find the answer to this. I will write up a fuller explanation of how to do this in due course as it can't only be me struggling with this one.
The bad news is that - in the course of my investigations - I found that Azure B2C (which is the next thing I need to add in) doesn't support this "On Behalf Of" flow - click here for details. That's a great shame as I think it's the most obvious use case for it! Oh well, back to the drawing board.
I'm currently working on a similar problem, using a Net5.0 Web app. The reason it appears to be working locally is you are signed into Visual Studio with a user who can access Azure SQL and those are the rights you get in the Db. The IDE is using those credentials in place of the Managed Service Identity, the latter gets used when you upload the app to Azure.
As you noted, in the App registration you need to grant permission to the App for Azure SQL Database user_impersonation.
In your code, you need to request a token from https://database.windows.net//.default (note the // as it's needed for v1 endpoints). By referencing /.default you are asking for all permissions you've selected for the app in the app registration portal.
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope
In Startup.cs you need to EnableTokenAcquisitionToCallDownstreamApi with the scope you require.
services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(new[]
{"https://database.windows.net//.default"})
// Adds the User and App InMemory Token Cache
.AddInMemoryTokenCaches();
services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the
// default policy
options.FallbackPolicy = options.DefaultPolicy;
});
services.AddDbContext<MyDatabaseContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("MyAzureConnection")));
// The database interface
services.AddScoped<ITodos, TodoData>();
services.AddRazorPages()
.AddRazorRuntimeCompilation()
.AddMvcOptions(o =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
o.Filters.Add(new AuthorizeFilter(policy));
})
.AddMicrosoftIdentityUI();
You also need to decorate your controllers with [AuthorizeForScopes(Scopes = new string[]{"https://database.windows.net//.default"}] and include the required scopes for that Controller. For Razor, it's at the top of the page model and requires a reference to `using Microsoft.Identity.Web;'
namespace ToDoApp.Pages.Todos
{
[AuthorizeForScopes(ScopeKeySection = "AzureSQL:BaseUrl")]
public class CreateModel : PageModel
I'm using a section in my appsettings.json for the scope and retrieving it using ScopeKeySection:
"AzureSQL": {
"BaseUrl": "https://database.windows.net//.default",
"Scopes": "user_impersonation"
}
This shows you where to include it for MVC, Razor and Blazor:
https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access#in-mvc-controllers
Finally, your DbContext needs a token which you could pass to it from the client app (perhaps...).
This is how I am doing it at the moment
public class MyDatabaseContext : DbContext
{
private readonly ITokenAcquisition _tokenAcquisition;
public MyDatabaseContext (ITokenAcquisition tokenAcquisition,
DbContextOptions<MyDatabaseContext> options)
: base(options)
{
_tokenAcquisition = tokenAcquisition;
string[] scopes = new[]{"https://database.windows.net//.default"};
var result = _tokenAcquisition.GetAuthenticationResultForUserAsync(scopes)
.GetAwaiter()
.GetResult();
token = result.AccessToken;
var connection = (SqlConnection)Database.GetDbConnection();
connection.AccessToken = result.token;
}
This is a flawed solution. If I restart the app and try to access it again I get an error Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user
It seems to be related to the TokenCache. If I sign out and in again or clear my browser cache the error is resolved. I've a workaround that signs the app in on failure, but it's deficient since I'm using the app's credentials.
However, it successfully connects to the Azure SQL Db as the user instead of the App with the user's rights instead. When I do solve the error (or find one) I will update this answer.
In my ASP.Net Core app, I have implemented Microsoft External Login. I now wish to override the default login callback, which is listed by documentation to be https://localhost:5001/signin-microsoft, if of course running on localhost and on that port. The instructions on here then state that the callback override would be something like this: https://contoso.azurewebsites.net/.auth/login/microsoftaccount/callback.
I am a bit confused on where the callback is meant to be implemented. At the moment I have ExternalLoginCallback() callback method implemented in a base Controller class. But from looking at the above example, it doesn't look like it should be part of a controller.
Should the callback be inside Startup.cs, a Controller, or some other file I am not currently aware of?
The instructions on here then state that the callback override would be something like this: https://contoso.azurewebsites.net/.auth/login/microsoftaccount/callback.
That is related to built-in authentication and authorization support in Azure App service . Do you host your app in Azure App service ?
If yes :
If you enable the Authentication and authorizationfeature of the app service , that means you are using the built-in authentication and authorization support in Azure . That feature will take over the authentication and authorization of you application , that means authentication and authorization still works even you delete the external Azure AD authentication codes in your application . Then you could just :
Use Authentication and authorizationfeature of the app service , delete the Owin Microsoft Account authentication middleware related codes .
Disable Authentication and authorizationfeature of the app service, use Microsoft Account external login( Microsoft.AspNetCore.Authentication.MicrosoftAccount package) .
If no :
Then you should follow document : Microsoft Account external login . You can config the callback url by :
microsoftOptions.CallbackPath = "/home/about";
But if you are using the ASP.NET Identity template with Microsoft Account external login . After Microsoft authentication , asp.net will check whether user's identity exists in database . Since ASP.NET Core 2.1 and later provides ASP.NET Core Identity as a Razor Class Library. If you want to redirect user to another page after authentication , you can :
Scaffold Identity in ASP.NET Core projects: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity?view=aspnetcore-2.2&tabs=visual-studio
After that ,modify the redirect url in Areas.Identity.Pages.Account.Login.cshtml.cs:
public IActionResult OnPost(string provider, string returnUrl = null)
{
returnUrl = "/home/contact";
// Request a redirect to the external login provider.
var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return new ChallengeResult(provider, properties);
}
Scenario:
Both Web application and Web API need to be authenticated and protected from the server side.
Requirement:
Web application is serving the contents for the browser and browser should be calling Web API directly (i.e. Browser to API).
Question:
Is it possible to authenticate both Web APP and the API using tokens?
Any sample code or clear direction would be highly appreciated.
Normally web applications are authenticated using cookies and APIs are authenticated using tokens.There are some sample projects available here but they are either browser to API (SPA token based) or Server side Web App calling API from server to server.
UPDATE 1
App is saving the TokenValidationParameters and used bootstrapContext.Token within the app controller to grab for server to server communication.
As per #dstrockis, I'm trying to grab the id_token from the Web App soon after the end of validation (not within the app contrller).
I'm using SecurityTokenValidated invoker in OpenIdConnectAuthenticationOptions.Notifications within the Startup class. SecurityTokenValidated receives a parameter of type SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> but I'm not sure where to find the id_token within it. Method is below.
private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
{
return new OpenIdConnectAuthenticationOptions
{
// For each policy, give OWIN the policy-specific metadata address, and
// set the authentication type to the id of the policy
MetadataAddress = String.Format(aadInstance, tenant, policy),
AuthenticationType = policy,
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = clientId,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
//NEW METHOD INVOKE ************************************
//******************************************************
SecurityTokenValidated = OnSecurityTokenValidated
//******************************************************
},
Scope = "openid",
ResponseType = "id_token",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
SaveSigninToken = true
},
};
}
//NEW METHOD ************************************
private Task OnSecurityTokenValidated(
SecurityTokenValidatedNotification<OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> arg)
{
//QUESTION ********************************************************
//How to find the just saved id_token using incoming parameter, arg
//*****************************************************************
return Task.FromResult(0);
}
UPDATE 2
Instead of SecurityTokenValidated, I tried AuthorizationCodeReceived and it's not getting called at all. As discussed here, my redirect url does have an ending slash as well.
Any Ideas?
Our ASP.NET OpenID Connect middleware which supports AAD B2C is built to rely on cookie authentication from a browser. It doesn't accept tokens in a header or anything like that for securing web pages. So I'd say if you want to serve HTML from your web app in the classic way, you need to use cookies to authenticate requests to the web app.
You can definitely get & store tokens within the browser and use those to access your web API, even if you use cookies to authenticate to the web app. There's two patterns I'd recommend:
Perform the initial login using the OpenID Connect Middleware, initiating the flow from the server side as described in the samples. Once the flow completes, the middleware will validate the resulting id_token and drop cookies in the browser for future requests. You can instruct the middleware to save the id_token for later use by using the line of code written here. You can then somehow pass that id_token down to your browser, cache it, and use it to make requests to the API.
The other pattern is the inverse. Start by initiating the login from javascript, using the single page app pattern from the B2C documentation. Cache the resulting id_tokens in the browser, and use them to make API calls. But when the login completes, you can send a request to your web app with the id_token in the body, triggering the OpenID Connect middleware to process the request and issue a session cookie. If you want to know the format of that request, I'd recommend inspecting a regular server side OpenID Connect flow.
Found the answer to my own question and adding here for the future reference.
After a successful validation, id_token can be accessed by invoking the SecurityTokenValidated notification. Code sample is below.
private Task OnSecurityTokenValidated(
SecurityTokenValidatedNotification<OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> arg)
{
//Id Token can be retrieved as below.
//**************************************
var token = arg.ProtocolMessage.IdToken;
return Task.FromResult(0);
}
However, saving this directly into a browser cookie may not be secure.
I have setup a SharePoint dev environment and managed to get a provider hosted app working with Certs etc (High Security)
This is all on-premise and we won't have a connection to ACS (now, I may have miss understood ACS, I presume its azure based and servers need to talk to server outside of the server room :) ).
My problem is:
The SharePoint site will not be using Windows Auth, we will be using a login form which will read details from another store.
If I review the code that VS generates I can see that it expects a Windows identity.
Can this be done? I would have expected my provider app not to need any auth as its hosted via SharePoint, it gets the claim from SharePoint so why does it need a Windows Identity as well as the SharePoint Claims.
As you said : The SharePoint site will not be using Windows Auth, we will be using a login form which will read details from another store.
Ans : You are talking about Form based authentication.
If I review the code that VS generates I can see that it expects a Windows identity.
Ans : You are correct. VS generates the code that expects windows identity [For dev environments]. You need to write separate function to get clientcontext for FBA using SharePointContextProvider class.
Context can be :CreateAppOnlyClientContextForSPHost(),CreateUserClientContextForSPHost()
I would have expected my provider app not to need any auth as its hosted via SharePoint
Ans : You can make IIS site [Hosting your provider hosted apps] to allow annonymous authentication.
You can get access token with this code.
var contextTokenString = TokenHelper.GetContextTokenFromRequest(Page.Request);
var appWeb = new Uri(clientContext.Web.Url);
if (contextTokenString != null)
{
SharePointContextToken contextToken =
TokenHelper.ReadAndValidateContextToken(contextTokenString, Request.Url.Authority);
string accessToken =
TokenHelper.GetAccessToken(contextToken, appWeb.Authority).AccessToken;
}
Get current user :-
// Get current context to get load siteurl
var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);
string webUrl =spContext.SPHostUrl.ToString();
//using (var clientContext = spContext.CreateUserClientContextForSPHost())
using (ClientContext clientContext = new ClientContext(webUrl))
{
clientContext.AuthenticationMode = ClientAuthenticationMode.FormsAuthentication;
clientContext.FormsAuthenticationLoginInfo = new FormsAuthenticationLoginInfo(uName, pswd);
Web web = clientContext.Web;
clientContext.Load(web);
clientContext.ExecuteQuery();
// Load SP user from login name found from httpcontext
string currentSPUser = string.Concat("<<<FBADomainPrefix>>>",User.Identity.Name);
var currentUser = clientContext.Web.EnsureUser(currentSPUser);
clientContext.Load(currentUser);
clientContext.ExecuteQuery();
}
I have an MVC5 web app using OWIN CookieAuthentication. When the user is successfully logged into the web app, a connection is made to a SignalR hub running in a console app, also configured with OWIN CookieAuthentication.
In the SignalR Startup I have specified GlobalHost.HubPipeline.RequireAuthentication() because I want to get the HubCallerContext.User property hydrated with the ClaimsPrincipal sent from the web app. Inspecting the cookies available, I can see that the auth cookie is received successfully, but I can't figure out how to set up the OWIN configuration so that the readonly principal gets set from it.
I've seen related post suggestions, such as variations upon the below:
var cookieAuthenticationOptions = new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
AuthenticationMode = AuthenticationMode.Active,
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
TimeSpan.FromSeconds(30),
(manager, user) => manager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie))
}
};
app.UseCookieAuthentication(cookieAuthenticationOptions);
But I cannot get these to have any effect, and in any case it seems to involve a lot of peripheral Identity framework related classes to be set up (which for now I've ported in from an MVC5 project template just to test with). I just want to hydrate the User property with the claims encrypted into the ticket, and then look up the identified user in my SignalR system however I see fit. How do I go about this?
EDIT: As a workaround I'm creating a separate "server cookie" at the time that I sign in the user in the web app. I binary serialize the ClaimsIdentity, encrypt that with AES, base 64 encode that and put that into the cookie. Then in my Hub class I have an on-demand User property which reads that cookie out and unpacks it. Not the ideal solution!