{"id":40645,"date":"2022-06-23T10:21:58","date_gmt":"2022-06-23T17:21:58","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=40645"},"modified":"2023-02-14T16:39:31","modified_gmt":"2023-02-15T00:39:31","slug":"incremental-asp-net-migration-tooling-preview-2","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/incremental-asp-net-migration-tooling-preview-2\/","title":{"rendered":"Incremental ASP.NET Migration Tooling Preview 2"},"content":{"rendered":"<p>Last month, we introduced <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/incremental-asp-net-to-asp-net-core-migration\/\">preview tooling to incrementally migrate ASP.NET apps to ASP.NET Core<\/a>. The tooling comprised two components &#8211; a Visual Studio extension that created a new ASP.NET Core app alongside an existing ASP.NET app such that endpoints could be gradually moved from one app to the other, and a library of System.Web adapter APIs allowing code to reference common System.Web APIs while targeting .NET Standard 2.0 and being usable in both ASP.NET and ASP.NET Core contexts.<\/p>\n<p>We recently released preview 2 of this ASP.NET migration tooling. The updated tooling includes improvements to the code generated by the Visual Studio extension, additional System.Web surface area in the adapter library, and the ability to share authentication between the ASP.NET and ASP.NET Core apps so that users only have to log in once even if their browsing experience involves endpoints served by both the old and new apps.<\/p>\n<p>As a reminder, development for the System.Web adapters is happening <a href=\"https:\/\/github.com\/dotnet\/systemweb-adapters\">on GitHub<\/a>. In that repository, you can find <a href=\"https:\/\/github.com\/dotnet\/systemweb-adapters\/tree\/main\/docs\">documentation<\/a>, browse the latest source code, or contribute by creating issues or pull requests.<\/p>\n<h2>Getting started<\/h2>\n<p>If this is your first time testing out the incremental ASP.NET migration tooling, you&#8217;ll need to install the <a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=ms-dotnettools.upgradeassistant\">Microsoft .NET Upgrade Assistant<\/a>\u00a0Visual Studio extension.<\/p>\n<p>When you use that extension to migrate an ASP.NET project, it will automatically add references to preview 2 versions of the System.Web adapters. But if you need to reference the System.Web adapters directly (to use them in a class library, for example), they can be installed <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.AspNetCore.SystemWebAdapters\/1.0.0-preview.2.22316.1\">from NuGet<\/a>: <code>&lt;PackageReference Include=\"Microsoft.AspNetCore.SystemWebAdapters\" Version=\"1.0.0-preview.2.22316.1\" \/&gt;<\/code>.<\/p>\n<h3>Upgrading migration tools<\/h3>\n<p>If you previously tested out preview 1 of the incremental ASP.NET migration tooling, you can update the Microsoft Project Migrations Visual Studio extension directly from within Visual Studio. Just click the &#8216;Manage Extensions&#8217; command in the Extensions menu. From there, go to the Updates tab and look for &#8220;Microsoft Project Migrations&#8221; and click the update button.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/06\/UpdateVSExtension.png\" alt=\"Updating the migration VS extension\" \/><\/p>\n<p>To update existing references to the System.Web adapters, just update to the preview 2 version of the NuGet package: <code>1.0.0-preview.2.22316.1<\/code>.<\/p>\n<h2>Sharing authentication<\/h2>\n<p>The <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/incremental-asp-net-to-asp-net-core-migration\/#incremental-migration\">previous blog post<\/a> explained how incremental migration and System.Web adapters work, in general. In preview 2, we&#8217;ve added the ability for apps that are migrating to ASP.NET Core to share authentication between the old and new projects. This works by deferring authentication decisions to the original ASP.NET app. When an app opts into shared authentication, an authentication handler is registered with the ASP.NET Core app which authenticates the user by making an HTTP request to the ASP.NET app including auth-relevant headers (<code>Authorization<\/code> and <code>Cookie<\/code> headers, by default, but this is configurable). A custom HTTP handler in the ASP.NET app will serve the request and return a serialized claims principal for the user who was authenticated based on the headers in the request.<\/p>\n<p>If the ASP.NET app isn&#8217;t able to authenticate a user (perhaps no one is signed in yet), no claims principal will be returned. Instead, the ASP.NET app will return the HTTP status code and auth-related response headers (<code>Location<\/code>, <code>Set-Cookie<\/code>, and <code>WWW-Authenticate<\/code>, by default) that it would have returned if a user had directly tried to access endpoints in the ASP.NET app requiring authorization. These values are stored in the ASP.NET Core app and, if authentication is challenged (because the user is accessing a protected endpoint, for example), they are used to return an HTTP response to the user that would be similar to the challenge response received directly from the ASP.NET app.<\/p>\n<p>Because the response from the ASP.NET app in case of a challenge may not exactly match what would make sense from the ASP.NET Core app (redirect locations would need fixed up to refer to the right host and original path referenced, for example), some processing happens to the response from the ASP.NET app before passing it along to the end user. The System.Web adapters already fixup known issues related to host and redirect path but if your specific authentication scenario requires any additional modification of responses from the ASP.NET app, you can implement the <code>Microsoft.AspNetCore.SystemWebAdapters.Authentication.IRemoteAppAuthenticationResultProcessor<\/code> interface and register the implementation in your ASP.NET Core app&#8217;s dependency injection container.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/06\/RemoteAuthFlowchart.png\" alt=\"Diagram of remote authentication workflow\" \/><\/p>\n<h3>Known limitations<\/h3>\n<p>Remote app authentication works with bearer token and cookie-based authentication schemes. It does not currently work with Windows authentication and there are known to be <a href=\"https:\/\/github.com\/dotnet\/systemweb-adapters\/issues\/72\">some issues<\/a> with WS-Federation authentication that are being investigated.<\/p>\n<p>Note, also, that logging out from the ASP.NET Core app in ASP.NET Identity scenarios does not work because logout in those scenarios is done with a POST request to the ASP.NET app which is currently disallowed due to anti-forgery token verification that does not work between the two apps. As a temporary workaround, apps can have logout actions redirect users to a page hosted by the ASP.NET app and have the user log out from there.<\/p>\n<h2>Walk-through<\/h2>\n<p>Let&#8217;s walk through an example of how to get started with sharing authentication in incremental ASP.NET migration scenarios.<\/p>\n<h3>Creating the initial app<\/h3>\n<p>To start, create a new ASP.NET MVC app and choose &#8216;Individual Accounts&#8217; for authentication. This will give us a starting point to migrate from that includes ASP.NET Identity authentication.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/06\/CreateNewMvcProject.png\" alt=\"Creating a new sample MVC project\" \/><\/p>\n<p>Launch the app to make sure it works and register a user.<\/p>\n<h3>Creating the migration project<\/h3>\n<p>Now, right click on the project in Visual Studio&#8217;s solution explorer and choose &#8216;Migrate Project&#8217; to use the Project Migrations extension to setup a new ASP.NET Core app that will run and deploy alongside this one and allow us to gradually migrate endpoints over. Be sure to choose an MVC template since the initial app is MVC.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/06\/CreateMigrationProject.png\" alt=\"Using the Project Migrations extension to create a migration project\" \/><\/p>\n<p>The new project will already have a reference to <code>Microsoft.AspNetCore.SystemWebAdapters<\/code>, but we will need to add a reference to that package to the original ASP.NET app so that it can configure the remote app endpoint on the ASP.NET side. Right-click the original app&#8217;s references folder, choose &#8216;Manage NuGet Packages&#8217; and add a reference to the <code>Microsoft.AspNetCore.SystemWebAdapters<\/code> package.<\/p>\n<h3>Configuring remote app authentication<\/h3>\n<p>At this point, we have our new ASP.NET Core app created and proxying requests it can&#8217;t serve to the original ASP.NET app. We now need to configure remote app authentication so that user identity is shared between the two apps.<\/p>\n<p>In the ASP.NET Core app&#8217;s <em>Program.cs<\/em> file add a call to <code>AddRemoteAppAuthentication<\/code> after the existing <code>AddSystemWebAdapters<\/code> call as shown here.<\/p>\n<pre><code class=\"language-CSharp\">builder.Services.AddSystemWebAdapters()\r\n    .AddRemoteAppAuthentication(true, options =&gt;\r\n     {\r\n         options.RemoteServiceOptions.RemoteAppUrl = \r\n            new(builder.Configuration[\"ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address\"]);\r\n         options.RemoteServiceOptions.ApiKey = \"test-key\";\r\n     });<\/code><\/pre>\n<p>This will register the remote app authentication handler.<\/p>\n<p>The first parameter passed to <code>AddRemoteAppAuthentication<\/code> is a boolean indicating whether remote app authentication should be the default authentication scheme for this app. If you pass true, then the remote app will be queried by default for the user&#8217;s identity for requests to any endpoint that doesn&#8217;t specify a different authentication scheme. This is the simplest way to enable remote app authentication globally, but has the downside of making an extra HTTP request for every endpoint whether you need to use the user&#8217;s identity or not. If you pass false for this parameter, then the remote app authentication handler will be registered but will only be used for endpoints that specifically request the remote app authentication scheme (by specifying the scheme in an authorize attribute, for example: <code>[Authorize(AuthenticationSchemes = RemoteAppAuthenticationDefaults.AuthenticationScheme)]<\/code>).<\/p>\n<p>The second parameter passed to <code>AddRemoteAppAuthentication<\/code> is a method for configuring <code>RemoteAppAuthenticationOptions<\/code>. The two properties that must be set on this options object are the <code>RemoteAppUrl<\/code> which is the base URL authentication requests should be sent to (we can retrieve this from the YARP configuration already set up by the incremental migration tooling, as shown in the sample above) and an <code>ApiKey<\/code> string which is used to authenticate the ASP.NET Core app as a caller that is allowed to authenticate users with the remote app. The <code>ApiKey<\/code> string can be any string that is securely shared between the ASP.NET and ASP.NET Core apps. In this walk-through, we&#8217;re using a hard-coded string but in real-world scenarios this should be securely loaded from the applications&#8217; environments or, even better, a secure store like Azure Key Vault.<\/p>\n<p>We also need to enable authentication middleware for the ASP.NET Core application, so add a call to <code>app.UseAuthentication()<\/code> after the existing call to <code>UseRouting<\/code> but before the call to <code>UseAuthorization<\/code>.<\/p>\n<p>Now that the ASP.NET Core app is setup to query the ASP.NET app for authentication, we need to configure the ASP.NET app to be able to respond to those requests. Navigate to the ASP.NET app&#8217;s <em>global.asax.cs<\/em> file and add the following call to the end of the <code>Application_Start<\/code> method:<\/p>\n<pre><code class=\"language-CSharp\">Application.AddSystemWebAdapters()\r\n    .AddRemoteAppAuthentication(options =&gt;\r\n        options.RemoteServiceOptions.ApiKey = \"test-key\");<\/code><\/pre>\n<p>You will also need to add an import statement <code>using Microsoft.AspNetCore.SystemWebAdapters;<\/code> to the top of global.asax.cs. This code should look familiar. Similar to the code that registered the authentication handler in the ASP.NET Core app, this call is configuring an HTTP handler in the ASP.NET app to handle requests for authentication. The only required option here is the <code>ApiKey<\/code> property which must, of course, match the API key used in the ASP.NET Core app.<\/p>\n<h3>Adding a test page<\/h3>\n<p>At this point, remote app authentication is configured so that the ASP.NET Core app will see the identity of users logged in to the ASP.NET app and if the ASP.NET Core app challenges authentication, it should redirect to the login page of the ASP.NET app. To test that out, though, we&#8217;ll need an ASP.NET Core endpoint that makes use of the user&#8217;s identity.<\/p>\n<p>First, replace the call to <code>app.MapControllers();<\/code> in the ASP.NET Core app&#8217;s <em>Program.cs<\/em> with a call to <code>MapDefaultControllerRoute<\/code> instead so that we can easily route to ASP.NET Core controllers:<\/p>\n<pre><code class=\"language-CSharp\">app.MapDefaultControllerRoute();<\/code><\/pre>\n<p>Next, add a <em>Controllers<\/em> folder to the ASP.NET Core app and create a simple controller called <code>UserInfoController<\/code> with a single Index action method:<\/p>\n<pre><code class=\"language-CSharp\">using Microsoft.AspNetCore.Mvc;\r\n\r\nnamespace MigrationDemoCore.Controllers;\r\n\r\npublic class UserInfoController : Controller\r\n{\r\n    [Authorize]\r\n    public IActionResult Index()\r\n    {\r\n        return View();\r\n    }\r\n}<\/code><\/pre>\n<p>And finally create a view for the controller&#8217;s Index endpoint that exercises <code>User.Identity<\/code>. Create a folder called <em>Views<\/em> in the project. Inside of that, create a folder called <em>UserInfo<\/em>. And finally create a view called <em>Index.cshtml<\/em> with contents similar to this:<\/p>\n<pre><code class=\"language-razor\">&lt;h1&gt;User Info&lt;\/h1&gt;\r\n&lt;h2&gt;@(User.Identity?.Name??\"Anonymous user\")&lt;\/h2&gt;\r\n&lt;h3&gt;Claims&lt;\/h3&gt;\r\n&lt;ul&gt;\r\n@foreach(var c in User.Claims)\r\n{\r\n    &lt;li&gt;@c.Type: @c.Value&lt;\/li&gt;\r\n}\r\n&lt;\/ul&gt;<\/code><\/pre>\n<p>With that, we can launch the sample application and see it all working end-to-end. Most of the endpoints of the app are still served by the original ASP.NET app. But if you navigate to the <code>\/UserInfo<\/code> route, that endpoint is served by the ASP.NET Core app. Because the endpoint is protected with an <code>[Authorize]<\/code> attribute, unauthenticated users will be redirected to the app&#8217;s login page when they try to access the endpoint. For authenticated users, despite <code>\/UserInfo<\/code> being served by the ASP.NET Core app, you can see that the user&#8217;s identity (provided by the ASP.NET app) is available!<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/06\/SharedAuth.png\" alt=\"User identity provided by ASP.NET, used in ASP.NET Core\" \/><\/p>\n<p>Note that even if you remove the <code>[Authorize]<\/code> attribute from <code>\/UserInfo<\/code>, the endpoint will still have access to the user&#8217;s identity if a user is logged in. If there is no authenticated user, <code>User.Identity<\/code> will be null.<\/p>\n<h2>Summary<\/h2>\n<p>With this second preview release of incremental ASP.NET migration tooling, a number of new features have been added &#8211; most notably the ability to share authentication between the ASP.NET and ASP.NET Core apps in incremental upgrade scenarios. Using this feature, developers should be able to gradually move endpoints from an existing ASP.NET app into a new ASP.NET Core app even when those endpoints require authenticating the user.<\/p>\n<p>The current shared authentication solution is a general-purpose one that should work for most types of authentication, but we are continuing to investigate additional remote authentication options that may be more effective or efficient for specific types of authentication. We are also exploring shared authentication that works by centralizing all authentication decisions with the ASP.NET <em>Core<\/em> app (rather than the ASP.NET app). Combining that functionality with what is already available in Preview 2 would allow developers to start moving endpoints to ASP.NET Core immediately but then shift authentication responsibilities to the ASP.NET Core app at some point during the migration process.<\/p>\n<p>In addition to the shared authentication work, we are also continuing to expand the System.Web adapters surface area to include more of the most-used APIs from System.Web.<\/p>\n<p>If you have any feedback on the tooling &#8211; whether that&#8217;s bug reports or feature suggestions &#8211; you can contribute to the project on <a href=\"https:\/\/github.com\/dotnet\/systemweb-adapters\">GitHub<\/a> by creating issues or pull requests.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introducing preview 2 of incremental ASP.NET migration tooling, including support for shared authentication.<\/p>\n","protected":false},"author":7413,"featured_media":40646,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,197,7509],"tags":[7697,3267,7648],"class_list":["post-40645","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-aspnet","category-aspnetcore","tag-migrate-to-aspnetcore","tag-migration","tag-upgrade"],"acf":[],"blog_post_summary":"<p>Introducing preview 2 of incremental ASP.NET migration tooling, including support for shared authentication.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/40645","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/7413"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=40645"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/40645\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/40646"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=40645"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=40645"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=40645"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}