26

I am using the new Identity UI package available since ASP.NET Core 2.1 was released. Using a newly generated MVC project, here are some page URLs that are available:

/Home/About
/Home/Contact
/Identity/Account/Login
/Identity/Account/Register

How can I configure routing to remove the /Identity/ part from the URLs?

Kirk Larkin
  • 60,745
  • 11
  • 150
  • 162
Phil K
  • 3,745
  • 4
  • 26
  • 48
  • 1
    Guys, ASP.NET Core 2.0 has new syntactic sugar @page "/Some/Other/Path" which enables you to remove either "/Identity" or "/Identity/Account". See https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-2.0 for reference: "Razor Pages makes coding page-focused scenarios easier and more productive". BlackTigerX should be an accepted answer. – Anton Lyhin May 11 '19 at 15:12
  • good answer @page "/some/path" – Moaz Salem Nov 14 '19 at 01:30

6 Answers6

17

It looks like this is not yet possible. Looking at the source code, it's clear that the Area name is hardcoded in IdentityDefaultUIConfigureOptions<TUser>:

private const string IdentityUIDefaultAreaName = "Identity";

This is used in a handful of places, including when configuring Razor Pages. e.g.:

options.Conventions.AuthorizeAreaFolder(IdentityUIDefaultAreaName, "/Account/Manage");

And also when configuring the Cookies authentication. e.g.:

options.LoginPath = $"/{IdentityUIDefaultAreaName}/Account/Login";

It's worth noting that IdentityDefaultUIConfigureOptions<TUser> itself is protected, so the ability to override the options does not appear to exist.

I've opened a Github issue to see if we can get feedback from those involved in the project itself.


2018-06-12 Update

Javier Calvarro Nelson from the ASP.NET Core Identity team provided some valuable feedback in the Github issue I raised, which can be summarised as follows:

The main reason for the Identity UI to be in an area is to minimize the impact on your app and to provide a clean separation between your app code and the Identity code.

Javier recommends one of the following options when wanting to customise the URLs:

  • Use the scaffolding element of the Default UI and make all necessary customisations yourself.
  • Use a redirection rule that points the old routes to the new routes.
  • Don't use the Default UI at all.

Although unsupported and not recommended, Javier also points out that it is possible to use a custom IPageApplicationModelConvention to override the URLs. However, in case you missed it, this is unsupported and not recommended.


2018-06-27 Update

The official documentation has now been updated to better explain said URL changes.

Kirk Larkin
  • 60,745
  • 11
  • 150
  • 162
  • AFAIK, the scaffolding is essentially just the views, not the controllers, so scaffolding Identity doesn't really open any new options than `.AddDefaultUi` as far as behavior/routing is concerned. The only option that still uses the IdentityUi is to add a mapping rule to work around the issue; see [UnknownQuestion's response here](https://github.com/aspnet/Identity/issues/1815). – Kevin Fichter Jun 27 '18 at 16:35
  • It has a negative impact on your SEO to have two domains with the same content. Instruct crawlers not to index _/identity/_ ```User-agent: * Disallow: /identity/``` and use the rewriting middleware with a perm redirect to the new url. – Hamed Sep 07 '20 at 09:23
7

The easiest thing to do, is to drag the Pages folder out of the Areas/Identity to the main project Remember that the @page directive (in the .cshtml) causes the views to be accessible directly for anything under "Pages" (the page is turned into an action) You could also rename the Account folder to some other name if you wanted to change the default /Account/Login etc pages

the @page directive can also be used to specify a custom path, such as: @page "/Login"

to have access to the login page directly by navigating to /Login

BlackTigerX
  • 5,686
  • 7
  • 35
  • 48
6

In your startup.cs you can change :

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

with :

    services.AddMvc().AddRazorPagesOptions(o => o.Conventions.AddAreaFolderRouteModelConvention("Identity", "/Account/", model =>
    {
        foreach (var selector in model.Selectors)
        {
            var attributeRouteModel = selector.AttributeRouteModel;
            attributeRouteModel.Order = -1;
            attributeRouteModel.Template = attributeRouteModel.Template.Remove(0, "Identity".Length);
        }
    })
).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

It will route:

/Identity/Account/Login to /Account/Login

/Identity/Account/Register to /Account/Register

etc...

To handle the ReturnUrl you can create a new Action:

    [Route("Identity/Account/Login")]
    public IActionResult LoginRedirect(string ReturnUrl)
    {
        return Redirect("/Account/Login?ReturnUrl=" + ReturnUrl);
    }
Yanga
  • 2,395
  • 1
  • 26
  • 27
1

URL Rewriting Middleware may be a solution:

var options = new RewriteOptions()
        .AddRewrite(@"^Account/(.*)", "Identity/Account/$1", skipRemainingRules: true);
        app.UseRewriter(options);
Corwin
  • 199
  • 1
  • 7
0

As far as the routing goes, its standard in web frameworks to have the authentication URLs fixed, Django does the same thing. Here's how to customize the view to your liking, so instead of removing /Identity/ from the routes, we'll tell Identity not to include their views and provide the route to ours.

Go to Startup.cs:

// USE METHOD WITH LESS DEFAULTS
//
// services.AddDefaultIdentity<IdentityUser>()
//    .AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentity<IdentityUser, IdentityRole>(options => options.Stores.MaxLengthForKeys = 128)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();
//
// ADD A ROUTE BELOW THE DEFAULT ROUTE
//
routes.MapRoute(
            name: "identity",
            template: "Identity/{controller=Account}/{action=Register}/{id?}");

Now we have everything setup but the view, so we need to make a route to it the way we normally do in mvc. Make an Account Controller. Change Index() to Register(). Make a folder in views named Account. Add a file Register.cshtml, here's the original html, customize to your needs:

<div class="container body-content">


<h2>Register</h2>

<div class="row">
    <div class="col-md-4">
        <form method="post" action="/Identity/Account/Register" novalidate="novalidate">
            <h4>Create a new account.</h4>
            <hr>
            <div class="text-danger validation-summary-valid" data-valmsg-summary="true"><ul><li style="display:none"></li>
</ul></div>
            <div class="form-group">
                <label for="Input_Email">Email</label>
                <input class="form-control" type="email" data-val="true" data-val-email="The Email field is not a valid e-mail address." data-val-required="The Email field is required." id="Input_Email" name="Input.Email" value="">
                <span class="text-danger field-validation-valid" data-valmsg-for="Input.Email" data-valmsg-replace="true"></span>
            </div>
            <div class="form-group">
                <label for="Input_Password">Password</label>
                <input class="form-control" type="password" data-val="true" data-val-length="The Password must be at least 6 and at max 100 characters long." data-val-length-max="100" data-val-length-min="6" data-val-required="The Password field is required." id="Input_Password" name="Input.Password">
                <span class="text-danger field-validation-valid" data-valmsg-for="Input.Password" data-valmsg-replace="true"></span>
            </div>
            <div class="form-group">
                <label for="Input_ConfirmPassword">Confirm password</label>
                <input class="form-control" type="password" data-val="true" data-val-equalto="The password and confirmation password do not match." data-val-equalto-other="*.Password" id="Input_ConfirmPassword" name="Input.ConfirmPassword">
                <span class="text-danger field-validation-valid" data-valmsg-for="Input.ConfirmPassword" data-valmsg-replace="true"></span>
            </div>
            <button type="submit" class="btn btn-default">Register</button>
        <input name="__RequestVerificationToken" type="hidden" value="CfDJ8IWbPHM_NTJDv_7HGewWzbbRveP09yQOznYdTWL2aN5X_4_eVbNE1w8D_qz7zegloVtdAhuVOJbJLQo0ja73FB3PgYycyGpn-DfX3fJqv4Cx8ns6Ygh6M7nMxV0eozO7hoDxUfPwrIJb2RcFtyzhPpMevZ4P0M8aVyBP55SP-5C4l23dCtDXXUOAY_YLwt67dw"></form>
    </div>
</div>


        <hr>
        <footer>
            <p>© 2018 - SqlServerApp</p>
        </footer>
    </div> 
Ryan Dines
  • 871
  • 8
  • 18
0

As of .net core 3.1 and 2021 (I know that there's .net 5, guilty as charged) and although it wouldn't generally remove the "identity" part of path, you can map generated razor pages one by one to desired routes:

in page header, there is by default just @page, so change it to your desired route @page "/login".

Please note, that if some mvc route matches route /login, it would use the corresponding controller action. If you son't want your mvc route to override razor pages routing, I would suggest to move endpoints.MapRazorPages(); in Configure method of Startup.cs above all (or at least some) mvc routes.