@ -0,0 +1,201 @@ |
|||
# How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications |
|||
|
|||
This guide demonstrates how to integrate AzureAD to an ABP application that enables users to sign in using OAuth 2.0 with credentials from **Azure Active Directory**. |
|||
|
|||
Adding Azure Active Directory is pretty straightforward in ABP framework. Couple of configurations needs to be done correctly. |
|||
|
|||
Two different **alternative approaches** for AzureAD integration will be demonstrated for better coverage. |
|||
|
|||
1. **AddAzureAD**: This approach uses Microsoft [AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/) which is very popular when users search the web about how to integrate AzureAD to their web application. |
|||
|
|||
2. **AddOpenIdConnect**: This approach uses default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for not only AzureAD but for all OpenId connections. |
|||
|
|||
> There is **no difference** in functionality between these approaches. AddAzureAD is an abstracted way of OpenIdConnection ([source](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122)) with predefined cookie settings. |
|||
> |
|||
> However there are key differences in integration to ABP applications because of default configurated signin schemes which will be explained below. |
|||
|
|||
## 1. AddAzureAD |
|||
|
|||
This approach uses the most common way to integrate AzureAD by using the [Microsoft AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/). |
|||
|
|||
If you choose this approach, you will need to install `Microsoft.AspNetCore.Authentication.AzureAD.UI` package to your **.Web** project. Also, since AddAzureAD extension uses [configuration binding](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration), you need to update your appsettings.json file located in your **.Web** project. |
|||
|
|||
#### **Updating `appsettings.json`** |
|||
|
|||
You need to add a new section to your `appsettings.json` which will be binded to configuration when configuring the `OpenIdConnectOptions`: |
|||
|
|||
````json |
|||
"AzureAd": { |
|||
"Instance": "https://login.microsoftonline.com/", |
|||
"TenantId": "<your-tenant-id>", |
|||
"ClientId": "<your-client-id>", |
|||
"Domain": "domain.onmicrosoft.com", |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
> Important configuration here is the CallbackPath. This value must be the same with one of your Azure AD-> app registrations-> Authentication -> RedirectUri. |
|||
|
|||
Then, you need to configure the `OpenIdConnectOptions` to complete the integration. |
|||
|
|||
#### Configuring OpenIdConnectOptions |
|||
|
|||
In your **.Web** project, locate your **ApplicationWebModule** and modify `ConfigureAuthentication` method with the following: |
|||
|
|||
````csharp |
|||
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
|||
{ |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
context.Services.AddAuthentication() |
|||
.AddIdentityServerAuthentication(options => |
|||
{ |
|||
options.Authority = configuration["AuthServer:Authority"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.ApiName = "Acme.BookStore"; |
|||
}) |
|||
.AddAzureAD(options => configuration.Bind("AzureAd", options)); |
|||
|
|||
context.Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => |
|||
{ |
|||
options.Authority = options.Authority + "/v2.0/"; |
|||
options.ClientId = configuration["AzureAd:ClientId"]; |
|||
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
|||
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
|||
options.RequireHttpsMetadata = false; |
|||
|
|||
options.TokenValidationParameters.ValidateIssuer = false; |
|||
options.GetClaimsFromUserInfoEndpoint = true; |
|||
options.SaveTokens = true; |
|||
options.SignInScheme = IdentityConstants.ExternalScheme; |
|||
|
|||
options.Scope.Add("email"); |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
> **Don't forget to:** |
|||
> |
|||
> * Add `.AddAzureAD(options => configuration.Bind("AzureAd", options))` after `.AddAuthentication()`. This binds your AzureAD appsettings and easy to miss out. |
|||
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. This will disable the default Microsoft claim type mapping. |
|||
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. Mapping this to [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) is important since default SignIn Manager behavior uses this claim type for external login information. |
|||
> * Add `options.SignInScheme = IdentityConstants.ExternalScheme` since [default signin scheme is `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). |
|||
> * Add `options.Scope.Add("email")` if you are using **v2.0** endpoint of AzureAD since v2.0 endpoint doesn't return the `email` claim as default. The [Account Module](../Modules/Account.md) uses `email` claim to [register external users](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). |
|||
|
|||
You are done and integration is completed. |
|||
|
|||
## 2. Alternative Approach: AddOpenIdConnect |
|||
|
|||
If you don't want to use an extra nuget package in your application, you can use the straight default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for all OpenId connections including AzureAD external authentication. |
|||
|
|||
You don't have to use `appsettings.json` configuration but it is a good practice to set AzureAD information in the `appsettings.json`. |
|||
|
|||
To get the AzureAD information from `appsettings.json`, which will be used in `OpenIdConnectOptions` configuration, simply add a new section to `appsettings.json` located in your **.Web** project: |
|||
|
|||
````json |
|||
"AzureAd": { |
|||
"Instance": "https://login.microsoftonline.com/", |
|||
"TenantId": "<your-tenant-id>", |
|||
"ClientId": "<your-client-id>", |
|||
"Domain": "domain.onmicrosoft.com", |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
Then, In your **.Web** project; you can modify the `ConfigureAuthentication` method located in your **ApplicationWebModule** with the following: |
|||
|
|||
````csharp |
|||
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
|||
{ |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
|
|||
context.Services.AddAuthentication() |
|||
.AddIdentityServerAuthentication(options => |
|||
{ |
|||
options.Authority = configuration["AuthServer:Authority"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.ApiName = "BookStore"; |
|||
}) |
|||
.AddOpenIdConnect("AzureOpenId", "Azure Active Directory OpenId", options => |
|||
{ |
|||
options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/"; |
|||
options.ClientId = configuration["AzureAd:ClientId"]; |
|||
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
|||
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.SaveTokens = true; |
|||
options.GetClaimsFromUserInfoEndpoint = true; |
|||
|
|||
options.Scope.Add("email"); |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
And that's it, integration is completed. Keep on mind that you can connect any other external authentication providers. |
|||
|
|||
## The Source Code |
|||
|
|||
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
|||
|
|||
# FAQ |
|||
|
|||
* Help! `GetExternalLoginInfoAsync` returns `null`! |
|||
|
|||
* There can be 2 reasons for this; |
|||
|
|||
1. You are trying to authenticate against wrong scheme. Check if you set **SignInScheme** to `IdentityConstants.ExternalScheme`: |
|||
|
|||
````csharp |
|||
options.SignInScheme = IdentityConstants.ExternalScheme; |
|||
```` |
|||
|
|||
2. Your `ClaimTypes.NameIdentifier` is `null`. Check if you added claim mapping: |
|||
|
|||
````csharp |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
```` |
|||
|
|||
|
|||
* Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error! |
|||
|
|||
* If you set your **CallbackPath** in appsettings as: |
|||
|
|||
````csharp |
|||
"AzureAd": { |
|||
... |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
your **Redirect URI** of your application in azure portal must be with <u>domain</u> like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`. |
|||
|
|||
* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error! |
|||
|
|||
|
|||
* This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add |
|||
|
|||
````csharp |
|||
options.Scope.Add("email"); |
|||
```` |
|||
|
|||
to your openid configuration. |
|||
|
|||
* How can I **debug/watch** which claims I get before they get mapped? |
|||
|
|||
* You can add a simple event under openid configuration to debug before mapping like: |
|||
|
|||
````csharp |
|||
options.Events.OnTokenValidated = (async context => |
|||
{ |
|||
var claimsFromOidcProvider = context.Principal.Claims.ToList(); |
|||
await Task.CompletedTask; |
|||
}); |
|||
```` |
|||
|
|||
|
|||
## See Also |
|||
|
|||
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). |
|||
* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md). |
|||
@ -0,0 +1,113 @@ |
|||
# How to Customize the Login Page for MVC / Razor Page Applications |
|||
|
|||
When you create a new application using the [application startup template](../Startup-Templates/Application.md), source code of the login page will not be inside your solution, so you can not directly change it. The login page comes from the [Account Module](../Modules/Account.md) that is used a [NuGet package](https://www.nuget.org/packages/Volo.Abp.Account.Web) reference. |
|||
|
|||
This document explains how to customize the login page for your own application. |
|||
|
|||
## Create a Login PageModel |
|||
|
|||
Create a new class inheriting from the [LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs) of the Account module. |
|||
|
|||
````csharp |
|||
public class CustomLoginModel : LoginModel |
|||
{ |
|||
public CustomLoginModel( |
|||
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider, |
|||
Microsoft.Extensions.Options.IOptions<Volo.Abp.Account.Web.AbpAccountOptions> accountOptions) |
|||
: base(schemeProvider, accountOptions) |
|||
{ |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> Naming convention is important here. If your class name doesn't end with `LoginModel`, you need to manually replace the `LoginModel` using the [dependency injection](../Dependency-Injection.md) system. |
|||
|
|||
Then you can override any method you need and add new methods and properties needed by the UI. |
|||
|
|||
## Overriding the Login Page UI |
|||
|
|||
Create folder named **Account** under **Pages** directory and create a **Login.cshtml** under this folder. It will automatically override the `Login.cshtml` file defined in the Account Module thanks to the [Virtual File System](../Virtual-File-System.md). |
|||
|
|||
A good way to customize a page is to copy its source code. [Click here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml) for the source code of the login page. At the time this document has been written, the source code was like below: |
|||
|
|||
````xml |
|||
@page |
|||
@using Volo.Abp.Account.Settings |
|||
@using Volo.Abp.Settings |
|||
@model Acme.BookStore.Web.Pages.Account.CustomLoginModel |
|||
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage |
|||
@inject Volo.Abp.Settings.ISettingProvider SettingProvider |
|||
@if (Model.EnableLocalLogin) |
|||
{ |
|||
<div class="card mt-3 shadow-sm rounded"> |
|||
<div class="card-body p-5"> |
|||
<h4>@L["Login"]</h4> |
|||
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) |
|||
{ |
|||
<strong> |
|||
@L["AreYouANewUser"] |
|||
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a> |
|||
</strong> |
|||
} |
|||
<form method="post" class="mt-4"> |
|||
<input asp-for="ReturnUrl" /> |
|||
<input asp-for="ReturnUrlHash" /> |
|||
<div class="form-group"> |
|||
<label asp-for="LoginInput.UserNameOrEmailAddress"></label> |
|||
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" /> |
|||
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label asp-for="LoginInput.Password"></label> |
|||
<input asp-for="LoginInput.Password" class="form-control" /> |
|||
<span asp-validation-for="LoginInput.Password" class="text-danger"></span> |
|||
</div> |
|||
<div class="form-check"> |
|||
<label asp-for="LoginInput.RememberMe" class="form-check-label"> |
|||
<input asp-for="LoginInput.RememberMe" class="form-check-input" /> |
|||
@Html.DisplayNameFor(m => m.LoginInput.RememberMe) |
|||
</label> |
|||
</div> |
|||
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-block btn-lg mt-3">@L["Login"]</abp-button> |
|||
</form> |
|||
</div> |
|||
|
|||
<div class="card-footer text-center border-0"> |
|||
<abp-button type="button" button-type="Link" name="Action" value="Cancel" class="px-2 py-0">@L["Cancel"]</abp-button> @* TODO: Only show if identity server is used *@ |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
@if (Model.VisibleExternalProviders.Any()) |
|||
{ |
|||
<div class="col-md-6"> |
|||
<h4>@L["UseAnotherServiceToLogIn"]</h4> |
|||
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post"> |
|||
<input asp-for="ReturnUrl" /> |
|||
<input asp-for="ReturnUrlHash" /> |
|||
@foreach (var provider in Model.VisibleExternalProviders) |
|||
{ |
|||
<button type="submit" class="btn btn-primary" name="provider" value="@provider.AuthenticationScheme" title="@L["GivenTenantIsNotAvailable", provider.DisplayName]">@provider.DisplayName</button> |
|||
} |
|||
</form> |
|||
</div> |
|||
} |
|||
|
|||
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) |
|||
{ |
|||
<div class="alert alert-warning"> |
|||
<strong>@L["InvalidLoginRequest"]</strong> |
|||
@L["ThereAreNoLoginSchemesConfiguredForThisClient"] |
|||
</div> |
|||
} |
|||
```` |
|||
|
|||
Just changed the `@model` to `Acme.BookStore.Web.Pages.Account.CustomLoginModel` to use the customized `PageModel` class. You can change it however your application needs. |
|||
|
|||
## The Source Code |
|||
|
|||
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
|||
|
|||
## See Also |
|||
|
|||
* [ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide](../UI/AspNetCore/Customization-User-Interface.md). |
|||
@ -0,0 +1,101 @@ |
|||
# How to Customize the SignIn Manager for ABP Applications |
|||
|
|||
After creating a new application using the [application startup template](../Startup-Templates/Application.md), you may want extend or change the default behavior of the SignIn Manager for your authentication and registration flow needs. ABP [Account Module](../Modules/Account.md) uses the [Identity Management Module](../Modules/Identity.md) for SignIn Manager and the [Identity Management Module](../Modules/Identity.md) uses default [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) ([see here](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17)). |
|||
|
|||
To write your Custom SignIn Manager, you need to extend [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) class and register it to the DI container. |
|||
|
|||
This document explains how to customize the SignIn Manager for your own application. |
|||
|
|||
## Create a CustomSignInManager |
|||
|
|||
Create a new class inheriting the [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) of Microsoft Identity package. |
|||
|
|||
````csharp |
|||
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser> |
|||
{ |
|||
public CustomSignInManager( |
|||
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager, |
|||
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, |
|||
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory, |
|||
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, |
|||
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger, |
|||
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, |
|||
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation) |
|||
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) |
|||
{ |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> It is important to use **Volo.Abp.Identity.IdentityUser** type for SignInManager to inherit, not the AppUser of your application. |
|||
|
|||
Afterwards you can override any of the SignIn Manager methods you need and add new methods and properties needed for your authentication or registration flow. |
|||
|
|||
## Overriding the GetExternalLoginInfoAsync Method |
|||
|
|||
In this case we'll be overriding the `GetExternalLoginInfoAsync` method which is invoked when a third party authentication is implemented. |
|||
|
|||
A good way to override a method is copying its [source code](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674). In this case, we will be using a minorly modified version of the source code which explicitly shows the namespaces of the methods and properties to help better understanding of the concept. |
|||
|
|||
````csharp |
|||
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null) |
|||
{ |
|||
var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); |
|||
var items = auth?.Properties?.Items; |
|||
if (auth?.Principal == null || items == null || !items.ContainsKey("LoginProviderKey")) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (expectedXsrf != null) |
|||
{ |
|||
if (!items.ContainsKey("XsrfKey")) |
|||
{ |
|||
return null; |
|||
} |
|||
var userId = items[XsrfKey] as string; |
|||
if (userId != expectedXsrf) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); |
|||
var provider = items[LoginProviderKey] as string; |
|||
if (providerKey == null || provider == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName |
|||
?? provider; |
|||
return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) |
|||
{ |
|||
AuthenticationTokens = auth.Properties.GetTokens() |
|||
}; |
|||
} |
|||
```` |
|||
|
|||
To get your overridden method invoked and your customized SignIn Manager class to work, you need to register your class to the [Dependency Injection System](../Dependency-Injection.md). |
|||
|
|||
## Register to Dependency Injection |
|||
|
|||
Registering `CustomSignInManager` should be done with adding **AddSignInManager** extension method of the [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) of the [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs). |
|||
|
|||
Inside your `.Web` project, locate the `YourProjectNameWebModule` and add the following code under the `PreConfigureServices` method to replace the old `SignInManager` with your customized one: |
|||
|
|||
````csharp |
|||
PreConfigure<IdentityBuilder>(identityBuilder => |
|||
{ |
|||
identityBuilder.AddSignInManager<CustomSignInManager>(); |
|||
}); |
|||
```` |
|||
|
|||
## The Source Code |
|||
|
|||
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
|||
|
|||
## See Also |
|||
|
|||
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). |
|||
* [Identity Management Module](../Modules/Identity.md). |
|||
@ -0,0 +1,9 @@ |
|||
# "How To" Guides |
|||
|
|||
This section contains "how to" guides for some specific questions frequently asked. While some of them are common development tasks and not directly related to the ABP Framework, we think it is useful to have some concrete examples those directly work with your ABP based applications. |
|||
|
|||
## Authentication |
|||
|
|||
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md) |
|||
* [How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications](Azure-Active-Directory-Authentication-MVC.md) |
|||
* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md) |
|||
@ -0,0 +1,74 @@ |
|||
# ContentSecurityStrategy |
|||
|
|||
`ContentSecurityStrategy` is an abstract class exposed by @abp/ng.core package. It helps you mark inline scripts or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). |
|||
|
|||
|
|||
|
|||
|
|||
## API |
|||
|
|||
|
|||
### constructor |
|||
|
|||
```js |
|||
constructor(public nonce?: string) |
|||
``` |
|||
|
|||
- `nonce` enables whitelisting inline script or styles in order to avoid using `unsafe-inline` in [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) and [style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles) directives. |
|||
|
|||
|
|||
### applyCSP |
|||
|
|||
```js |
|||
applyCSP(element: HTMLScriptElement | HTMLStyleElement): void |
|||
``` |
|||
|
|||
This method maps the aforementioned properties to the given `element`. |
|||
|
|||
|
|||
|
|||
|
|||
## LooseContentSecurityPolicy |
|||
|
|||
`LooseContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It requires `nonce` and marks given `<script>` or `<style>` tag with it. |
|||
|
|||
|
|||
|
|||
|
|||
## NoContentSecurityPolicy |
|||
|
|||
`NoContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It does not mark inline scripts and styles as safe. You can consider it as a noop alternative. |
|||
|
|||
|
|||
|
|||
|
|||
## Predefined Content Security Strategies |
|||
|
|||
Predefined content security strategies are accessible via `CONTENT_SECURITY_STRATEGY` constant. |
|||
|
|||
|
|||
### Loose |
|||
|
|||
```js |
|||
CONTENT_SECURITY_STRATEGY.Loose(nonce: string) |
|||
``` |
|||
|
|||
`nonce` will be set. |
|||
|
|||
|
|||
### None |
|||
|
|||
```js |
|||
CONTENT_SECURITY_STRATEGY.None() |
|||
``` |
|||
|
|||
Nothing will be done. |
|||
|
|||
|
|||
|
|||
|
|||
## See Also |
|||
|
|||
- [DomInsertionService](./Dom-Insertion-Service.md) |
|||
- [ContentStrategy](./Content-Strategy.md) |
|||
|
|||
@ -0,0 +1,95 @@ |
|||
# ContentStrategy |
|||
|
|||
`ContentStrategy` is an abstract class exposed by @abp/ng.core package. It helps you create inline scripts or styles. |
|||
|
|||
## API |
|||
|
|||
|
|||
### constructor |
|||
|
|||
```js |
|||
constructor( |
|||
public content: string, |
|||
protected domStrategy?: DomStrategy, |
|||
protected contentSecurityStrategy?: ContentSecurityStrategy |
|||
) |
|||
``` |
|||
|
|||
- `content` is set to `<script>` and `<style>` elements as `textContent` property. |
|||
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_) |
|||
- `contentSecurityStrategy` is the `ContentSecurityStrategy` that will be used on the created element before inserting it. (_default: None_) |
|||
|
|||
Please refer to [DomStrategy](./Dom-Strategy.md) and [ContentSecurityStrategy](./Content-Security-Strategy.md) documentation for their usage. |
|||
|
|||
|
|||
### createElement |
|||
|
|||
```js |
|||
createElement(): HTMLScriptElement | HTMLStyleElement |
|||
``` |
|||
|
|||
This method creates and returns a `<script>` or `<style>` element with `content` set as `textContent`. |
|||
|
|||
|
|||
### insertElement |
|||
|
|||
```js |
|||
insertElement(): void |
|||
``` |
|||
|
|||
This method creates and inserts a `<script>` or `<style>` element. |
|||
|
|||
|
|||
## ScriptContentStrategy |
|||
|
|||
`ScriptContentStrategy` is a class that extends `ContentStrategy`. It lets you **insert a `<script>` element to the DOM**. |
|||
|
|||
## StyleContentStrategy |
|||
|
|||
`StyleContentStrategy` is a class that extends `ContentStrategy`. It lets you **insert a `<style>` element to the DOM**. |
|||
|
|||
|
|||
## Predefined Content Strategies |
|||
|
|||
Predefined content strategies are accessible via `CONTENT_STRATEGY` constant. |
|||
|
|||
|
|||
### AppendScriptToBody |
|||
|
|||
```js |
|||
CONTENT_STRATEGY.AppendScriptToBody(content: string) |
|||
``` |
|||
|
|||
Creates a `<script>` element with the given content and places it at the **end** of `<body>` tag in the document. |
|||
|
|||
|
|||
### AppendScriptToHead |
|||
|
|||
```js |
|||
CONTENT_STRATEGY.AppendScriptToHead(content: string) |
|||
``` |
|||
|
|||
Creates a `<script>` element with the given content and places it at the **end** of `<head>` tag in the document. |
|||
|
|||
|
|||
### AppendStyleToHead |
|||
|
|||
```js |
|||
CONTENT_STRATEGY.AppendStyleToHead(content: string) |
|||
``` |
|||
|
|||
Creates a `<style>` element with the given content and places it at the **end** of `<head>` tag in the document. |
|||
|
|||
|
|||
### PrependStyleToHead |
|||
|
|||
```js |
|||
CONTENT_STRATEGY.PrependStyleToHead(content: string) |
|||
``` |
|||
|
|||
Creates a `<style>` element with the given content and places it at the **beginning** of `<head>` tag in the document. |
|||
|
|||
|
|||
## See Also |
|||
|
|||
- [DomInsertionService](./Dom-Insertion-Service.md) |
|||
@ -0,0 +1,60 @@ |
|||
# CrossOriginStrategy |
|||
|
|||
`CrossOriginStrategy` is a class exposed by @abp/ng.core package. Its instances define how a source referenced by an element will be retrieved by the browser and are consumed by other classes such as `LoadingStrategy`. |
|||
|
|||
|
|||
## API |
|||
|
|||
|
|||
### constructor |
|||
|
|||
```js |
|||
constructor( |
|||
public crossorigin: 'anonymous' | 'use-credentials', |
|||
public integrity?: string |
|||
) |
|||
``` |
|||
|
|||
- `crossorigin` is mapped to [the HTML attribute with the same name](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin). |
|||
- `integrity` is a hash for validating a remote resource. Its use is explained [here](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity). |
|||
|
|||
|
|||
### setCrossOrigin |
|||
|
|||
```js |
|||
setCrossOrigin(element: HTMLElement): void |
|||
``` |
|||
|
|||
This method maps the aforementioned properties to the given `element`. |
|||
|
|||
|
|||
|
|||
|
|||
## Predefined Cross-Origin Strategies |
|||
|
|||
Predefined cross-origin strategies are accessible via `CROSS_ORIGIN_STRATEGY` constant. |
|||
|
|||
|
|||
### Anonymous |
|||
|
|||
```js |
|||
CROSS_ORIGIN_STRATEGY.Anonymous(integrity?: string) |
|||
``` |
|||
|
|||
`crossorigin` will be set as `"anonymous"` and `integrity` is optional. |
|||
|
|||
|
|||
### UseCredentials |
|||
|
|||
```js |
|||
CROSS_ORIGIN_STRATEGY.UseCredentials(integrity?: string) |
|||
``` |
|||
|
|||
`crossorigin` will be set as `"use-credentials"` and `integrity` is optional. |
|||
|
|||
|
|||
|
|||
|
|||
## What's Next? |
|||
|
|||
- [LoadingStrategy](./Loading-Strategy.md) |
|||
@ -0,0 +1,91 @@ |
|||
# How to Insert Scripts and Styles |
|||
|
|||
You can use the `DomInsertionService` in @abp/ng.core package in order to insert scripts and styles in an easy and explicit way. |
|||
|
|||
|
|||
## Getting Started |
|||
|
|||
You do not have to provide the `DomInsertionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
|||
|
|||
```js |
|||
import { DomInsertionService } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
}) |
|||
class DemoComponent { |
|||
constructor(private domInsertionService: DomInsertionService) {} |
|||
} |
|||
``` |
|||
|
|||
## Usage |
|||
|
|||
You can use the `insertContent` method of `DomInsertionService` to create a `<script>` or `<style>` element with given content in the DOM at the desired position. |
|||
|
|||
|
|||
### How to Insert Scripts |
|||
|
|||
The first parameter of `insertContent` method expects a `ContentStrategy`. If you pass a `ScriptContentStrategy` instance, the `DomInsertionService` will create a `<script>` element with given `content` and place it in the designated DOM position. |
|||
|
|||
```js |
|||
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
}) |
|||
class DemoComponent { |
|||
constructor(private domInsertionService: DomInsertionService) {} |
|||
|
|||
ngOnInit() { |
|||
this.domInsertionService.insertContent( |
|||
CONTENT_STRATEGY.AppendScriptToBody('alert()') |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
In the example above, `<script>alert()</script>` element will place at the **end** of `<body>`. |
|||
|
|||
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. |
|||
|
|||
|
|||
### How to Insert Styles |
|||
|
|||
If you pass a `StyleContentStrategy` instance as the first parameter of `insertContent` method, the `DomInsertionService` will create a `<style>` element with given `content` and place it in the designated DOM position. |
|||
|
|||
```js |
|||
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
}) |
|||
class DemoComponent { |
|||
constructor(private domInsertionService: DomInsertionService) {} |
|||
|
|||
ngOnInit() { |
|||
this.domInsertionService.insertContent( |
|||
CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}') |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
In the example above, `<style>body {margin: 0;}</style>` element will place at the **end** of `<head>`. |
|||
|
|||
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. |
|||
|
|||
|
|||
## API |
|||
|
|||
### insertContent |
|||
|
|||
```js |
|||
insertContent(strategy: ContentStrategy): void |
|||
``` |
|||
|
|||
`strategy` parameter is the primary focus here and is explained above. |
|||
|
|||
|
|||
## What's Next? |
|||
|
|||
- [TrackByService](./Track-By-Service.md) |
|||
@ -0,0 +1,89 @@ |
|||
# DomStrategy |
|||
|
|||
`DomStrategy` is a class exposed by @abp/ng.core package. Its instances define how an element will be attached to the DOM and are consumed by other classes such as `LoadingStrategy`. |
|||
|
|||
|
|||
## API |
|||
|
|||
|
|||
### constructor |
|||
|
|||
```js |
|||
constructor( |
|||
public target?: HTMLElement, |
|||
public position?: InsertPosition |
|||
) |
|||
``` |
|||
|
|||
- `target` is an HTMLElement (_default: document.head_). |
|||
- `position` defines where the created element will be placed. All possible values of `position` can be found [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) (_default: 'beforeend'_). |
|||
|
|||
|
|||
### insertElement |
|||
|
|||
```js |
|||
insertElement(element: HTMLElement): void |
|||
``` |
|||
|
|||
This method inserts given `element` to `target` based on the `position`. |
|||
|
|||
|
|||
|
|||
## Predefined Dom Strategies |
|||
|
|||
Predefined dom strategies are accessible via `DOM_STRATEGY` constant. |
|||
|
|||
|
|||
### AppendToBody |
|||
|
|||
```js |
|||
DOM_STRATEGY.AppendToBody() |
|||
``` |
|||
|
|||
`insertElement` will place the given `element` at the end of `<body>`. |
|||
|
|||
|
|||
### AppendToHead |
|||
|
|||
```js |
|||
DOM_STRATEGY.AppendToHead() |
|||
``` |
|||
|
|||
`insertElement` will place the given `element` at the end of `<head>`. |
|||
|
|||
|
|||
### PrependToHead |
|||
|
|||
```js |
|||
DOM_STRATEGY.PrependToHead() |
|||
``` |
|||
|
|||
`insertElement` will place the given `element` at the beginning of `<head>`. |
|||
|
|||
|
|||
### AfterElement |
|||
|
|||
```js |
|||
DOM_STRATEGY.AfterElement(target: HTMLElement) |
|||
``` |
|||
|
|||
`insertElement` will place the given `element` after (as a sibling to) the `target`. |
|||
|
|||
|
|||
### BeforeElement |
|||
|
|||
```js |
|||
DOM_STRATEGY.BeforeElement(target: HTMLElement) |
|||
``` |
|||
|
|||
`insertElement` will place the given `element` before (as a sibling to) the `target`. |
|||
|
|||
|
|||
|
|||
|
|||
## See Also |
|||
|
|||
- [DomInsertionService](./Dom-Insertion-Service.md) |
|||
- [LazyLoadService](./Lazy-Load-Service.md) |
|||
- [LoadingStrategy](./Loading-Strategy.md) |
|||
- [ContentStrategy](./Content-Strategy.md) |
|||
@ -0,0 +1,213 @@ |
|||
# How to Lazy Load Scripts and Styles |
|||
|
|||
You can use the `LazyLoadService` in @abp/ng.core package in order to lazy load scripts and styles in an easy and explicit way. |
|||
|
|||
|
|||
|
|||
|
|||
## Getting Started |
|||
|
|||
You do not have to provide the `LazyLoadService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
|||
|
|||
```js |
|||
import { LazyLoadService } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
}) |
|||
class DemoComponent { |
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
|
|||
## Usage |
|||
|
|||
You can use the `load` method of `LazyLoadService` to create a `<script>` or `<link>` element in the DOM at the desired position and force the browser to download the target resource. |
|||
|
|||
|
|||
|
|||
### How to Load Scripts |
|||
|
|||
The first parameter of `load` method expects a `LoadingStrategy`. If you pass a `ScriptLoadingStrategy` instance, the `LazyLoadService` will create a `<script>` element with given `src` and place it in the designated DOM position. |
|||
|
|||
```js |
|||
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<some-component *ngIf="libraryLoaded$ | async"></some-component> |
|||
` |
|||
}) |
|||
class DemoComponent { |
|||
libraryLoaded$ = this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/some-library.js'), |
|||
); |
|||
|
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
The `load` method returns an observable to which you can subscibe in your component or with an `async` pipe. In the example above, the `NgIf` directive will render `<some-component>` only **if the script gets successfully loaded or is already loaded before**. |
|||
|
|||
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once. |
|||
|
|||
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy. |
|||
|
|||
|
|||
|
|||
### How to Load Styles |
|||
|
|||
If you pass a `StyleLoadingStrategy` instance as the first parameter of `load` method, the `LazyLoadService` will create a `<link>` element with given `href` and place it in the designated DOM position. |
|||
|
|||
```js |
|||
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<some-component *ngIf="stylesLoaded$ | async"></some-component> |
|||
` |
|||
}) |
|||
class DemoComponent { |
|||
stylesLoaded$ = this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousStyleToHead('/assets/some-styles.css'), |
|||
); |
|||
|
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
The `load` method returns an observable to which you can subscibe in your component or with an `AsyncPipe`. In the example above, the `NgIf` directive will render `<some-component>` only **if the style gets successfully loaded or is already loaded before**. |
|||
|
|||
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once. |
|||
|
|||
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy. |
|||
|
|||
|
|||
|
|||
### Advanced Usage |
|||
|
|||
You have quite a bit of **freedom to define how your lazy load will work**. Here is an example: |
|||
|
|||
```js |
|||
const domStrategy = DOM_STRATEGY.PrependToHead(); |
|||
|
|||
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous( |
|||
'sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh', |
|||
); |
|||
|
|||
const loadingStrategy = new StyleLoadingStrategy( |
|||
'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css', |
|||
domStrategy, |
|||
crossOriginStrategy, |
|||
); |
|||
|
|||
this.lazyLoad.load(loadingStrategy, 1, 2000); |
|||
``` |
|||
|
|||
This code will create a `<link>` element with given url and integrity hash, insert it to to top of the `<head>` element, and retry once after 2 seconds if first try fails. |
|||
|
|||
|
|||
A common usecase is **loading multiple scripts and/or styles before using a feature**: |
|||
|
|||
```js |
|||
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
|||
import { frokJoin } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<some-component *ngIf="scriptsAndStylesLoaded$ | async"></some-component> |
|||
` |
|||
}) |
|||
class DemoComponent { |
|||
private stylesLoaded$ = forkJoin( |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library-dark-theme.css'), |
|||
), |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library.css'), |
|||
), |
|||
); |
|||
|
|||
private scriptsLoaded$ = forkJoin( |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/library.js'), |
|||
), |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/other-library.css'), |
|||
), |
|||
); |
|||
|
|||
scriptsAndStylesLoaded$ = forkJoin(this.scriptsLoaded$, this.stylesLoaded$); |
|||
|
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
RxJS `forkJoin` will load all scripts and styles in parallel and emit only when all of them are loaded. So, when `<some-component>` is placed, all required dependencies will be available. |
|||
|
|||
> Noticed we have prepended styles to the document head? This is sometimes necessary, because your application styles may be overriding some of the library styles. In such a case, you must be careful about the order of prepended styles. They will be placed one-by-one and, **when prepending, the last one placed will be on top**. |
|||
|
|||
|
|||
Another frequent usecase is **loading dependent scripts in order**: |
|||
|
|||
```js |
|||
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
|||
import { concat } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<some-component *ngIf="scriptsLoaded$ | async"></some-component> |
|||
` |
|||
}) |
|||
class DemoComponent { |
|||
scriptsLoaded$ = concat( |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.PrependAnonymousScriptToHead('/assets/library.js'), |
|||
), |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/script-that-requires-library.js'), |
|||
), |
|||
); |
|||
|
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
In this example, the second file needs the first one to be loaded beforehand. RxJS `concat` function will let you load all scripts one-by-one in the given order and emit only when all of them are loaded. |
|||
|
|||
|
|||
|
|||
|
|||
## API |
|||
|
|||
|
|||
|
|||
### loaded |
|||
|
|||
```js |
|||
loaded: Set<string> |
|||
``` |
|||
|
|||
All previously loaded paths are available via this property. It is a simple [JavaScript Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set). |
|||
|
|||
|
|||
|
|||
### load |
|||
|
|||
```js |
|||
load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event> |
|||
``` |
|||
|
|||
- `strategy` parameter is the primary focus here and is explained above. |
|||
- `retryTimes` defines how many times the loading will be tried again before fail (_default: 2_). |
|||
- `retryDelay` defines how much delay there will be between retries (_default: 1000_). |
|||
|
|||
|
|||
|
|||
|
|||
## What's Next? |
|||
|
|||
- [DomInsertionService](./Dom-Insertion-Service.md) |
|||
@ -0,0 +1,110 @@ |
|||
# LoadingStrategy |
|||
|
|||
`LoadingStrategy` is an abstract class exposed by @abp/ng.core package. There are two loading strategies extending it: `ScriptLoadingStrategy` and `StyleLoadingStrategy`. Implementing the same methods and properties, both of these strategies help you define how your lazy loading will work. |
|||
|
|||
|
|||
|
|||
|
|||
## API |
|||
|
|||
### constructor |
|||
|
|||
```js |
|||
constructor( |
|||
public path: string, |
|||
protected domStrategy?: DomStrategy, |
|||
protected crossOriginStrategy?: CrossOriginStrategy |
|||
) |
|||
``` |
|||
|
|||
- `path` is set to `<script>` elements as `src` and `<link>` elements as `href` attribute. |
|||
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_) |
|||
- `crossOriginStrategy` is the `CrossOriginStrategy` that will be used on the created element before inserting it. (_default: Anonymous_) |
|||
|
|||
Please refer to [DomStrategy](./Dom-Strategy.md) and [CrossOriginStrategy](./Cross-Origin-Strategy.md) documentation for their usage. |
|||
|
|||
|
|||
### createElement |
|||
|
|||
```js |
|||
createElement(): HTMLScriptElement | HTMLLinkElement |
|||
``` |
|||
|
|||
This method creates and returns a `<script>` or `<link>` element with `path` set as `src` or `href`. |
|||
|
|||
|
|||
### createStream |
|||
|
|||
```js |
|||
createStream(): Observable<Event> |
|||
``` |
|||
|
|||
This method creates and returns an observable stream that emits on success and throws on error. |
|||
|
|||
|
|||
|
|||
## ScriptLoadingStrategy |
|||
|
|||
`ScriptLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a script**. |
|||
|
|||
|
|||
|
|||
## StyleLoadingStrategy |
|||
|
|||
`StyleLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a style**. |
|||
|
|||
|
|||
|
|||
## Predefined Loading Strategies |
|||
|
|||
Predefined content security strategies are accessible via `LOADING_STRATEGY` constant. |
|||
|
|||
|
|||
### AppendAnonymousScriptToHead |
|||
|
|||
```js |
|||
LOADING_STRATEGY.AppendAnonymousScriptToHead(src: string, integrity?: string) |
|||
``` |
|||
|
|||
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<head>` tag in the document. |
|||
|
|||
|
|||
### PrependAnonymousScriptToHead |
|||
|
|||
```js |
|||
LOADING_STRATEGY.PrependAnonymousScriptToHead(src: string, integrity?: string) |
|||
``` |
|||
|
|||
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **beginning** of `<head>` tag in the document. |
|||
|
|||
|
|||
### AppendAnonymousScriptToBody |
|||
|
|||
```js |
|||
LOADING_STRATEGY.AppendAnonymousScriptToBody(src: string, integrity?: string) |
|||
``` |
|||
|
|||
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<body>` tag in the document. |
|||
|
|||
|
|||
### AppendAnonymousStyleToHead |
|||
|
|||
```js |
|||
LOADING_STRATEGY.AppendAnonymousStyleToHead(href: string, integrity?: string) |
|||
``` |
|||
|
|||
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **end** of `<head>` tag in the document. |
|||
|
|||
|
|||
### PrependAnonymousStyleToHead |
|||
|
|||
```js |
|||
LOADING_STRATEGY.PrependAnonymousStyleToHead(href: string, integrity?: string) |
|||
``` |
|||
|
|||
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **beginning** of `<head>` tag in the document. |
|||
|
|||
|
|||
## See Also |
|||
|
|||
- [LazyLoadService](./Lazy-Load-Service.md) |
|||
@ -0,0 +1,42 @@ |
|||
# Popovers |
|||
|
|||
## Introduction |
|||
|
|||
`abp-popover` is the abp tag for popover messages. |
|||
|
|||
Basic usage: |
|||
|
|||
````xml |
|||
<abp-button abp-popover="Hi, i'm popover content!"> |
|||
Popover Default |
|||
</abp-button> |
|||
```` |
|||
|
|||
|
|||
|
|||
## Demo |
|||
|
|||
See the [popovers demo page](https://bootstrap-taghelpers.abp.io/Components/Popovers) to see it in action. |
|||
|
|||
## Attributes |
|||
|
|||
### disabled |
|||
|
|||
A value indicates if the element should be disabled for interaction. If this value is set to `true`, `dismissable` attribute will be ignored. Should be one of the following values: |
|||
|
|||
* `false` (default value) |
|||
* `true` |
|||
|
|||
### dismissable |
|||
|
|||
A value indicates to dismiss the popovers on the user's next click of a different element than the toggle element. Should be one of the following values: |
|||
|
|||
* `false` (default value) |
|||
* `true` |
|||
|
|||
### hoverable |
|||
|
|||
A value indicates if the popover content will be displayed on mouse hover. Should be one of the following values: |
|||
|
|||
* `false` (default value) |
|||
* `true` |
|||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 88 KiB |
@ -0,0 +1,3 @@ |
|||
# 如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证 |
|||
|
|||
TODO... |
|||
@ -0,0 +1,113 @@ |
|||
# 如何为MVC / Razor页面应用程序自定义登录页面 |
|||
|
|||
当你使用[应用程序启动模板](../Startup-Templates/Application.md)创建了一个新的应用程序, 登录页面的源代码并不在你的解决方案中,所以你不能直接更改. 它来自[账户模块](../Modules/Account.md),使用[NuGet包](https://www.nuget.org/packages/Volo.Abp.Account.Web)引用. |
|||
|
|||
本文介绍了如何为自己的应用程序自定义登录页面. |
|||
|
|||
## 创建登录 PageModel |
|||
|
|||
创建一个新的类继承账户模块的[LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs). |
|||
|
|||
````csharp |
|||
public class CustomLoginModel : LoginModel |
|||
{ |
|||
public CustomLoginModel( |
|||
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider, |
|||
Microsoft.Extensions.Options.IOptions<Volo.Abp.Account.Web.AbpAccountOptions> accountOptions) |
|||
: base(schemeProvider, accountOptions) |
|||
{ |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> 在这里命令约定很重要. 如果你的类名不是以 `LoginModel` 结束,你需要手动在[依赖注入](../Dependency-Injection.md)系统替换 `LoginModel`. |
|||
|
|||
然后你可以覆盖任何方法并添加用户界面需要的新方法和属性. |
|||
|
|||
## 重写登录页面UI |
|||
|
|||
在 **Pages** 目录下创建名为 **Account** 的文件夹,并在这个文件夹中创建 `Login.cshtml` ,借助[虚拟文件系统](../Virtual-File-System.md)它会自动覆盖账户模块的页面文件. |
|||
|
|||
自定义页面一个很好的开始是复制它的源代码. [点击这里](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml)找到登录页面的源码. 在编写本文档时,源代码如下: |
|||
|
|||
````xml |
|||
@page |
|||
@using Volo.Abp.Account.Settings |
|||
@using Volo.Abp.Settings |
|||
@model Acme.BookStore.Web.Pages.Account.CustomLoginModel |
|||
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage |
|||
@inject Volo.Abp.Settings.ISettingProvider SettingProvider |
|||
@if (Model.EnableLocalLogin) |
|||
{ |
|||
<div class="card mt-3 shadow-sm rounded"> |
|||
<div class="card-body p-5"> |
|||
<h4>@L["Login"]</h4> |
|||
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) |
|||
{ |
|||
<strong> |
|||
@L["AreYouANewUser"] |
|||
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a> |
|||
</strong> |
|||
} |
|||
<form method="post" class="mt-4"> |
|||
<input asp-for="ReturnUrl" /> |
|||
<input asp-for="ReturnUrlHash" /> |
|||
<div class="form-group"> |
|||
<label asp-for="LoginInput.UserNameOrEmailAddress"></label> |
|||
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" /> |
|||
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label asp-for="LoginInput.Password"></label> |
|||
<input asp-for="LoginInput.Password" class="form-control" /> |
|||
<span asp-validation-for="LoginInput.Password" class="text-danger"></span> |
|||
</div> |
|||
<div class="form-check"> |
|||
<label asp-for="LoginInput.RememberMe" class="form-check-label"> |
|||
<input asp-for="LoginInput.RememberMe" class="form-check-input" /> |
|||
@Html.DisplayNameFor(m => m.LoginInput.RememberMe) |
|||
</label> |
|||
</div> |
|||
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-block btn-lg mt-3">@L["Login"]</abp-button> |
|||
</form> |
|||
</div> |
|||
|
|||
<div class="card-footer text-center border-0"> |
|||
<abp-button type="button" button-type="Link" name="Action" value="Cancel" class="px-2 py-0">@L["Cancel"]</abp-button> @* TODO: Only show if identity server is used *@ |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
@if (Model.VisibleExternalProviders.Any()) |
|||
{ |
|||
<div class="col-md-6"> |
|||
<h4>@L["UseAnotherServiceToLogIn"]</h4> |
|||
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post"> |
|||
<input asp-for="ReturnUrl" /> |
|||
<input asp-for="ReturnUrlHash" /> |
|||
@foreach (var provider in Model.VisibleExternalProviders) |
|||
{ |
|||
<button type="submit" class="btn btn-primary" name="provider" value="@provider.AuthenticationScheme" title="@L["GivenTenantIsNotAvailable", provider.DisplayName]">@provider.DisplayName</button> |
|||
} |
|||
</form> |
|||
</div> |
|||
} |
|||
|
|||
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) |
|||
{ |
|||
<div class="alert alert-warning"> |
|||
<strong>@L["InvalidLoginRequest"]</strong> |
|||
@L["ThereAreNoLoginSchemesConfiguredForThisClient"] |
|||
</div> |
|||
} |
|||
```` |
|||
|
|||
只需更改 `@model` 为 `Acme.BookStore.Web.Pages.Account.CustomLoginModel` 使用自定义的 `PageModel` 类. 你可以做任何应用程序需要的更改. |
|||
|
|||
## 本文的源代码 |
|||
|
|||
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码. |
|||
|
|||
## 另请参阅 |
|||
|
|||
* [ASP.NET Core (MVC / Razor Pages) 用户界面自定义指南](../UI/AspNetCore/Customization-User-Interface.md). |
|||
@ -0,0 +1,3 @@ |
|||
# 如何为ABP应用程序定制SignIn Manager |
|||
|
|||
TODO... |
|||
@ -0,0 +1,9 @@ |
|||
# "如何" 指南 |
|||
|
|||
本部分包含一些常见问题的 "如何" 指南. 尽管其中是一些常见的开发任务和ABP并不直接相关,但我们认为有一些具体的示例可以直接与基于ABP的应用程序一起使用. |
|||
|
|||
## Authentication |
|||
|
|||
* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md) |
|||
* [如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证](Azure-Active-Directory-Authentication-MVC.md) |
|||
* [如何为ABP应用程序定制SignIn Manager](Customize-SignIn-Manager.md) |
|||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 88 KiB |
@ -0,0 +1,34 @@ |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions |
|||
{ |
|||
public static class PermissionDefinitionContextExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Finds and disables a permission with the given <paramref name="name"/>.
|
|||
/// Returns false if given permission was not found.
|
|||
/// </summary>
|
|||
/// <param name="context">Permission definition context</param>
|
|||
/// <param name="name">Name of the permission</param>
|
|||
/// <returns>
|
|||
/// Returns true if given permission was found.
|
|||
/// Returns false if given permission was not found.
|
|||
/// </returns>
|
|||
public static bool TryDisablePermission( |
|||
[NotNull] this IPermissionDefinitionContext context, |
|||
[NotNull] string name) |
|||
{ |
|||
Check.NotNull(context, nameof(context)); |
|||
Check.NotNull(name, nameof(name)); |
|||
|
|||
var permission = context.GetPermissionOrNull(name); |
|||
if (permission == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
permission.IsEnabled = false; |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.ExceptionHandling |
|||
{ |
|||
public class NullExceptionNotifier : IExceptionNotifier |
|||
{ |
|||
public static NullExceptionNotifier Instance { get; } = new NullExceptionNotifier(); |
|||
|
|||
private NullExceptionNotifier() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public Task NotifyAsync(ExceptionNotificationContext context) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
} |
|||