I've set up the following authentication in my MVC client.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(_ =>
{
//_.DefaultScheme = "Cookies";
//_.DefaultChallengeScheme = "oidc";
_.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie("Cokies")
.AddFacebook()
.AddGoogle()
.AddJwtBearer()
.AddOpenIdConnect("oidc", _ => { });
Since the default is set to JWT, I imagine that the others (Cookie, Facebook, Google and OpenIdConnect) are simply disregarded if the authorization decorator is invoked with no parameters.
However, I simply get 401 Unauthorized with no additional information. For instance, if I comment out the default scheme statement for JWT and activate the cookie one like this:
{
_.DefaultScheme = "Cookies";
_.DefaultChallengeScheme = "oidc";
//_.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}...
I get developer error page saying that client ID is unknown. But in this case, I only get the default page in Chrome with the error code. I was considering that maybe the default challenge scheme needs to be set but it makes no sense to do that for JWT, right? Also, I see no field for that in JwtBearerDefaults class.
What am I missing in my MVC client to make it authenticate properly? Or is the issue in my configuration of the IDS4?
I've googled for a while but I'm not getting any hits that talk to me and that I'd recognize as meaningful. Might be my ignorance, though...
A shorter version still producing the same issue looks like this.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(_ =>
{
_.Authority = "https://localhost:44300";
_.Audience = "http://localhost:5001";
});
My IDS4 runs on port 44300 (checked with .well-known) and the API is on port 5001.
Related
I am using .net 5, Identity Web Ui to access Microsoft Graph. Where can I configure my Redirect URI?
I need to specify the full Uri, since the generated one from callbackUri is incorrect due to being behind a Load Balancer with SSL offload.
Here is my current ConfigureServices section
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
I was facing a similar problem with a WebApp exposed only behind a front door, the WebApp had to call a custom downstream WebApi.
My service configuration that worked on my localhost dev machine:
// AzureAdB2C
services
.AddMicrosoftIdentityWebAppAuthentication(
Configuration,
"AzureAdB2C", subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true)
.EnableTokenAcquisitionToCallDownstreamApi(p =>
{
p.RedirectUri = redUri; // NOT WORKING, WHY?
p.EnablePiiLogging = true;
},
[... an array with my needed scopes]
)
.AddInMemoryTokenCaches();
I tried the AddDownstreamWebApi but did not manage to make it work so I just fetched the needed token with ITokenAcquisition and added it to an HttpClient to make my request.
Then I needed AzureAd/B2C login redirect to the uri with the front door url:
https://example.org/signin-oidc and things broke. I solved it like this:
First of all you have to add this url to your App registration in the azure portal, very important is case sensitive it cares about trailing slashes and I suspect having many urls that point to the very same controller and the order of these have some impact, I just removed everything and kept the bare minimum.
Then in the configure services method:
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SaveTokens = true; // this saves the token for the downstream api
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctxt =>
{
// Invoked before redirecting to the identity provider to authenticate. This can be used to set ProtocolMessage.State
// that will be persisted through the authentication process. The ProtocolMessage can also be used to add or customize
// parameters sent to the identity provider.
ctxt.ProtocolMessage.RedirectUri = "https://example.org/signin-oidc";
await Task.Yield();
}
};
});
With that the redirect worked, but I entered a loop between the protected page and the AzureB2C login.
After a succesful login and a correct redirect to the signin-oidc controller (created by the Identity.Web package) I was correctly redirected again to the page that started all this authorization thing, but there it did not find the authorization. So I added/modded also this:
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.Secure = CookieSecurePolicy.Always;
});
With this the authorization worked, but I was not able to get the token to call the downstream API, before this redirect thing ITokenAcquisition worked, but now when trying to get the token it throws an exception.
So in my controller/service to get the token I modified and used:
var accessToken = await _contextAccessor.HttpContext
.GetTokenAsync(OpenIdConnectDefaults.AuthenticationScheme, "access_token");
So now with the token I add it to my HttpRequestMessage like this:
request.Headers.Add("Authorization", $"Bearer {accessToken}");
I lived on StackOverflow and microsoft docs for 3 days, I am not sure this is all "recommended" but this worked for me.
I had the same problem running an asp.net application under Google Cloud Run, which terminates the TLS connection. I was getting the error:
AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application.
Using fiddler, I examined the request to login.microsoftonline.com and found that the query parameter redirect_uri exactly matched the url I'd configured in the application in Azure except that it started http rather than https.
I initially tried the other answers involving handling the OpenIdConnectEvents event and updating the redirect uri. This fixed the redirect_url parameter in the call to login.microsoftonline.com and it then worked until I added in the graph api. Then I found my site's signin-oidc page would give its own error about the redirect uri not matching. This would then cause it to go into a loop between my site and login.microsoftonline.com repeatedly trying to authenticate until eventually I'd get a login failure.
On further research ASP.net provides middleware to properly handle this scenario. Your SSL load balancer should add the standard header X-Forwarded-Proto with value HTTPS to the request. It should also send the X-Forwarded-For header with the originating IP address which could be useful for debugging, geoip etc.
In your ASP.net application, to configure the middleware:
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
Then enable the middleware:
app.UseForwardedHeaders();
Importantly, you must include this before the calls to app.UseAuthentication/app.UseAuthorization that depends on it.
Source: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0
If your load balancer doesn't add the X-Forwarded-Proto header and can't be configured to do so then the document above outlines other options.
I was facing with similar issue for 3 days. The below code helped me to get out of the issue.
string[] initialScopes = Configuration.GetValue<string>("CallApi:ScopeForAccessToken")?.Split(' ');
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddInMemoryTokenCaches();
services.AddControllers();
services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser().Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SaveTokens = true; // this saves the token for the downstream api
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctxt =>
{
ctxt.ProtocolMessage.RedirectUri = "https://example.org/signin-oidc";
await Task.Yield();
}
};
});
I am Authenticating against an OAuth endpoint where I can only configure 1 callback domain. (and localhost is whitelisted).
I have my web app running in Azure (myapp.azurewebsites.net) and have it available with two custom domains (myapp.cc and myapp.eu). When I use the default setup, the CallbackPath can only be a relative path (to the current domain)
The code documentation of CallbackPath indicates it's relative to the application's base path:
/// <summary>
/// The request path within the application's base path where the user-agent will be returned.
/// The middleware will process this request when it arrives.
/// </summary>
public PathString CallbackPath { get; set; }
I want to make sure the CallBack happens to the (only) domain that I whitelisted on the OAuth backend. I know I can implement everything manually, but I was hoping there would be an easy way to work around this design and still benefit from the baked in Authentication options.
So even if a user is logging on on the myapp.cc or the myapp.eu or the myapp.azurewebsites.net , it should redirect to myapp.azurewebsites.net/ (which is whitelisted on my Auth service)
A part of my Startup.cs file is pasted below:
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "MyService";
})
.AddCookie()
.AddOAuth("MyService", "MyService",
options =>
{
options.ClientId = settings.ClientId;
options.ClientSecret = settings.ClientOauthSecret;
options.CallbackPath = "/relativeonlypath";
options.SaveTokens = true;
options.SignInScheme = IdentityConstants.ExternalScheme;
/// ... removed for brevity
}
);
Any idea on how to implement this?
Thank you
I'm not sure it's possible, because to verify that the user is redirected to your application as part of a "genuine" authentication flow, the ASP.NET OAuth handler performs the following steps:
Before redirecting the user to the OAuth service, ASP.NET Core generates a "correlation" cookie that is tied to the current domain; and
When the user is redirected to the app, the handler looks for this cookie and validates its content.
So if the correlation cookie is generated in step #1 for one domain, let's say myapp.cc, and the user is redirected to another domain, myapp.azurewebsites.net, ASP.NET Core might not be able to read it because the browser will not have included it in the redirection request.
Note
As seen in the first comments, the original thought was to leverage the SameSiteproperty of the correlation cookie to have it sent by the browser to the second domain.
This was all wrong, apologies!
I now think that you have 2 different options:
Redirect every request from myapp.cc and myapp.eu to myapp.azurewebsites.net, so that when the authentication flow happens, we're already on the right domain; or
Redirect the user to the myapp.azurewebsites.net domain before redirecting them to the OAuth server.
I won't go into the first solution, as there's plenty of ways to achieve this.
Here's some code that I haven't tested that could work for the second solution:
services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "MyService";
})
.AddCookie()
.AddOAuth("MyService", options =>
{
options.Events.OnRedirectToAuthorizationEndpoint = context =>
{
var currentRequestUri = new Uri(context.Request.GetDisplayUrl());
// 1. If we're not on the correct domain, redirect the user to the same page, but on the expected domain.
// The assumption is that the authentication flow will also kick in on the other domain (see 2).
if (currentRequestUri.Host != "myapp.azurewebsites.net")
{
var applicationRedirectUri = new UriBuilder(currentRequestUri)
{
Host = "myapp.azurewebsites.net"
}.Uri.ToString();
context.Response.Redirect(applicationRedirectUri);
return Task.CompletedTask;
}
// 2. If we reach here, it means we're on the right domain, so we can redirect to the OAuth server.
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});
I am having some issues after making some tweaks to an IdentityServer4 Quickstart sample solution, specifically the 8_AspNetIdentity sample.
I'll preface this by saying I'm not sure if what I'm trying to do is just not supported, or if I'm doing it wrong.
This sample solution contains the following projects relevant to my question:
an IdentityServer,
an MVC client (named MVCClient) that uses OpenIdConnect to authenticate its users,
a web API client (named API) that uses bearer authentication for its users
a console app (named ResourceOwnerClient) designed to be a client of the API
What I am trying to do is merge the API project into the MVCClient, so that the MVCClient could both authenticate the users from its MVC website with OIDC, and also the ResourceOwnerClient using bearer authentication.
I made the following changes to the MVCClient's Startup.cs:
changed services.AddMvc(); to:
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder(new[]
{
JwtBearerDefaults.AuthenticationScheme,
CookieAuthenticationDefaults.AuthenticationScheme,
"oidc"
})
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
added JWT bearer options to the services.AddAuthentication():
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
})
Now technically this did work, as both the ResourceOwnerClient and the MVC users can successfully authenticate with the MVCClient. I however have one caveat:
When I authenticate with a user from the MVC side, I noticed that there are two identities in my current User. Both are identical in terms of claims, etc. This only happens when I put a breakpoint in the MVCClient, on the IdentityServer there is only one identity.
On the IdentityServer, I have registered a UserClaimsPrincipalFactory which adds my own custom claims to the ClaimsIdentity. In the two identities on the IdentityServer, I can see the claims duplicated. So instead of having one identity with two custom claims, I see two identities which each have 4 custom claims. The CreateAsync method in my UserClaimsPrincipalFactory is also getting hit 5 times for a single login.
Although this behaviour is strange, it does not seem to be having any negative impacts. But this is only a proof of concept for a larger application that I'm building, and I'm afraid I may run into issues in the future because of it.
If anyone has attempted this sort of thing before, or knows why this behaviour could be happening, any help would be appreciated.
While nothing bad should happen with this design, I would completely remake it. Why? Because you are mixing a Client and an ApiResource, and they should be logically separated. A Client is an application, something some user interacts with, even if it was a headless one (i.e an automated service); while an ApiResource consists of resources that are provided to Clients, so no user can interact with it directly.
You could add two authentications against IdentityServer, one as API (and add it as JwtBearer) and one as a Client (and add it as Cookies). You can then use [Authorize(AuthenticationSchemes = "JwtBearer")] and = "Cookies" depending on the function of that Action/Controller.
Leaving that aside, the problem is that your application is getting one Identity for the MVC side and one for the API side, since it has no way of telling which one you want.
Just so you have an idea, this is how one of my IdentityServers with ASP.NET Core Identtiy look like, where you can login against it using the UI and also hit the REST endpoints with a JwtToken:
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = Configuration["IdentityServerUrl"];
options.ApiName = Configuration["ApiName"];
options.RequireHttpsMetadata = false;
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
})
.AddCookie(IdentityConstants.ExternalScheme, o =>
{
o.Cookie.Name = IdentityConstants.ExternalScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5.0);
})
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
})
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5.0);
});
Haven't found any really helpful articles or SO questions regarding this problem.
I want to behave my API in the following way:
All controller actions should only be available for cookie authenticated users
There are 2 login actions for getting a cookie by form (username/pw) or sso with the windows user (this is rather easy to do)
My problems with this currently:
When the API returns 401 (the correct status code) for a controller action, the browser automatically defaults to re-triggering the request with a NTLM token. The user doesn't have a cookie and is still able to call the API action because the windows authentication scheme is still registered somehow
It works as I want it to when I decorate ALL of the API actions with:
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
In this case it seems that the Windows Authentication Scheme is never used but only cookies, which is what I want.
I want this to be the only allowed behavior though without having to decorate all of the actions explicitly and never allow Windows Authentication Scheme (apart from the windows login action)
I can also kind of getting it working when I make sure that the Cookie Authentication doesn't return 401 but returns 403, which doesn't make the browser to re-trigger the request with NTLM authentication. This is a workaround at best though.
The relevant parts of Startup.cs currently look like this:
services.AddAuthentication(opt =>
{
opt.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(ConfigureApplicationCookie);
Cookie config:
private static void ConfigureApplicationCookie(CookieAuthenticationOptions options)
{
options.Cookie.Name = Assembly.GetEntryAssembly().GetName().Name;
options.Cookie.HttpOnly = true;
options.Cookie.Expiration = TimeSpan.FromHours(1);
options.SlidingExpiration = true;
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
};
options.LogoutPath = null;
options.LoginPath = null;
}
IIS (and IIS express) is configured to use Anonymous and Windows Authentication.
I've a .Net Core 2.2 web application MVC in which I've added API controllers and SignalR hubs. On the other side, I've a mobile app that calls the hub methods. Before calling hubs from the app, I am authenticating my users through an API call - getting back a JWT Token - and using this token for future requests, this way I can use Context.User.Identity.Name in my hub methods:
public static async Task<string> GetValidToken(string userName, string password)
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(_API_BASE_URI);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
LoginViewModel loginVM = new LoginViewModel() { Email = userName, Password = password, RememberMe = false };
var formContent = Newtonsoft.Json.JsonConvert.SerializeObject(loginVM);
var content = new StringContent(formContent, Encoding.UTF8, "application/json");
HttpResponseMessage responseMessage;
try
{
responseMessage = await client.PostAsync("/api/user/authenticate", content);
var responseJson = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); ;
var jObject = JObject.Parse(responseJson);
_TOKEN = jObject.GetValue("token").ToString();
return _TOKEN;
}catch
[...]
Then using the token:
_connection = new HubConnectionBuilder().WithUrl(ApiCommunication._API_BASE_URI + "/network", options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
}).Build();
So far so good. It's working as expected on my mobile app. But in order to make it work I had to set this piece of code on server side (Startup.cs):
services.AddAuthentication(options =>
{
options .DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options .DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
...
This prevents me for using cookie authentication anymore and therefore the mvc web app is no more working as expected as it's not able to get the current authenticated user amongs requests.
Removing the lines:
options .DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options .DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
makes the web app working correctly but not the mobile app anymore (hub calls fail due to Context.User.Identity.Name equals to null).
I've been searching all around about how to handle different schemes (in my case cookie + jwt) and from my understanding, this is by design not possible anymore.
Is there any possible workaround to use double scheme or am I missing something?
I thought maybe I shoud host 2 separate projects instead and use one with Cookie authentication and the other one with JWT?
Thanks in advance.
There are multiple ways to solve the issue you encounter, but first let's go through why it's not currently working.
What DefaultAuthenticateScheme means
When you set a value to the DefaultAuthenticateScheme property of AuthenticationOptions, you instruct the authentication middleware to try and authenticate every single HTTP request against that specific scheme. I'm going to assume that you're using ASP.NET Identity for cookie-based authentication, and when you call AddIdentity, it registers the cookie authentication scheme as the default one for authentication purposes; you can see this in the source code on GitHub.
However, it doesn't mean you can't use any other authentication scheme in your application.
The authorization system default policy
If all the protected endpoints of your application are meant to be accessible to clients authenticated with cookies or JWTs, one option is to use the authorization system default policy. That special policy is used when you use "empty" instances of the AuthorizeAttribute class — either as an attribute to decorate controllers/actions, or globally at the app level with a new AuthorizeFilter(new AuthorizeAttribute()).
The default policy is set to only require an authenticated user, but doesn't define which authentication schemes need to be "tried" to authenticate the request. The result is that it relies on the authentication process already having been performed. It explains the behavior you're experiencing where only one of the 2 schemes works at a time.
We can change the default policy with a bit of code:
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("<your-cookie-authentication-scheme", "your-jwt-authentication-scheme")
.Build();
})
Specific authorization policies
If you find yourself in a situation where you require some endpoints to only be accessible to clients authenticated with cookies and others with JWTs, you can take advantage of authorization policies.
They work exactly like the default policy, expect you get to pick on an endpoint basis which one applies. You can add policies like so:
services.AddAuthorization(options =>
{
options.AddPolicy("Cookies", new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("<your-cookie-authentication-scheme")
.Build());
options.AddPolicy("JWT", new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("<your-jwt-authentication-scheme")
.Build());
})
You can then refer to these policies in appropriate endpoints by decorating them with [Authorize(Policy = "<policy-name>")]. As a side note, if the only differentiator between your policies is the authentication scheme, it's possible to achieve the same result without creating policies, and referring to the appropriate authentication scheme(s) in [Authorize] attributes with the AuthenticationSchemes property.
Policies are valuable when you have more complex rules, like that specific claim needs this specific value, for example.
I hope this helps, let me know how you go! 👍