Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AspNet.Security.OAuth.Providers.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
<Project Path="src/AspNet.Security.OAuth.Instagram/AspNet.Security.OAuth.Instagram.csproj" />
<Project Path="src/AspNet.Security.OAuth.JumpCloud/AspNet.Security.OAuth.JumpCloud.csproj" />
<Project Path="src/AspNet.Security.OAuth.KakaoTalk/AspNet.Security.OAuth.KakaoTalk.csproj" />
<Project Path="src/AspNet.Security.OAuth.Kick/AspNet.Security.OAuth.Kick.csproj" />
<Project Path="src/AspNet.Security.OAuth.Keycloak/AspNet.Security.OAuth.Keycloak.csproj" />
<Project Path="src/AspNet.Security.OAuth.Kook/AspNet.Security.OAuth.Kook.csproj" />
<Project Path="src/AspNet.Security.OAuth.Kroger/AspNet.Security.OAuth.Kroger.csproj" />
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ If a provider you're looking for does not exist, consider making a PR to add one
| JumpCloud | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.JumpCloud?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.JumpCloud/ "Download AspNet.Security.OAuth.JumpCloud from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.JumpCloud?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.JumpCloud "Download AspNet.Security.OAuth.JumpCloud from MyGet.org") | [Documentation](https://jumpcloud.com/support/sso-with-oidc "JumpCloud developer documentation") |
| KakaoTalk | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.KakaoTalk?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.KakaoTalk/ "Download AspNet.Security.OAuth.KakaoTalk from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.KakaoTalk?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.KakaoTalk "Download AspNet.Security.OAuth.KakaoTalk from MyGet.org") | [Documentation](https://developers.kakao.com/docs/latest/en/kakaologin/common "KakaoTalk developer documentation") |
| Keycloak | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Keycloak?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Keycloak/ "Download AspNet.Security.OAuth.Keycloak from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Keycloak?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Keycloak "Download AspNet.Security.OAuth.Keycloak from MyGet.org") | [Documentation](https://www.keycloak.org/docs/latest/authorization_services/#_service_overview "Keycloak developer documentation") |
| Kick | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Kick?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Kick/ "Download AspNet.Security.OAuth.Kick from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Kick?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Kick "Download AspNet.Security.OAuth.Kick from MyGet.org") | [Documentation](https://docs.kick.com/getting-started/introduction "Kick developer documentation") |
Comment thread
danbopes marked this conversation as resolved.
Outdated
| KOOK | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Kook?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Kook/ "Download AspNet.Security.OAuth.Kook from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Kook?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Kook "Download AspNet.Security.OAuth.Kook from MyGet.org") | [Documentation](https://developer.kookapp.cn/doc/oauth2 "KOOK developer documentation") |
| Kroger | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Kroger?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Kroger/ "Download AspNet.Security.OAuth.Kroger from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Kroger?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Kroger "Download AspNet.Security.OAuth.Kroger from MyGet.org") | [Documentation](https://developer.kroger.com/reference/#section/Authentication "Kroger developer documentation") |
| Lichess | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Lichess?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Lichess/ "Download AspNet.Security.OAuth.Lichess from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Lichess?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Lichess "Download AspNet.Security.OAuth.Lichess from MyGet.org") | [Documentation](https://lichess.org/api#section/Authentication "Lichess developer documentation") |
Expand Down
21 changes: 21 additions & 0 deletions src/AspNet.Security.OAuth.Kick/AspNet.Security.OAuth.Kick.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageValidationBaselineVersion>10.1.0</PackageValidationBaselineVersion>
<!-- TODO Remove once published to NuGet.org -->
<DisablePackageBaselineValidation>true</DisablePackageBaselineValidation>
<TargetFrameworks>$(DefaultNetCoreTargetFramework)</TargetFrameworks>
Comment thread
danbopes marked this conversation as resolved.
</PropertyGroup>

<PropertyGroup>
<Description>ASP.NET Core security middleware enabling Kick authentication.</Description>
<Authors>Daniel Beaupre</Authors>
<PackageTags>aspnetcore;authentication;kick;oauth;security</PackageTags>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
</ItemGroup>

</Project>
18 changes: 18 additions & 0 deletions src/AspNet.Security.OAuth.Kick/KickAuthenticationConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/

namespace AspNet.Security.OAuth.Kick;

/// <summary>
/// Contains constants specific to the <see cref="KickAuthenticationHandler"/>.
/// </summary>
public static class KickAuthenticationConstants
{
public static class Claims
{
public static readonly string ProfilePicture = "urn:kick:profilepicture";
}
}
48 changes: 48 additions & 0 deletions src/AspNet.Security.OAuth.Kick/KickAuthenticationDefaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/

namespace AspNet.Security.OAuth.Kick;

/// <summary>
/// Default values used by the Kick authentication middleware.
/// </summary>
public static class KickAuthenticationDefaults
{
/// <summary>
/// Default value for <see cref="AuthenticationScheme.Name"/>.
/// </summary>
public const string AuthenticationScheme = "Kick";

/// <summary>
/// Default value for <see cref="AuthenticationScheme.DisplayName"/>.
/// </summary>
public static readonly string DisplayName = "Kick";

/// <summary>
/// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>.
/// </summary>
public static readonly string Issuer = "Kick";

/// <summary>
/// Default value for <see cref="RemoteAuthenticationOptions.CallbackPath"/>.
/// </summary>
public static readonly string CallbackPath = "/signin-kick";

/// <summary>
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
/// </summary>
public static readonly string AuthorizationEndpoint = "https://id.kick.com/oauth/authorize";

/// <summary>
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
/// </summary>
public static readonly string TokenEndpoint = "https://id.kick.com/oauth/token";

/// <summary>
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
/// </summary>
public static readonly string UserInformationEndpoint = "https://api.kick.com/public/v1/users";
}
74 changes: 74 additions & 0 deletions src/AspNet.Security.OAuth.Kick/KickAuthenticationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/

using AspNet.Security.OAuth.Kick;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extension methods to add Kick authentication capabilities to an HTTP application pipeline.
/// </summary>
public static class KickAuthenticationExtensions
{
/// <summary>
/// Adds <see cref="KickAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Kick authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddKick([NotNull] this AuthenticationBuilder builder)
{
return builder.AddKick(KickAuthenticationDefaults.AuthenticationScheme, options => { });
}

/// <summary>
/// Adds <see cref="KickAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Kick authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <param name="configuration">The delegate used to configure the Kick options.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddKick(
[NotNull] this AuthenticationBuilder builder,
[NotNull] Action<KickAuthenticationOptions> configuration)
{
return builder.AddKick(KickAuthenticationDefaults.AuthenticationScheme, configuration);
}

/// <summary>
/// Adds <see cref="KickAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Kick authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <param name="scheme">The authentication scheme associated with this instance.</param>
/// <param name="configuration">The delegate used to configure the Kick options.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddKick(
[NotNull] this AuthenticationBuilder builder,
[NotNull] string scheme,
[NotNull] Action<KickAuthenticationOptions> configuration)
{
return builder.AddKick(scheme, KickAuthenticationDefaults.DisplayName, configuration);
}

/// <summary>
/// Adds <see cref="KickAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Kick authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <param name="scheme">The authentication scheme associated with this instance.</param>
/// <param name="caption">The optional display name associated with this instance.</param>
/// <param name="configuration">The delegate used to configure the Kick options.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddKick(
[NotNull] this AuthenticationBuilder builder,
[NotNull] string scheme,
[CanBeNull] string caption,
[NotNull] Action<KickAuthenticationOptions> configuration)
{
return builder.AddOAuth<KickAuthenticationOptions, KickAuthenticationHandler>(scheme, caption, configuration);
}
}
73 changes: 73 additions & 0 deletions src/AspNet.Security.OAuth.Kick/KickAuthenticationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/

using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace AspNet.Security.OAuth.Kick;

/// <summary>
/// Authentication handler for Kick OAuth.
/// </summary>
public partial class KickAuthenticationHandler : OAuthHandler<KickAuthenticationOptions>
{
public KickAuthenticationHandler(
[NotNull] IOptionsMonitor<KickAuthenticationOptions> options,
[NotNull] ILoggerFactory logger,
[NotNull] UrlEncoder encoder)
: base(options, logger, encoder)
{
}

protected override async Task<AuthenticationTicket> CreateTicketAsync(
[NotNull] ClaimsIdentity identity,
[NotNull] AuthenticationProperties properties,
[NotNull] OAuthTokenResponse tokens)
{
using var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);

using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
await Log.UserProfileErrorAsync(Logger, response, Context.RequestAborted);
throw new HttpRequestException("An error occurred while retrieving the user profile.");
}

using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted));

var principal = new ClaimsPrincipal(identity);
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
context.RunClaimActions();

await Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
}

private static partial class Log
{
internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken)
{
UserProfileError(
logger,
response.StatusCode,
response.Headers.ToString(),
await response.Content.ReadAsStringAsync(cancellationToken));
}

[LoggerMessage(1, LogLevel.Error, "An error occurred while retrieving the user profile: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")]
private static partial void UserProfileError(
ILogger logger,
System.Net.HttpStatusCode status,
string headers,
string body);
}
}
49 changes: 49 additions & 0 deletions src/AspNet.Security.OAuth.Kick/KickAuthenticationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/

using System.Security.Claims;
using System.Text.Json;
using static AspNet.Security.OAuth.Kick.KickAuthenticationConstants;

namespace AspNet.Security.OAuth.Kick;

/// <summary>
/// Defines a set of options used by <see cref="KickAuthenticationHandler"/>.
/// </summary>
public class KickAuthenticationOptions : OAuthOptions
{
public KickAuthenticationOptions()
{
ClaimsIssuer = KickAuthenticationDefaults.Issuer;
CallbackPath = KickAuthenticationDefaults.CallbackPath;

AuthorizationEndpoint = KickAuthenticationDefaults.AuthorizationEndpoint;
TokenEndpoint = KickAuthenticationDefaults.TokenEndpoint;
UserInformationEndpoint = KickAuthenticationDefaults.UserInformationEndpoint;

Scope.Add("user:read");

ClaimActions.MapCustomJson(ClaimTypes.NameIdentifier, user => GetData(user, "user_id"));
ClaimActions.MapCustomJson(ClaimTypes.Name, user => GetData(user, "name"));
ClaimActions.MapCustomJson(ClaimTypes.Email, user => GetData(user, "email"));
ClaimActions.MapCustomJson(Claims.ProfilePicture, user => GetData(user, "profile_picture"));

// Kick requires PKCE (OAuth 2.1)
UsePkce = true;
}

private static string? GetData(JsonElement user, string key)
{
if (!user.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Array)
{
return null;
}

return data.EnumerateArray()
.Select(p => p.GetString(key))
.FirstOrDefault();
}
}
59 changes: 59 additions & 0 deletions test/AspNet.Security.OAuth.Providers.Tests/Kick/KickTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/

using Microsoft.AspNetCore.WebUtilities;
using static AspNet.Security.OAuth.Kick.KickAuthenticationConstants;

namespace AspNet.Security.OAuth.Kick;

public class KickTests(ITestOutputHelper outputHelper) : OAuthTests<KickAuthenticationOptions>(outputHelper)
{
public override string DefaultScheme => KickAuthenticationDefaults.AuthenticationScheme;

protected internal override void RegisterAuthentication(AuthenticationBuilder builder)
{
builder.AddKick(options => ConfigureDefaults(builder, options));
}

[Theory]
[InlineData(ClaimTypes.NameIdentifier, "123456")]
[InlineData(ClaimTypes.Name, "testuser")]
[InlineData(ClaimTypes.Email, "test@example.com")]
[InlineData(Claims.ProfilePicture, "https://files.kick.com/images/user/123456/profile_image.png")]
Comment thread
danbopes marked this conversation as resolved.
Outdated
public async Task Can_Sign_In_Using_Kick(string claimType, string claimValue)
=> await AuthenticateUserAndAssertClaimValue(claimType, claimValue);

[Fact]
public async Task BuildChallengeUrl_Generates_Correct_Url_With_Pkce()
{
// Arrange
var options = new KickAuthenticationOptions();

var redirectUrl = "https://my-site.local/signin-kick";

// Act
Uri actual = await BuildChallengeUriAsync(
options,
redirectUrl,
(options, loggerFactory, encoder) => new KickAuthenticationHandler(options, loggerFactory, encoder));

// Assert
actual.ShouldNotBeNull();
actual.ToString().ShouldStartWith("https://id.kick.com/oauth/authorize?");

var query = QueryHelpers.ParseQuery(actual.Query);

query.ShouldContainKey("state");
query.ShouldContainKeyAndValue("client_id", options.ClientId);
query.ShouldContainKeyAndValue("redirect_uri", redirectUrl);
query.ShouldContainKeyAndValue("response_type", "code");
query.ShouldContainKeyAndValue("scope", "user:read");

// Kick requires PKCE
query.ShouldContainKey(OAuthConstants.CodeChallengeKey);
query.ShouldContainKey(OAuthConstants.CodeChallengeMethodKey);
}
}
Loading