From dbf1be4c5d5ecacfbc9c5ff7a3312807018c6bf6 Mon Sep 17 00:00:00 2001 From: "t.ruspekhofer" Date: Sun, 13 Feb 2022 20:40:15 +0100 Subject: [PATCH] implemented Discord OAuth2 --- Lieb.sln | 6 ---- Lieb/App.razor | 15 +++++--- Lieb/Data/AccountController.cs | 32 +++++++++++++++++ Lieb/Data/WeatherForecast.cs | 13 ------- Lieb/Data/WeatherForecastService.cs | 20 ----------- Lieb/DiscordOAuth2/DiscordDefaults.cs | 12 +++++++ Lieb/DiscordOAuth2/DiscordExtensions.cs | 22 ++++++++++++ Lieb/DiscordOAuth2/DiscordHandler.cs | 36 +++++++++++++++++++ Lieb/DiscordOAuth2/DiscordOptions.cs | 33 +++++++++++++++++ Lieb/Lieb.csproj | 6 +--- Lieb/Pages/Counter.razor | 18 ---------- Lieb/Pages/FetchData.razor | 48 ------------------------- Lieb/Pages/RedirectToLogin.razor | 11 ++++++ Lieb/Pages/_Host.cshtml | 3 +- Lieb/Program.cs | 24 ++++++++++++- Lieb/Shared/MainLayout.razor | 9 +++++ Lieb/Shared/SurveyPrompt.razor | 16 --------- Lieb/appsettings.json | 4 +++ 18 files changed, 195 insertions(+), 133 deletions(-) create mode 100644 Lieb/Data/AccountController.cs delete mode 100644 Lieb/Data/WeatherForecast.cs delete mode 100644 Lieb/Data/WeatherForecastService.cs create mode 100644 Lieb/DiscordOAuth2/DiscordDefaults.cs create mode 100644 Lieb/DiscordOAuth2/DiscordExtensions.cs create mode 100644 Lieb/DiscordOAuth2/DiscordHandler.cs create mode 100644 Lieb/DiscordOAuth2/DiscordOptions.cs delete mode 100644 Lieb/Pages/Counter.razor delete mode 100644 Lieb/Pages/FetchData.razor create mode 100644 Lieb/Pages/RedirectToLogin.razor delete mode 100644 Lieb/Shared/SurveyPrompt.razor diff --git a/Lieb.sln b/Lieb.sln index 83bc8b6..f38c7eb 100644 --- a/Lieb.sln +++ b/Lieb.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 17.0.32126.317 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lieb", "Lieb\Lieb.csproj", "{48554958-F16E-466A-B9B7-F17511FDA415}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.OAuth2", "Discord.OAuth2\Discord.OAuth2.csproj", "{9376C31F-F349-4A6C-B763-BD9DCE188607}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -17,10 +15,6 @@ Global {48554958-F16E-466A-B9B7-F17511FDA415}.Debug|Any CPU.Build.0 = Debug|Any CPU {48554958-F16E-466A-B9B7-F17511FDA415}.Release|Any CPU.ActiveCfg = Release|Any CPU {48554958-F16E-466A-B9B7-F17511FDA415}.Release|Any CPU.Build.0 = Release|Any CPU - {9376C31F-F349-4A6C-B763-BD9DCE188607}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9376C31F-F349-4A6C-B763-BD9DCE188607}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9376C31F-F349-4A6C-B763-BD9DCE188607}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9376C31F-F349-4A6C-B763-BD9DCE188607}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Lieb/App.razor b/Lieb/App.razor index 6fd3ed1..d61232d 100644 --- a/Lieb/App.razor +++ b/Lieb/App.razor @@ -1,12 +1,17 @@  - + + + + + - Not found - -

Sorry, there's nothing at this address.

-
+ + +

Sorry, there's nothing at this address.

+
+
diff --git a/Lieb/Data/AccountController.cs b/Lieb/Data/AccountController.cs new file mode 100644 index 0000000..1808460 --- /dev/null +++ b/Lieb/Data/AccountController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Mvc; + +namespace Lieb.Data +{ + [Route("[controller]/[action]")] // Microsoft.AspNetCore.Mvc.Route + public class AccountController : ControllerBase + { + public IDataProtectionProvider Provider { get; } + + public AccountController(IDataProtectionProvider provider) + { + Provider = provider; + } + + [HttpGet] + public IActionResult Login(string returnUrl = "/") + { + return Challenge(new AuthenticationProperties { RedirectUri = returnUrl }, "Discord"); + } + + [HttpGet] + public async Task Logout(string returnUrl = "/") + { + //This removes the cookie assigned to the user login. + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return LocalRedirect(returnUrl); + } + } +} diff --git a/Lieb/Data/WeatherForecast.cs b/Lieb/Data/WeatherForecast.cs deleted file mode 100644 index 0f6bed2..0000000 --- a/Lieb/Data/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Lieb.Data -{ - public class WeatherForecast - { - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -} \ No newline at end of file diff --git a/Lieb/Data/WeatherForecastService.cs b/Lieb/Data/WeatherForecastService.cs deleted file mode 100644 index 20f987b..0000000 --- a/Lieb/Data/WeatherForecastService.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Lieb.Data -{ - public class WeatherForecastService - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - public Task GetForecastAsync(DateTime startDate) - { - return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = startDate.AddDays(index), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }).ToArray()); - } - } -} \ No newline at end of file diff --git a/Lieb/DiscordOAuth2/DiscordDefaults.cs b/Lieb/DiscordOAuth2/DiscordDefaults.cs new file mode 100644 index 0000000..b995c60 --- /dev/null +++ b/Lieb/DiscordOAuth2/DiscordDefaults.cs @@ -0,0 +1,12 @@ +namespace Discord.OAuth2 +{ + public static class DiscordDefaults + { + public const string AuthenticationScheme = "Discord"; + public const string DisplayName = "Discord"; + + public static readonly string AuthorizationEndpoint = "https://discordapp.com/api/oauth2/authorize"; + public static readonly string TokenEndpoint = "https://discordapp.com/api/oauth2/token"; + public static readonly string UserInformationEndpoint = "https://discordapp.com/api/users/@me"; + } +} diff --git a/Lieb/DiscordOAuth2/DiscordExtensions.cs b/Lieb/DiscordOAuth2/DiscordExtensions.cs new file mode 100644 index 0000000..9ae8269 --- /dev/null +++ b/Lieb/DiscordOAuth2/DiscordExtensions.cs @@ -0,0 +1,22 @@ + +using System; +using Microsoft.AspNetCore.Authentication; +using Discord.OAuth2; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class DiscordAuthenticationOptionsExtensions + { + public static AuthenticationBuilder AddDiscord(this AuthenticationBuilder builder) + => builder.AddDiscord(DiscordDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddDiscord(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddDiscord(DiscordDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddDiscord(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddDiscord(authenticationScheme, DiscordDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddDiscord(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + => builder.AddOAuth(authenticationScheme, displayName, configureOptions); + } +} \ No newline at end of file diff --git a/Lieb/DiscordOAuth2/DiscordHandler.cs b/Lieb/DiscordOAuth2/DiscordHandler.cs new file mode 100644 index 0000000..09457b9 --- /dev/null +++ b/Lieb/DiscordOAuth2/DiscordHandler.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.Extensions.Options; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Text.Json; + +namespace Discord.OAuth2 +{ + internal class DiscordHandler : OAuthHandler + { + public DiscordHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + protected override async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) + { + var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await Backchannel.SendAsync(request, Context.RequestAborted); + if (!response.IsSuccessStatusCode) + throw new HttpRequestException($"Failed to retrieve Discord user information ({response.StatusCode})."); + + var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement); + context.RunClaimActions(); + + await Events.CreatingTicket(context); + return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); + } + } +} diff --git a/Lieb/DiscordOAuth2/DiscordOptions.cs b/Lieb/DiscordOAuth2/DiscordOptions.cs new file mode 100644 index 0000000..d705c68 --- /dev/null +++ b/Lieb/DiscordOAuth2/DiscordOptions.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Http; +using System.Security.Claims; + +namespace Discord.OAuth2 +{ + /// Configuration options for . + public class DiscordOptions : OAuthOptions + { + /// Initializes a new . + public DiscordOptions() + { + CallbackPath = new PathString("/signin-discord"); + AuthorizationEndpoint = DiscordDefaults.AuthorizationEndpoint; + TokenEndpoint = DiscordDefaults.TokenEndpoint; + UserInformationEndpoint = DiscordDefaults.UserInformationEndpoint; + Scope.Add("identify"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id", ClaimValueTypes.UInteger64); + ClaimActions.MapJsonKey(ClaimTypes.Name, "username", ClaimValueTypes.String); + ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email); + ClaimActions.MapJsonKey("urn:discord:discriminator", "discriminator", ClaimValueTypes.UInteger32); + ClaimActions.MapJsonKey("urn:discord:avatar", "avatar", ClaimValueTypes.String); + ClaimActions.MapJsonKey("urn:discord:verified", "verified", ClaimValueTypes.Boolean); + } + + /// Gets or sets the Discord-assigned appId. + public string AppId { get => ClientId; set => ClientId = value; } + /// Gets or sets the Discord-assigned app secret. + public string AppSecret { get => ClientSecret; set => ClientSecret = value; } + } +} diff --git a/Lieb/Lieb.csproj b/Lieb/Lieb.csproj index 5a58a59..5e963bc 100644 --- a/Lieb/Lieb.csproj +++ b/Lieb/Lieb.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -6,8 +6,4 @@ enable - - - - diff --git a/Lieb/Pages/Counter.razor b/Lieb/Pages/Counter.razor deleted file mode 100644 index ef23cb3..0000000 --- a/Lieb/Pages/Counter.razor +++ /dev/null @@ -1,18 +0,0 @@ -@page "/counter" - -Counter - -

Counter

- -

Current count: @currentCount

- - - -@code { - private int currentCount = 0; - - private void IncrementCount() - { - currentCount++; - } -} diff --git a/Lieb/Pages/FetchData.razor b/Lieb/Pages/FetchData.razor deleted file mode 100644 index 9f20ab8..0000000 --- a/Lieb/Pages/FetchData.razor +++ /dev/null @@ -1,48 +0,0 @@ -@page "/fetchdata" - -Weather forecast - -@using Lieb.Data -@inject WeatherForecastService ForecastService - -

Weather forecast

- -

This component demonstrates fetching data from a service.

- -@if (forecasts == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-} - -@code { - private WeatherForecast[]? forecasts; - - protected override async Task OnInitializedAsync() - { - forecasts = await ForecastService.GetForecastAsync(DateTime.Now); - } -} diff --git a/Lieb/Pages/RedirectToLogin.razor b/Lieb/Pages/RedirectToLogin.razor new file mode 100644 index 0000000..9352552 --- /dev/null +++ b/Lieb/Pages/RedirectToLogin.razor @@ -0,0 +1,11 @@ +@using Microsoft.AspNetCore.Components +@inject NavigationManager Navigation + +@code +{ + protected override void OnInitialized() + { + //This auto redirects a user to the login page + Navigation.NavigateTo("Account/Login", true); + } +} diff --git a/Lieb/Pages/_Host.cshtml b/Lieb/Pages/_Host.cshtml index 03dc147..7d14b20 100644 --- a/Lieb/Pages/_Host.cshtml +++ b/Lieb/Pages/_Host.cshtml @@ -5,4 +5,5 @@ Layout = "_Layout"; } - +@(await Html.RenderComponentAsync(RenderMode.Server)) + diff --git a/Lieb/Program.cs b/Lieb/Program.cs index 56d6d08..40674ce 100644 --- a/Lieb/Program.cs +++ b/Lieb/Program.cs @@ -1,4 +1,6 @@ +using Discord.OAuth2; using Lieb.Data; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; @@ -7,7 +9,20 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); -builder.Services.AddSingleton(); +builder.Services.AddAuthentication(opt => +{ + opt.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.DefaultChallengeScheme = DiscordDefaults.AuthenticationScheme; +}) + .AddCookie() + .AddDiscord(x => + { + x.AppId = builder.Configuration["Discord:AppId"]; + x.AppSecret = builder.Configuration["Discord:AppSecret"]; + + x.SaveTokens = true; + }); var app = builder.Build(); @@ -19,11 +34,18 @@ if (!app.Environment.IsDevelopment()) app.UseHsts(); } + app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); +app.UseAuthentication(); + +app.UseEndpoints(endpoints => +{ + endpoints.MapDefaultControllerRoute(); +}); app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); diff --git a/Lieb/Shared/MainLayout.razor b/Lieb/Shared/MainLayout.razor index da08087..4974751 100644 --- a/Lieb/Shared/MainLayout.razor +++ b/Lieb/Shared/MainLayout.razor @@ -10,6 +10,15 @@
diff --git a/Lieb/Shared/SurveyPrompt.razor b/Lieb/Shared/SurveyPrompt.razor deleted file mode 100644 index e3e6429..0000000 --- a/Lieb/Shared/SurveyPrompt.razor +++ /dev/null @@ -1,16 +0,0 @@ -
- - @Title - - - Please take our - brief survey - - and tell us what you think. -
- -@code { - // Demonstrates how a parent component can supply parameters - [Parameter] - public string? Title { get; set; } -} diff --git a/Lieb/appsettings.json b/Lieb/appsettings.json index 10f68b8..51206b8 100644 --- a/Lieb/appsettings.json +++ b/Lieb/appsettings.json @@ -5,5 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, + "Discord": { + "AppId": "942448872335220806", + "AppSecret": "5tsNxK9LtFpNqxqQnLkSJ1B0HJ7P7YUF" + }, "AllowedHosts": "*" }