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.
Related
I have created an Azure SignalR (Serverless) reosurce in azure portal.
Then I have created an azure function HttpTrigger locally that references Microsoft.Azure.WebJobs.Extensions.SignalRService. In my azure function I have this code:
`public static class HttpTrigger
{
[FunctionName("Negotiate")]
public static SignalRConnectionInfo Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
[SignalRConnectionInfo(HubName = "notificationHub")] SignalRConnectionInfo connectionInfo,
ILogger log)
{
log.LogInformation("Returning connection: " + connectionInfo.Url + "" + connectionInfo.AccessToken);
return connectionInfo;
}
[FunctionName("Notify")]
public static async Task<IActionResult> Notify([HttpTrigger(AuthorizationLevel.Function, "get", Route=null)] HttpRequest req,
[SignalR(HubName = "notificationHub")] IAsyncCollector<SignalRMessage> signalRMessage,
ILogger log)
{
log.LogInformation("Notify");
string msg = string.Format("Message from agent! {0} ", DateTime.Now);
await signalRMessage.AddAsync(
new SignalRMessage
{
Target = "notifications",
Arguments = new[] { msg }
});
return new OkObjectResult("ok");
}
}
`
Also in my azure function, this is what my local.settings.json looks like:
`
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"AzureSignalRConnectionString": "myconnstringhere"
},
"Host": {
"LocalHttpPort": 7071,
"CORS": "http://localhost:53377",
"CORSCredentials": true
}
}
To also solve the CORS problem, I have added http://localhost:53377 domain of my client part project.
My client part is a separate asp.net web application project . So here I am connecting to this azure function like this:
`
<script>
$(document).ready(function(){
const connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:7071/api/")
.configureLogging(signalR.LogLevel.Information)
.build();
connection.onclose(start);
start(connection);
});
async function start(connection){
try {
await connection.start();
console.log("SignalR connected.");
connection.on("notifications", (message) => {
$("#detailcontainer").html(message);
console.log(message)
});
}
catch(err) {
console.log(err);
}
}
</script>
Now, I have published my azure function. But now it is not working anymore. It gets an error saying unauthorized when triggering /api/negotiate.
My azure function is a .net 6 project while the client app is a net framework 4.8. Is this because my client app is still in webforms?
I have added the connection string of my azure signalR to the application settings having a name format like this: Azure__SignalR__ConnectionString
I also have configured CORS allowed origins for my azure function, I added my client localhost app.
Closing this one because I found the answer. And it was really annoying that I have missed this one out.
I replaced AuthorizationLevel.Function to AuthorizationLevel.Anonymous. Because I am just passing the domain/api of my azure function and letting the signalR do its thing on their JS.
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.
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 have created openiddict token in dot net core api and that application is hosted at localhost/UserManagementAPI. When I try to authorize same API then I am able to do it. But when I try to use same token and Authorize other API localhost/SalesAPI it gives me unauthorized access error.
Token generation code is as below UserManagementAPI/startup.cs
services.AddAuthentication().AddOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/authorize";
options.AllowInsecureHttp = true;
options.Provider.OnValidateTokenRequest = context =>
{
if (!context.Request.IsPasswordGrantType() && !context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.CompletedTask;
}
if (string.IsNullOrEmpty(context.ClientId))
{
context.Skip();
return Task.CompletedTask;
}
if (string.Equals(context.ClientId, "client_id", StringComparison.Ordinal) &&
string.Equals(context.ClientSecret, "client_secret", StringComparison.Ordinal))
{
context.Validate();
}
return Task.CompletedTask;
};
options.Provider.OnHandleTokenRequest = context =>
{
if (context.Request.IsPasswordGrantType())
{
if (!string.Equals(context.Request.Username, "testusername", StringComparison.Ordinal) ||
!string.Equals(context.Request.Password, "testpassword", StringComparison.Ordinal))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid user credentials.");
return Task.CompletedTask;
}
var identity = new ClaimsIdentity(context.Scheme.Name,
OpenIdConnectConstants.Claims.Name,
OpenIdConnectConstants.Claims.Role);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, Guid.NewGuid().ToString());
identity.AddClaim("userid", "1001",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Scheme.Name);
ticket.SetAccessTokenLifetime(TimeSpan.FromDays(1));
ticket.SetScopes(OpenIdConnectConstants.Scopes.Profile);
context.Validate(ticket);
}
return Task.CompletedTask;
};
});
I have added below code to validate token in localhost/SalesAPI startup.cs
services.AddOpenIddict();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Bearer";
options.DefaultChallengeScheme = "Bearer";
}).AddOAuthValidation();
I dont want to use authorization server.
with above code I am able to authorize other api's from localhost/UserManagementAPI (same api is responsible to generate a token)
please let me know If I am missing something here.
It's worth noting you're not using OpenIddict in this snippet, but AspNet.Security.OpenIdConnect.Server, the low-level OpenID Connect server middleware that powers OpenIddict 1.x and 2.x.
If your resource server is located in a separate application, you'll need to configure that application to use the same ASP.NET Core Data Protection keys as the main application. Take a look at ASOS - Token validation is not working when having separate authorization server and the resource server for more information on how to do that.
I have used the IdentityServer v3, now I want one website to be both the identity host and the web api host.
The authority option is not used to validate the token. I have verified the token endpoint and the token validation endpoint is working as expected (I can get and validate a token using postman). I used the [Authorize] attribute to decorate my controller method. Full logging is enabled on IdentityServer, nothing is logged when making an api request with a header name 'Authorization' with the value like 'Bearer mytokenhere'.
This is a vNext website on ASP.NET 5 using the Visual Studio 2015 CTP6.
app.UseMvc();
var certFile = AppDomain.CurrentDomain.BaseDirectory + "\\myawesomesite.pfx";
app.Map("/core", core =>
{
var factory = InMemoryFactory.Create(
users: Users.Get(),
clients: Clients.Get(),
scopes: Scopes.Get());
var idsrvOptions = new IdentityServerOptions
{
SiteName = "Lektieplan",
Factory = factory,
RequireSsl = false,
SigningCertificate = new X509Certificate2(certFile, "lektieplan"),
CorsPolicy = CorsPolicy.AllowAll,
LoggingOptions = new LoggingOptions { EnableWebApiDiagnostics = true,EnableHttpLogging = true, IncludeSensitiveDataInLogs = true, WebApiDiagnosticsIsVerbose = true }
};
core.UseIdentityServer(idsrvOptions);
});
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:57540/core",
RequiredScopes = new[] { "api1" },
});
And my project.json
My dependencies:
"Microsoft.AspNet.Server.IIS": "1.0.0-beta3",
"Microsoft.AspNet.Mvc": "6.0.0-beta3",
"Microsoft.AspNet.StaticFiles": "1.0.0-beta3",
"Microsoft.AspNet.Server.WebListener": "1.0.0-beta3",
"Thinktecture.IdentityServer3": "1.3.0.0",
"Microsoft.AspNet.Owin": "1.0.0.0-beta3",
"Microsoft.AspNet.Security.DataProtection": "1.0.0.0-beta3",
"Thinktecture.IdentityServer3.AccessTokenValidation": "1.2.2",
"Autofac": "4.0.0-alpha1",
"log4net": "2.0.3"
I seems to me that some of the provided samples works because of a cookie based option. I don't want to use the cookies.
Is UseIdentityServerBearerTokenAuthentication your only auth type? Do you have any filters defined for MVC?
I would try to split the apps into separate katana pipelines, so they don't conflict at all.
Pseudo:
app.Map("/core", a => a.UseIdsrv());
app.Map("/somethingweb", a => a.UseMvc());
app.Map("/api", a => {
a.UseBearerTokenAuth();
a.UseWebApi(); //or Mvc from now on, with v5
});
Guessing you would need to add cookieauth to that mvc pipeline as well, depending on what you want to achieve.