I'm new to AzureAD authentication. I setup my Web API with below settings in startup.cs
services.AddAuthentication(sharedopt => sharedopt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer("AzureAd", options =>
{
options.Audience = Configuration.GetValue<string>("AzureAd:Audience");
options.Authority = Configuration.GetValue<string>("AzureAd:Instance")
+ Configuration.GetValue<string>("AzureAd:TenantId");
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidIssuer = Configuration.GetValue<string>("AzureAd:Issuer"),
ValidAudience = Configuration.GetValue<string>("AzureAd:Audience")
};
});
I am expecting my Client App (Angular) will attach Authorization header in its requests and thus it will get access to API
But when I execute the Web API and trying to open any API with Authorize, it triggers this error
InvalidOperationException: No authenticationScheme was specified, and
there was no DefaultChallengeScheme found. The default schemes can be
set using either AddAuthentication(string defaultScheme) or
AddAuthentication(Action configureOptions).
I already specified JWTBearerDefaults.AuthenticationScheme. Still why its not accepting?
Please remove the first "AzureAd" parameter from AddJwtBearer call.
TLDR: When you call AddAuthentication you set the default scheme to JwtBearerDefaults.AuthenticationScheme which is string "Bearer".
This tells the authentication middleware to authenticate all requests (unless specified otherwise e.g. via Authorize attribute with schemes) to use a set of handlers and configurations organized by the shceme name "Bearer".
However you didn't register that scheme. Your call to AddJwtBearer registers a scheme named "AzureAd" instead of "Bearer".
Authentication middleware cannot find the matching scheme and hence the error.
If you don't specify the "AzureAd" parameter, below version of AddJwtBearer is invoked:
builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, configureOptions);
As we can see, it registers the JwtBearer authentication with scheme "Bearer" matching your default scheme.
You might have missed to register
services.ConfigureApiAuthentication(Configuration); in the startup class
Make sure you don't have an [Authorize] attribute above your Controller class definition or any associated methods for which you don't want any Authentication. I copy/pasted a Controller class and didn't notice it had an [Authorize] attribute above the class definition. I removed that and the problem was resolved.
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'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! 👍
Is there any way to tell IdentityServer4's authentication system to allow multiple issuers for the tokens?
I have an application that is using Identity Server to issue bearer tokens, and as long as the front end and the back end use the same URL to get tokens from authentication works fine.
However, I now have a need to have the same site accessed through multiple CNAMEs, meaning that the client will request tokens from two different URLs.
The error that is sent to the logs is:
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware[7]
Bearer was not authenticated. Failure message: IDX10205: Issuer validation failed. Issuer: 'http://domainb.com'. Did not match: validationParameters.ValidIssuer: 'http://domaina.com' or validationParameters.ValidIssuers: 'null'.
The presence of a ValidIssuers collection seems to indicate that you can set multiple places from which the API will accept tokens, but I cannot find anything like that exposed in options exposed by UseIdentityServerAuthentication.
I am aware of the Authority option, but that only allows me to set a single valid authority.
Is there are any way of setting multiple valid issuers, or setting it to use something other than the hostname as the issuer id?
UPDATE
My identity server configuration on the server side looks like this:
services.AddIdentityServer(options => {
options.IssuerUri = "http://authserver"; })
.AddAspNetIdentity<ApplicationUser>();
this is from the auth server side of things.
On the client API, the UseIdentityServerAuthentication call looks like this:
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions()
{
Authority = AppSettingsConfigurationRoot["Authentication:AuthorityEndpoint"],
RequireHttpsMetadata = false,
ApiName = "rqapi",
AutomaticAuthenticate = true,
ClaimsIssuer = "http://localhost:5001"
});
The address in the {{AppSettingsConfigurationROot["Authentication:AuthorityEndpoint"] is usually set at the public DNS name of the server so that the token issuer as seen by AngularJS matches the URL of the IdentityServer from the point of view of the C# API.
As Original Poster wrote in a comment, the (now, 2020, deprecated) IdentityServer4.AccessTokenValidation package doesn't expose the right options. To read more about the recent deprecation check this blogpost, but if you still are using it, here's how I solved this issue.
The AddIdentityServerAuthentication(...) extension method is a wrapper (the code is super readable!) to combine two authentication schemes:
JwtBearer
OAuth2Introspection
It uses its own configuration class, and simply doesn't expose all the JwtBearer options (possibly just an omission, possibly because some options are not valid for both schemes.
If -like me- you only need JwtBearer you might get away with simply using just that, and using the ValidIssuers array. So:
services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.Authority = "https://example.org";
options.Audience = "foo-api"; // options.ApiName in the IDS4 variant
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuers = new[]
{
"https://example.org", // first issuer
"https://example.com", // some other issuer
},
NameClaimType = "name", // To mimick IDS4's variant
RoleClaimType = "role", // To mimick IDS4's variant
};
});
As far as I understand, this will use example.org as the Authority and get the openid-configuration and so forth from that domain. But any JWT token offered to this API would be accepted as long as one of the ValidIssuers is the iss (issuer claim) in the token.
On my current webapi project I have set a swagger oauth security definition with implicit flow and authorize url https://login.microsoftonline.com/ + tenant Id. The scopes are the same as in the github exapmle for the Swashbuckle.AspNetCore nuget , this is the link https://github.com/domaindrivendev/Swashbuckle.AspNetCore. But when i try to authenticate on swagger online editor, this one https://editor.swagger.io/, I can't get the token back and get a 404 exception. What do I need to set in my azure portal registered app to return a token back to the online swagger editor ?
According to your description, I created my .Net Core 2.0 Web API application and created the AAD app on Azure Portal. The configuration under ConfigureServices would look like this:
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
// Configure the app to use Jwt Bearer Authentication
.AddJwtBearer(jwtOptions =>
{
jwtOptions.Authority = string.Format(CultureInfo.InvariantCulture, "https://sts.windows.net/{0}/", Configuration["AzureAd:TenantId"]);
jwtOptions.Audience = Configuration["AzureAd:WebApiApp:ClientId"];
});
For Swagger UI, I also created a new AAD app on Azure Portal and add permissions to access the Web API app as follows:
Then, I added the following code snippet for defining the OAuth2.0 scheme as follows:
// Define the OAuth2.0 scheme that's in use (i.e. Implicit Flow)
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = string.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/oauth2/authorize", Configuration["AzureAd:TenantId"]),
Scopes = new Dictionary<string, string>
{
{ "user_impersonation", "Access Bruce-WebAPI-NetCore" }
}
});
// Enable operation filter based on AuthorizeAttribute
c.OperationFilter<SecurityRequirementsOperationFilter>();
And use the following code for initializing the middleware to serve swagger-ui.
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.ConfigureOAuth2(
Configuration["AzureAd:SwaggerApp:ClientId"],
Configuration["AzureAd:SwaggerApp:ClientSecret"],
Configuration["AzureAd:SwaggerApp:RedirectUri"], //http://localhost:30504/swagger/o2c.html
"Bruce-WebAPI-NetCore-Swagger",
additionalQueryStringParameters: new Dictionary<string, string>(){
{ "resource",Configuration["AzureAd:WebApiApp:ClientId"]}
});
});
Test:
but it still returns AADSTS50001 Resource identifier is not provided
During my processing, I encountered the similar issue. At last, I found that the resource parameter is not specified. Then, I set the additionalQueryStringParameters parameter for ConfigureOAuth2. Here is my code sample WebAPI-Swagger-NetCore, you could refer to it.
Moreover, for adding access scopes to your resource application (Web API), you could follow the Adding access scopes to your resource application section under here. Also, my SecurityRequirementsOperationFilter did not assign the scope requirements to operations based on AuthorizeAttribute provided here. You could specific the supported scopes under AddSecurityDefinition, then for your controller or action you could mark it as [Authorize(AuthenticationSchemes = "Bearer", Policy = "{scope}")]. Details, you could follow this sample.
I have Auth Service hosted on some url. All my microservices requested validation to auth on each requests. In StartUp.cs of each services I have
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = WebConfigurationManager.AppSettings["IdentityServerURL"],
ValidationMode = ValidationMode.ValidationEndpoint,
//ValidationMode = ValidationMode.Local,
RequiredScopes = new[] { "user-api" },
});
It works fine! And in my controller's method in this case I have as you can see
{role: consumer}
But if I change
ValidationMode = ValidationMode.Local,
My request doesn't pass Authorization because values of roles has prefixes
And according to this my request doesn't pass autorization. What should I do in case
ValidationMode = ValidationMode.Local
to have normal value of claims role?
Microsoft apply a claims mapping when the token is received. To remove this default mapping, add this to your Configuration method at startup:
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
For more information:
How to use InboundClaimTypeMap for claim mapping?
Update of System.IdentityModel.Tokens.Jwt causing breaking change in IdentityServer3 Client