ASPNETCore SignalR authentication with Reference token - c#

We are using ASPNETCore.SignalR 1.1.0 inside our Web API (netcoreapp2.2).
Authentication : We are using IdentityServer4 authentication for our project.
Startup.cs
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://IdentityServerDomainURL:8081/";
options.RequireHttpsMetadata = false;
options.ApiName = "name";
options.ApiSecret = "secret";
});
In WebAPI application we have added our SignalR Hub.
A JavaScript client connects to this Hub.
Following is the code of JS client for connecting to the hub.
var connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:52177/SignalRHub/",
{
accessTokenFactory: () => "referencetokenValue"
}).build();
The JS client is passing the Reference token at the time of connecting to the Hub.
We need to use Reference token authentication for SignalR in the WebAPI project.
In Microsoft`s site only JWT token authentication documentation for SignalR is provided. Did not find any document anywhere regarding Reference tokens.
Need help in adding the configuration for reference token authentication in startup.cs file.

Found the solution here.
Any token coming in query string can be added in the request headers as follows:-
app.Use(async (context, next) =>
{
if (context.Request.Path.Value.StartsWith("/SignalRHub/"))
{
var bearerToken = context.Request.Query["access_token"].ToString();
if (!String.IsNullOrEmpty(bearerToken))
context.Request.Headers.Add("Authorization", new string[] { "bearer " + bearerToken });
}
await next();
});
The above code has to be added in the Configure function of startup class.

Related

Secure ASP.Net Core 3.1 API with Bearer Tokens from Azure AD

I have a .net core 3.1 web API. I want to secure the endpoints using bearer tokens from Azure AD.
I have followed This guide. I have a registered an app for the API and an app for the client.
In my API, I have added a registration for the jwt (as outlined in the code)
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.Audience = Configuration["AzureAd:ResourceId"];
opt.Authority = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}";
});
And my configuration section for the API looks like this
"AzureAd": {
"ResourceId": "api://<client-id-guid>",
"Instance": "https://login.microsoftonline.com/",
"TenantId": "<tenant-id-guid>"
}
where the client-id-guid is the client id of the api application (used the default configuration for the api:// address).
Then, for my client, I implemented a test in a new solution to get the token and call a secured endpoint.
AadConfiguration config = AadConfiguration.ReadFromJsonFile("appsettings.json");
IConfidentialClientApplication app;
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithClientSecret(config.ClientSecret)
.WithAuthority(new Uri(config.Authority))
.Build();
string[] ResourceIds = new string[] {config.ResourceID};
var token = await app.AcquireTokenForClient(ResourceIds)
.ExecuteAsync();
Assert.IsFalse(string.IsNullOrWhiteSpace(token.AccessToken));
var request = new RestRequestBuilder("https://my-api-staging.azurewebsites.net/api")
.WithSegments("health-check", "auth")
.WithBearerToken(token.AccessToken)
.Build();
try
{
var response = await _restHelper.GetJsonAsync<dynamic>(request);
Assert.IsFalse(string.IsNullOrEmpty(response?.serverTime?.ToString()));
}
catch (RestException e)
{
Assert.IsFalse(true, e.ResponseContent);
}
And the configuration for this test
{
"Instance": "https://login.microsoftonline.com/{0}",
"TenantId": "<tenant-id-guid>",
"ClientId": "<client-id-guid>",
"ClientSecret": "<client-secret>",
"ResourceId": "api://<api-client-id-guid>/.default"
}
Tenant Id is the sale for both the API and the test application
The client ID is the client id for the client app registration, and its secret
The resource Id is the API's application id, which is the api:// with the api's client Id
I can retrieve the access token just fine. However, when I call an endpoint using the access token I am given, I get the following error:
Couldn't find a valid certificate with subject 'CN=TODO.azurewebsites.net' on the 'CurrentUser\My'
I'm not sure why my authentication is failing when I have followed the steps in the document.
I can retrieve the access token just fine. However, when I call an endpoint using the access token I am given, I get the following error:
I follow the guide and test in my site which works very well.
After getting access token, you can try to use it in postman to send request to core api. While the core api is 3.1 version, so it do not have /api to route it.
And you can use HttpClient to call core webapi.
var token = await app.AcquireTokenForClient(ResourceIds).ExecuteAsync();
var accesstoken = token.AccessToken;
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + accesstoken);
var requestURl = new Uri($"https://xxxx.azurewebsites.net/weatherforecast");
var response = client.GetAsync(requestURl).Result;
string result = response.Content.ReadAsStringAsync().Result;
}

AspNetCore rejects preflight messages

We have a HttpSys listener which should accept authentication as either NTLM, Negotiate or JWT.
Problem is that it looks like HttpSys rejects both preflight messages and messages with Bearer token (JWT)
Our listener is build like this
_host = new WebHostBuilder()
.UseHttpSys(options =>
{
options.Authentication.Schemes = AuthenticationSchemes.NTLM | AuthenticationSchemes.Negotiate;
options.Authentication.AllowAnonymous = false;
})
.UseUrls($"http://+:{PortNo}/")
.UseUnityServiceProvider(IocContainer)
.ConfigureServices(services => { services.AddSingleton(_startUpConfig); })
.UseStartup<StartUp>()
.Build();
We add CORS and Authentication to services:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(o => o.AddPolicy("AllowAll", builder =>
{
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().AllowCredentials().WithOrigins("*");
}));
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = HttpSysDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.Events = new JwtBearerEvents { OnTokenValidated = context => AuthMiddleWare.VerifyJwt(context, _jwtPublicKey) };
});
We run an angular application in Chrome, which is rejected with the following error message
"Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. "
Also any Bearer token message is rejected. Debugging reveals that our code to verify JWT bearer is never reached (AuthMiddleWare.VerifyJwt)
My guess is that HttpSys rejects any message not carrying Either Ntlm or Negotiate token. Only I have no idea how to fix that
In .net Framework we used the AuthenticationSchemeSelectorDelegate to run the following code, which allowed OPTIONS messages and messages with Bearer token to pass through the HttpSys listener
public AuthenticationSchemes EvaluateAuthentication(HttpListenerRequest request)
{
if (request.HttpMethod == "OPTIONS")
{
return AuthenticationSchemes.Anonymous;
}
if (request.Headers["Authorization"] != null && request.Headers["Authorization"].Contains("Bearer "))
{
return AuthenticationSchemes.Anonymous;
}
return AuthenticationSchemes.IntegratedWindowsAuthentication;
}
We have this working now. Basically the problem was that allowing all three authentication methods is not a supported scenario in Asp Net Core.
So the trick was to implement our own authentication in the pipeline.
Also see this github issue:
https://github.com/aspnet/AspNetCore/issues/13135

How to integrate swagger with Azure Active Directory OAuth

I'm trying to setup Swagger in my AspNetCore 2.1 application using Azure Active Directory V2 but I cannot seem to get it right. I am able to configure the setup so that swagger prompts, redirects and successfully authenticates my client/user but when passing the bearer token to the server results in the error Bearer error="invalid_token", error_description="The signature is invalid". I have created a GitHub repository with the project I am trying to get work with all its configuration (https://github.com/alucard112/auth-problem)
I have managed to get the V1 endpoint working, by setting the resource to the Client Id of the AAD app, which results in the JWT token having the 'aud' set to the app client Id. In the V2 endpoint the 'aud' is being set to what I think is the Graph API resource '00000003-0000-0000-c000-000000000000'. I believe this is my problem at the moment, although am not 100 % sure. The V2 endpoints don't seem to have a way to define the audience like the V1 did unless of course there is some oversight from my side.
My Startup file is structured as follows:
The authentication is setup as the following:
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.Authority = $"https://login.microsoftonline.com/{tenantId}";
options.TokenValidationParameters = new TokenValidationParameters
{
// In multi-tenant apps you should disable issuer validation:
ValidateIssuer = false,
// In case you want to allow only specific tenants,
// you can set the ValidIssuers property to a list of valid issuer ids
// or specify a delegate for the IssuerValidator property, e.g.
// IssuerValidator = (issuer, token, parameters) => {}
// the validator should return the issuer string
// if it is valid and throw an exception if not
};
});
And the swagger is setup as follows:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Title = "Protected Api",
});
c.OperationFilter<SecurityRequirementsOperationFilter>();
//IMATE - StevensW
// Define the OAuth2.0 scheme that's in use (i.e. Implicit Flow)
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize",
TokenUrl = $"https://login.microsoftonline.com/common/{tenantId}/v2.0/token",
Scopes = new Dictionary<string, string>
{
{ "openid", "Unsure" },
{ "profile", "Also Unsure" }
}
});
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.OAuthClientId(Configuration.GetValue<string>("AzureAd:ClientId"));
c.OAuthAppName("Protected API");
// c.OAuthUseBasicAuthenticationWithAccessCodeGrant();
// NEVER set the client secret here. It will ve exposed in the html of the swagger page if you "view source" and its not needed for OpenID Auth
// c.OAuthClientSecret(Configuration.GetValue<string>("AzureAd:ClientId"));
});
I am hoping to configure the swagger UI to use AAD's V2 endpoint and allow for a multi-tenant login that allows successfully authenticated API calls to be executed. Any help or direction would be greatly appreciated.
I ended up fixing the problem I was having. Working through this post helped me understand my mistakes.
The first mistake was my actual AAD app registration. I had not set a scope for the application under "Expose an API". Because they deprecated the resource property in V2, the way you would set the resource was to create a scope with the format api"//{application ID}/{scope_name}. After I made this change my AAD application was now correctly configured.
After that, I needed to add an additional section to my startup file:
return services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
// This is an Azure AD v2.0 Web API
options.Authority += "/v2.0";
// The valid audiences are both the Client ID (options.Audience) and api://{ClientID}
options.TokenValidationParameters.ValidAudiences = new string[] { options.Audience, $"api://{options.Audience}" };
options.TokenValidationParameters.ValidateIssuer = false;
});
Note: the link above provided an alternative solution to turning off the validation of the issuer if anyone is interested.
My AppSettings file was also simplified by only needing to define the Instance, TenantId, and ClientId.
Then from a swagger perspective, I just needed to add an additional scope to the security definition matching the one I created in my AAD application.
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
Scopes = new Dictionary<string, string>
{
{ "openid", "Sign In Permissions" },
{ "profile", "User Profile Permissions" },
{ $"api://{clientId}/access_as_user", "Application API Permissions" }
}
});
After these changes my application is now working as expected.
for v2 endpoint, update the accessTokenAcceptedVersion in Manifest of AAD from null to 2. It will work.

Can't protect internal api using IdentityServer 4 (500 error)

A bit of background, I have an IdenityServer 4 project that I use to protect access to an mvc project that I have (Using ASP.NET Identity).
Now what I also wanted was an api that is protected via client credentials that returns some information.
What I did was make a new core api project and this was working fine with the client protection, however, I wanted to move the api so it was within IdenityServer.
e.g. localhost:5000/api/info/getinfo
Now I have moved the code over I get a 500 error when I use the attribute [Authorize(AuthenticationSchemes = "Bearer")]
I can use the DiscoveryClient to get a successful token using the credentials but can't with any request unless they are not authorized.
So in ID I set up my start up like this:
services.AddMvc();
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
// Configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients(Configuration))
.AddInMemoryApiResources(Config.GetApiResources())
.AddAspNetIdentity<ApplicationUser>();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetSection("Authority").Value;
options.RequireHttpsMetadata = false;
options.ApiName = "IdentityInfoApi";
});
And then for my api call that is protected I tag it with: [Authorize(AuthenticationSchemes = "Bearer")]
but this returns me a 500 error, now when I use the tag: [Authorize] it works but that's because the user is logged into the mvc app and the response is an html page and not the json object i want.
At the moment I'm using a unit test to hit the api and the code looks like this:
var client = new HttpClient();
var disco = DiscoveryClient.GetAsync("https://localhost:5000").Result;
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse = tokenClient.RequestClientCredentialsAsync("IdentityInfoApi").Result;
client.SetBearerToken(tokenResponse.AccessToken);
var response = client.GetAsync("https://localhost:5000/api/info/getinfo").Result;
if (!response.IsSuccessStatusCode)
{
var userResult = response.Content.ReadAsStringAsync().Result;
var result = JsonConvert.DeserializeObject<PagedUserList>(userResult);
Assert.NotNull(result);
}
Is there something wrong with my setup of ID, the client code or can you not use ID in this way?
Thank's for your help
After a lot of playing around I believe I found the fix.
You must define AddAuthentication() before AddIdentity() or in other words you must configure the api before Identity Server
It's fine to do it any way round if your api is external but not if it is within the Identity Server app it'self.
My new code looks like this:
//Configure api
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "IdentityInfoApi";
});
//end
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration.GetSection("SMTP"));
services.AddMvc();
// Configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients(Configuration))
.AddInMemoryApiResources(Config.GetApiResources())
.AddAspNetIdentity<ApplicationUser>();
Hope this helps anyone else

Authorization error (401) in .net core when calling api to api in windows authentication

Have 2 Web API's created using .Net Core 2.0 and hosted internally in IIS under windows authentication (anonymous disabled) on same server. Both API's run on same service account as well with appropriate permissions/rolegroups in Active Directory. However, get 401 unauthorized error when consuming one API from the other. Using HTTPClient to make API calls. Note that, it works when accessing the 2nd API endpoint directly via browser but not from another API.
Decorated with Authorize filter in controller
[Authorize(Policy = "ValidRoleGroup")]
Start up code in ConfigureServices in both api services as below.
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddAuthorization(options =>
{
options.AddPolicy("ValidRoleGroup", policy => policy.RequireRole(Configuration["SecuritySettings:ValidRoleGroup"]));
});
services.AddMvc(configure =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
configure.Filters.Add(new AuthorizeFilter(policy));
});
services.Configure<IISOptions>(options =>
{
options.AutomaticAuthentication = true;
options.ForwardClientCertificate = true;
});
services.AddMvc();
services.AddScoped<HttpClient>(c => new HttpClient(new HttpClientHandler()
{
UseDefaultCredentials = true,
PreAuthenticate = true,
ClientCertificateOptions = ClientCertificateOption.Automatic,
}));
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
The 401 errors went away after adding registry entries as described in below article (Method 1)
https://support.microsoft.com/en-us/help/896861/you-receive-error-401-1-when-you-browse-a-web-site-that-uses-integrate
Note that the Value data should be your actual domain URL (XXX.com) and not machine name.

Categories