One of my supported projects recently had the task of analyzing the possibility of migrating from .NET framework 4.5 to .Net Core in case of the need for refactoring and raking up a large amount of accumulated technical debt. The choice fell on the target platform .NET Core 3.0, since, according to the developers of Microsoft, with the release of version 3.0, the necessary steps for migrating legacy code will decrease several times. Especially we were attracted to it by EntityFramework 6.3 release plans for .Net Core i.e. most of the code based on EF 6.2 can be left “as is” in a migrated project on net core.
With the data level, it seems, it became clear, however, another big part of the code porting was the level of security, which, unfortunately, after a quick audit, you will have to almost completely throw it out and rewrite it from scratch. Fortunately, the project already used part of ASP NET Identity, in the form of storing users and other “bicycles” attached to the side.
This raises the logical question: if the security part will have to make many changes, why not immediately implement the approaches recommended in the form of industry standards, namely: bring the application to use Open Id connect and OAuth through the IdentityServer4 framework.
So, we have been given: there is a JavaScript application in Angular (Client in terms of IS4), it uses some subset of WebAPI (Resources), there is also a database of outdated ASP NET Identity with user logins that must be reused after the update (so as not to start everyone else times), plus in some cases it is necessary to give the opportunity to log into the system through Windows authentication on the side of IdentityServer4. Those. There are times when users work through a local area network in an ActiveDirectory domain.
The main solution for migrating user data is to manually (or using automated tools) write a migration script between the old and the new Identity data schema. We, in turn, used the automated data schema comparison application and generated an SQL script, depending on the version of Identity, the target migration script will contain different update instructions. The main thing here is not to forget to coordinate the EFMigrationsHistory table, if EF was used before and will be planned, for example, to expand the IdentityUser entity to additional fields.
But how to correctly configure IdentityServer4 and configure it together with Windows accounts will now be described below.
For NDA reasons, I won’t describe how we managed to implement IS4 in our project, however, in this article I’ll show you in a simple ASP.NET Core site created from scratch what steps you need to take to get a fully configured and functional application that uses IdentityServer4 for authentication and authentication purposes.
In order to realize the desired behavior, we have to take the following steps:
For brevity reasons, all three components (IdentityServer, WebAPI, Angular client) will be in the same project. The selected type of interaction between the client and IdentityServer (GrantType) is Implicit flow, when the access_token is passed to the application side in the browser, and then used to interact with the WebAPI. Closer to release, judging by the changes in the ASP.NET Core repository, Implicit flow will be replaced by Authorization Code + PKCE.)
In the process of creating and modifying the application, the .NET Core command line interface will be widely used, it must be installed on the system in the place with the latest version of preview Core 3.0 (at the time of writing article 3.0.100-preview7-012821).
The release of IdentityServer version 4 was marked by the complete cutting out of the UI from this framework. Now developers have the full right to determine the main interface of the authorization server themselves. There are several ways. One of the popular ones is to use the UI from the QuickStart UI package, which can be found in the official repository on github .
Another, no less convenient, way is integration with ASP NET Core Identity UI, in this case, the developer needs to correctly configure the corresponding middleware in the project. This method will be described later.
Let's start by creating a simple web project. To do this, execute the following instruction on the command line:
dotnet new webapp -n IdentityServer4WebApp
After execution, the output will be a web application framework, which will gradually be brought to the state we need. Here you need to make a reservation that .Net Core 3.0 for Identity uses more lightweight RazorPages, in contrast to the heavyweight MVC.
Now you need to add IdentityServer support to our project. To do this, install the necessary packages:
dotnet add package Microsoft.AspNetCore.ApiAuthorization.IdentityServer -v 3.0.0-preview7.19365.7 dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore -v 3.0.0-preview7.19365.7 dotnet add package Microsoft.EntityFrameworkCore.Tools -v 3.0.0-preview7.19362.6 dotnet add package Microsoft.EntityFrameworkCore.Sqlite -v 3.0.0-preview7.19362.6
In addition to links to authorization server packages, here we have added Entity Framework support for storing user information in the Identity ecosystem. For simplicity, we will use the SQLite database.
To initialize the database, create our user model and database context, for this we declare two ApplicationUser classes, inherited from IdentityUser in the Models folder and ApplicationDbContext , inherited from: ApiAuthorizationDbContext in the Data folder.
Next, you need to configure the use of the EntityFramework context and create the database. To do this, we write the context into the ConfigureServices method of the Startup class:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options =>options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); services.AddRazorPages(); }
And add the connection string in appsettings.json
"ConnectionStrings": { "DefaultConnection": "Data Source=data.db" },
Now you can create the initial migration and initialize the database schema. It is worth noting that you need the installed tool for ef core (for the preview in question you need version 3.0.0-preview7.19362.6).
dotnet ef migrations add Init dotnet ef database update
If all the previous steps were completed without errors, the SQLite data file data.db should appear in your project.
At this stage, we can fully configure and test the full-fledged ability to use Asp.Net Core Identity. To do this, make changes to the Startup methods . Configure and Startup.ConfigureServices .
//Startup.ConfigureServices: services.AddDefaultIdentity<ApplicationUser>() .AddEntityFrameworkStores<ApplicationDbContext>(); //Startup. Configure: app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); });
With these lines we embed the possibility of authentication and authorization in the request processing pipeline. And also add the default user interface for Identity.
It remains only to tweak the UI, add to the Pages \ Shared a new Razor view with the name _LoginPartial.cshtml and the following contents:
@using IdentityServer4WebApp.Models @using Microsoft.AspNetCore.Identity @inject SignInManager<ApplicationUser> SignInManager @inject UserManager<ApplicationUser> UserManager <ul class="navbar-nav"> @if (SignInManager.IsSignedIn(User)) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a> </li> <li class="nav-item"> <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/"> <button type="submit" class="nav-link btn btn-link text-dark">Logout</button> </form> </li> } else { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a> </li> } </ul>
The above presentation code should add links to the Identity interface area with built-in user controls in the navigation panel (login and password, registration, etc.)
To achieve the rendering of new menu items, we simply modify the _Layout.cshtml file by adding the rendering of this partial view.
<ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a> </li> </ul> </div> <partial name="_LoginPartial" /> <!––––> </div>
And now let's try to run our application and click on the links that appear in the head
menu, the user should see a page with a welcome and request
enter login and password. In this case, you can register and log in - all
should work.
IdentityServer4 developers have done an excellent job of improving the integration of ASP.NET Identity and the server framework itself. To add the ability to use OAuth2 tokens, you need to supplement our project with some new instructions in the code.
In the penultimate line of the Startup.ConfigureServices method , add the IS4 agreement configuration on top of ASP.NET Core Identity:
services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
The AddApiAuthorization method instructs the framework to use a specific supported configuration, mainly through the appsettings.json file. At the moment, the built-in IS4 management capabilities are not so flexible and should be regarded as a starting point for building your applications. In any case, you can use the overloaded version of this method and configure the parameters in more detail via callback.
Next, we call the helper method, which configures the application to check the JWT tokens issued by the framework.
services.AddAuthentication() .AddIdentityServerJwt();
Finally, in the Startup.Configure method , add middleware for
Providing Open ID Connect Endpoints
app.UseAuthentication(); app.UseAuthorization(); app.UseIdentityServer();//<-
As mentioned above, the helper methods used read the configuration in
application settings file appsettings.json , in which we must add a new
IdentityServer section.
"IdentityServer": { "Clients": { "TestIdentityAngular": { "Profile": "IdentityServerSPA" } } }
In this section, a client is defined with the name TestIdentityAngular, which we will assign to the future browser client and a specific configuration profile.
Application Profiles is a new IdentityServer configuration tool that provides several predefined configurations with the ability to refine certain parameters. We will use the IdentityServerSPA profile, designed for cases when the browser client and the framework are located in the same project and have the following parameters:
Other possible profiles are SPA (application without IS4), IdentityServerJwt (API shared with IS4), API (separate API).
In addition, the configuration registers resources:
As you know, IdentityServer uses certificates for signing tokens, their parameters can also be set in the configuration file, so at the time of testing we can use
x509 test certificate, for this you need to specify it in the "Key" section of the appsettings.Development.json file.
"IdentityServer": { "Key": { "Type": "Development" } }
Now we can say that the backend that allows you to use IdentityServer is ready and you can start implementing the browser application.
Our browser-based SPA will be written on the Angular platform. The application will contain two pages, one for unauthorized users and the other for authenticated users. The examples use version 8.1.2
First, create the future framework:
ng new ClientApp
In the process of creating you need to answer “yes” to the proposal to use routing. And a little stylize the page through the bootstrap library:
cd ClientApp ng add bootstrap
Next, you need to add SPA hosting support to our main application. First you need to fix the csproj project - add information about our browser application.
<PropertyGroup> <TargetFramework>netcoreapp3.0</TargetFramework> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion> <IsPackable>false</IsPackable> <SpaRoot>ClientApp\</SpaRoot> <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes> <BuildServerSideRenderer>false</BuildServerSideRenderer> </PropertyGroup> … <ItemGroup> <Content Remove="$(SpaRoot)**" /> <None Remove="$(SpaRoot)**" /> <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" /> </ItemGroup> <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') "> <Exec Command="node --version" ContinueOnError="true"> <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> </Exec> <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." /> <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." /> <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" /> </Target>
After that, install the special nuget package to support browser applications.
dotnet add package Microsoft.AspNetCore.SpaServices.Extensions -v 3.0.0-preview7.19365.7
And we use its auxiliary methods:
//Startup. ConfigureServices: services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/dist"; }); //Startup. Configure: app.UseSpa(spa => { spa.Options.SourcePath = "ClientApp"; if (env.IsDevelopment()) { spa.UseAngularCliServer(npmScript: "start"); } });
In addition to calling new methods, you need to delete the Razor Index.chtml and _ViewStart.chtml pages so that the SPA services now provide the content.
If everything was done in accordance with the instructions, when the application starts, the default page will appear on the screen.
Now you need to configure routing, for this we add to the project 2
component:
ng generate component Home -t=true -s=true --skipTests=true ng generate component Data -t=true -s=true --skipTests=true
We write them in the routing table:
const routes: Routes = [ { path: '', component: HomeComponent, pathMatch: 'full' }, { path: 'data', component: DataComponent } ];
And we modify the app.component.html file to display the menu items correctly.
<header> <nav class='navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3'> <div class="container"> <a class="navbar-brand" [routerLink]='["/"]'>Client App</a> <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse" [ngClass]='{"show": isExpanded}'> <ul class="navbar-nav flex-grow"> <li class="nav-item" [routerLinkActive]='["link-active"]' [routerLinkActiveOptions]='{ exact: true }'> <a class="nav-link text-dark" [routerLink]='["/"]'>Home</a> </li> <li class="nav-item" [routerLinkActive]='["link-active"]'> <a class="nav-link text-dark" [routerLink]='["/data"]'>Web api data</a> </li> </ul> </div> </div> </nav> </header> <div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> </div> <div class="router-outlet"> <router-outlet></router-outlet> </div>
At this step, you can complete the basic preparation of the application framework for implementing interaction through tokens issued by IdentityServer.
The current stage of preparing the framework of our SPA can be called completed and now we should start implementing the module that is responsible for interacting with the server part using the OpenID Connect and OAuth protocols. Fortunately, the developers from Microsoft have already implemented such code, and now you can simply borrow this module from them. Since my article is written based on ASP.NET Core 3.0 pre-release 7, we will take all the code using the release tag “v3.0.0-preview7.19365.7” on github .
Before importing the code, you must install the oidc-client library, which
provides many interfaces for browser applications, as well as
supports management of user sessions and access tokens. For
To start working with it, you need to install the appropriate package.
npm install oidc-client@1.8.0
Now in our SPA it is necessary to implement a module that encapsulates full interaction according to the required protocols. To do this, you need to completely take the ApiAuthorizationModule module from the above ASP.NET Core repository label and add all its files to the application.
In addition, you must import it into the main module of the AppModule application:
@NgModule({ declarations: [ AppComponent, HomeComponent, DataComponent ], imports: [ BrowserModule, HttpClientModule, ApiAuthorizationModule,//<- AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
To display new menu items in the imported module there is an app-login-menu component,
it can be completely changed to suit your needs and add
a link to it in the navigation section of the app.component.html view .
The API authorization module for configuring the OpenID connect of the SPA client must use a special endpoint in the application backend, for its implementation we
must follow these steps:
The code of the created controller is presented below:
[ApiController] public class OidcConfigurationController: ControllerBase { private readonly IClientRequestParametersProvider _clientRequestParametersProvider; public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider) { _clientRequestParametersProvider = clientRequestParametersProvider; } [HttpGet("_configuration/{clientId}")] public IActionResult GetClientRequestParameters([FromRoute]string clientId) { var parameters = _clientRequestParametersProvider.GetClientParameters(HttpContext, clientId); return Ok(parameters); } }
You also need to configure point API support for the backend application.
//Startup.ConfigureServices: services.AddControllers();//<- services.AddRazorPages(); //Startup. Configure: app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); endpoints.MapControllers(); });
Now it is time to launch the application. Two items should appear on the main page in the top menu - Login and Register . Also, at startup, the imported authorization module will request from the server side the client configuration, which will subsequently be taken into account in the protocol. An example configuration output is shown below:
{ "authority": "https://localhost:44367", "client_id": "TestIdentityAngular", "redirect_uri": "https://localhost:44367/authentication/login-callback", "post_logout_redirect_uri": "https://localhost:44367/authentication/logout-callback", "response_type": "id_token token", "scope": "IdentityServer4WebAppAPI openid profile" }
As you can see, the client during the interaction expects to receive the id token and access token, and it is also configured for the access area to our API.
Now, if we select the Login menu item, we should be redirected to the page of our IdentityServer4 and here we can enter the username and password, and if they are correct, we will be immediately transferred back to the browser application, which in turn will receive id_token and access_token . As you can see below, the app-login-menu component itself determined that the authorization completed successfully and displayed a “greeting”, as well as a button for Logout .
When you open the "developer tools" in the browser, you can see in the backstage all the interaction using the OIDC / OAuth protocol. This is getting authorization server information
via endpoint .well-known / openid-configuration and pooling session activity through connect / checksession access point. In addition, the authorization module is configured for the “silent updating of tokens” mechanism, when when the access token expires, the system independently passes the authorization steps in a hidden iframe. You can disable token auto-updates by setting the value of the includeIdTokenInSilentRenew parameter to false in the authorize.service.ts file.
Now you can deal with restricting access to unauthorized users from the components of the SPA application, as well as some API controllers on the back. In order to demonstrate some API, we will create an ExchangeRateItem class in the Models folder , as well as a controller in the Controller folder that returns some random data.
//Controller: [ApiController] public class ExchangeRateController { private static readonly string[] Currencies = new[] { "EUR", "USD", "BGN", "AUD", "CNY", "TWD", "NZD", "TND", "UAH", "UYU", "MAD" }; [HttpGet("api/rates")] public IEnumerable<ExchangeRateItem> Get() { var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new ExchangeRateItem { FromCurrency = "RUR", ToCurrency = Currencies[rng.Next(Currencies.Length)], Value = Math.Round(1.0+ 1.0/rng.Next(1, 100),2) }) .ToArray(); } } //Models: public class ExchangeRateItem { public string FromCurrency { get; set; } public string ToCurrency { get; set; } public double Value { get; set; } }
Next, on the front-end side, create a new component that will receive
and display data on exchange rates from the newly created controller.
ng generate component ExchangeRate -t=true -s=true --skipTests=true
The content of the component should look like this:
import { Component, OnInit, Input } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of, Subject } from 'rxjs'; import { catchError } from 'rxjs/operators'; @Component({ selector: 'app-exchange-rate', template: ` <div class="alert alert-danger" *ngIf="errorMessage | async as msg"> {{msg}} </div> <table class='table table-striped'> <thead> <tr> <th>From currency</th> <th>To currency</th> <th>Rate</th> </tr> </thead> <tbody> <tr *ngFor="let rate of rates | async"> <td>{{ rate.fromCurrency }} </td> <td>{{ rate.toCurrency }}</td> <td>{{ rate.value }}</td> </tr> </tbody> </table> `, styles: [] }) export class ExchangeRateComponent implements OnInit { public rates: Observable<ExchangeRateItem[]>; public errorMessage: Subject<string>; @Input() public apiUrl: string; constructor(private http: HttpClient) { this.errorMessage = new Subject<string>(); } ngOnInit() { this.rates = this.http.get<ExchangeRateItem[]>("/api/"+this.apiUrl).pipe(catchError(this.handleError(this.errorMessage)) ); } private handleError(subject: Subject<string>): (te:any) => Observable<ExchangeRateItem[]> { return (error) => { let message = ''; if (error.error instanceof ErrorEvent) { message = `Error: ${error.error.message}`; } else { message = `Error Code: ${error.status}\nMessage: ${error.message}`; } subject.next(message); let emptyResult: ExchangeRateItem[] = []; return of(emptyResult); } } } interface ExchangeRateItem { fromCurrency: string; toCurrency: string; value: number; }
Now it remains to start using it on the app-data page, simply writing the line <app-exchange-rate apiUrl = "rates"> </app-exchange-rate> in the template and you can start the project again. When we navigate along the target path, we will see that the component received the data and displayed it in the form of a table.
Next, we will try to add a request for authorization of access to the controller’s API. To do this, add the [Authorize] attribute for the ExchangeRateController class and run SPA again, however, after we switch back to the component that calls our API, we will see an error, indicating authorization headers.
To correctly add an authorization token to outgoing requests, you can
Engage the Angular Interceptors interceptor mechanism. Fortunately, the imported module already contains the required type, we only need to register it in the base application module.
providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthorizeInterceptor, multi: true } ],
After these steps, everything should work out correctly. If you look at the developer tools again, the browser will see a new Bearer access_token authorization header. On the backend, this token will be validated by IdentityServer and it will also give permission to call a secure API point.
At the end of the example of integration with the authorization server, you can put the Activation Guard on the route with the exchange rate data in the SPA, it will prevent users from switching to the page if they are not currently authorized. This protector is also presented in the previously imported module, you just need to hang it on the target route.
{ path: 'data', component: DataComponent, canActivate: [AuthorizeGuard] }
Now, in the case when the user has not logged in to the application and selected a link to our protected component, he will immediately be redirected to the authorization page with a request to enter a username and password. The resulting code is available on github .
There is a separate Microsoft.AspNetCore.Authentication.Google Nuget package for connecting login through Google accounts for ASP.NET core 1.1 / 2.0 +, however, due to changes in the policy of the corporation itself, Microsoft has plans for ASP.NET Core 3.0+ recognize it as obsolete . And now it is recommended to connect using the auxiliary method OpenIdConnectExtensions and AddOpenIdConnect , which we will use in this article.
Install the OpenIdConnect extension:
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect -v 3.0.0-preview7.19365.7
To get started, we need to get two key values ​​from Google - Id Client and Client Secret, for this it is proposed to perform the following steps:
Secret Manager . , .
dotnet user-secrets init dotnet user-secrets set "Authentication:Google:ClientId" " ClientID" dotnet user-secrets set "Authentication:Google:ClientSecret" " ClientSecret"
Google.
services.AddAuthentication() .AddOpenIdConnect("Google", "Google", o => { IConfigurationSection googleAuthNSection = Configuration.GetSection("Authentication:Google"); o.ClientId = googleAuthNSection["ClientId"]; o.ClientSecret = googleAuthNSection["ClientSecret"]; o.Authority = "https://accounts.google.com"; o.ResponseType = OpenIdConnectResponseType.Code; o.CallbackPath = "/signin-google"; }) .AddIdentityServerJwt();
, SPA Microsoft, ActiveDirectory. , Html ASP.NET, WebForms .., Windows WindowsIdentity, , . Identity Server, Windows, claims id_token access_token . , IS4 , ,
github . , ASP.NET Core Identity 3.0.
Identity, Razor Login ExternalLogin ( CLI aspnet-codegenerator ):
dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design dotnet aspnet-codegenerator identity -dc IdentityServer4WebApp.Data.ApplicationDbContext --files "Account.Login;Account.ExternalLogin"
, Area Identity , .
, . , Identity I AuthenticationSchemeProvider. GetAllSchemesAsync() DisplayName != null, Windows DisplayName = null. LoginModel OnGetAsync :
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); // << >> ExternalLogins =(await _schemeProvider.GetAllSchemesAsync()).Where(x => x.DisplayName != null ||(x.Name.Equals(IISDefaults.AuthenticationScheme,StringComparison.OrdinalIgnoreCase))).ToList();
private readonly AuthenticationSchemeProvider _schemeProvider . View Login.cshtml :
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> << >> <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @(provider.DisplayName ??provider.Name) account">@(provider.DisplayName ??provider.Name)</button>
, windows launchSettings.json
( IIS, web.config ).
"iisSettings": { "windowsAuthentication": true, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:15479", "sslPort": 44301 } },
«Windows» .
SPA IdentityServer . «» [AllowAnonymous] LoginModel [Authorize(AuthenticationSchemes = "Windows")] , , WindowsIdentity.
ExternalLogin , Identity Windows . ProcessWindowsLoginAsync .
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl) { var result = await HttpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme); if (result?.Principal is WindowsPrincipal wp) { var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl }); var props = _signInManager.ConfigureExternalAuthenticationProperties(IISDefaults.AuthenticationScheme, redirectUrl); props.Items["scheme"] = IISDefaults.AuthenticationScheme; var id = new ClaimsIdentity(IISDefaults.AuthenticationScheme); id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name)); id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name)); id.AddClaim(new Claim(ClaimTypes.NameIdentifier, wp.Identity.Name)); var wi = wp.Identity as WindowsIdentity; var groups = wi.Groups.Translate(typeof(NTAccount)); var hasUsersGroup = groups.Any(i => i.Value.Contains(@"BUILTIN\Users", StringComparison.OrdinalIgnoreCase)); id.AddClaim(new Claim("hasUsersGroup", hasUsersGroup.ToString())); await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props); return Redirect(props.RedirectUri); } return Challenge(IISDefaults.AuthenticationScheme); }
, .
ExternalLoginModel.OnPost :
if (IISDefaults.AuthenticationScheme == provider) { return await ProcessWindowsLoginAsync(returnUrl); }
Claim Windows Claim «hasUsersGroup», ID access, . ASP.NET Identity UserClaims. ExternalLoginModel .
private async Task UpdateClaims(ExternalLoginInfo info, ApplicationUser user, params string[] claimTypes) { if (claimTypes == null) { return; } var claimTypesHash = new HashSet<string>(claimTypes); var claims = (await _userManager.GetClaimsAsync(user)).Where(c => claimTypesHash.Contains(c.Type)).ToList(); await _userManager.RemoveClaimsAsync(user, claims); foreach (var claimType in claimTypes) { if (info.Principal.HasClaim(c => c.Type == claimType)) { claims = info.Principal.FindAll(claimType).ToList(); await _userManager.AddClaimsAsync(user, claims); } } }
OnPostConfirmationAsync (
).
result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: false); _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); await UpdateClaims(info, user, "hasUsersGroup");// return LocalRedirect(returnUrl); }
OnGetCallbackAsync , .
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true); if (result.Succeeded) { var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); await UpdateClaims(info, user, "hasUsersGroup");//
, WebAPI
«hasUsersGroup». «ShouldHasUsersGroup»
services.AddAuthorization(options => { options.AddPolicy("ShouldHasUsersGroup", policy => { policy.RequireClaim("hasUsersGroup");}); });
ExchangeRateController
Policy.
[Authorize(Policy = "ShouldHasUsersGroup")] [HttpGet("api/internalrates")] public IEnumerable<ExchangeRateItem> GetInternalRates() { return Get().Select(i=>{i.Value=Math.Round(i.Value-0.02,2);return i;}); }
view .
ng generate component InternalData -t=true -s=true --skipTests=true
template .
//internal-data.component.ts: template: `<app-exchange-rate apiUrl="internalrates"></app-exchange-rate> `, //app-routing.module.ts: { path: ' internaldata', component: InternalDataComponent, canActivate: [AuthorizeGuard] } //app.component.html: <li class="nav-item" [routerLinkActive]='["link-active"]'> <a class="nav-link text-dark" [routerLink]='["/internaldata"]'>Internal api data</a> </li>
, , . , accsee_token
claim hasUsersGroup ,
ApiResources . , , appsettings.json , Startup. ConfigureServices .
services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => { var apiResource = options.ApiResources.First(); apiResource.UserClaims = new[] { "hasUsersGroup" }; });
, , windows , .
, – Guard claim «hasUsersGroup» « ». Guard :
ng generate guard AuthorizeWindowsGroupGuard --skipTests=true
:
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map,tap} from 'rxjs/operators'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router'; import { AuthorizeService } from "./authorize.service"; import { ApplicationPaths, QueryParameterNames } from './api-authorization.constants'; @Injectable({ providedIn: 'root' }) export class AuthorizeWindowsGroupGuardGuard implements CanActivate{ constructor(private authorize: AuthorizeService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { return this.authorize.getUser().pipe(map((u: any) => !!u && !!u.hasUsersGroup)).pipe(tap((isAuthorized:boolean) => this.handleAuthorization(isAuthorized, state)));; } private handleAuthorization(isAuthenticated: boolean, state: RouterStateSnapshot) { if (!isAuthenticated) { window.location.href = "/Identity/Account/Login?" + QueryParameterNames.ReturnUrl + "=/"; } } }
, , .
{ path: 'internaldata', component: InternalDataComponent, canActivate: [AuthorizeWindowsGroupGuardGuard]
IdentityServer, claims ( sub , profile ), «hasUsersGroup». IdentityResource, - IdentityResources Startup.ConfigureServices .
var identityResource = new IdentityResource { Name = "customprofile", DisplayName = "Custom profile", UserClaims = new[] { "hasUsersGroup" }, }; identityResource.Properties.Add(ApplicationProfilesPropertyNames.Clients, "*"); options.IdentityResources.Add(identityResource);
- , windows « » SPA – , , Guard .
, ASP.NET Core 3.0 IdentityServer4, . preview , . , github .
Source: https://habr.com/ru/post/461433/
All Articles