From 456e194abe0890010ecbb0bd586be29897e6acd2 Mon Sep 17 00:00:00 2001 From: Zijian Date: Thu, 4 Jul 2024 15:18:58 +1000 Subject: [PATCH] Polymorphic API for oAuth2 --- .../PolymorphismClient.cs | 57 ++++++- .../OAuth2RequestBinderProvider.cs | 11 +- DemoTextJsonWeb/PolymorphismController.cs | 95 ++++++++---- DemoTextJsonWeb/Program.cs | 5 +- Fonlow.Auth.PayloadConverters/Models.cs | 11 +- .../TokenResponseConverter.cs | 109 ++++++++++++++ README.md | 2 + .../PolymorphismApiIntegration.cs | 140 +++++++++++------- 8 files changed, 339 insertions(+), 91 deletions(-) create mode 100644 Fonlow.Auth.PayloadConverters/TokenResponseConverter.cs diff --git a/DemoCoreWeb.ClientApiTextJson/PolymorphismClient.cs b/DemoCoreWeb.ClientApiTextJson/PolymorphismClient.cs index 3a9060ca..a1931da0 100644 --- a/DemoCoreWeb.ClientApiTextJson/PolymorphismClient.cs +++ b/DemoCoreWeb.ClientApiTextJson/PolymorphismClient.cs @@ -28,7 +28,7 @@ public PolymorphismClient(System.Net.Http.HttpClient client, JsonSerializerOptio this.jsonSerializerSettings = jsonSerializerSettings; } - public async Task PostRopcTokenRequestToAuthAsync(Fonlow.Auth.Models.ROPCRequst model, Action handleHeaders = null) + public async Task PostRopcTokenRequestAsFormDataToAuthAsync(Fonlow.Auth.Models.ROPCRequst model, Action handleHeaders = null) { var requestUri = "api/Polymorphism"; using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri); @@ -47,7 +47,34 @@ public PolymorphismClient(System.Net.Http.HttpClient client, JsonSerializerOptio responseMessage.EnsureSuccessStatusCodeEx(); if (responseMessage.StatusCode == System.Net.HttpStatusCode.NoContent) { return null; } var stream = await responseMessage.Content.ReadAsStreamAsync(); - return JsonSerializer.Deserialize(stream, jsonSerializerSettings); + return JsonSerializer.Deserialize(stream, jsonSerializerSettings); + } + finally + { + responseMessage.Dispose(); + } + } + + public async Task PostRefreshTokenRequestAsFormDataToAuthAsync(Fonlow.Auth.Models.RefreshAccessTokenRequest model, Action handleHeaders = null) + { + var requestUri = "api/Polymorphism"; + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri); + var pairs = new KeyValuePair[] + { + new KeyValuePair( "grant_type", model.GrantType ), + new KeyValuePair( "RefreshTokenString", model.RefreshToken ), + new KeyValuePair ( "something", model.Scope ) + }; + var content = new FormUrlEncodedContent(pairs); + httpRequestMessage.Content = content; + handleHeaders?.Invoke(httpRequestMessage.Headers); + var responseMessage = await client.SendAsync(httpRequestMessage); + try + { + responseMessage.EnsureSuccessStatusCodeEx(); + if (responseMessage.StatusCode == System.Net.HttpStatusCode.NoContent) { return null; } + var stream = await responseMessage.Content.ReadAsStreamAsync(); + return JsonSerializer.Deserialize(stream, jsonSerializerSettings); } finally { @@ -55,6 +82,32 @@ public PolymorphismClient(System.Net.Http.HttpClient client, JsonSerializerOptio } } + /// + /// POST api/Polymorphism/PostROPCRequst + /// + public async Task PostROPCRequstToAuthAsync(Fonlow.Auth.Models.ROPCRequst model, Action handleHeaders = null) + { + var requestUri = "api/Polymorphism"; + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri); + var content = System.Net.Http.Json.JsonContent.Create(model, mediaType: null, jsonSerializerSettings); + httpRequestMessage.Content = content; + handleHeaders?.Invoke(httpRequestMessage.Headers); + var responseMessage = await client.SendAsync(httpRequestMessage); + try + { + responseMessage.EnsureSuccessStatusCodeEx(); + if (responseMessage.StatusCode == System.Net.HttpStatusCode.NoContent) { return null; } + var stream = await responseMessage.Content.ReadAsStreamAsync(); + return JsonSerializer.Deserialize(stream, jsonSerializerSettings); + } + finally + { + responseMessage.Dispose(); + } + } + + + /// /// POST api/Polymorphism/PostRequestBase /// diff --git a/DemoTextJsonWeb/OAuth2RequestBinderProvider.cs b/DemoTextJsonWeb/OAuth2RequestBinderProvider.cs index 677eecd2..e522ace5 100644 --- a/DemoTextJsonWeb/OAuth2RequestBinderProvider.cs +++ b/DemoTextJsonWeb/OAuth2RequestBinderProvider.cs @@ -37,9 +37,16 @@ public RequestModelBinder(Dictionary binder { this.binders = binders; } - // https://www.c-sharpcorner.com/article/polymorphic-model-binding-in-net/ this may help + public async Task BindModelAsync(ModelBindingContext bindingContext) { + if (bindingContext.HttpContext.Request.ContentType.Contains("application/json")) + { + return; + } + //using var sr = new StreamReader(bindingContext.HttpContext.Request.Body); + //var json = await sr.ReadToEndAsync(); //only work for Json payload + var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "grant_type"); //todo: extract JsonPropertyName value or NewtonsoSoft JsonPropery value var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue; @@ -71,7 +78,7 @@ public async Task BindModelAsync(ModelBindingContext bindingContext) if (newBindingContext.Result.IsModelSet) { - //(newBindingContext.Result.Model as RequestBase).GrantType = modelTypeValue; + (newBindingContext.Result.Model as RequestBase).GrantType = modelTypeValue; // Setting the ValidationState ensures properties on derived types are correctly bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry { diff --git a/DemoTextJsonWeb/PolymorphismController.cs b/DemoTextJsonWeb/PolymorphismController.cs index bf0b6f0d..49da452b 100644 --- a/DemoTextJsonWeb/PolymorphismController.cs +++ b/DemoTextJsonWeb/PolymorphismController.cs @@ -9,39 +9,80 @@ namespace DemoWebApi.Controllers public class PolymorphismController : ControllerBase { [HttpPost] - [Consumes("application/x-www-form-urlencoded")] - public async Task PostTokenRequest([FromForm] RequestBase model) + [Consumes("application/x-www-form-urlencoded")] //need explicit declaration for sharing endpoint + public async Task PostTokenRequestAsFormData([FromForm] RequestBase model) { - return model; - } + if (model.GrantType == "password" && model is ROPCRequst) + { + return new AccessTokenResponse + { + TokenType = "bearer", + AccessToken = "AccessTokenString", + ExpiresIn = 100, + RefreshToken = "RefreshTokenString", + Scope = "some scope" + }; + } else if (model.GrantType == "refresh_token" && model is RefreshAccessTokenRequest) + { + return new AccessTokenResponse + { + TokenType = "bearer", + AccessToken = "NewAccessTokenString", + ExpiresIn = 100, + RefreshToken = "NewRefreshTokenString", + Scope = "some scope" + }; + } - [HttpPost] - [Route("PostRequestBase")] - public async Task PostRequestBase([FromBody] RequestBase model) - { - return model; + throw new NotSupportedException(); } - [HttpPost] - [Route("PostROPCRequst")] - public async Task PostROPCRequst([FromBody] ROPCRequst model) - { - return model; - } + //[HttpPost] + //[Consumes("application/json")] //need explicit declaration for sharing endpoint + //public async Task PostTokenRequest([FromBody] RequestBase model) + //{ + // if (model.GrantType == "password" && model is ROPCRequst) + // { + // return new AccessTokenResponse + // { + // TokenType = "bearer", + // AccessToken = "AccessTokenString", + // ExpiresIn = 100, + // RefreshToken = "RefreshTokenString", + // Scope = "some scope" + // }; + // } - [HttpPost] - [Route("PostROPCRequst2")] - public async Task PostROPCRequst2([FromBody] ROPCRequst model) - { - return model; - } + // throw new NotSupportedException(); + //} - [HttpPost] - [Route("PostROPCRequst3")] - public async Task PostROPCRequst3([FromBody] RequestBase model) - { - return model as ROPCRequst; - } + //[HttpPost] + //[Route("PostRequestBase")] + //public async Task PostRequestBase([FromBody] RequestBase model) + //{ + // return model; + //} + + //[HttpPost] + //[Route("PostROPCRequst")] + //public async Task PostROPCRequst([FromBody] ROPCRequst model) + //{ + // return model; + //} + + //[HttpPost] + //[Route("PostROPCRequst2")] + //public async Task PostROPCRequst2([FromBody] ROPCRequst model) + //{ + // return model; + //} + + //[HttpPost] + //[Route("PostROPCRequst3")] + //public async Task PostROPCRequst3([FromBody] RequestBase model) + //{ + // return model as ROPCRequst; + //} } } diff --git a/DemoTextJsonWeb/Program.cs b/DemoTextJsonWeb/Program.cs index 0b593550..3a57daf6 100644 --- a/DemoTextJsonWeb/Program.cs +++ b/DemoTextJsonWeb/Program.cs @@ -30,7 +30,7 @@ #if DEBUG configure.Conventions.Add(new Fonlow.CodeDom.Web.ApiExplorerVisibilityEnabledConvention());//To make ApiExplorer be visible to WebApiClientGen #endif - //configure.ModelBinderProviders.Insert(0, new OAuth2RequestBinderProvider()); + configure.ModelBinderProviders.Insert(0, new OAuth2RequestBinderProvider()); }) .AddJsonOptions(// as of .NET 7/8, could not handle JS/CS test cases getInt2D, postInt2D and PostDictionaryOfPeople, around 14 C# test cases fail. options => @@ -47,7 +47,8 @@ options.JsonSerializerOptions.Converters.Add(new Fonlow.Text.Json.DateOnlyExtensions.DateOnlyJsonConverter()); options.JsonSerializerOptions.Converters.Add(new Fonlow.Text.Json.DateOnlyExtensions.DateTimeJsonConverter()); options.JsonSerializerOptions.Converters.Add(new Fonlow.Text.Json.DateOnlyExtensions.DateTimeOffsetJsonConverter()); - options.JsonSerializerOptions.Converters.Add(new Fonlow.Text.Json.Auth.TokenRequestConverter()); + //options.JsonSerializerOptions.Converters.Add(new Fonlow.Text.Json.Auth.TokenRequestConverter()); + options.JsonSerializerOptions.Converters.Add(new Fonlow.Text.Json.Auth.TokenResponseConverter()); }); diff --git a/Fonlow.Auth.PayloadConverters/Models.cs b/Fonlow.Auth.PayloadConverters/Models.cs index aefd67aa..c769a030 100644 --- a/Fonlow.Auth.PayloadConverters/Models.cs +++ b/Fonlow.Auth.PayloadConverters/Models.cs @@ -4,6 +4,7 @@ namespace Fonlow.Auth.Models { +// Data contract attbibutes are basically for NewtonSoft.Json which respects these attributes //[JsonPolymorphic(TypeDiscriminatorPropertyName = "grant_type")] //[JsonDerivedType(typeof(ROPCRequst), "password")] //[JsonDerivedType(typeof(RefreshAccessTokenRequest), "refresh_token")] @@ -56,6 +57,9 @@ public class RefreshAccessTokenRequest : RequestBase [DataContract] public abstract class TokenResponseBase { + /// + /// Such as bearer or Bearer + /// [Required] [JsonPropertyName("token_type")] [DataMember(Name = "token_type")] @@ -66,18 +70,13 @@ public abstract class TokenResponseBase /// Section 5.1 /// [DataContract] - public class AccessTokenResponse + public class AccessTokenResponse : TokenResponseBase { [JsonPropertyName("access_token")] [DataMember(Name = "access_token")] [Required] public string AccessToken { get; set; } - [JsonPropertyName("token_type")] - [DataMember(Name = "token_type")] - [Required] - public string TokenType { get; set; } - /// /// In the spec, it is recommended, however, it is bad in practice if not required. /// diff --git a/Fonlow.Auth.PayloadConverters/TokenResponseConverter.cs b/Fonlow.Auth.PayloadConverters/TokenResponseConverter.cs new file mode 100644 index 00000000..131fe15f --- /dev/null +++ b/Fonlow.Auth.PayloadConverters/TokenResponseConverter.cs @@ -0,0 +1,109 @@ +using Fonlow.Auth.Models; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Fonlow.Text.Json.Auth +{ + public sealed class TokenResponseConverter : JsonConverter + { + public override bool HandleNull => true; + + public override void Write(Utf8JsonWriter writer, TokenResponseBase value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString("token_type", value.TokenType); + switch (value.TokenType) + { + case "bearer": + case "Bearer": + AccessTokenResponse accessTokenResponse = value as AccessTokenResponse; + writer.WriteString("access_token", accessTokenResponse.AccessToken); + writer.WriteNumber("expires_in", Convert.ToDecimal(accessTokenResponse.ExpiresIn)); + if (!string.IsNullOrWhiteSpace(accessTokenResponse.RefreshToken)) + { + writer.WriteString("refresh_token", accessTokenResponse.RefreshToken); + } + + if (!string.IsNullOrWhiteSpace(accessTokenResponse.Scope)) + { + writer.WriteString("scope", accessTokenResponse.Scope); + } + break; + default: + throw new NotSupportedException(); + } + + writer.WriteEndObject(); + } + + public override TokenResponseBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + string propertyName = reader.GetString(); + if (propertyName != "token_type") + { + throw new JsonException(); + } + + reader.Read(); + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + + var typeDiscriminator = reader.GetString(); + + switch (typeDiscriminator) + { + case "bearer": + case "Bearer": + var accessTokenResponse = new AccessTokenResponse(); + accessTokenResponse.TokenType = typeDiscriminator; + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return accessTokenResponse; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + propertyName = reader.GetString(); + if (reader.Read()) + { + switch (propertyName) + { + case "access_token": + accessTokenResponse.AccessToken = reader.GetString(); + break; + case "refresh_token": + accessTokenResponse.AccessToken = reader.GetString(); + break; + case "expires_in": + accessTokenResponse.ExpiresIn = reader.GetInt32(); + break; + case "scope": + accessTokenResponse.Scope = reader.GetString(); + break; + } + } + } + } + + return accessTokenResponse; + default: + throw new JsonException(); + } + } + } +} diff --git a/README.md b/README.md index 69674523..216661be 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +Generate client API codes in C# and TypeScript from ASP.NET (Core) Web API directly without involving Swagger/OpenAPI or Swashbuckle, therefore maximizing the support for data types of your Code First approach of ASP.NET Web API. + Strongly Typed Client API Generators generate strongly typed client API in C# codes and TypeScript codes. You may then provide or publish either the generated source codes or the compiled client API libraries to other developers for developing client programs. # Products diff --git a/Tests/IntegrationTestsTextJson/PolymorphismApiIntegration.cs b/Tests/IntegrationTestsTextJson/PolymorphismApiIntegration.cs index 611e2f42..47ca7fba 100644 --- a/Tests/IntegrationTestsTextJson/PolymorphismApiIntegration.cs +++ b/Tests/IntegrationTestsTextJson/PolymorphismApiIntegration.cs @@ -14,81 +14,117 @@ public PolymorphismApiIntegration(PolymorphismFixture fixture) readonly PolymorphismClient api; - [Fact] - public void TestPostRopcRequest() - { - var r = api.PostROPCRequst(new ROPCRequst - { - GrantType = "password", - Username = "MyName", - Password = "MyPassword" - }); + //[Fact] + //public void TestPostRopcRequest() + //{ + // var r = api.PostROPCRequst(new ROPCRequst + // { + // GrantType = "password", + // Username = "MyName", + // Password = "MyPassword" + // }); - Assert.Equal("password", r.GrantType); - Assert.Equal("MyName", r.Username); - } + // Assert.Equal("password", r.GrantType); + // Assert.Equal("MyName", r.Username); + //} - /// - /// Concrete in, base out - /// - [Fact] - public void TestPostRopcRequest2() - { - RequestBase r = api.PostROPCRequst2(new ROPCRequst - { - GrantType = "password", - Username = "MyName", - Password = "MyPassword" - }); + ///// + ///// Concrete in, base out + ///// + //[Fact] + //public void TestPostRopcRequest2() + //{ + // RequestBase r = api.PostROPCRequst2(new ROPCRequst + // { + // GrantType = "password", + // Username = "MyName", + // Password = "MyPassword" + // }); - Assert.Equal("password", r.GrantType); - Assert.Equal("MyName", (r as ROPCRequst).Username); - } + // Assert.Equal("password", r.GrantType); + // Assert.Equal("MyName", (r as ROPCRequst).Username); + //} - [Fact] - public void TestPostRopcRequest3() - { - ROPCRequst r = api.PostROPCRequst3(new ROPCRequst - { - GrantType = "password", - Username = "MyName", - Password = "MyPassword" - }); + //[Fact] + //public void TestPostRopcRequest3() + //{ + // ROPCRequst r = api.PostROPCRequst3(new ROPCRequst + // { + // GrantType = "password", + // Username = "MyName", + // Password = "MyPassword" + // }); + + // Assert.Equal("password", r.GrantType); + // Assert.Equal("MyName", r.Username); + //} + + //[Fact] + //public void TestPostRequestBase() + //{ + // RequestBase r = api.PostRequestBase(new ROPCRequst + // { + // GrantType = "password", + // Username = "MyName", + // Password = "MyPassword" + // }); + + // Assert.Equal("password", r.GrantType); + // var r2 = r as ROPCRequst; + // Assert.Equal("MyName", r2.Username); + //} - Assert.Equal("password", r.GrantType); - Assert.Equal("MyName", r.Username); - } [Fact] - public void TestPostRequestBase() + public async Task TestPostRopcTokenRequestAsFormDataToAuthAsync() { - RequestBase r = api.PostRequestBase(new ROPCRequst + var r = await api.PostRopcTokenRequestAsFormDataToAuthAsync(new ROPCRequst { GrantType = "password", Username = "MyName", Password = "MyPassword" }); - Assert.Equal("password", r.GrantType); - var r2 = r as ROPCRequst; - Assert.Equal("MyName", r2.Username); + Assert.Equal("bearer", r.TokenType); + Assert.Equal("AccessTokenString", r.AccessToken); + Assert.Equal("RefreshTokenString", r.RefreshToken); + Assert.Equal("some scope", r.Scope); + Assert.Equal(100, r.ExpiresIn); } - [Fact] - public async Task TestPostRopcTokenRequestToAuthAsync() + public async Task TestPostRefreshTokenRequestAsFormDataToAuthAsync() { - var r = await api.PostRopcTokenRequestToAuthAsync(new ROPCRequst + var r = await api.PostRefreshTokenRequestAsFormDataToAuthAsync(new RefreshAccessTokenRequest { - GrantType = "password", - Username = "MyName", - Password = "MyPassword" + GrantType = "refresh_token", + RefreshToken="RefreshTokenString" }); - Assert.Equal("password", r.GrantType); - Assert.Equal("MyName", r.Username); + Assert.Equal("bearer", r.TokenType); + Assert.Equal("NewAccessTokenString", r.AccessToken); + Assert.Equal("NewRefreshTokenString", r.RefreshToken); + Assert.Equal("some scope", r.Scope); + Assert.Equal(100, r.ExpiresIn); } + //[Fact] + //public async Task TestPostROPCRequstToAuthAsync() + //{ + // var r = await api.PostROPCRequstToAuthAsync(new ROPCRequst + // { + // GrantType = "password", + // Username = "MyName", + // Password = "MyPassword" + // }); + + // Assert.Equal("bearer", r.TokenType); + // Assert.Equal("AccessTokenString", r.AccessToken); + // Assert.Equal("RefreshTokenString", r.RefreshToken); + // Assert.Equal("some scope", r.Scope); + // Assert.Equal(100, r.ExpiresIn); + //} + }