diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/07- ASP.NET Core Identity - Authentication & Authorization.md b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/07- ASP.NET Core Identity - Authentication & Authorization.md index 48be31d82d..11d5f84cd0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/07- ASP.NET Core Identity - Authentication & Authorization.md +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/07- ASP.NET Core Identity - Authentication & Authorization.md @@ -9,7 +9,7 @@ Welcome to **Stage 7** of the Boilerplate project tutorial! In this stage, you w 1. [Authentication Architecture](#authentication-architecture) - [JWT Token-Based Authentication](#jwt-token-based-authentication) - [Session Management](#session-management) - - [Single Sign-On (SSO) Support](#single-sign-on-sso-support) + - [External Identity Support](#external-identity-support) 2. [Authorization and Access Control](#authorization-and-access-control) - [Role-Based and Permission-Based Authorization](#role-based-and-permission-based-authorization) - [Policy-Based Authorization](#policy-based-authorization) @@ -17,10 +17,11 @@ Welcome to **Stage 7** of the Boilerplate project tutorial! In this stage, you w 3. [Identity Configuration](#identity-configuration) 4. [Security Best Practices](#security-best-practices) 5. [One-Time Token System](#one-time-token-system) -6. [Code Examples from the Project](#code-examples-from-the-project) +6. [Advanced Topics](#advanced-topics) + - [JWT Token Signing with PFX Certificates](#jwt-token-signing-with-pfx-certificates) + - [Keycloak Integration](#keycloak-integration) 7. [Hands-On Exploration](#hands-on-exploration) 8. [Video Tutorial](#video-tutorial) -9. [Summary](#summary) **Important**: All topics related to WebAuthn, passkeys, and passwordless authentication are explained in [Stage 24](/.docs/24-%20WebAuthn%20and%20Passwordless%20Authentication%20(Advanced).md). @@ -118,26 +119,21 @@ private async Task CreateUserSession(Guid userId, CancellationToken --- -### Single Sign-On (SSO) Support +### External Identity Support -The project supports **external authentication providers** for Single Sign-On (SSO). +The project supports **external authentication providers** for Single Sign-On (SSO) and Social sign-in purposes. #### Supported Providers -You can easily configure SSO with: +The following external identity providers have been already configured: -- **Google** - OAuth 2.0 authentication -- **GitHub** - OAuth authentication for developers -- **Microsoft/Azure AD** - Enterprise identity integration (Azure AD B2C/Entra) -- **Twitter** - Social authentication -- **Apple** - Sign in with Apple -- **Facebook** - Social media authentication -- **Duende Identity Server / Keycloak** - OpenID Connect provider -- And many other OAuth/OpenID Connect providers - -#### Enterprise SSO - -The project is configured to connect to a **Keycloak (If URL provided) or Duende Identity Server (A public demo server) instance** to demonstrate how external OpenID Connect providers work. +- **Google** +- **Microsoft/Azure Entra/Azure AD B2C** +- **Twitter (X)** +- **Apple** +- **GitHub** +- **Facebook** +- **Keycloak** Free, Open Source Identity and Access Management #### Configuration @@ -164,23 +160,23 @@ External provider settings are configured in [`appsettings.json`](/src/Server/Bo } ``` -#### Social Sign-In Flow +#### External Sign-In Flow -From [`IdentityController.SocialSignIn.cs`](/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.SocialSignIn.cs): +From [`IdentityController.ExternalSignIn.cs`](/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.ExternalSignIn.cs): ```csharp [HttpGet] -public async Task SocialSignIn(string provider, string? returnUrl = null, +public async Task ExternalSignIn(string provider, string? returnUrl = null, int? localHttpPort = null, [FromQuery] string? origin = null) { - var redirectUrl = Url.Action(nameof(SocialSignInCallback), "Identity", + var redirectUrl = Url.Action(nameof(ExternalSignInCallback), "Identity", new { returnUrl, localHttpPort, origin }); var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); return new ChallengeResult(provider, properties); } ``` -When a social sign-in is successful, the system: +When a external sign-in is successful, the system: 1. Retrieves user information from the external provider 2. Either finds an existing user or creates a new one 3. Automatically confirms email/phone if the provider provides them @@ -660,127 +656,124 @@ Let's walk through a password reset scenario: --- -## Code Examples from the Project +## Advanced Topics -### Sign Up with Validation +### JWT Token Signing with PFX Certificates -From [`IdentityController.cs`](/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs): +By default, the Bit Boilerplate uses a string-based secret (`JwtIssuerSigningKeySecret`) for signing JWT tokens in the [`AppJwtSecureDataFormat`](/src/Server/Boilerplate.Server.Api/Services/Identity/AppJwtSecureDataFormat.cs) class. While this approach is valid and secure, using a **PFX certificate** is considered best practice for production environments, especially when: -```csharp -[HttpPost] -public async Task SignUp(SignUpRequestDto request, CancellationToken cancellationToken) -{ - request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber); - - // Validate Google reCAPTCHA - if (await googleRecaptchaService.Verify(request.GoogleRecaptchaResponse, cancellationToken) is false) - throw new BadRequestException(Localizer[nameof(AppStrings.InvalidGoogleRecaptchaResponse)]); - - // Check for existing user - var existingUser = await userManager.FindUserAsync( - new() { Email = request.Email, PhoneNumber = request.PhoneNumber }); - - if (existingUser is not null) - { - if (await userConfirmation.IsConfirmedAsync(userManager, existingUser) is false) - { - await SendConfirmationToken(existingUser, request.ReturnUrl, cancellationToken); - throw new BadRequestException(Localizer[nameof(AppStrings.UserIsNotConfirmed)]) - .WithData("UserId", existingUser.Id); - } - else - { - throw new BadRequestException(Localizer[nameof(AppStrings.DuplicateEmailOrPhoneNumber)]) - .WithData("UserId", existingUser.Id); - } - } +- You need to share JWT validation across multiple backend services +- You want to follow industry-standard cryptographic practices +- You're deploying to enterprise environments with strict security requirements - // Create new user - var userToAdd = new User { LockoutEnabled = true }; - await userStore.SetUserNameAsync(userToAdd, request.UserName!, cancellationToken); - - if (string.IsNullOrEmpty(request.Email) is false) - { - await userEmailStore.SetEmailAsync(userToAdd, request.Email!, cancellationToken); - } - - if (string.IsNullOrEmpty(request.PhoneNumber) is false) - { - await userPhoneNumberStore.SetPhoneNumberAsync(userToAdd, request.PhoneNumber!, cancellationToken); - } +**Why We Didn't Use PFX by Default** - await userManager.CreateUserWithDemoRole(userToAdd, request.Password!); - await SendConfirmationToken(userToAdd, request.ReturnUrl, cancellationToken); -} +We chose the string-based secret as the default because: +- **Easier Deployment**: PFX certificates require additional configuration on shared hosting providers +- **Simplified Development**: Developers can get started immediately without certificate management +- **Good Security**: String-based secrets with HS512 are still cryptographically secure + +**How to Migrate to PFX Certificates** + +If you want to use PFX certificates, you'll need to modify [`AppJwtSecureDataFormat`](/src/Server/Boilerplate.Server.Api/Services/Identity/AppJwtSecureDataFormat.cs) to use `AsymmetricSecurityKey` instead of `SymmetricSecurityKey`: + +```csharp +// Instead of: +IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings.Identity.JwtIssuerSigningKeySecret)) + +// Use: +var certificate = new X509Certificate2("path/to/certificate.pfx", "password"); +IssuerSigningKey = new X509SecurityKey(certificate) ``` -### Password Reset Flow +**Protecting ASP.NET Core Data Protection Keys** -From [`IdentityController.ResetPassword.cs`](/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.ResetPassword.cs): +Additionally, you should protect the Data Protection keys stored in the database. In [`Program.Services.cs`](/src/Server/Boilerplate.Server.Api/Program.Services.cs), update the following code: ```csharp -[HttpPost] -public async Task ResetPassword(ResetPasswordRequestDto request, CancellationToken cancellationToken) -{ - request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber); - var user = await userManager.FindUserAsync(request) - ?? throw new ResourceNotFoundException(Localizer[nameof(AppStrings.UserNotFound)]) - .WithData("Identifier", request); +services.AddDataProtection() + .PersistKeysToDbContext() + .ProtectKeysWithCertificate(certificate); // Add this line +``` - // Check if token has expired - var expired = (DateTimeOffset.Now - user.ResetPasswordTokenRequestedOn) - > AppSettings.Identity.ResetPasswordTokenLifetime; +**Cross-Service JWT Validation** - if (expired) - throw new BadRequestException(nameof(AppStrings.ExpiredToken)) - .WithData("UserId", user.Id); +When using PFX certificates, you can share the **public key** with other backend services to validate JWTs issued by your ASP.NET Core Identity system. Other services can use the `AddJwtAuthentication` method to validate tokens without needing the private key. - // Check if user is locked out - if (await userManager.IsLockedOutAsync(user)) - { - var tryAgainIn = (user.LockoutEnd! - DateTimeOffset.UtcNow).Value; - throw new BadRequestException( - Localizer[nameof(AppStrings.UserLockedOut), - tryAgainIn.Humanize(culture: CultureInfo.CurrentUICulture)]) - .WithData("UserId", user.Id) - .WithExtensionData("TryAgainIn", tryAgainIn); - } +This enables scenarios where: +- Multiple microservices validate the same JWT +- Third-party services can verify your tokens - // Verify the token - bool tokenIsValid = await userManager.VerifyUserTokenAsync( - user!, - TokenOptions.DefaultPhoneProvider, - FormattableString.Invariant($"ResetPassword,{user.ResetPasswordTokenRequestedOn?.ToUniversalTime()}"), - request.Token!); +--- - if (tokenIsValid is false) - { - await userManager.AccessFailedAsync(user); - throw new BadRequestException(nameof(AppStrings.InvalidToken)) - .WithData("UserId", user.Id); - } +### Keycloak Integration - // Reset the password - var result = await userManager.ResetPasswordAsync( - user!, - await userManager.GeneratePasswordResetTokenAsync(user!), - request.Password!); +Bit Boilerplate includes built-in support for **Keycloak**, a free, open-source identity and access management solution. Keycloak provides enterprise-grade features like: - if (result.Succeeded is false) - throw new ResourceValidationException(result.Errors - .Select(e => new LocalizedString(e.Code, e.Description)).ToArray()) - .WithData("UserId", user.Id); +- Centralized user management +- Single Sign-On (SSO) across multiple applications +- Fine-grained authorization +- User federation (LDAP, Active Directory) - // Reset access failed count and invalidate the token - await ((IUserLockoutStore)userStore).ResetAccessFailedCountAsync(user, cancellationToken); - user.ResetPasswordTokenRequestedOn = null; // Invalidates the token - var updateResult = await userManager.UpdateAsync(user); - if (updateResult.Succeeded is false) - throw new ResourceValidationException(updateResult.Errors - .Select(e => new LocalizedString(e.Code, e.Description)).ToArray()) - .WithData("UserId", user.Id); -} -``` +#### Keycloak in .NET Aspire + +When you run the project with .NET Aspire enabled (default configuration), Keycloak is automatically started as a containerized service. This provides a complete identity server for development and testing without any manual setup. + +#### Demo User Accounts + +The Keycloak instance comes pre-configured with the following demo accounts (Provided by [src\Server\Boilerplate.Server.AppHost\Realms\dev-realm.json](..\src\Server\Boilerplate.Server.AppHost\Realms\dev-realm.json)): + +| Username | Password | Role | Description | +|----------|----------|------|-------------| +| test | 123456 | Admin | Full administrative access | +| bob | bob | Demo | Standard demo user | +| alice | alice | Demo | Standard demo user | + +#### How Keycloak Mapping Works + +The Boilerplate template integrates Keycloak with ASP.NET Core Identity through a custom mapping system in [`AppUserClaimsPrincipalFactory`](/src/Server/Boilerplate.Server.Api/Services/Identity/AppUserClaimsPrincipalFactory.cs): + +**1. Groups → Roles** +- Keycloak **groups** are mapped to ASP.NET Core Identity **roles** +- Users inherit all roles from their group memberships + +**2. Attributes → Claims** +- **User attributes** in Keycloak become individual claims +- **Group attributes** are also mapped to claims + +**3. Keycloak Roles → Custom Mapping** (Not required with the current project setup) +- Keycloak's built-in **roles** (distinct from groups) are **not automatically mapped** +- These roles follow a different structure than ASP.NET Core Identity roles + +#### Real-Time Claim Synchronization + +The `AppUserClaimsPrincipalFactory` retrieves the latest claims from Keycloak on every ASP.NET Core Identity token refresh: + +**Security Benefits:** +- **Automatic Deactivation**: If a user is disabled or deleted in Keycloak, access is immediately revoked +- **Fresh Claims**: Every token refresh fetches the latest permissions from Keycloak +- **Session Validation**: Expired or revoked Keycloak sessions trigger `UnauthorizedException` + +#### JWT Token Issuance Flow + +Despite using Keycloak for authentication, the final JWT tokens are still issued by **ASP.NET Core Identity**: + +1. User signs in through Keycloak (via OpenID Connect) +2. `AppUserClaimsPrincipalFactory` retrieves claims from Keycloak +3. ASP.NET Core Identity merges Keycloak claims with local claims +4. A new JWT is issued and signed using the configured secret (or PFX certificate) +5. The JWT is sent to the client and used for all subsequent API requests + +**Validation:** +- When the JWT is sent back to the backend, **ASP.NET Core Identity validates it** + +#### Using JWTs with Other Backend Services + +If you want to share the JWT with other backend services (e.g., microservices), follow these steps: + +1. **Switch to PFX Certificates** (as described in the previous section) +2. **Share the Public Key** with other services +3. Other services use `AddJwtAuthentication` to validate the token --- @@ -816,7 +809,7 @@ public async Task ResetPassword(ResetPasswordRequestDto request, CancellationTok - Try using an already-used token ### 6. External Providers -- Try **social sign-in** with the configured demo provider +- Try **External sign-in** with the configured demo provider - Observe how email confirmation works with external providers ### 7. Permissions and Policies @@ -853,7 +846,6 @@ public async Task ResetPassword(ResetPasswordRequestDto request, CancellationTok ### AI Wiki: Answered Questions * [How does a `refresh token` function in a Boilerplate project template?](https://deepwiki.com/search/how-does-a-refresh-token-funct_6a75fa66-ab98-4367-bd1a-24b081fbf88c) * [What would happen when I use [AuthorizedApi]](https://deepwiki.com/search/what-would-happen-when-i-use-a_c525d59d-5c55-489b-8f95-69f6df7c743d) -* [Give me high level overview of social sign-in flow](https://deepwiki.com/search/give-me-high-level-overview-of_059d227a-0ffe-4077-9e01-ba9f61456a3f) * [Give me high level overview of two factor auth setup and usage flows](https://deepwiki.com/search/give-me-high-level-overview-of_1883503f-2e34-41ca-821a-1246d332990f) Ask your own question [here](https://wiki.bitplatform.dev) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/14- Response Caching System.md b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/14- Response Caching System.md index 077b553185..cb8dfa7485 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/14- Response Caching System.md +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/14- Response Caching System.md @@ -462,7 +462,7 @@ The project uses the **FusionCache** library for server-side caching: - **Output Cache Backend**: Powers the ASP.NET Core Output Cache implementation (Layer 4) - **Data Caching**: Provides data caching via `IFusionCache` interface for caching arbitrary data (database query results, computed values, etc.) in addition to HTTP responses -- **Flexible Storage**: Supports multiple backends (in-memory, Redis, etc) for both response and data caching +- **Flexible Storage**: Supports multiple backends (in-memory, Redis, hybrid etc) for both response and data caching --- diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json index 8450bbfd9c..89bc9c641b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json @@ -233,7 +233,8 @@ "parameters": { "source": "name", "toLower": true - } + }, + "replaces": "boilerplate" }, "nameToAppId": { "type": "generated", diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorize.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorize.razor deleted file mode 100644 index 4a6c201cff..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorize.razor +++ /dev/null @@ -1,12 +0,0 @@ -@attribute [Authorize] - -@* This route is disabled by default. Uncomment and enable it only if necessary, - as it may expose authentication-related endpoints. *@ -@* -@attribute [Route(Urls.Authorize)] -@attribute [Route("{culture?}" + Urls.Authorize)] -*@ - -@inherits AppComponentBase - - diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorize.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorize.razor.cs deleted file mode 100644 index d6d097446a..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorize.razor.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace Boilerplate.Client.Core.Components.Pages; - -/// -/// If you need an authentication process similar to SSO/OAuth, navigate to /authorize with `client_id`, -/// an appropriately encoded `redirect_uri`, and `state`. -/// -/// The user can sign in (or sign up if necessary) using various authentication methods provided by project template, -/// such as social sign-in, 2FA, magic link, and OTP. After authentication, the system redirects to the specified app -/// with an access token and other relevant authentication details. -/// -/// Example Usage: -/// Opening: -/// http://localhost:5030/authorize?client_id=SampleClient&redirect_uri=https://sampleclient.azurewebsites.net/signInCallback&state=/carts -/// http://localhost:5030/authorize?client_id=NopClient&redirect_uri=https%3A%2F%2Fsampleclient.azurewebsites.net%2FsignInCallback&state=%2Fcarts -/// -/// Redirects to: -/// https://sampleclient.azurewebsites.net/signInCallback?access_token=di1d98cxh913fh29ufhnfunxw9&token_type=Bearer&expires_in=3600&state=/carts -/// -/// Note: -/// This route is **disabled by default** for security reasons. -/// To enable it, **uncomment the route definition in the corresponding Razor page** (`@attribute [Route(Urls.Authorize)]`) -/// -public partial class Authorize -{ - [AutoInject] private AuthManager authManager = default!; - [AutoInject] private IAuthTokenProvider authTokenProvider = default!; - - [Parameter, SupplyParameterFromQuery(Name = "client_id")] public string? ClientId { get; set; } - [Parameter, SupplyParameterFromQuery(Name = "redirect_uri")] public string? RedirectUri { get; set; } - [Parameter, SupplyParameterFromQuery(Name = "state")] public string? State { get; set; } - - private Dictionary clients = new(StringComparer.OrdinalIgnoreCase) // Client configurations; can also be fetched from a server API. - { - { - "SampleClient", - [ - "https://sampleclient.azurewebsites.net/signInCallback" - ] - } - }; - - protected override async Task OnAfterFirstRenderAsync() - { - await base.OnAfterFirstRenderAsync(); - - if (clients.TryGetValue(ClientId!, out var clientAllowedRedirectUrls) is false) - { - NavigationManager.NavigateTo($"{RedirectUri}#error=invalid_missing_client_id&state={Uri.EscapeDataString(State ?? "")}"); - return; - } - - if (clientAllowedRedirectUrls.Any(clientUrl => string.Equals(clientUrl, RedirectUri, StringComparison.InvariantCultureIgnoreCase)) is false) - { - NavigationManager.NavigateTo($"{RedirectUri}#error=invalid_redirect_uri&state={Uri.EscapeDataString(State ?? "")}"); - return; - } - - // Attempt to refresh the user's authentication token. - var accessToken = await authManager.RefreshToken(requestedBy: "AuthorizePage"); - - if (string.IsNullOrEmpty(accessToken)) - return; // If the token is expired, the session is deleted, or the user is logged out, redirecting back to sign-in will occur. - - // Parse the access token and calculate its expiration duration. - var token = IAuthTokenProvider.ParseAccessToken(accessToken, validateExpiry: false); - var expiresIn = long.Parse(token.FindFirst("exp")!.Value) - long.Parse(token.FindFirst("iat")!.Value); - - NavigationManager.NavigateTo($"{RedirectUri}?access_token={accessToken}&token_type=Bearer&expires_in={expiresIn}&state={Uri.EscapeDataString(State ?? "")}"); - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalIdentityProviders.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalIdentityProviders.razor new file mode 100644 index 0000000000..1c1d56338f --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalIdentityProviders.razor @@ -0,0 +1,58 @@ +@inherits AppComponentBase + +
+ + @if (isLoadingProviders) + { + @for (int i = 0; i < 4; i++) + { + + } + } + else + { + @if (supportedProviders.Contains("Keycloak")) + { + + + + } + @if (supportedProviders.Contains("Google")) + { + + + + } + @if (supportedProviders.Contains("GitHub")) + { + + + + } + @if (supportedProviders.Contains("Twitter")) + { + + + + } + @if (supportedProviders.Contains("Apple")) + { + + + + } + @if (supportedProviders.Contains("AzureAD")) + { + + + + } + @if (supportedProviders.Contains("Facebook")) + { + + + + } + } + +
\ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialRow.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalIdentityProviders.razor.cs similarity index 86% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialRow.razor.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalIdentityProviders.razor.cs index b5aeeb79c0..3bdbc2d48a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialRow.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalIdentityProviders.razor.cs @@ -2,7 +2,7 @@ namespace Boilerplate.Client.Core.Components.Pages.Identity.Components; -public partial class SocialRow +public partial class ExternalIdentityProviders { private bool isLoadingProviders = true; private string[] supportedProviders = []; @@ -18,7 +18,7 @@ protected override async Task OnInitAsync() { try { - var providers = await IdentityController.GetSupportedSocialAuthSchemes(CurrentCancellationToken); + var providers = await IdentityController.GetSupportedExternalAuthSchemes(CurrentCancellationToken); supportedProviders = providers; } finally @@ -33,5 +33,5 @@ protected override async Task OnInitAsync() private async Task HandleApple() => await OnClick.InvokeAsync("Apple"); private async Task HandleAzureAD() => await OnClick.InvokeAsync("AzureAD"); private async Task HandleFacebook() => await OnClick.InvokeAsync("Facebook"); - private async Task HandleIdentityServerDemo() => await OnClick.InvokeAsync("IdentityServerDemo"); + private async Task HandleKeycloak() => await OnClick.InvokeAsync("Keycloak"); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialRow.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalIdentityProviders.razor.scss similarity index 85% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialRow.razor.scss rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalIdentityProviders.razor.scss index 67cc43d478..f5b42cc39d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialRow.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalIdentityProviders.razor.scss @@ -3,7 +3,7 @@ section { } ::deep { - .social-button { + .external-sign-in-button { width: 60px; height: 60px; display: flex; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialButton.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalSignInButton.razor similarity index 92% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialButton.razor rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalSignInButton.razor index f22d4cdb2b..91c7a90590 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialButton.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/ExternalSignInButton.razor @@ -10,7 +10,7 @@ OnClick="OnClick" Size="BitSize.Small" IsEnabled="IsWaiting is false" - Class="social-button" + Class="external-sign-in-button" Variant="BitVariant.Fill" ButtonType="BitButtonType.Button" Color="BitColor.SecondaryBackground"> diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/IdentityServerDemoIcon.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/IdentityServerDemoIcon.razor deleted file mode 100644 index 66e9fc6b1c..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/IdentityServerDemoIcon.razor +++ /dev/null @@ -1,11 +0,0 @@ -@inherits StaticComponent - - - - - - - - - - diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/KeycloakIcon.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/KeycloakIcon.razor new file mode 100644 index 0000000000..e96a232eae --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/KeycloakIcon.razor @@ -0,0 +1,6 @@ +@inherits StaticComponent + + + + + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialRow.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialRow.razor deleted file mode 100644 index af7cfe23f3..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/Components/SocialRow.razor +++ /dev/null @@ -1,58 +0,0 @@ -@inherits AppComponentBase - -
- - @if (isLoadingProviders) - { - @for (int i = 0; i < 4; i++) - { - - } - } - else - { - @if (supportedProviders.Contains("IdentityServerDemo")) - { - - - - } - @if (supportedProviders.Contains("Google")) - { - - - - } - @if (supportedProviders.Contains("GitHub")) - { - - - - } - @if (supportedProviders.Contains("Twitter")) - { - - - - } - @if (supportedProviders.Contains("Apple")) - { - - - - } - @if (supportedProviders.Contains("AzureAD")) - { - - - - } - @if (supportedProviders.Contains("Facebook")) - { - - - - } - } - -
\ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor index a2ef195b85..648e491bd6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor @@ -25,7 +25,7 @@ - + @Localizer[AppStrings.Or] @Localizer[AppStrings.Or] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor.cs index 56a08d128a..59b09158d2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor.cs @@ -107,7 +107,7 @@ private async Task DoSignIn() isWaiting = true; successfulSignIn = false; - await InvokeAsync(StateHasChanged); // Social sign-in callback will eventually call this method, so we need to update the UI immediately. See ClientAppMessages.SOCIAL_SIGN_IN_CALLBACK references. + await InvokeAsync(StateHasChanged); // External sign-in callback will eventually call this method, so we need to update the UI immediately. See ClientAppMessages.EXTERNAL_SIGN_IN_CALLBACK references. try { @@ -202,16 +202,16 @@ private async Task DoSignIn() finally { isWaiting = false; - await InvokeAsync(StateHasChanged); // Social sign-in callback will eventually call this method, so we need to update the UI immediately. See ClientAppMessages.SOCIAL_SIGN_IN_CALLBACK references. + await InvokeAsync(StateHasChanged); // External sign-in callback will eventually call this method, so we need to update the UI immediately. See ClientAppMessages.EXTERNAL_SIGN_IN_CALLBACK references. } } - private async Task SocialSignIn(string provider) + private async Task ExternalSignIn(string provider) { try { pubSubUnsubscribe?.Invoke(); - pubSubUnsubscribe = PubSubService.Subscribe(ClientAppMessages.SOCIAL_SIGN_IN_CALLBACK, async (uriString) => + pubSubUnsubscribe = PubSubService.Subscribe(ClientAppMessages.EXTERNAL_SIGN_IN_CALLBACK, async (uriString) => { // Check out SignInModalService for more details var uri = uriString!.ToString(); @@ -245,7 +245,7 @@ private async Task SocialSignIn(string provider) var port = localHttpServer.EnsureStarted(); - var redirectUrl = await identityController.GetSocialSignInUri(provider, GetReturnUrl(), port is -1 ? null : port, CurrentCancellationToken); + var redirectUrl = await identityController.GetExternalSignInUri(provider, GetReturnUrl(), port is -1 ? null : port, CurrentCancellationToken); await externalNavigationService.NavigateTo(redirectUrl); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor index 4f92cfac55..d807ae41e8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor @@ -17,7 +17,7 @@ - + @Localizer[AppStrings.Or] @Localizer[AppStrings.Or] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.cs index f373100136..6ba1f2b6e6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.cs @@ -86,19 +86,19 @@ private void NavigateToConfirmPage() NavigationManager.NavigateTo(confirmUrl, replace: true); } - private async Task SocialSignUp(string provider) + private async Task ExternalSignUp(string provider) { try { - pubSubUnsubscribe = PubSubService.Subscribe(ClientAppMessages.SOCIAL_SIGN_IN_CALLBACK, async (uriString) => + pubSubUnsubscribe = PubSubService.Subscribe(ClientAppMessages.EXTERNAL_SIGN_IN_CALLBACK, async (uriString) => { - // Social sign-in creates a new user automatically, so we only need to navigate to the sign-in page to automatically sign-in the user by provided OTP. + // External sign-in creates a new user automatically, so we only need to navigate to the sign-in page to automatically sign-in the user by provided OTP. NavigationManager.NavigateTo(uriString!.ToString()!, replace: true); }); var port = localHttpServer.EnsureStarted(); - var redirectUrl = await identityController.GetSocialSignInUri(provider, ReturnUrlQueryString, port is -1 ? null : port, CurrentCancellationToken); + var redirectUrl = await identityController.GetExternalSignInUri(provider, ReturnUrlQueryString, port is -1 ? null : port, CurrentCancellationToken); await externalNavigationService.NavigateTo(redirectUrl); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebInteropApp.ts b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebInteropApp.ts index d00783a2b3..21c06baa99 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebInteropApp.ts +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebInteropApp.ts @@ -10,8 +10,8 @@ export class WebInteropApp { const action = urlParams.get('actionName'); switch (action) { - case 'SocialSignInCallback': - await WebInteropApp.socialSignInCallback(); + case 'ExternalSignInCallback': + await WebInteropApp.externalSignInCallback(); break; case 'GetWebAuthnCredential': await WebInteropApp.getWebAuthnCredential(); @@ -43,7 +43,7 @@ export class WebInteropApp { } } - private static async socialSignInCallback() { + private static async externalSignInCallback() { const urlParams = new URLSearchParams(location.search); const urlToOpen = urlParams.get('url')!.toString(); const localHttpPort = urlParams.get('localHttpPort')?.toString(); @@ -64,7 +64,7 @@ export class WebInteropApp { if (!localHttpPort) { // Blazor WebAssembly, Auto or Server: if (window.opener) { - window.opener.postMessage({ key: 'PUBLISH_MESSAGE', message: 'SOCIAL_SIGN_IN_CALLBACK', payload: urlToOpen }); + window.opener.postMessage({ key: 'PUBLISH_MESSAGE', message: 'EXTERNAL_SIGN_IN_CALLBACK', payload: urlToOpen }); } else { WebInteropApp.autoClose = false; @@ -74,7 +74,7 @@ export class WebInteropApp { } // Blazor Hybrid: - await fetch(`http://localhost:${localHttpPort}/api/SocialSignInCallback?urlToOpen=${encodeURIComponent(urlToOpen)}`, { + await fetch(`http://localhost:${localHttpPort}/api/ExternalSignInCallback?urlToOpen=${encodeURIComponent(urlToOpen)}`, { method: 'POST', credentials: 'omit' }); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientAppMessages.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientAppMessages.cs index 137fd39827..be2ba58feb 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientAppMessages.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientAppMessages.cs @@ -112,8 +112,8 @@ public partial class ClientAppMessages //#endif /// - /// A publisher that publishes this message notifies that the user's social sign-in process has completed. - /// When a user completes social sign-in in a separate window, this message is published to notify the app. + /// A publisher that publishes this message notifies that the user's external sign-in process has completed. + /// When a user completes external sign-in in a separate window, this message is published to notify the app. /// - public const string SOCIAL_SIGN_IN_CALLBACK = nameof(SOCIAL_SIGN_IN_CALLBACK); + public const string EXTERNAL_SIGN_IN_CALLBACK = nameof(EXTERNAL_SIGN_IN_CALLBACK); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DefaultExternalNavigationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DefaultExternalNavigationService.cs index 3a008c12bb..6f66402c17 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DefaultExternalNavigationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DefaultExternalNavigationService.cs @@ -36,11 +36,11 @@ public async Task NavigateTo(string url) else { pubSubUnsubscribe?.Invoke(); - pubSubUnsubscribe = pubSubService.Subscribe(ClientAppMessages.SOCIAL_SIGN_IN_CALLBACK, async _ => + pubSubUnsubscribe = pubSubService.Subscribe(ClientAppMessages.EXTERNAL_SIGN_IN_CALLBACK, async _ => { if (lastOpenedWindowId != null) { - await window.Close(lastOpenedWindowId); // It's time to close the social sign-in popup / tab. + await window.Close(lastOpenedWindowId); // It's time to close the external sign-in popup / tab. } }); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignInModalService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignInModalService.cs index 5dc98039ff..eaeee5c6e0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignInModalService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignInModalService.cs @@ -5,7 +5,7 @@ namespace Boilerplate.Client.Core.Services; /// -/// When users opt for social sign-in, they are seamlessly authenticated, whether they are new or returning users. +/// When users opt for external sign-in, they are seamlessly authenticated, whether they are new or returning users. /// To provide a similarly streamlined experience for email/password sign-in, this service displays a modal dialog, enabling users to log in quickly without leaving the current page. /// For optimal use of this service, it is recommended to remove the sign-up page and its associated links from the project entirely. /// Optionally, you may also eliminate the password field from the sign-in form to allow users to authenticate solely via phone/OTP or email/magic-link. diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/MainActivity.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/MainActivity.cs index 7796d5aacf..76414fafa4 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/MainActivity.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/MainActivity.cs @@ -20,7 +20,7 @@ namespace Boilerplate.Client.Maui.Platforms.Android; DataPathPrefixes = [ "/en-US", "/en-GB", "/nl-NL", "/fa-IR", "sv-SE", "hi-IN", "zh-CN", "es-ES", "fr-FR", "ar-SA", "de-DE", PageUrls.Confirm, PageUrls.ForgotPassword, PageUrls.Settings, PageUrls.ResetPassword, PageUrls.SignIn, - PageUrls.SignUp, PageUrls.NotAuthorized, PageUrls.NotFound, PageUrls.Terms, PageUrls.About, PageUrls.Authorize, + PageUrls.SignUp, PageUrls.NotAuthorized, PageUrls.NotFound, PageUrls.Terms, PageUrls.About, PageUrls.Roles, PageUrls.Users, //#if (module == "Admin") PageUrls.AddOrEditProduct, PageUrls.Categories, PageUrls.Dashboard, PageUrls.Products, diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs index f55aab1345..c8562d6563 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs @@ -66,14 +66,14 @@ await MainThread.InvokeOnMainThreadAsync(() => .WithUrlPrefix($"http://localhost:{port}") .WithMode(AppPlatform.IsWindows ? HttpListenerMode.Microsoft : HttpListenerMode.EmbedIO)) .WithCors() - .WithModule(new ActionModule("/api/SocialSignInCallback", HttpVerbs.Post, async ctx => + .WithModule(new ActionModule("/api/ExternalSignInCallback", HttpVerbs.Post, async ctx => { try { await MainThread.InvokeOnMainThreadAsync(() => { var urlToOpen = ctx.Request.QueryString["urlToOpen"]; - pubSubService.Publish(ClientAppMessages.SOCIAL_SIGN_IN_CALLBACK, urlToOpen); + pubSubService.Publish(ClientAppMessages.EXTERNAL_SIGN_IN_CALLBACK, urlToOpen); }); } finally diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/web-interop-app.html b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/web-interop-app.html index f515d03d51..46f51469f3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/web-interop-app.html +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/web-interop-app.html @@ -1,12 +1,12 @@