Skip to content

Commit

Permalink
Polymorphic API for oAuth2
Browse files Browse the repository at this point in the history
  • Loading branch information
zijianhuang committed Jul 4, 2024
1 parent 97af50d commit 456e194
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 91 deletions.
57 changes: 55 additions & 2 deletions DemoCoreWeb.ClientApiTextJson/PolymorphismClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public PolymorphismClient(System.Net.Http.HttpClient client, JsonSerializerOptio
this.jsonSerializerSettings = jsonSerializerSettings;
}

public async Task<Fonlow.Auth.Models.ROPCRequst> PostRopcTokenRequestToAuthAsync(Fonlow.Auth.Models.ROPCRequst model, Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
public async Task<Fonlow.Auth.Models.AccessTokenResponse> PostRopcTokenRequestAsFormDataToAuthAsync(Fonlow.Auth.Models.ROPCRequst model, Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
{
var requestUri = "api/Polymorphism";
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri);
Expand All @@ -47,14 +47,67 @@ 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<Fonlow.Auth.Models.ROPCRequst>(stream, jsonSerializerSettings);
return JsonSerializer.Deserialize<Fonlow.Auth.Models.AccessTokenResponse>(stream, jsonSerializerSettings);
}
finally
{
responseMessage.Dispose();
}
}

public async Task<Fonlow.Auth.Models.AccessTokenResponse> PostRefreshTokenRequestAsFormDataToAuthAsync(Fonlow.Auth.Models.RefreshAccessTokenRequest model, Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
{
var requestUri = "api/Polymorphism";
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri);
var pairs = new KeyValuePair<string, string>[]
{
new KeyValuePair<string, string>( "grant_type", model.GrantType ),
new KeyValuePair<string, string>( "RefreshTokenString", model.RefreshToken ),
new KeyValuePair<string, string> ( "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<Fonlow.Auth.Models.AccessTokenResponse>(stream, jsonSerializerSettings);
}
finally
{
responseMessage.Dispose();
}
}

/// <summary>
/// POST api/Polymorphism/PostROPCRequst
/// </summary>
public async Task<Fonlow.Auth.Models.AccessTokenResponse> PostROPCRequstToAuthAsync(Fonlow.Auth.Models.ROPCRequst model, Action<System.Net.Http.Headers.HttpRequestHeaders> 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<Fonlow.Auth.Models.AccessTokenResponse>(stream, jsonSerializerSettings);
}
finally
{
responseMessage.Dispose();
}
}



/// <summary>
/// POST api/Polymorphism/PostRequestBase
/// </summary>
Expand Down
11 changes: 9 additions & 2 deletions DemoTextJsonWeb/OAuth2RequestBinderProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,16 @@ public RequestModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> 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;

Expand Down Expand Up @@ -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
{
Expand Down
95 changes: 68 additions & 27 deletions DemoTextJsonWeb/PolymorphismController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,80 @@ namespace DemoWebApi.Controllers
public class PolymorphismController : ControllerBase
{
[HttpPost]
[Consumes("application/x-www-form-urlencoded")]
public async Task<RequestBase> PostTokenRequest([FromForm] RequestBase model)
[Consumes("application/x-www-form-urlencoded")] //need explicit declaration for sharing endpoint
public async Task<TokenResponseBase> 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<RequestBase> PostRequestBase([FromBody] RequestBase model)
{
return model;
throw new NotSupportedException();
}

[HttpPost]
[Route("PostROPCRequst")]
public async Task<ROPCRequst> PostROPCRequst([FromBody] ROPCRequst model)
{
return model;
}
//[HttpPost]
//[Consumes("application/json")] //need explicit declaration for sharing endpoint
//public async Task<TokenResponseBase> 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<RequestBase> PostROPCRequst2([FromBody] ROPCRequst model)
{
return model;
}
// throw new NotSupportedException();
//}

[HttpPost]
[Route("PostROPCRequst3")]
public async Task<ROPCRequst> PostROPCRequst3([FromBody] RequestBase model)
{
return model as ROPCRequst;
}
//[HttpPost]
//[Route("PostRequestBase")]
//public async Task<RequestBase> PostRequestBase([FromBody] RequestBase model)
//{
// return model;
//}

//[HttpPost]
//[Route("PostROPCRequst")]
//public async Task<ROPCRequst> PostROPCRequst([FromBody] ROPCRequst model)
//{
// return model;
//}

//[HttpPost]
//[Route("PostROPCRequst2")]
//public async Task<RequestBase> PostROPCRequst2([FromBody] ROPCRequst model)
//{
// return model;
//}

//[HttpPost]
//[Route("PostROPCRequst3")]
//public async Task<ROPCRequst> PostROPCRequst3([FromBody] RequestBase model)
//{
// return model as ROPCRequst;
//}

}
}
5 changes: 3 additions & 2 deletions DemoTextJsonWeb/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand All @@ -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());

});

Expand Down
11 changes: 5 additions & 6 deletions Fonlow.Auth.PayloadConverters/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -56,6 +57,9 @@ public class RefreshAccessTokenRequest : RequestBase
[DataContract]
public abstract class TokenResponseBase
{
/// <summary>
/// Such as bearer or Bearer
/// </summary>
[Required]
[JsonPropertyName("token_type")]
[DataMember(Name = "token_type")]
Expand All @@ -66,18 +70,13 @@ public abstract class TokenResponseBase
/// Section 5.1
/// </summary>
[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; }

/// <summary>
/// In the spec, it is recommended, however, it is bad in practice if not required.
/// </summary>
Expand Down
109 changes: 109 additions & 0 deletions Fonlow.Auth.PayloadConverters/TokenResponseConverter.cs
Original file line number Diff line number Diff line change
@@ -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<TokenResponseBase>
{
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();
}
}
}
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading

0 comments on commit 456e194

Please sign in to comment.