I have 3 projects:
Client App
ASP.NET API App
IdentityServer4 MVC App
I am able to send a request from API to IDP but trying to send a request from Client to IDP yields
"CORS request made for path: /api/Trial/TrialAction from origin: https://localhost:44389 but
was ignored because path was not for an allowed IdentityServer CORS endpoint"
even though I added the following to the IDP:
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", policyBuilder => policyBuilder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
and
// ...
app.UseRouting();
app.UseIdentityServer();
app.UseCors("CorsPolicy");
app.UseAuthorization();
// ...
The interesting part is, I can send a request from API to IDP without adding CORS configuration to IDP. What am I doing wrong?
Config.cs:
public static class Config
{
public static IEnumerable<IdentityResource> Ids =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
};
public static IEnumerable<ApiResource> Apis =>
new ApiResource[]
{
new ApiResource("myapi",
"My API",
new [] { "membershipType" }
)
};
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "mywebclient",
ClientName = "My Web Client",
AllowedGrantTypes = GrantTypes.Code, // Authorization code flow with PKCE protection
RequireClientSecret = false, // Without client secret
RequirePkce = true,
RedirectUris = { "https://localhost:44389/authentication/login-callback" },
PostLogoutRedirectUris = { "https://localhost:44389/authentication/logout-callback" },
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"albidersapi"
},
AllowedCorsOrigins = { "https://localhost:44389" },
RequireConsent = false,
}
};
}
do yo have the client and API in the same project as IdentityServer? I typically recommend that you keep them apart.
A wild guess could be to swap these two lines:
app.UseIdentityServer();
app.UseCors("CorsPolicy");
Because apparently IdentityServer captures the request to the API?
The most likely issue is that your call from your client to your API is not including the access token.
The debug log is coming from this file here. If you look at where your debug statement is originating from you will see that it is checking if the path matches any within IdentityServerOptions.Cors.CorsPaths. Here is an image of what those paths generally are from a debug service I made.
These paths are just the default information and authentication endpoints for IdentityServer4. In other words it thinks your request is unauthenticated because it likely isn't including the access token.
If you are using IdentityServer4's template logging implementation with Serilog, then you can also add this to your appsettings.json to see what the ASP.NET Core CORS middleware has to say. It will be logging after IdentityServer4's log
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.Authentication": "Debug",
"Microsoft.AspNetCore.Cors": "Information",
"System": "Warning"
}
}
}
Here is what my debug log looked like when I made a request to an endpoint with a proper CORS policy, but the request didn't include its access token.
[21:05:47 Debug] IdentityServer.Hosting.CorsPolicyProvider CORS request made for path: /api/v1.0/users/{guid}/organizations from origin: https://localhost:44459 but was ignored because path was not for an allowed IdentityServer CORS endpoint
[21:05:47 Information] Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware No CORS policy found for the specified request.
So it's not a CORS issue really. It's an access token or authentication issue. It is also possible, however, that your endpoint isn't being hit properly. However, you should be receiving a 404 on the client in addition to the log seen above.
Related
I'm having some issues with an ASP.NET Core 6 Web API and a react front end using firebase auth. I get a 401 every time the react app requests an authorized endpoint (but 200 with postman).
Using ASP.NET Core 6
I know the token I am using works fine because when I request with postman using the same bearer token I get a 200 response.
I have also tried to set ValidateIssuer = false & ValidateAudience = false & ValidateLifetime = false with no luck
Front end request (when the user logs in via the firebase/auth signInWithEmailAndPassword method
const testFetch = async () => {
getIdToken(auth.currentUser!).then(async (token) => {
const res = await fetch('https://localhost:51437/test/private', {
method: 'GET',
headers: {
Authentication: `Bearer ${token}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
const result = await res.json();
console.log(result);
});
};
I can also request non authorized endpoints from my web app and get them correctly so shouldn't be anything to do with cors
Adding JWT bearer auth scheme:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.IncludeErrorDetails = true;
opt.Authority = $"https://securetoken.google.com/{builder.Configuration["Firebase:ID"]}";
opt.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidIssuer = $"https://securetoken.google.com/{builder.Configuration["Firebase:ID"]}",
ValidateAudience = true,
ValidAudience = builder.Configuration["Firebase:ID"],
ValidateLifetime = true
};
});
Setup for auth:
app.UseCors(x => x.AllowAnyMethod().AllowAnyHeader().SetIsOriginAllowed(origin => true).AllowCredentials());
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
Controller:
[ApiController]
[Route("[controller]")]
public class TestController : Controller
{
public IActionResult Index()
{
return Ok("Hello world");
}
[HttpGet("private")]
[Authorize]
public IActionResult Private()
{
return Ok(new
{
Message = "Hello from a private endpoint!"
});
}
}
Request logs
[00:41:14 DBG] AuthenticationScheme: Bearer was not authenticated.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
[00:41:14 INF] Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
AuthenticationScheme: Bearer was challenged.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: AuthenticationScheme: Bearer was challenged.
[00:41:14 INF] AuthenticationScheme: Bearer was challenged.
After debugging it looks like my API seems to be removing the Authorization header from my front end app which is an expo web app (react) but not when the request is from postman.
The request is sent at-least in the network tab with the correct bearer
Using this or https://nikiforovall.github.io/aspnetcore/dotnet/2022/08/24/dotnet-keycloak-auth.html tutorial I have setup test user and realm. I can call localhost:8080/realms/Test/protocol/openid-connect/token with client secret and user id and password from postman and it gives me access and refresh token. Now I need to call dotnet endpoint and make sure the user is who he is. But I can not find a way to establish this part as I'm always getting 401 unauthorized. Perhaps it is not setup or my authorization bearer string is not formed correctly.
How can I simply call to an endpoint, check authorization and return a response back?
Dotnet Code:
using System.Security.Claims;
using Api;
using Keycloak.AuthServices.Authentication;
using Keycloak.AuthServices.Authorization;
using Keycloak.AuthServices.Sdk.Admin;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
var host = builder.Host;
host.ConfigureLogger();
services
.AddEndpointsApiExplorer()
.AddSwagger();
var authenticationOptions = configuration
.GetSection(KeycloakAuthenticationOptions.Section)
.Get<KeycloakAuthenticationOptions>();
services.AddKeycloakAuthentication(authenticationOptions);
var authorizationOptions = configuration
.GetSection(KeycloakProtectionClientOptions.Section)
.Get<KeycloakProtectionClientOptions>();
services
.AddAuthorization(o => o.AddPolicy("IsAdmin", b =>
{
b.RequireResourceRoles("default-roles-test");
/*b.RequireRealmRoles("admin");
b.RequireResourceRoles("r-admin");
// TokenValidationParameters.RoleClaimType is overriden
// by KeycloakRolesClaimsTransformation
b.RequireRole("r-admin");*/
})
)
.AddKeycloakAuthorization(authorizationOptions);
var adminClientOptions = configuration
.GetSection(KeycloakAdminClientOptions.Section)
.Get<KeycloakAdminClientOptions>();
services.AddKeycloakAdminHttpClient(adminClientOptions);
var app = builder.Build();
app
.UseSwagger()
.UseSwaggerUI();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/", (ClaimsPrincipal user) =>
{
// TokenValidationParameters.NameClaimType is overriden based on keycloak specific claim
app.Logger.LogInformation("{#User}", user.Identity.Name);
return "Hello world. "+ user.Identity.Name;
}).RequireAuthorization("IsAdmin");
app.Run();
appsettings.json keycloack config:
"Keycloak": {
"realm": "Test",
"auth-server-url": "http://localhost:8080/",
"ssl-required": "none",
"resource": "test-client",
"verify-token-audience": false,
"client-secret": "P4JgvFhjY0ftGSLDYmYn7diZhjoLnHon",
"confidential-port": 0
}
Request sending to this endpoint from postman (perhaps the issue is here with correct sending format):
I have setup a web api to call from Microsoft Graph using a username/password credential passed to it via a vuejs client application. As I tried to extend it out and add new permissions (namely Team.ReadBasic.All). I get a:
Missing scope permissions on the request. API requires one of 'Team.ReadBasic.All, TeamSettings.Read.All, TeamSettings.ReadWrite.All, User.Read.All, Directory.Read.All, User.ReadWrite.All, Directory.ReadWrite.All'. Scopes on the request 'AllSites.Read, openid, profile, Tasks.ReadWrite, User.Read, User.ReadBasic.All, email'
However I have updated my Azure Ad Application with the correct API Permissions (Delegated Team.ReadBasic.All) and I have added the scope like so
appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "mydomain",
"TenantId": "mytenantId",
"ClientId": "myclientId",
"ClientSecret": "myclientsecret",
"Scopes": "User.Read,Tasks.ReadWrite,Team.ReadBasic.All",
"CallbackPath": "/signin-oidc"
},
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "User.Read,Tasks.ReadWrite,Team.ReadBasic.All"
With my startup using middleware to test the api.
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
Configuration.Bind("AzureAd", options);
options.Authority = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/v2.0";
options.TokenValidationParameters.ValidAudiences = new string[] { Configuration["AzureAd:ClientId"], $"api://{Configuration["AzureAd:ClientId"]}" };
});
var userPassCodeCredentail = new UsernamePasswordCredential("", "", Configuration["AzureAd:TenantId"], Configuration["AzureAd:ClientId"]);
services.AddSingleton(_ => new GraphServiceClient(userPassCodeCredentail, Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(',')));
AddSwagger(services);
With Add Swagger Being
services.AddOpenApiDocument(document =>
{
document.AddSecurity("bearer", Enumerable.Empty<string>(), new NSwag.OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.OAuth2,
Description = "Azure AAD Authentication",
Flow = OpenApiOAuth2Flow.Implicit,
Flows = new NSwag.OpenApiOAuthFlows()
{
Implicit = new OpenApiOAuthFlow()
{
Scopes = new Dictionary<string, string>
{
{ $"api://{Configuration["AzureAd:ClientId"]}/user_impersonation", "Access Application" },
{ $"api://{Configuration["AzureAd:ClientId"]}/Team.ReadBasic.All", "Access Team" },
},
AuthorizationUrl = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/v2.0/authorize",
TokenUrl = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/v2.0/token",
},
},
});
document.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("bearer"));
Attempting to authorize this way doesn't allow me to consent to the new permission to then use the api call. As the consent panel won't appear when I complete the Microsoft page login process.
Edited: I have added the api permissions to the Azure Ad and ensured that the redirect urls and user consent is set to "Allow consent for all apps"
Try changing the user consent settings
Go to Azure Active Directory and then Enterprise applications from their consent and permissions now select User Consent settings
Under User consent for applications choose the consent settings you want to provide to user
Can you follow the tutorial here and ensure you have successfully completed all the steps?
Add Azure AD authentication
I'm currently developing an Angular 8 Frontend & asp.net core 3.1 backend application where I use Azure AD to authenticate users.
The flow of the application goes as followed.
The user tries to login or access a route that is protected/guarded in my Angular app so the user gets redirected to the Azure login page. So now the Angular app has the required token to send along to my asp backend. My backend receives the token along with an API request. backend checks token & responds to the API call.
PROBLEM
When making the API call, I get redirected to the login page, but after logging in. I get
this error.
After I log in, I receive a valid token from azure via the URL: http://localhost:4200/#id_token=<< TOKEN >>
These are the headers from the request found in the network tab in my browser.
Not sure about this, but I don't see any tokens passed in those headers. Although I'm using HTTP INTERCEPTORS to add the token to my headers of each request. Can this be the problem?
MY CODE
Backend
Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//other config services
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureActiveDirectory", options));
services.AddCors((opt =>
{
opt.AddPolicy("FrontEnd", builder => builder
.WithOrigins("http://localhost:4200")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
);
}));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//other configurations
app.UseAuthentication();
app.UseAuthorization();
app.UseCors("FrontEnd");
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Simple API
[Route("api/[controller]")]
[ApiController]
[EnableCors("FrontEnd")]
[Authorize]
public class GezinnenController : ControllerBase
{
//Constructor & properties
// GET: api/Gezinnen
[HttpGet]
public async Task<ActionResult<IEnumerable<GezinForLijstDto>>> GetGezinnen()
{
var gezinnen = await _unitOfWork.GezinRepo.GetAllAsync(null, "Personen,Gemeente");
var gezinnenForLijstDto = _mapper.Map<IEnumerable<GezinForLijstDto>>(gezinnen);
return Ok(gezinnenForLijstDto);
}
Angular
App.module.ts
import { HttpClientModule, HTTP_INTERCEPTORS } from '#angular/common/http';
import { MsalModule, MsalInterceptor } from '#azure/msal-angular';
//other imports
export const protectedResourceMap:
[string, string[]][] = [['https://localhost:5001/api/gezinnen', ['api://<<API Client ID>>/api-access']] ];
#NgModule({
declarations: [
...
],
imports: [
...
MsalModule.forRoot({
clientID: '<<Frontend Client ID>>',
authority: 'https://login.microsoftonline.com/<<API tenant ID>>',
consentScopes: [ 'user.read', 'api://<<API Client ID>>/api-access' ],
protectedResourceMap: protectedResourceMap }),
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: MsalInterceptor,
multi: true
}],
bootstrap: [
AppComponent
]
})
export class AppModule { }
Can someone help me fix this? I'm starting to get desperate :-)
To resolve CORS error, In your startup.cs, Can you move app.UseCors("FrontEnd") above app.UseAuthentication()
Workround:
1.Modify the server to add the header Access-Control-Allow-Origin: * to enable cross-origin requests from anywhere (or specify a domain instead of *). This should solve your problem.
2.Using Angular proxy
For more details, you could refer to this article.
I am using Visual Studio 2015 Enterprise and ASP.NET vNext Beta8 to build an endpoint that both issues and consumes JWT tokens. I Originally approached this by generating the tokens myself, as described here.
Later a helpful article by #Pinpoint revealed that AspNet.Security.OpenIdConnect.Server (a.k.a. OIDC) can be configured to issue and consume the tokens for me.
So I followed those instructions, stood up an endpoint, and by submitting an x-www-form-urlencoded post from postman I receive back a legit token:
{
"token_type": "bearer",
"access_token": "eyJ0eXAiO....",
"expires_in": "3599"
}
This is great but also where I get stuck. Now, how do I annotate a controller action so that it demands this bearer token?
I thought all I would have to do is decorate my controller method with the
[Authorize("Bearer")], add an authentication scheme:
services.AddAuthorization
(
options =>
{
options.AddPolicy
(
JwtBearerDefaults.AuthenticationScheme,
builder =>
{
builder.
AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).
RequireAuthenticatedUser().
Build();
}
);
}
);
And then call my controller action with the "Authorization bearer eyJ0eXAiO...." header as I had done in my previous example. Sadly, all this approach seems to do though is generate an exception:
An unhandled exception occurred while processing the request.
SocketException: No connection could be made because the target machine actively refused it 127.0.0.1:50000
WebException: Unable to connect to the remote server
HttpRequestException: An error occurred while sending the request.
IOException: IDX10804: Unable to retrieve document from: 'http://localhost:50000/.well-known/openid-configuration'.
Microsoft.IdentityModel.Logging.LogHelper.Throw(String message, Type exceptionType, EventLevel logLevel, Exception innerException)
InvalidOperationException: IDX10803: Unable to obtain configuration from: 'http://localhost:50000/.well-known/openid-configuration'. Inner Exception: 'IDX10804: Unable to retrieve document from: 'http://localhost:50000/.well-known/openid-configuration'.'.
Consider the following steps to reproduce (but please don't consider this production worthy code):
Apply the ASP.NET Beta8 tooling as described here
Open Visual Studio Enterprise 2015 and create a new Web API ASP.NET 5 Preview Template project
Change project.json
{
"webroot": "wwwroot",
"version": "1.0.0-*",
"dependencies": {
"Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8",
"Microsoft.AspNet.Mvc": "6.0.0-beta8",
"Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8",
"Microsoft.AspNet.Authentication.JwtBearer": "1.0.0-beta8",
"AspNet.Security.OpenIdConnect.Server": "1.0.0-beta3",
"Microsoft.AspNet.Authentication.OpenIdConnect": "1.0.0-beta8",
"Microsoft.Framework.ConfigurationModel.Json": "1.0.0-beta4",
"Microsoft.AspNet.Diagnostics": "1.0.0-beta8"
},
"commands": {
"web": "Microsoft.AspNet.Server.Kestrel"
},
"frameworks": {
"dnx451": { }
},
"exclude": [
"wwwroot",
"node_modules"
],
"publishExclude": [
".user",
".vspscc"
]
}
Change Startup.cs as follows (this is courtesy of #Pinpoint's original article; I have removed comments and added the AddAuthorization snip):
public class Startup
{
public Startup(IHostingEnvironment env)
{
}
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization
(
options =>
{
options.AddPolicy
(
JwtBearerDefaults.AuthenticationScheme,
builder =>
{
builder.
AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).
RequireAuthenticatedUser().
Build();
}
);
}
);
services.AddAuthentication();
services.AddCaching();
services.AddMvc();
services.AddOptions();
}
// Configure is called after ConfigureServices is called.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IOptions<AppSettings> appSettings)
{
app.UseDeveloperExceptionPage();
// Add a new middleware validating access tokens issued by the OIDC server.
app.UseJwtBearerAuthentication(options => {
options.AutomaticAuthentication = true;
options.Audience = "http://localhost:50000/";
options.Authority = "http://localhost:50000/";
options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>
(
metadataAddress : options.Authority + ".well-known/openid-configuration",
configRetriever : new OpenIdConnectConfigurationRetriever(),
docRetriever : new HttpDocumentRetriever { RequireHttps = false }
);
});
// Add a new middleware issuing tokens.
app.UseOpenIdConnectServer
(
configuration =>
{
configuration.Options.TokenEndpointPath= "/authorization/v1";
configuration.Options.AllowInsecureHttp = true;
configuration.Provider = new OpenIdConnectServerProvider {
OnValidateClientAuthentication = context =>
{
context.Skipped();
return Task.FromResult<object>(null);
},
OnGrantResourceOwnerCredentials = context =>
{
var identity = new ClaimsIdentity(OpenIdConnectDefaults.AuthenticationScheme);
identity.AddClaim( new Claim(ClaimTypes.NameIdentifier, "todo") );
identity.AddClaim( new Claim("urn:customclaim", "value", "token id_token"));
context.Validated(new ClaimsPrincipal(identity));
return Task.FromResult<object>(null);
}
};
}
);
app.UseMvc();
}
}
Change wizarded ValuesController.cs to specify an Authorize attribute:
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET: api/values
[Authorize("Bearer")]
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
Run the project, and acquire a token using postman. To acquire a token use x-www-form-urlencoded POST with "grant_type" of "password", "username" anything, "password" anything and "resource" the address of the API endpoint. My particular URL for example is http://localhost:37734/authorization/v1.
Copy the Base64 encoded token, then use the token to call the wizarded values controller using postman. To use the token make a GET with the headers Content-Type application/json and Authorization bearer eyJ0eXAiO....(your token). My particular URL is http://localhost:37734/api/values.
Observe the exception mentioned previously.
If the [Authorize("Bearer")] approach I'm trying above is the wrong way to go I would be very appreciative if someone could help me understand best practices for how to ingest the JWT token using OIDC.
Thank you.
options.Authority corresponds to the issuer address (i.e the address of your OIDC server).
http://localhost:50000/ doesn't seem to be correct as you're using http://localhost:37734/ later in your question. Try fixing the URL and give it another try.