Skip to content

Commit

Permalink
Implement Typeform OAuth provider (#897)
Browse files Browse the repository at this point in the history
Implement Typeform OAuth provider.
  • Loading branch information
denis-goncharenko authored Jun 14, 2024
1 parent ab98a32 commit e2d2ced
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 0 deletions.
7 changes: 7 additions & 0 deletions AspNet.Security.OAuth.Providers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.PingO
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.JumpCloud", "src\AspNet.Security.OAuth.JumpCloud\AspNet.Security.OAuth.JumpCloud.csproj", "{8AF5DDBE-2631-4E71-9045-73A6356CE86B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Typeform", "src\AspNet.Security.OAuth.Typeform\AspNet.Security.OAuth.Typeform.csproj", "{31333261-A9C2-4AEB-AA6C-AC66DB4FA966}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Calendly", "src\AspNet.Security.OAuth.Calendly\AspNet.Security.OAuth.Calendly.csproj", "{ADAC649F-A8CC-4CF2-8C34-288F7DEBBE69}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Airtable", "src\AspNet.Security.OAuth.Airtable\AspNet.Security.OAuth.Airtable.csproj", "{83C37AC5-51FB-47CD-8CBE-77AA114FF6F3}"
Expand Down Expand Up @@ -684,6 +686,10 @@ Global
{8AF5DDBE-2631-4E71-9045-73A6356CE86B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8AF5DDBE-2631-4E71-9045-73A6356CE86B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8AF5DDBE-2631-4E71-9045-73A6356CE86B}.Release|Any CPU.Build.0 = Release|Any CPU
{31333261-A9C2-4AEB-AA6C-AC66DB4FA966}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{31333261-A9C2-4AEB-AA6C-AC66DB4FA966}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31333261-A9C2-4AEB-AA6C-AC66DB4FA966}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31333261-A9C2-4AEB-AA6C-AC66DB4FA966}.Release|Any CPU.Build.0 = Release|Any CPU
{ADAC649F-A8CC-4CF2-8C34-288F7DEBBE69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ADAC649F-A8CC-4CF2-8C34-288F7DEBBE69}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ADAC649F-A8CC-4CF2-8C34-288F7DEBBE69}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -801,6 +807,7 @@ Global
{101681FB-569F-4941-B943-2AD380039BE0} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
{CF8C4235-6AE6-404E-B572-4FF4E85AB5FF} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
{8AF5DDBE-2631-4E71-9045-73A6356CE86B} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
{31333261-A9C2-4AEB-AA6C-AC66DB4FA966} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
{ADAC649F-A8CC-4CF2-8C34-288F7DEBBE69} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
{83C37AC5-51FB-47CD-8CBE-77AA114FF6F3} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
{55975423-C9C0-4C47-AD00-0F012F30AD3C} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ If a provider you're looking for does not exist, consider making a PR to add one
| Trovo | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Trovo?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Trovo/ "Download AspNet.Security.OAuth.Trovo from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Trovo?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Trovo "Download AspNet.Security.OAuth.Trovo from MyGet.org") | [Documentation](https://developer.trovo.live/docs/APIs.html "Trovo developer documentation") |
| Twitch | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Twitch?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Twitch/ "Download AspNet.Security.OAuth.Twitch from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Twitch?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Twitch "Download AspNet.Security.OAuth.Twitch from MyGet.org") | [Documentation](https://dev.twitch.tv/docs/authentication/ "Twitch developer documentation") |
| Twitter | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Twitter?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Twitter/ "Download AspNet.Security.OAuth.Twitter from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Twitter?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Twitter "Download AspNet.Security.OAuth.Twitter from MyGet.org") | [Documentation](https://developer.twitter.com/en/docs/authentication/oauth-2-0 "Twitter developer documentation") |
| Typeform | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Typeform?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Typeform/ "Download AspNet.Security.OAuth.Typeform from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Typeform?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Typeform "Download AspNet.Security.OAuth.Typeform from MyGet.org") | [Documentation](https://www.typeform.com/developers/get-started/applications/ "Typeform developer documentation") |
| Untappd | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Untappd?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Untappd/ "Download AspNet.Security.OAuth.Untappd from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Untappd?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Untappd "Download AspNet.Security.OAuth.Untappd from MyGet.org") | [Documentation](https://untappd.com/api/docs#authentication "Untappd developer documentation") |
| Vimeo | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Vimeo?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Vimeo/ "Download AspNet.Security.OAuth.Vimeo from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Vimeo?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Vimeo "Download AspNet.Security.OAuth.Vimeo from MyGet.org") | [Documentation](https://developer.vimeo.com/api/authentication "Vimeo developer documentation") |
| Visual Studio (Azure DevOps) | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.VisualStudio?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.VisualStudio/ "Download AspNet.Security.OAuth.VisualStudio from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.VisualStudio?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.VisualStudio "Download AspNet.Security.OAuth.VisualStudio from MyGet.org") | [Documentation](https://docs.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops "Azure DevOps developer documentation") |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(DefaultNetCoreTargetFramework)</TargetFrameworks>
</PropertyGroup>

<!-- TODO Enable once this provider is published to NuGet.org -->
<PropertyGroup>
<DisablePackageBaselineValidation>true</DisablePackageBaselineValidation>
<PackageValidationBaselineVersion>8.0.1</PackageValidationBaselineVersion>
</PropertyGroup>

<PropertyGroup>
<Description>ASP.NET Core security middleware enabling Typeform authentication.</Description>
<Authors>Denys Goncharenko</Authors>
<PackageTags>aspnetcore;authentication;oauth;typeform;security</PackageTags>
</PropertyGroup>

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

</Project>
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.Typeform;

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

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

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

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

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

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

/// <summary>
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
/// </summary>
public static readonly string UserInformationEndpoint = "https://api.typeform.com/me";
}
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 Microsoft.Extensions.DependencyInjection;

namespace AspNet.Security.OAuth.Typeform;

/// <summary>
/// Extension methods to add Typeform authentication capabilities to an HTTP application pipeline.
/// </summary>
public static class TypeformAuthenticationExtensions
{
/// <summary>
/// Adds <see cref="TypeformAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Typeform authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static AuthenticationBuilder AddTypeform([NotNull] this AuthenticationBuilder builder)
{
return builder.AddTypeform(TypeformAuthenticationDefaults.AuthenticationScheme, options => { });
}

/// <summary>
/// Adds <see cref="TypeformAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Typeform authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <param name="configuration">The delegate used to configure the OpenID 2.0 options.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static AuthenticationBuilder AddTypeform(
[NotNull] this AuthenticationBuilder builder,
[NotNull] Action<TypeformAuthenticationOptions> configuration)
{
return builder.AddTypeform(TypeformAuthenticationDefaults.AuthenticationScheme, configuration);
}

/// <summary>
/// Adds <see cref="TypeformAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Typeform 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 Typeform options.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddTypeform(
[NotNull] this AuthenticationBuilder builder,
[NotNull] string scheme,
[NotNull] Action<TypeformAuthenticationOptions> configuration)
{
return builder.AddTypeform(scheme, TypeformAuthenticationDefaults.DisplayName, configuration);
}

/// <summary>
/// Adds <see cref="TypeformAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Typeform 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 Typeform options.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddTypeform(
[NotNull] this AuthenticationBuilder builder,
[NotNull] string scheme,
[CanBeNull] string caption,
[NotNull] Action<TypeformAuthenticationOptions> configuration)
{
return builder.AddOAuth<TypeformAuthenticationOptions, TypeformAuthenticationHandler>(scheme, caption, configuration);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.Net.Mime;
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.Typeform;

public partial class TypeformAuthenticationHandler : OAuthHandler<TypeformAuthenticationOptions>
{
public TypeformAuthenticationHandler(
[NotNull] IOptionsMonitor<TypeformAuthenticationOptions> 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(MediaTypeNames.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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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;

namespace AspNet.Security.OAuth.Typeform;

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

AuthorizationEndpoint = TypeformAuthenticationDefaults.AuthorizationEndpoint;
TokenEndpoint = TypeformAuthenticationDefaults.TokenEndpoint;
UserInformationEndpoint = TypeformAuthenticationDefaults.UserInformationEndpoint;

Scope.Add("accounts:read");

ClaimActions.MapCustomJson(ClaimTypes.NameIdentifier, user => user.GetString("user_id"));
ClaimActions.MapCustomJson(ClaimTypes.Name, user => user.GetString("alias"));
ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.GetString("email"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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.Typeform;

public class TypeformTests(ITestOutputHelper outputHelper) : OAuthTests<TypeformAuthenticationOptions>(outputHelper)
{
public override string DefaultScheme => TypeformAuthenticationDefaults.AuthenticationScheme;

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

[Theory]
[InlineData(ClaimTypes.NameIdentifier, "id")]
[InlineData(ClaimTypes.Name, "Test Name")]
[InlineData(ClaimTypes.Email, "testuser@example.com")]
public async Task Can_Sign_In_Using_Typeform(string claimType, string claimValue)
=> await AuthenticateUserAndAssertClaimValue(claimType, claimValue);
}
25 changes: 25 additions & 0 deletions test/AspNet.Security.OAuth.Providers.Tests/Typeform/bundle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://raw.githubusercontent.com/justeat/httpclient-interception/master/src/HttpClientInterception/Bundles/http-request-bundle-schema.json",
"items": [
{
"uri": "https://api.typeform.com/oauth/token",
"method": "POST",
"contentFormat": "json",
"contentJson": {
"access_token": "secret-access-token",
"token_type": "Bearer",
"refresh_token": "secret-refresh-token",
"expires_in": 3600
}
},
{
"uri": "https://api.typeform.com/me",
"contentFormat": "json",
"contentJson": {
"user_id": "id",
"email": "testuser@example.com",
"alias": "Test Name"
}
}
]
}

0 comments on commit e2d2ced

Please sign in to comment.