using System.Data; using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Encodings.Web; using FitConnect.Encryption; using FitConnect.Models; using FitConnect.Services; using FitConnect.Services.Interfaces; using Jose; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Base64Url = IdentityModel.Base64Url; using Route = FitConnect.Services.Models.v1.Routes.Route; namespace FitConnect; public class Router : IRouter { private readonly FitConnectEnvironment _environment; private readonly ILogger? _logger; private readonly IRouteService _routeService; private readonly ISelfServicePortalService _selfServicePortalService; public Router(FitConnectEnvironment environment, ILogger? logger = null) { _environment = environment; _logger = logger; _routeService = new RouteService(environment.RoutingUrl, "v1", logger); _selfServicePortalService = new SelfServicePortalService(environment.SspUrl, "v1", logger); } public async Task<List<Route>> FindDestinationsAsync(string leiaKey, string? ags = null, string? ars = null, string? areaId = null, bool skipValidation = false) { var routes = await _routeService.GetDestinationIdAsync(leiaKey, ags, ars, areaId); if (skipValidation) return routes; foreach (var route in routes) { _logger?.LogInformation("Testing destination {DestinationId}", route.DestinationId); var verifyJwt = await VerifyDestinationSignature(route); if (!verifyJwt) { throw new Exception($"Invalid destination signature"); } verifyJwt &= await VerifyDestinationParametersSignature(route); if (!verifyJwt) { throw new Exception($"Invalid destination parameter signature"); } verifyJwt &= VerifySubmissionHost(route); if (!verifyJwt) { throw new Exception( $"SubmissionHost does not match DesitnationParamtersSingatures issuer"); } } return routes; } private bool VerifySubmissionHost(Route route) { var signature = new JsonWebToken(route.DestinationSignature); var payload = JsonConvert.DeserializeObject<dynamic>( Base64UrlEncoder.Decode(signature.EncodedPayload)); string? submissionHost = payload?.submissionHost; return route.DestinationParameters.SubmissionUrl.EndsWith(submissionHost); } private async Task<bool> VerifyDestinationParametersSignature(Route route) { // Get Key from SubmissionAPI var submissionKey = await GetSubmissionServiceValidationJwk(route.DestinationParameters.SubmissionUrl); var parameterJson = JsonConvert.SerializeObject(route.DestinationParameters, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.None, ContractResolver = new OrderedContractResolver() }); var encodedParameter = Base64Url.Encode(Encoding.UTF8.GetBytes(parameterJson)); var signature = route.DestinationParametersSignature.Replace("..", $".{encodedParameter}."); // KID to check is in signature header kid var header = JsonConvert.DeserializeObject<dynamic>( Base64UrlEncoder.Decode(route.DestinationParametersSignature.Split('.')[0]) ); var kid = (string)header.kid; _logger?.LogInformation("Testing with kid: {kid}", kid); return FitEncryption.VerifyJwt(signature, new JsonWebKeySet(submissionKey).Keys.First(k => k.Kid == kid), _logger); } private async Task<bool> VerifyDestinationSignature(Route route) { var validation = await _selfServicePortalService.GetValidationJwk(); var token = new JsonWebToken(route.DestinationSignature); var key = validation.Keys.First(k => k.Kid == token.Kid); var verifyJwt = FitEncryption.VerifyJwt(route.DestinationSignature, key, _logger); return verifyJwt; } /// <summary> /// /// </summary> /// <returns></returns> public async Task<string> GetSubmissionServiceValidationJwk(string baseUrl) { var client = new HttpClient() { BaseAddress = new Uri(baseUrl), }; var result = await client.GetAsync("/.well-known/jwks.json"); return await result.Content.ReadAsStringAsync(); } /// <summary> /// Finding Areas /// </summary> /// <param name="filter"></param> /// <param name="totalCount"></param> /// <param name="offset"></param> /// <param name="limit"></param> /// <returns></returns> public IEnumerable<Area> GetAreas(string filter, out int totalCount, int offset = 0, int limit = 100) { var dto = _routeService.GetAreas(filter, offset, limit).Result; totalCount = dto?.TotalCount ?? 0; return dto?.Areas ?? new List<Area>(); } } public class OrderedContractResolver : DefaultContractResolver { protected override System.Collections.Generic.IList<JsonProperty> CreateProperties( System.Type type, MemberSerialization memberSerialization) { NamingStrategy = new CamelCaseNamingStrategy(); return base.CreateProperties(type, memberSerialization).OrderBy(p => p.PropertyName) .ToList(); } }