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!
Related
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
}
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.
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 am trying to create a Web API that allows the API's clients (native mobile apps) to login using a 3rd party cloud storage provider. I'm using the following general flow from Microsoft:
Here is what I am trying to achieve:
I am using the default ASP.NET Web API Visual Studio template with external authentication, along with the OWin.Security.Providers Nuget package for Dropbox login functionality, and the existing built-in login functionality for Google (Drive) and Microsoft (OneDrive).
The issue I'm having is that the built-in functionality all seems to do the authentication and authorization as part of one flow. For example, if I set up the following in Startup.Auth.cs:
DropboxAuthenticationOptions dropboxAuthOptions = new DropboxAuthenticationOptions
{
AppKey = _dropboxAppKey,
AppSecret = _dropboxAppSecret
};
app.UseDropboxAuthentication(dropboxAuthOptions);
... and navigate to this url from my web browser:
http://<api_base_url>/api/Account/ExternalLogin?provider=Dropbox&response_type=token&client_id=self&redirect_uri=<api_base_url>
I am successfully redirected to Dropbox to login:
https://www.dropbox.com/1/oauth2/authorize?response_type=code&client_id=<id>&redirect_uri=<redirect_uri>
... and then after I grant access, am redirected back to:
http://<api_base_url>/Help#access_token=<access_token>&token_type=bearer&expires_in=1209600
... as you can see the token is part of that, so could be extracted. The problem is that the client needs to be the one navigating to Dropbox and returning the authorization code back up to the Web API, and the Web API would send the authorization code back to the third party to get the token which would then be returned to the client... as shown in the diagram above. I need the ExternalLogin action in the AccountController to somehow retrieve the Dropbox url and return that to the client (it would just be a json response), but I don't see a way to retrieve that (it just returns a ChallengeResult, and the actual Dropbox url is buried somewhere). Also, I think I need a way to separately request the token from the third party based on the authorization code.
This post seems a little similar to what I am trying to do:
Registering Web API 2 external logins from multiple API clients with OWIN Identity
... but the solution there seems to require the client to be an MVC application, which is not necessarily the case for me. I want to keep this as simple as possible on the client side, follow the flow from my diagram above, but also not reinvent the wheel (reuse as much as possible of what already exists in the OWIN/OAuth2 implementation). Ideally I don't want the client to have to reference any of the OWIN/OAuth libraries since all I really need the client to do is access an external url provided by the API (Dropbox in my example), have the user input their credentials and give permission, and send the resulting authorization code back up to the api.
Conceptually this doesn't sound that hard but I have no idea how to implement it and still use as much of the existing OAuth code as possible. Please help!
To be clear, the sample I mentioned in the link you posted CAN be used with any OAuth2 client, using any supported flow (implicit, code or custom). When communicating with your own authorization server, you can of course use the implicit flow if you want to use JS or mobile apps: you just have to build an authorization request using response_type=token and extract the access token from the URI fragment on the JS side.
http://localhost:55985/connect/authorize?client_id=myClient&redirect_uri=http%3a%2f%2flocalhost%3a56854%2f&response_type=token
For reference, here's the sample: https://github.com/aspnet-security/AspNet.Security.OpenIdConnect.Server/tree/dev/samples/Mvc/Mvc.Server
In case you'd prefer a simpler approach (that would involve no custom OAuth2 authorization server), here's another option using the OAuth2 bearer authentication middleware and implementing a custom IAuthenticationTokenProvider to manually validate the opaque token issued by Dropbox. Unlike the mentioned sample (that acts like an authorization proxy server between Dropbox and the MVC client app), the JS app is directly registered with Dropbox.
You'll have to make a request against the Dropbox profile endpoint (https://api.dropbox.com/1/account/info) with the received token to validate it and build an adequate ClaimsIdentity instance for each request received by your API. Here's a sample (but please don't use it as-is, it hasn't been tested):
public sealed class DropboxAccessTokenProvider : AuthenticationTokenProvider {
public override async Task ReceiveAsync(AuthenticationTokenReceiveContext context) {
using (var client = new HttpClient()) {
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.dropbox.com/1/account/info");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.Token);
var response = await client.SendAsync(request);
if (response.StatusCode != HttpStatusCode.OK) {
return;
}
var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
var identity = new ClaimsIdentity("Dropbox");
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, payload.Value<string>("uid")));
context.SetTicket(new AuthenticationTicket(identity, new AuthenticationProperties()));
}
}
}
You can easily plug it via the AccessTokenProvider property:
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions {
AccessTokenProvider = new DropboxAccessTokenProvider()
});
It has its own downsides: it requires caching to avoid flooding the Dropbox endpoint and is not the right way to go if you want to accept tokens issued by different providers (e.g Dropbox, Microsoft, Google, Facebook).
Not to mention that if offers a very low security level: since you can't verify the audience of the access token (i.e the party the token was issued to), you can't ensure that the access token was issued to a client application you fully trust, which allows any third party developer to use his own Dropbox tokens with your API without having to request user's consent.
This is - obviously - a major security concern and that's why you SHOULD prefer the approach used in the linked sample. You can read more about confused deputy attacks on this thread: https://stackoverflow.com/a/17439317/542757.
Good luck, and don't hesitate if you still need help.
I'll try to be explicit.
I have a Front End app (MVC), it can communicate with a Facade Web Api (Resource) and get token from an authentication server.
My problem is I can't create an identity in MVC app. This is mu Startup class.
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
});
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
}
When I try to go to a method controller decorated with [Authorize], I get 401 error instead to be redirected to Login page.
Please, I would appreciate any help, advice or example.
Regards,
Typically, unless your app is doing postback's you do not need to enable the cookie authentication with login path. That is for the oauth password login flow (grant_type) where you are internally authorizing your users against your identity database. If you're redirecting to an external authorization api (like facebook) then you don't need to set a login path in your application since the first authorization endpoint that gets hit is your external callback (after you send them to facebook, they will send them back to your external endpoint). The redirect you are getting is because cookie authentication is registered as active authentication mode so it redirects 401's to the login path you set (overriding other OWIN middleware).
If you want to house your authorization server in house, have a look at the link below, it will at the least get you setup with JWT support -
http://bitoftech.net/2014/10/27/json-web-token-asp-net-web-api-2-jwt-owin-authorization-server/