Skip to content

Commit

Permalink
feat: add GetSigninUrl Api (#93)
Browse files Browse the repository at this point in the history
* feat: add GetSigninUrl api and imporve userinfo api

* feat: avoid Duplicate entry
  • Loading branch information
dacongda authored Feb 16, 2024
1 parent 24d7beb commit 2417ec0
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 20 deletions.
2 changes: 1 addition & 1 deletion src/Casdoor.Client/Abstractions/ICasdoorAccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ public interface ICasdoorAccountClient

public Task<CasdoorLaravelResponse?> User(CancellationToken cancellationToken = default);

public Task<CasdoorUserInfo?> UserInfo(CancellationToken cancellationToken = default);
public Task<CasdoorUserInfo?> UserInfo(string accessToken, CancellationToken cancellationToken = default);
}
2 changes: 2 additions & 0 deletions src/Casdoor.Client/Abstractions/ICasdoorClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ public interface ICasdoorClient :
ICasdoorAdapterClient, ICasdoorCertClient, ICasdoorPaymentClient, ICasdoorPricingClient, ICasdoorProductClient, ICasdoorSessionClient,
ICasdoorRoleClient, ICasdoorRecordClient
{
public string GetSigninUrl(string redirectUrl);
public string GetSigninUrl(string codeVerifier, bool noRedirect);

}
9 changes: 8 additions & 1 deletion src/Casdoor.Client/CasdoorClient.AccountApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,16 @@ public partial class CasdoorClient
return await _httpClient.GetFromJsonAsync<CasdoorLaravelResponse?>(url, cancellationToken: cancellationToken);
}

public virtual async Task<CasdoorUserInfo?> UserInfo(CancellationToken cancellationToken = default)
public virtual async Task<CasdoorUserInfo?> UserInfo(string accessToken = "" ,CancellationToken cancellationToken = default)
{
var url = _options.GetActionUrl("userinfo");

if (accessToken != "")
{
var queryMap = new QueryMapBuilder().Add("accessToken", accessToken).QueryMap;
url = _options.GetActionUrl("userinfo", queryMap);
}

return await _httpClient.GetFromJsonAsync<CasdoorUserInfo?>(url, cancellationToken: cancellationToken);
}
}
6 changes: 5 additions & 1 deletion src/Casdoor.Client/CasdoorClient.CasbinApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ private async Task<CasdoorResponse> DoEnforceAsync(
"application/json")
};
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
request.SetBasicAuthentication(_options.ClientId, _options.ClientSecret);

if (_options.ClientSecret != "")
{
request.SetBasicAuthentication(_options.ClientId, _options.ClientSecret);
}

var response = await _httpClient.SendAsync(request, cancellationToken);
string responseContent = await response.Content.ReadAsStringAsync(); // netstandard2.0 does not support cancellationToken
Expand Down
2 changes: 1 addition & 1 deletion src/Casdoor.Client/CasdoorClient.TokenApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public virtual async Task<TokenResponse> RequestAuthorizationCodeTokenAsync(stri
{
var request = new AuthorizationCodeTokenRequest
{
Code = code, RedirectUri = redirectUri, CodeVerifier = codeVerifier
Code = code, RedirectUri = redirectUri, CodeVerifier = codeVerifier, ClientId = _options.ClientId
};
request = await ApplyConfigurationAsync(request, cancellationToken);
return await _httpClient.RequestAuthorizationCodeTokenAsync(request, cancellationToken: cancellationToken);
Expand Down
3 changes: 3 additions & 0 deletions src/Casdoor.Client/CasdoorClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ public CasdoorClient SetBearerToken(string accessToken)
_httpClient.SetBearerToken(accessToken);
return this;
}

public string GetSigninUrl(string redirectUrl) => _options.GetSigninUrl(redirectUrl);
public string GetSigninUrl(string codeVerifier, bool noRedirect) => _options.GetSigninUrl(_options.CallbackPath, codeVerifier, noRedirect);
}
28 changes: 17 additions & 11 deletions src/Casdoor.Client/Extensions/CasdoorOptionsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Encodings.Web;
Expand All @@ -21,17 +22,27 @@

namespace Casdoor.Client;

// TODO: Need to support PKCE
// TODO: Need to improve the url APIs

public static class CasdoorClientOptionsExtension
{
internal static string GetSigninUrl(this CasdoorOptions options, string redirectUrl)
{
const string scope = "read";
string state = options.ApplicationName;
string urlEncodeRedirectUrl = Base64UrlEncoder.Encode(redirectUrl);
return
$"{options.Path.LoginAuthorizePath}?client_id={options.ClientId}&response_type=code&redirect_uri={urlEncodeRedirectUrl}&scope={scope}&state={state}";
$"{options.Endpoint}{options.Path.LoginAuthorizePath}?client_id={options.ClientId}&response_type=code&redirect_uri={redirectUrl}&scope={options.Scope}&state={state}";
}

internal static string GetSigninUrl(this CasdoorOptions options, string redirectUrl, string codeVerifier, bool noRedirect)
{
var baseUrl = GetSigninUrl(options, redirectUrl);

var sha256Instance = SHA256.Create();
byte[] bytes = Encoding.Default.GetBytes(codeVerifier);
byte[] chanllengeCodeEncoded = sha256Instance.ComputeHash(bytes);
string chanllengeCodeBase64Encoded = Convert.ToBase64String(chanllengeCodeEncoded).Replace("+", "-").Replace("/", "_").Replace("=", "");

return $"{baseUrl}&code_challenge={chanllengeCodeBase64Encoded}&code_challenge_method=S256&noRedirect={noRedirect.ToString().ToLower()}";
}

internal static string GetActionUrl(this CasdoorOptions options, string action,
Expand Down Expand Up @@ -74,9 +85,9 @@ internal static string GetSignupUrl(this CasdoorOptions options, string redirect

const string scope = "read";
string state = options.ApplicationName;
string urlEncodeRedirectUrl = Base64UrlEncoder.Encode(redirectUrl);
//string urlEncodeRedirectUrl = Base64UrlEncoder.Encode(redirectUrl);
return
$"{options.Path.SignupAuthorizePath}?client_id={options.ClientId}&response_type=code&redirect_uri={urlEncodeRedirectUrl}&scope={scope}&state={state}";
$"{options.Endpoint}{options.Path.SignupAuthorizePath}?client_id={options.ClientId}&response_type=code&redirect_uri={redirectUrl}&scope={scope}&state={state}";
}

internal static string GetUserProfileUrl(this CasdoorOptions options, string username, string accessToken = "")
Expand Down Expand Up @@ -164,11 +175,6 @@ public static CasdoorOptions Validate(this CasdoorOptions options)
throw new ArgumentException("ClientId is required.");
}

if (string.IsNullOrWhiteSpace(options.ClientSecret))
{
throw new ArgumentException("ClientSecret is required.");
}

if (string.IsNullOrWhiteSpace(options.OrganizationName))
{
throw new ArgumentException("OrganizationName is required.");
Expand Down
4 changes: 2 additions & 2 deletions src/Casdoor.Client/Options/CasdoorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ public class CasdoorOptions
public class CasdoorPathOptions
{
public string ApiPath { get; set; } = "/api";
public string LoginAuthorizePath { get; set; } = "/api/login/oauth/authorize";
public string SignupAuthorizePath { get; set; } = "/api/signup/oauth/authorize";
public string LoginAuthorizePath { get; set; } = "/login/oauth/authorize";
public string SignupAuthorizePath { get; set; } = "/signup/oauth/authorize";
public string TokenPath { get; set; } = "/api/login/oauth/access_token";
}

Expand Down
76 changes: 76 additions & 0 deletions tests/Casdoor.Client.UnitTests/ApiClientTests/ClientTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Casdoor.Client.UnitTests.Fixtures;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Win32.SafeHandles;
using Xunit.Abstractions;

namespace Casdoor.Client.UnitTests.ApiClientTests;

public class ClientTest : IClassFixture<ServicesFixture>, IClassFixture<ServicesFixtureWithoutSecret>
{
private readonly ServicesFixture _servicesFixture;
private readonly ServicesFixtureWithoutSecret _servicesFixtureWithoutSecret;
private readonly ITestOutputHelper _testOutputHelper;

public ClientTest(ServicesFixture servicesFixture, ServicesFixtureWithoutSecret servicesFixtureWithoutSecret, ITestOutputHelper testOutputHelper)
{
_servicesFixture = servicesFixture;
_servicesFixtureWithoutSecret = servicesFixtureWithoutSecret;
_testOutputHelper = testOutputHelper;
}

[Fact]
public async void TestClient()
{
var tokenClient = _servicesFixture.ServiceProvider.GetService<ICasdoorClient>();
string name = "Token_" + new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds().ToString();
string code = "Code_" + new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds().ToString();
_testOutputHelper.WriteLine($"test with token name {name}");
string owner = "admin";
var verifier = "test";

var sha256Instance = SHA256.Create();
byte[] bytes = Encoding.Default.GetBytes(verifier);
byte[] chanllengeCodeEncoded = sha256Instance.ComputeHash(bytes);
string chanllengeCodeBase64Encoded = Convert.ToBase64String(chanllengeCodeEncoded).Replace("+", "-").Replace("/", "_").Replace("=", "");

CasdoorToken token = new CasdoorToken()
{
Owner = owner,
Name = name,
CreatedTime = DateTime.Now.ToString(),
Code = code,
AccessToken = code + "123456",
CodeChallenge = chanllengeCodeBase64Encoded,
Application = "app-example",
Organization = "casbin",
User = "admin",
CodeExpireIn = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds() + 19600
};

// Add the object
Task<CasdoorResponse?> responseAsync = tokenClient.AddTokenAsync(token);
CasdoorResponse? response = await responseAsync;
_testOutputHelper.WriteLine($"{response.Status} {response.Msg}");
Assert.Equal(CasdoorConstants.DefaultCasdoorSuccessStatus, response.Status);

var userClient = _servicesFixtureWithoutSecret.ServiceProvider.GetService<ICasdoorClient>();

if (userClient == null)
{
Assert.NotNull(userClient);
return;
}

var requestedToken = await userClient.RequestAuthorizationCodeTokenAsync(token.Code, "http://localhost:5000/callback", verifier);

Assert.Equal(requestedToken.AccessToken, token.AccessToken);
}

}

2 changes: 1 addition & 1 deletion tests/Casdoor.Client.UnitTests/ApiClientTests/TokenTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public TokenTest(ServicesFixture servicesFixture, ITestOutputHelper testOutputHe
public async void TestToken()
{
var tokenClient = _servicesFixture.ServiceProvider.GetService<ICasdoorClient>();
string name = "Token_" + new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString();
string name = "Token_" + new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds().ToString();
_testOutputHelper.WriteLine($"test with token name {name}");
string owner = "admin";

Expand Down
22 changes: 20 additions & 2 deletions tests/Casdoor.Client.UnitTests/Fixtures/ServicesFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,26 @@ public ServicesFixture()
options.Endpoint = "https://demo.casdoor.com";
options.OrganizationName = "casbin";
options.ApplicationName = "app-example";
options.ClientId = "294b09fbc17f95daf2fe";
options.ClientSecret = "dd8982f7046ccba1bbd7851d5c1ece4e52bf039d";
options.ClientId = "b800a86702dd4d29ec4d";
options.ClientSecret = "1219843a8db4695155699be3a67f10796f2ec1d5";
options.ApplicationType = "webapp";
}).BuildServiceProvider();
}

public IServiceProvider ServiceProvider { get; set; }
}

public class ServicesFixtureWithoutSecret
{
public ServicesFixtureWithoutSecret()
{
ServiceProvider = new ServiceCollection()
.AddCasdoorClient(options =>
{
options.Endpoint = "https://demo.casdoor.com";
options.OrganizationName = "casbin";
options.ApplicationName = "app-example";
options.ClientId = "b800a86702dd4d29ec4d";
options.ApplicationType = "webapp";
}).BuildServiceProvider();
}
Expand Down

0 comments on commit 2417ec0

Please sign in to comment.