Im trying to send a PUT to my Web API and am struggling a bit as to how I should construct the actual Http request. Below is an integration test sample. It works fine using HttpMessageInvoker to call the Web API Put, but I want to use HttpClient in test also since that is what I'll be using in the business layer.
[TestMethod]
public void Verify_UpdateBudgetData_Http_PUT()
{
int budgetId = 1;
string appId = "DummyApp";
string userId = "Dummy";
string value = "400";
string filterJSON =
"{dimensionFilter:{\"Demo_Konto\":[\"3000\"],\"Demo_AO\":[\"200\"]},valueSpreadType:{\"Value1\":0}}";
HttpConfiguration config = new HttpConfiguration();
Konstrukt.SL.AggregationEngine.WebApiConfig.Register(config, new SL.AggregationEngine.AutofacStandardModule());
HttpServer server = new HttpServer(config);
/*this works*/
using (HttpMessageInvoker client = new HttpMessageInvoker(server))
{
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put,
String.Format("http://localhost/AggregationEngine/UpdateBudgetData/{0}/{1}/{2}/{3}/{4}",
budgetId, appId, userId, value, filterJSON)))
using (HttpResponseMessage response = client.SendAsync(request, CancellationToken.None).Result)
{
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, "Wrong http status returned");
}
};
/*this does not work*/
using (var client = new HttpClient())
{
//client.BaseAddress = new Uri("http://localhost");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var responseMessage =
client.PutAsync(
String.Format("http://localhost/AggregationEngine/UpdateBudgetData/{0}/{1}/{2}/{3}/{4}",
budgetId, appId, userId, value, filterJSON), new StringContent("")).Result;
Assert.AreEqual(HttpStatusCode.OK, responseMessage.StatusCode, "Wrong http status returned");
}
}
Here is my WebApiConfig-class
public static class WebApiConfig
{
public static void Register(HttpConfiguration config, Autofac.Module moduleToAppend)
{
config.Routes.MapHttpRoute(
name: "UpdateBudgetData",
routeTemplate: "AggregationEngine/{controller}/{budgetId}/{appId}/{userId}/{value}/{filterJSON}",
defaults: new { filter = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "GetBudgetAndRefData",
routeTemplate: "AggregationEngine/{controller}/{budgetId}/{userId}/{filterJSON}",
defaults: new { filter = RouteParameter.Optional }
);
config.EnableCors();
config.EnableSystemDiagnosticsTracing();
// Autofac container
// if not configured here you'll not have dependencies provided to your WebApiControllers when called
var builder = new ContainerBuilder(); // yes, it is a different container here
builder.RegisterAssemblyTypes( // register Web API Controllers
Assembly.GetExecutingAssembly())
.Where(t =>
!t.IsAbstract && typeof(ApiController).IsAssignableFrom(t))
.InstancePerLifetimeScope();
// register your graph - shared
builder.RegisterModule(
new AutofacStandardModule()); // same as with ASP.NET MVC Controllers
if (moduleToAppend != null)
{
builder.RegisterModule(moduleToAppend);
}
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(
container);
}
public static void Register(HttpConfiguration config)
{
Register(config, null);
}
}
How can I fix the HttpClient call to PutAsync? Should I embed the FilterJSON parameter in the body? If so, how to do that? I've tried that but then the FromBody parametger was null...
I got it working by using the FromBody tag in the controller and then wrapping that parameter in the http request body. An important note to is to prefix the parameter with an "=" sign to make sure it was interpreted correctly by the controller. Also I removed the same parameter from the route config. Finally to make the client to server request work I had to replace HttpServer Class with httpselfhostserver
Related
The situation I have:
Blazor Server App , .Net6.0.9 with Microsoft Identity Platform.
Blazor Server App is registered in the App Registration on Tenant-1
Client-API-1 is also resigered in the App Registration on Tenant-1
Login actions are done against/with the ClientId of the Client-API-1 registration and work fine.
In the API-Management I've added on the Inbound processing Polecies Validate-jwt like this:
(source of Microsoft)
<policies>
<inbound>
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid!!!">
<openid-config url="https://login.microsoftonline.com/11a14169-89cc-44e8-95d7-xxxxxxxxxxxx/v2.0/.well-known/openid-configuration" />
<required-claims>
<claim name="aud">
<value>{client-id-of-Client-API-1-on-App-Registration}</value>
</claim>
</required-claims>
</validate-jwt>
In Service looks like this:
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Identity.Web;
using System.Net.Http.Headers;
namespace BlazorSAAppJwt.Data
{
public class ApimService : IApimService
{
private AuthenticationStateProvider _authenticationStateProvider { get; set; }
private readonly ITokenAcquisition _tokenAcquisition;
public ApimService(AuthenticationStateProvider AuthenticationStateProvider, ITokenAcquisition tokenAcquisition)
{
_authenticationStateProvider = AuthenticationStateProvider;
_tokenAcquisition = tokenAcquisition;
}
//public async Task<string?> GetResponseAsync(string path, CancellationToken cancellationToken)
public async Task<string?> GetResponseAsync(string path)
{
try
{
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
if (authState?.User?.Identity?.IsAuthenticated ?? false)
{
using var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("https://apimanagement.azure-api.net/");
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.DefaultRequestHeaders.Add("email", authState.User.Identity.Name);
httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "{My APIM suvbscriptionkey}"); // APIM
httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Trace", "true");
// This gets the UserToken to get data from Microsoft Graph for the scopes: User.Read & Mail.Read
var token = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "User.Read", "Mail.Read" });
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(token);
var dataRequest = await httpClient.GetAsync("https://graph.microsoft.com/beta/me");
string? userDisplayName = "";
if (dataRequest.IsSuccessStatusCode)
{
var userData = System.Text.Json.JsonDocument.Parse(await dataRequest.Content.ReadAsStreamAsync());
userDisplayName = userData.RootElement.GetProperty("displayName").GetString();
}
//Remove the previous Authorization-header for the Microsoft Graph call
httpClient.DefaultRequestHeaders.Remove("Authorization");
//Add the Application token to the Authorization for APIM
//NOTE!!! Here is where the JWT token should be used!!!!
string jwt = "How do I get the jwt here to add and send to the APIM";
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");
//HttpResponseMessage response = await httpClient.GetAsync($"{path.ToLower()}", cancellationToken);
HttpResponseMessage response = await httpClient.GetAsync($"{path.ToLower()}");
if (response.IsSuccessStatusCode)
{
string clientApiResult = await response.Content.ReadAsStringAsync();
return clientApiResult;
}
else
{
throw new UnauthorizedAccessException($"(Graph) User Display Name: {userDisplayName}" +
$"{Environment.NewLine}Response from APIM call: {response}");
}
}
else
{
// "The user is NOT authenticated.";
throw new UnauthorizedAccessException();
}
return default;
}
catch (Exception ex)
{
var iets = ex.Message;
throw;
}
}
}
}
I receive the UserDisplayName from the Graph API-call.
My program.cs
using BlazorSAAppJwt.Data;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
var builder = WebApplication.CreateBuilder(args);
var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ') ?? builder.Configuration["MicrosoftGraph:Scopes"]?.Split(' ');
var azureSection = builder.Configuration.GetSection("AzureAd");
var microsoftGraphSection = builder.Configuration.GetSection("MicrosoftGraph");
// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
//.AddMicrosoftGraph(microsoftGraphSection) // Nuget Microsoft.Identity.Web.MicrosoftGraph
.AddInMemoryTokenCaches();
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddTokenAcquisition();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddSingleton<ApimService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
What do I miss, and how do I setup my Blasor Server App to use the JWT token?
EDIT:
The API-calls on the APIM is not going to change and will call the Client-Api that is not exposed to the internet.
Who knows that the call:
var token = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "User.Read", "Mail.Read" });
Retruns the JWT... It does and what it is you need to use in the request header
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(token);
And make the call
HttpResponseMessage response = await httpClient.GetAsync($"{path.ToLower()}", cancellationToken);
I have a question regarding JWT authentication in ASP.NET Core and Claims, because I don't know if I get everything correctly.
When I create a JWT token in ASP.NET I add some Claims, some of which can be custom. What happens when the request with JWT token is sent from the client to API. How is User.Claims filled ? Does it use the claims that are read from JWT?
I would like to create a custom Identity provider ( don't want to use this provided by ASP.NET), with my own tables for user data, roles etc. I don't want store all important data required to fulfill the policy in JWT token (the amount of information stored in token matters, as well as security matters). Is it possible to store only basic claims (like user id, name etc) in JWT token, and then re-fetch other required data DB/ Cache? Along with that, I would like to use the standard mechanism for [Authorize] and the Policy mechanism.
How to make this all work: Custom User Identity + JWT + Standard ASP.NET policy-based authorization + claims fetched from DB/Cache on every request? How to achieve this?
Asp Net Core
First step is write the method that configure Jwt authentication:
// Configure authentication with JWT (Json Web Token).
public void ConfigureJwtAuthService(IServiceCollection services)
{
// Enable the use of an [Authorize(AuthenticationSchemes =
// JwtBearerDefaults.AuthenticationScheme)]
// attribute on methods and classes to protect.
services.AddAuthentication().AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = JwtController.SecurityKey,
ValidAudience = JwtController.Audience,
ValidIssuer = JwtController.Issuer,
// When receiving a token, check that we've signed it.
ValidateIssuerSigningKey = true,
// When receiving a token, check that it is still valid.
ValidateLifetime = true,
// This defines the maximum allowable clock skew when validating
// the lifetime. As we're creating the tokens locally and validating
// them on the same machines which should have synchronised time,
// this can be set to zero.
ClockSkew = TimeSpan.FromMinutes(0)
};
});
}
Now inside the ConfigureServices() method of the Startup.cs, we can call ConfigureJwtAuthService() method to configure the Jwt authentication.
This is the complete Startup.cs:
using System;
using Autofac;
using ExpertCodeBlogWebApp.Controllers;
using ExpertCodeBlogWebApp.Domain;
using ExpertCodeBlogWebApp.Domain.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SpaServices.Webpack;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
namespace ExpertCodeBlogWebApp
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add
// services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Configure jwt autenticazione
ConfigureJwtAuthService(services);
// Repositories
services.AddScoped<IUserRepository, UserRepository>();
// Create the Autofac container builder for dependency injection
var builder = new ContainerBuilder();
// Add any Autofac modules or registrations.
builder.RegisterModule(new AutofacModule());
// Return ServiceProvider
var serviceProvider = services.BuildServiceProvider();
return serviceProvider;
}
// Configure authentication with JWT (Json Web Token).
public void ConfigureJwtAuthService(IServiceCollection services)
{
// Enable the use of an [Authorize(AuthenticationSchemes =
// JwtBearerDefaults.AuthenticationScheme)]
// attribute on methods and classes to protect.
services.AddAuthentication().AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = JwtController.SecurityKey,
ValidAudience = JwtController.Audience,
ValidIssuer = JwtController.Issuer,
// When receiving a token, check that we've signed it.
ValidateIssuerSigningKey = true,
// When receiving a token, check that it is still valid.
ValidateLifetime = true,
// This defines the maximum allowable clock skew when validating
// the lifetime.
// As we're creating the tokens locally and validating them on the
// same machines which should have synchronised time, this can be
// set to zero.
ClockSkew = TimeSpan.FromMinutes(0)
};
});
}
// This method gets called by the runtime. Use this method to configure
// the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
{
HotModuleReplacement = true
});
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
}
}
// For dependency injection.
public class AutofacModule : Module
{
// Dependency Injection with Autofact
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<UserRepository>().As<IUserRepository>()
.SingleInstance();
}
}
}
The JwtController.cs
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using AutoMapper;
using ExpertCodeBlogWebApp.Domain;
using ExpertCodeBlogWebApp.Domain.Interfaces;
using ExpertCodeBlogWebApp.Domain.Models;
using ExpertCodeBlogWebApp.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
namespace ExpertCodeBlogWebApp.Controllers
{
[Route("api/[controller]")]
public class JwtController : Controller
{
#region Private Members
// JWT-related members
private TimeSpan TokenExpiration;
private SigningCredentials SigningCredentials;
// EF and Identity members, available through DI
private MyDbContext DbContext;
private IUserRepository _userRepository;
private readonly ILogger _logger;
#endregion Private Members
#region Static Members
private static readonly string PrivateKey = "my_PrivateKey";
public static readonly SymmetricSecurityKey SecurityKey =
new SymmetricSecurityKey(Encoding.ASCII.GetBytes(PrivateKey));
public static readonly string Issuer = "my_Issuer";
public static readonly string Audience = "my_Audience";
#endregion Static Members
#region Constructor
// I have used Autofac in the Startup.cs for dependency injection)
public JwtController(
MyDbContext dbContext,
IUserRepository userRepository,
ILogger<JwtController> logger)
{
_logger = logger;
_userRepository = userRepository;
// Instantiate JWT-related members
TokenExpiration = TimeSpan.FromMinutes(10);
SigningCredentials = new SigningCredentials(SecurityKey,
SecurityAlgorithms.HmacSha256);
// Instantiate through Dependency Injection with Autofact
DbContext = dbContext;
}
#endregion Constructor
#region Public Methods
// Manages the request for a new authentication or the refresh of an
// already established one
[HttpPost("token")]
public async Task<IActionResult>
Authentication([FromBody]JwtRequestViewModel jwt)
{
if (ModelState.IsValid)
{
string grantType = jwt.GrantType;
if (grantType == "password")
{
string userName = jwt.UserName;
string password = jwt.Password;
// Password check required
var user = await
_userRepository.GetUserInfoWithCheckPwd(userName, password);
// Check if user is expired (check the ExpireDate property)
if (UserExpired(user))
return BadRequest($"Account of {user.Name} expired!");
if (UserEnabled(user))
return await GenerateToken(user);
else
return BadRequest("User name or password invalid.");
}
}
else if (grantType == "refresh_token")
{
string userName = jwt.UserName;
// Refresh token (no password check required)
var user = await _userRepository.GetUserInfoByName(userName);
// Check if user is expired (check the ExpireDate property)
if (UserExpired(user))
return BadRequest($"Account of {user.Name} expired!");
string token = jwt.Token;
if (token == user.Token)
{
// Generate token and send it via a json-formatted string
return await GenerateToken(user);
}
else
{
return BadRequest("User token invalid.");
}
}
else
return BadRequest("Authentication type invalid.");
}
else
return BadRequest("Request invalid.");
}
#endregion Public Methods
#region Private Methods
private bool UserExpired(Users utente)
{
if (utente != null)
return utente.ExpireDate.CompareTo(DateTime.Now) < 0;
return true;
}
private bool UserEnabled(Users utente)
{
if (utente != null)
return utente.Enabled == true;
return false;
}
private JsonSerializerSettings DefaultJsonSettings
{
get
{
return new JsonSerializerSettings()
{
Formatting = Formatting.Indented
};
}
}
private async Task<IActionResult> GenerateToken(Users user)
{
try
{
if (user != null)
{
var handler = new JwtSecurityTokenHandler();
DateTime newTokenExpiration = DateTime.Now.Add(TokenExpiration);
ClaimsIdentity identity = new ClaimsIdentity(
new GenericIdentity(user.Name, "TokenAuth"),
new[] { new Claim("ID", user.Id.ToString())}
);
var securityToken = handler.CreateToken(new SecurityTokenDescriptor
{
Issuer = JwtController.Issuer,
Audience = JwtController.Audience,
SigningCredentials = SigningCredentials,
Subject = identity,
Expires = newTokenExpiration
});
string encodedToken = handler.WriteToken(securityToken);
// Update token data on database
await _userRepository.UpdateTokenData(user.Name, encodedToken,
newTokenExpiration);
// Build the json response
// (I use Automapper to maps an object into another object)
var jwtResponse = Mapper.Map<JwtResponseViewModel>(user);
jwtResponse.AccessToken = encodedToken;
jwtResponse.Expiration = (int)TokenExpiration.TotalSeconds;
return Ok(jwtResponse);
}
return NotFound();
}
catch(Exception e)
{
return BadRequest(e.Message);
}
}
#endregion
}
}
On my project I use Angular. For call JwtController method by Angular:
login(userName: string, password: string)
{
return this.getLoginEndpoint(userName, password)
.map((response: Response) => this.processLoginResponse(response));
}
getLoginEndpoint(userName: string, password: string): Observable<Response>
{
// Body
// JwtRequest is a model class that I use to send info to the controller
let jwt = new JwtRequest();
jwt.GrantType = "password";
jwt.UserName = userName;
jwt.Password = password;
jwt.ClientId = "my_Issuer";
// Post requiest (I use getAuthHeader that attach to the header the
// authentication token, but it can also be omitted because it is ignored
// by the JwtController
return this.http.post(this.loginUrl, JSON.stringify(jwt),
this.getAuthHeader(true))
}
protected getAuthHeader(includeJsonContentType?: boolean): RequestOptions
{
// Hera I use this.authService.accessToken that is a my service where
// I have store the token received from the server
let headers = new Headers({
'Authorization': 'Bearer ' + this.authService.accessToken });
if (includeJsonContentType)
headers.append("Content-Type", "application/json");
headers.append("Accept", `application/vnd.iman.v01+json,
application/json, text/plain, */*`);
headers.append("App-Version", "01");
return new RequestOptions({ headers: headers });
}
private processLoginResponse(response: Response)
{
// process the response..
}
On the controllers classes (or methods) that you want to be accessible only by authenticated users (not on your JwtController because its method must be accessible by all users) you can set:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
To call from Angular the controller method that require authentication, you need to attach the token into the header with the getAuthHeader() method.
I hope this post can help you.
yes it uses the claim stored in jwt token
look at the httpcontext object for claims that are stored in token when you created the token
this link can also be helpfull https://joonasw.net/view/adding-custom-claims-aspnet-core-2
I'm using Unity3D v5.5.1, with AWS-SDK-Unity v3.3.37.0.
Since the Api Gateway doesn't generate an SDK for C#/Unity3D I'm trying to sign (SigV4) the request my self and have encountered difficulties.
I've tried both manually signing and using the AWS4Signer.cs class.
The Api Gateway method has the Invoke with caller credentials, and just returns a Hello World as a response.
Within unity I have a facebook login button which returns the FB credentials and tokens. Using Cognito Federated Identity's GetCredentialsAsync method I get an ImmutableCredentials object with the Key, Secret and a Token.
To access the api gateway url I'm using the AWS4Signer class here to construct a signed request. In the example below I've tried both adding the security token to the url parameters and without, also signing it and not signing with the token. All options don't work (As stated in this post)
This results in either the following responses:
1. The request signature we calculated does not match the signature you provided
The security token included in the request is invalid.
How can I correctly sign the request from Unity3D?
Thanks in advance
TestGet method:
IEnumerator TestGet (ImmutableCredentials response)
{
ApiGatewayConfig clientConfig = new ApiGatewayConfig(); // a class I created wrapping the ClientConfig.cs
var metrics = new RequestMetrics();
var awsAccessKeyId = response.AccessKey;
var awsSecretAccessKey = response.SecretKey;
var awsToken = response.Token;
AmazonWebServiceRequest req = new MyRequest(); // a clas I created wrapping the AmazonWebServiceRequest.cs class
var url = "https://<url_to_api>.execute-api.us-east-1.amazonaws.com/dev/securehello";
IRequest request = new DefaultRequest(req,"execute-api");
request.UseQueryString = true;
request.HttpMethod = "GET";
request.Endpoint = new System.Uri (url);
request.ResourcePath = url;
request.ContentStream = new MemoryStream();
request.Parameters.Add("X-Amz-Expires",AWS4PreSignedUrlSigner.MaxAWS4PreSignedUrlExpiry.ToString(CultureInfo.InvariantCulture));
request.AuthenticationRegion = "us-east-1";
request.AlternateEndpoint = RegionEndpoint.USEast1;
request.UseSigV4 = true;
request.Headers.Add("X-Amz-Security-Token",awsToken);
request.Parameters.Add("X-Amz-Security-Token",awsToken);
AWS4Signer signer = new AWS4Signer();
Debug.Log ("a");
signer.Sign(request,clientConfig,metrics,awsAccessKeyId,awsSecretAccessKey);
var signerRes = signer.SignRequest(request,clientConfig,metrics,awsAccessKeyId,awsSecretAccessKey);
Debug.Log ("b");
var myParams = string.Format("{0}&X-Amz-Security-Token={1}",signerRes.ForQueryParameters,awsToken);
var dict = myParams.Split('&').Select(p=> p.Split('=')).GroupBy(p => p[0]).Select(p => p.First()).ToDictionary(p => p[0], p=>System.Uri.UnescapeDataString(p[1]));
var myEncodedParams = string.Empty;
bool isFirst = true;
foreach (var key in dict.Keys) {
myEncodedParams += string.Format("{0}{1}={2}",isFirst ? "" : "&",key,WWW.EscapeURL(dict[key]));
isFirst = false;
}
var finalUrl = string.Format ("{0}?{1}", request.Endpoint.AbsoluteUri,myEncodedParams);
UnityWebRequest uwr = new UnityWebRequest (finalUrl, "GET", new DownloadHandlerBuffer (), null);
Debug.Log ( string.Format("\n\n\n{0}\n\n\n",finalUrl));
Debug.Log ("Starting WebRequest");
yield return uwr.Send();
if (uwr.isError) {
Debug.LogError (uwr.error);
} else {
Debug.Log (uwr.downloadHandler.text);
}
Helper classes:
public class ApiGatewayConfig : ClientConfig
{
private static readonly string UserAgentString =
InternalSDKUtils.BuildUserAgentString("3.3.37.0");
private string _userAgent = UserAgentString;
public ApiGatewayConfig ()
{
this.AuthenticationServiceName = "execute-api";
}
/// <summary>
/// The constant used to lookup in the region hash the endpoint.
/// </summary>
public override string RegionEndpointServiceName
{
get
{
return "execute-api";
}
}
/// <summary>
/// Gets the ServiceVersion property.
/// </summary>
public override string ServiceVersion
{
get
{
return "2015-07-09";
}
}
/// <summary>
/// Gets the value of UserAgent property.
/// </summary>
public override string UserAgent
{
get
{
return _userAgent;
}
}
}
public class MyRequest : AmazonWebServiceRequest
{
public MyRequest () {}
}
Solved.
I've created some examples showing how to do this. Still work in progress, example shows how to sign a POST request from Unity 3D to Api Gateway endpoint that has "Invoke with caller credentials" (AWS_IAM).
Unity 3D Client:
https://github.com/guywald/serverless-auth-msg-board-unity3d-client
AWS Serverless backend (using Serverless Framework):
https://github.com/guywald/serverless-auth-msg-board
In a katana web api, I'm using:
appBuilder.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://...",
ValidationMode = ValidationMode.Local,
RequiredScopes = new[] { "..." },
});
This appears to nicely find the public signing key(s) from the authority and (hopefully?) cache them, etc. Although I haven't tried it, I understand there's an equivalent for ASP.NET Core.
Now I need to do the same thing but not in a web api middleware. So I'm trying to find the code that IdentityServer3.AccessTokenValidation.IdentityServerBearerTokenValidationMiddleware uses to do this. All I can see is that it calls UseOAuthBearerAuthentication, which seems to be in Microsoft.Owin.Security.OAuth. I haven't been able to find a version of that source code that seems to match the signature.
It seems to me that under the covers, somebody is probably using System.IdentityModel.Tokens.JwtSecurityTokenHandler and putting a nice little snippet of code into the IssuerSigningKeyResolver of the TokenValidationParameters. That nice little snippet is getting the signing keys from the metadata address. Anybody know what that code is, or have a piece that works well? Obviously, I could write it but I hate to re-invent the wheel, plus mine would be un-tested.
We are using this class that plugs into the JWT handler:
https://github.com/IdentityServer/IdentityServer3.AccessTokenValidation/blob/master/source/AccessTokenValidation/Plumbing/DiscoveryDocumentIssuerSecurityTokenProvider.cs
Thanks, leastprivilege. Looking deeper at your DiscoverydocumentIssuerSecurityTokenProvider class, I found ConfigurationManager<OpenIdConnectConfiguration>. Using that, I have come up with the following helper class for access token validation outside of OWIN middleware.
Feedback solicited!
using Microsoft.IdentityModel.Protocols;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
namespace WpfClient
{
public class AccessTokenValidator
{
protected TokenValidationParameters _accessTokenValidationParameters;
public AccessTokenValidator(string stsRoot)
{
stsRoot = stsRoot.TrimEnd('/');
var discoveryEndpoint = stsRoot + "/.well-known/openid-configuration";
var webHandler = new WebRequestHandler();
var httpClient = new HttpClient(webHandler);
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(discoveryEndpoint, httpClient);
_accessTokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = stsRoot,
RequireSignedTokens = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeyResolver = (string token, SecurityToken securityToken, SecurityKeyIdentifier keyIdentifier, TokenValidationParameters validationParameters) => {
var signingTokens = configurationManager.GetConfigurationAsync().Result.JsonWebKeySet.GetSigningTokens();
foreach (var signingToken in signingTokens)
{
foreach (var clause in keyIdentifier)
{
var key = signingToken.ResolveKeyIdentifierClause(clause);
if (key != null)
{
return key;
}
}
}
return null;
},
RequireExpirationTime = true,
ValidateAudience = false, // See https://github.com/IdentityServer/IdentityServer3/issues/1365: "OAuth2 does not use the term 'audience' ... it instead uses the term 'scope' ... 'audience' and the 'aud' claim are JWT specific concepts."
ValidateLifetime = true,
};
}
public void ValidateAccessToken(string accessToken, IEnumerable<string> requiredScopes, IEnumerable<string> requiredRoles)
{
ClaimsPrincipal claimsPrincipal;
SecurityToken securityToken;
var handler = new JwtSecurityTokenHandler();
claimsPrincipal = handler.ValidateToken(accessToken, _accessTokenValidationParameters, out securityToken);
if (claimsPrincipal == null)
{
throw new NullReferenceException("ClaimsPrincipal object returned is null");
}
RequireClaims("scope", requiredScopes, claimsPrincipal);
RequireClaims("role", requiredRoles, claimsPrincipal);
}
private static void RequireClaims(string type, IEnumerable<string> requiredValues, ClaimsPrincipal claimsPrincipal)
{
if (requiredValues != null)
{
var haveClaims = claimsPrincipal.FindAll(type);
var missingRequiredValues = requiredValues.Where(s => !haveClaims.Any(c => c.Value.Equals(s, StringComparison.InvariantCultureIgnoreCase))).ToArray();
if (missingRequiredValues.Any())
{
var list = string.Join(", ", missingRequiredValues);
throw new InvalidDataException($"Missing required {type} claims: {list}");
}
}
}
}
}
I liked the idea of the helper class, but jon-brichoux's implementation still has a lot of code (e.g. iterating over signingTokens) you'd think would be handled by libraries. Microsoft's libraries also have a lot of solid code including refreshing the keys regularly. That implementation looks to be similar to what's recommended by Microsoft at Obtaining SecurityKeys for Validation Dynamically.
A blog post, Manually validating a JWT using .NET, is good but it hits the server and gets the metadata on every validation. Here's what I came up with that seems to work as expected.
You will need to reference the Microsoft.IdentityModel.Protocols.OpenIdConnect and Microsoft.IdentityModel.Protocols NuGet packages.
public class JwtTokenValidator
{
protected static TokenValidationParameters _validationParameters;
public JwtTokenValidator(string metadataAddress, string audience)
{
_validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = audience,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataAddress, new OpenIdConnectConfigurationRetriever()),
};
}
public void ValidateToken(string token)
{
var jsonWebTokenHandler = new JsonWebTokenHandler();
var tokenValidationResult = jsonWebTokenHandler.ValidateToken(token, _validationParameters);
if (!tokenValidationResult.IsValid)
{
// Handle each exception which tokenValidationResult can contain as appropriate for your service
// Your service might need to respond with a http response instead of an exception.
if (tokenValidationResult.Exception != null)
throw tokenValidationResult.Exception;
throw new InvalidOperationException("invalid token"); // TODO: throw an application-appropriate exception
}
}
}
Usage:
// cache single instance at application initialization
JwtTokenValidator jwtTokenValidator = new JwtTokenValidator($"https:.../.well-known/openid-configuration", "[audience]");
// live check in controller or wherever
jwtTokenValidator.ValidateToken("eyJ0eX...");
There is a good example for sharing HttpSession between Websocket and Rest service. (Spring DispatchServlet cannot find resource within Jetty) But it doesn't work for me. I'm not sure is there any thing I'm missing?
I'm using Jetty as websocket server and also I created a WebApp as well which injected by SpringConfig.
private void init() throws Exception
{
Server server = new Server();
// Create SSL Connector
ServerConnector serverConnector = getSSLConnector(server);
// Bundle to server
server.setConnectors(new Connector[] { serverConnector });
// Create request handler collection
HandlerCollection handlers = new HandlerCollection();
// Add WebSocket handler
final ServletContextHandler servletContextHandler = getWebSocketContextHandler();
handlers.addHandler(servletContextHandler);
// Add Servlet handler
handlers.addHandler(getWebAppServletContextHandler());
server.setHandler(handlers);
// Initial WebSocket
WebSocketServerContainerInitializer.configureContext(servletContextHandler);
// Start Jetty
server.start();
server.join();
}
Both WebSocket and Rest are working under same port perfectly, of course, with different context paths.
Now, I created a Rest service:
#RequestMapping(value = "/login", method = RequestMethod.POST)
#Consumes({ MediaType.APPLICATION_JSON_VALUE })
#Produces({ MediaType.APPLICATION_JSON_VALUE })
public #ResponseBody Message login(#RequestBody Credential credential, #Context HttpServletRequest servlerRequest)
{
...
HttpSession session = servlerRequest.getSession(true);
session.setAttribute("userName", credential.getUserName());
...
Message message = new Message();
...
return message;
}
In this service I created a HttpSession and stored something in. As I said, it works, and so does the session.
Rest client:
public void login() throws KeyManagementException, NoSuchAlgorithmException
{
final String loginServiceUri = HTTP_SERVICE_BASE_URI + "/login";
ClientConfig clientConfig = new DefaultClientConfig();
...
Client client = Client.create(clientConfig);
WebResource webResource = client.resource(loginServiceUri);
ClientResponse response = webResource
.type("application/json")
.post(ClientResponse.class, new Credential("user","pass"));
if (response.getStatus() != 200) {
throw new RuntimeException("Failed : HTTP error code : " + response.getStatus());
}
List<NewCookie>cookies = response.getCookies();
ClientEndpointConfigurator.setCookies(cookies); <== Store cookies as well as session to ClientEndpointConfigrator class
Message message = response.getEntity(Message.class);
...
}
ClientEndpointConfigrator class has a static list for all cookies which like this:
public class ClientEndpointConfigurator extends ClientEndpointConfig.Configurator {
private static List<NewCookie> cookies = null;
public static void setCookies(List<NewCookie> cookies) {
ClientEndpointConfigurator.cookies = cookies;
}
...
#Override
public void beforeRequest(Map<String, List<String>> headers) {
...
if(null != cookies)
{
List<String> cookieList = new ArrayList<String>();
for(NewCookie cookie: cookies)
{
cookieList.add(cookie.toString());
}
headers.put("Cookie", cookieList);
}
...
}
}
beforeRequest() method will put all cookies to request header. If you inspect the cookieList, you will see:
[JSESSIONID=tvum36z6j2bc1p9uf2gumxguh;Version=1;Path=/rs;Secure]
Things looks prefect.
Finally, create a server end ServerEndpointConfigurator class, and override the modifyHandshake() method to retrieve the session and cookies
public class SpringServerEndpointConfigurator extends ServerEndpointConfig.Configurator {
#Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
super.modifyHandshake(sec, request, response);
httpSession = (HttpSession)request.getHttpSession(); <== **It returns null here!**
...
}
}
}
I can't get my HttpSession back! and if you print headers out, you will see the cookie has been changed:
Cookie: JSESSIONID="tvum36z6j2bc1p9uf2gumxguh";$Path="/rs"
Any one knows what's the reason?
All right, I figured it out, it's because I put WebSocket and Rest to different context handler. Jetty keeps handlers isolate to each other. To share session information, you have to put them together.
But if someone does want to separate them, it is still possible done by sharing SessionManager or SessionHandler. There are many ways to achieve this, you can inject SessionHandler to each ServletContext or just define it as a static variable and put it on somewhere every one can reach, each way works.