implemented Discord OAuth2
This commit is contained in:
parent
9365e22874
commit
dbf1be4c5d
18 changed files with 195 additions and 133 deletions
6
Lieb.sln
6
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
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
<DiscordAuthTest.Pages.RedirectToLogin/>
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
<CascadingAuthenticationState>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</CascadingAuthenticationState>
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
|
32
Lieb/Data/AccountController.cs
Normal file
32
Lieb/Data/AccountController.cs
Normal file
|
@ -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<IActionResult> Logout(string returnUrl = "/")
|
||||
{
|
||||
//This removes the cookie assigned to the user login.
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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<WeatherForecast[]> 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());
|
||||
}
|
||||
}
|
||||
}
|
12
Lieb/DiscordOAuth2/DiscordDefaults.cs
Normal file
12
Lieb/DiscordOAuth2/DiscordDefaults.cs
Normal file
|
@ -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";
|
||||
}
|
||||
}
|
22
Lieb/DiscordOAuth2/DiscordExtensions.cs
Normal file
22
Lieb/DiscordOAuth2/DiscordExtensions.cs
Normal file
|
@ -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<DiscordOptions> configureOptions)
|
||||
=> builder.AddDiscord(DiscordDefaults.AuthenticationScheme, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddDiscord(this AuthenticationBuilder builder, string authenticationScheme, Action<DiscordOptions> configureOptions)
|
||||
=> builder.AddDiscord(authenticationScheme, DiscordDefaults.DisplayName, configureOptions);
|
||||
|
||||
public static AuthenticationBuilder AddDiscord(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<DiscordOptions> configureOptions)
|
||||
=> builder.AddOAuth<DiscordOptions, DiscordHandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
}
|
36
Lieb/DiscordOAuth2/DiscordHandler.cs
Normal file
36
Lieb/DiscordOAuth2/DiscordHandler.cs
Normal file
|
@ -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<DiscordOptions>
|
||||
{
|
||||
public DiscordHandler(IOptionsMonitor<DiscordOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
|
||||
: base(options, logger, encoder, clock)
|
||||
{
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticationTicket> 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);
|
||||
}
|
||||
}
|
||||
}
|
33
Lieb/DiscordOAuth2/DiscordOptions.cs
Normal file
33
Lieb/DiscordOAuth2/DiscordOptions.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.OAuth;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Discord.OAuth2
|
||||
{
|
||||
/// <summary> Configuration options for <see cref="DiscordHandler"/>. </summary>
|
||||
public class DiscordOptions : OAuthOptions
|
||||
{
|
||||
/// <summary> Initializes a new <see cref="DiscordOptions"/>. </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary> Gets or sets the Discord-assigned appId. </summary>
|
||||
public string AppId { get => ClientId; set => ClientId = value; }
|
||||
/// <summary> Gets or sets the Discord-assigned app secret. </summary>
|
||||
public string AppSecret { get => ClientSecret; set => ClientSecret = value; }
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
@ -6,8 +6,4 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Discord.OAuth2\Discord.OAuth2.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
@page "/counter"
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
@page "/fetchdata"
|
||||
|
||||
<PageTitle>Weather forecast</PageTitle>
|
||||
|
||||
@using Lieb.Data
|
||||
@inject WeatherForecastService ForecastService
|
||||
|
||||
<h1>Weather forecast</h1>
|
||||
|
||||
<p>This component demonstrates fetching data from a service.</p>
|
||||
|
||||
@if (forecasts == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Temp. (C)</th>
|
||||
<th>Temp. (F)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var forecast in forecasts)
|
||||
{
|
||||
<tr>
|
||||
<td>@forecast.Date.ToShortDateString()</td>
|
||||
<td>@forecast.TemperatureC</td>
|
||||
<td>@forecast.TemperatureF</td>
|
||||
<td>@forecast.Summary</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
|
||||
}
|
||||
}
|
11
Lieb/Pages/RedirectToLogin.razor
Normal file
11
Lieb/Pages/RedirectToLogin.razor
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -5,4 +5,5 @@
|
|||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<component type="typeof(App)" render-mode="ServerPrerendered" />
|
||||
@(await Html.RenderComponentAsync<App>(RenderMode.Server))
|
||||
<!--<component type="typeof(App)" render-mode="ServerPrerendered" />-->
|
||||
|
|
|
@ -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<WeatherForecastService>();
|
||||
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");
|
||||
|
|
|
@ -10,6 +10,15 @@
|
|||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<a href="#">Hello, @context.User.Identity.Name!</a>
|
||||
<a href="Account/Logout">Log out</a>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<a href="Account/Login">Log in</a>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<div class="alert alert-secondary mt-4">
|
||||
<span class="oi oi-pencil me-2" aria-hidden="true"></span>
|
||||
<strong>@Title</strong>
|
||||
|
||||
<span class="text-nowrap">
|
||||
Please take our
|
||||
<a target="_blank" class="font-weight-bold link-dark" href="https://go.microsoft.com/fwlink/?linkid=2149017">brief survey</a>
|
||||
</span>
|
||||
and tell us what you think.
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// Demonstrates how a parent component can supply parameters
|
||||
[Parameter]
|
||||
public string? Title { get; set; }
|
||||
}
|
|
@ -5,5 +5,9 @@
|
|||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Discord": {
|
||||
"AppId": "942448872335220806",
|
||||
"AppSecret": "5tsNxK9LtFpNqxqQnLkSJ1B0HJ7P7YUF"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue