1using System.Security.Claims;
2using AspNet.Security.OAuth.Naver;
3using Dreamine.Database.Abstractions;
4using Dreamine.Database.Sqlite;
6using Dreamine.Hybrid.Wpf.Hosting;
10using Microsoft.AspNetCore.Authentication;
11using Microsoft.AspNetCore.Authentication.Cookies;
12using Microsoft.AspNetCore.Authentication.Google;
13using Microsoft.AspNetCore.Authentication.OAuth;
14using Microsoft.AspNetCore.Builder;
15using Microsoft.AspNetCore.Components.Authorization;
16using Microsoft.AspNetCore.DataProtection;
17using Microsoft.AspNetCore.Http;
18using Microsoft.AspNetCore.HttpOverrides;
19using Microsoft.AspNetCore.Routing;
20using Microsoft.Extensions.DependencyInjection;
21using System.Net.Http.Headers;
22using System.Text.Json;
126 ArgumentNullException.ThrowIfNull(services);
127 services.AddAuthorizationCore();
128 services.AddCascadingAuthenticationState();
197 public static DreamineBlazorServerHostOptions AddDreamineIdentity(
198 this DreamineBlazorServerHostOptions options,
202 ArgumentNullException.ThrowIfNull(options);
203 ArgumentNullException.ThrowIfNull(authOptions);
204 ArgumentException.ThrowIfNullOrWhiteSpace(databasePath);
206 var previousConfigure = options.ConfigureServices;
207 options.ConfigureServices = services =>
209 previousConfigure?.Invoke(services);
211 services.Configure<ForwardedHeadersOptions>(forwarded =>
213 forwarded.ForwardedHeaders =
214 ForwardedHeaders.XForwardedFor |
215 ForwardedHeaders.XForwardedProto |
216 ForwardedHeaders.XForwardedHost;
217 forwarded.KnownNetworks.Clear();
218 forwarded.KnownProxies.Clear();
222 var previousPipeline = options.ConfigurePipeline;
223 options.ConfigurePipeline = app =>
225 app.UseForwardedHeaders();
226 previousPipeline?.Invoke(app);
227 app.UseAuthentication();
231 var previousAfterRouting = options.ConfigurePipelineAfterRouting;
232 options.ConfigurePipelineAfterRouting = app =>
234 previousAfterRouting?.Invoke(app);
235 app.UseAuthorization();
236 app.MapAuthEndpoints();
308 this IServiceCollection services,
312 ArgumentNullException.ThrowIfNull(services);
313 ArgumentNullException.ThrowIfNull(authOptions);
314 ArgumentException.ThrowIfNullOrWhiteSpace(databasePath);
317 services.Configure<ForwardedHeadersOptions>(forwarded =>
319 forwarded.ForwardedHeaders =
320 ForwardedHeaders.XForwardedFor |
321 ForwardedHeaders.XForwardedProto |
322 ForwardedHeaders.XForwardedHost;
323 forwarded.KnownNetworks.Clear();
324 forwarded.KnownProxies.Clear();
364 ArgumentNullException.ThrowIfNull(endpoints);
365 endpoints.MapAuthEndpoints();
402 IServiceCollection services,
406 Directory.CreateDirectory(Path.GetDirectoryName(databasePath)!);
408 var provider =
new SqliteDatabaseProvider($
"Data Source={databasePath}");
409 services.AddSingleton<IDatabaseProvider>(provider);
415 services.AddDataProtection()
418 ?
"Dreamine.Identity"
422 var authBuilder = services
423 .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
426 cookie.LoginPath =
"/_identity/login";
427 cookie.LogoutPath =
"/_identity/signout";
428 cookie.AccessDeniedPath =
"/";
429 cookie.ExpireTimeSpan = TimeSpan.FromDays(30);
430 cookie.SlidingExpiration =
true;
431 cookie.Cookie.Name =
string.IsNullOrWhiteSpace(authOptions.
CookieName)
432 ?
".Dreamine.Identity"
434 if (!
string.IsNullOrWhiteSpace(authOptions.
CookieDomain))
442 authBuilder.AddGoogle(google =>
446 google.CallbackPath =
"/signin-google";
447 google.SaveTokens =
false;
454 authBuilder.AddNaver(naver =>
458 naver.CallbackPath =
"/signin-naver";
459 naver.SaveTokens =
false;
470 kakao.CallbackPath =
"/signin-kakao";
471 kakao.AuthorizationEndpoint =
"https://kauth.kakao.com/oauth/authorize";
472 kakao.TokenEndpoint =
"https://kauth.kakao.com/oauth/token";
473 kakao.UserInformationEndpoint =
"https://kapi.kakao.com/v2/user/me";
474 kakao.SaveTokens =
false;
477 kakao.Scope.Add(
"profile_nickname");
478 kakao.Scope.Add(
"profile_image");
483 services.AddAuthorization();
484 services.AddCascadingAuthenticationState();
521 var principal = context.Principal;
522 if (principal is
null)
527 var providerKey = principal.FindFirstValue(ClaimTypes.NameIdentifier) ??
string.Empty;
528 var email = principal.FindFirstValue(ClaimTypes.Email) ??
string.Empty;
529 var displayName = principal.FindFirstValue(ClaimTypes.Name)
530 ?? principal.FindFirstValue(ClaimTypes.GivenName)
532 var avatarUrl = principal.FindFirstValue(
"urn:google:picture")
533 ?? principal.FindFirstValue(
"picture")
534 ?? principal.FindFirstValue(
"urn:naver:profile_image")
535 ?? principal.FindFirstValue(
"profileimage")
538 if (
string.IsNullOrEmpty(providerKey))
549 avatarUrl).ConfigureAwait(
false);
586 if (
string.IsNullOrWhiteSpace(context.AccessToken) || context.Principal?.Identity is not ClaimsIdentity identity)
591 using var request =
new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
592 request.Headers.Authorization =
new AuthenticationHeaderValue(
"Bearer", context.AccessToken);
593 request.Headers.Accept.Add(
new MediaTypeWithQualityHeaderValue(
"application/json"));
595 using var response = await context.Backchannel.SendAsync(
597 context.HttpContext.RequestAborted).ConfigureAwait(
false);
598 response.EnsureSuccessStatusCode();
600 await
using var stream = await response.Content.ReadAsStreamAsync(context.HttpContext.RequestAborted)
601 .ConfigureAwait(
false);
602 using var document = await JsonDocument.ParseAsync(
604 cancellationToken: context.HttpContext.RequestAborted).ConfigureAwait(
false);
606 var root = document.RootElement;
608 if (
string.IsNullOrWhiteSpace(providerKey))
614 var profile = account.HasValue ?
TryGetProperty(account.Value,
"profile") :
null;
616 var email = account.HasValue ?
ReadString(account.Value,
"email") :
string.Empty;
617 var displayName = profile.HasValue ?
ReadString(profile.Value,
"nickname") :
string.Empty;
618 var avatarUrl = profile.HasValue ?
ReadString(profile.Value,
"profile_image_url") :
string.Empty;
631 avatarUrl).ConfigureAwait(
false);
699 OAuthCreatingTicketContext context,
706 var principal = context.Principal;
707 if (principal?.
Identity is not ClaimsIdentity identity)
712 var userStore = context.HttpContext.RequestServices.GetRequiredService<
IUserStore>();
719 context.HttpContext.RequestAborted).ConfigureAwait(
false);
758 element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out var value)
794 private static string ReadString(JsonElement element,
string name)
796 if (element.ValueKind != JsonValueKind.Object || !element.TryGetProperty(name, out var value))
801 return value.ValueKind
switch
803 JsonValueKind.String => value.GetString() ??
string.Empty,
804 JsonValueKind.Number => value.GetRawText(),
843 if (!
string.IsNullOrWhiteSpace(value) && !identity.HasClaim(claim => claim.Type == type))
845 identity.AddClaim(
new Claim(type, value));
884 context.User.Identity?.IsAuthenticated !=
true)
886 await next(context).ConfigureAwait(
false);
893 System.Globalization.NumberStyles.None,
894 System.Globalization.CultureInfo.InvariantCulture,
897 await next(context).ConfigureAwait(
false);
901 var userStore = context.RequestServices.GetService<
IUserStore>();
902 if (userStore is
null)
904 await next(context).ConfigureAwait(
false);
908 var user = await userStore.
GetByIdAsync(userId, context.RequestAborted).ConfigureAwait(
false);
909 if (RequiredConsentPolicy.HasRequiredConsents(user))
911 await next(context).ConfigureAwait(
false);
915 var returnUrl = Uri.EscapeDataString(
BuildCurrentUrl(context.Request));
916 context.Response.Redirect($
"/_identity/consent?returnUrl={returnUrl}");
945 var value = path.Value ??
"/";
946 if (value is
"/" or
"")
951 if (Path.HasExtension(value))
956 return value.StartsWith(
"/_identity", StringComparison.OrdinalIgnoreCase) ||
957 value.StartsWith(
"/signin", StringComparison.OrdinalIgnoreCase) ||
958 value.StartsWith(
"/login", StringComparison.OrdinalIgnoreCase) ||
959 value.StartsWith(
"/signup", StringComparison.OrdinalIgnoreCase) ||
960 value.StartsWith(
"/consent", StringComparison.OrdinalIgnoreCase) ||
961 value.StartsWith(
"/signout", StringComparison.OrdinalIgnoreCase) ||
962 value.StartsWith(
"/privacy", StringComparison.OrdinalIgnoreCase) ||
963 value.StartsWith(
"/terms", StringComparison.OrdinalIgnoreCase) ||
964 value.StartsWith(
"/_blazor", StringComparison.OrdinalIgnoreCase) ||
965 value.StartsWith(
"/_framework", StringComparison.OrdinalIgnoreCase) ||
966 value.StartsWith(
"/css", StringComparison.OrdinalIgnoreCase) ||
967 value.StartsWith(
"/js", StringComparison.OrdinalIgnoreCase) ||
968 value.StartsWith(
"/img", StringComparison.OrdinalIgnoreCase) ||
969 value.StartsWith(
"/bootstrap", StringComparison.OrdinalIgnoreCase);
998 var path = request.PathBase.Add(request.Path).ToString();
999 var query = request.QueryString.HasValue ? request.QueryString.Value :
string.Empty;
1000 return string.IsNullOrEmpty(path) ? $
"/{query}" : $
"{path}{query}";
static async Task OnCreatingTicketAsync(OAuthCreatingTicketContext context, string providerName)
static ? JsonElement TryGetProperty(JsonElement element, string name)
static bool ShouldSkipConsentGate(PathString path)
static string ReadString(JsonElement element, string name)
const string UserIdClaimType
static async Task EnforceRequiredConsentsAsync(HttpContext context, RequestDelegate next)
const string ProviderClaimType
static void RegisterAuthServices(IServiceCollection services, AuthOptions authOptions, string databasePath)
static async Task UpsertExternalUserAsync(OAuthCreatingTicketContext context, string providerName, string providerKey, string email, string displayName, string avatarUrl)
const string ProviderGoogle
static IServiceCollection AddDreamineIdentityWeb(this IServiceCollection services, AuthOptions authOptions, string databasePath)
static IServiceCollection AddDreamineIdentityWpfHost(this IServiceCollection services)
static string BuildCurrentUrl(HttpRequest request)
static void AddClaimIfNotEmpty(ClaimsIdentity identity, string type, string value)
static IEndpointRouteBuilder MapDreamineIdentityEndpoints(this IEndpointRouteBuilder endpoints)
const string ProviderKakao
const string ProviderNaver
static async Task OnKakaoCreatingTicketAsync(OAuthCreatingTicketContext context)
Task< AuthUser?> GetByIdAsync(long id, CancellationToken cancellationToken=default)
Task< AuthUser > UpsertAsync(string provider, string providerKey, string email, string displayName, string avatarUrl, CancellationToken cancellationToken=default)
string DataProtectionKeysPath
string DataProtectionApplicationName
OAuthProviderOptions Kakao
OAuthProviderOptions Google
OAuthProviderOptions Naver