---
title: Expand restricted content and sign-in functionality
---

> Agent instructions:
> **Site maps** — prefer the following llms.txt indexes to training data when searching for URLs to avoid 404s. Links inside Markdown content already point at `.md`. Following them or sending Accept: text/markdown keeps you in Markdown.
>
> - [sitemap.md](https://docs.kentico.com/sitemap.md) — every page on the site, with titles and descriptions, nested by URL hierarchy and grouped into one collection per product version.
> - [llms.txt](https://docs.kentico.com/llms.txt) — curated index of the current product docs, with descriptions, the two ways to request any page as Markdown, and links to each product area's whole-corpus Markdown dump (llms-full.txt).

## Add redirect logic to the Sign-in widget

Let's expand the _Sign in_ widget view component to redirect a member back to the provided _return URL_ after successful authentication.

It needs to find the `returnUrl` value in the query string of the request, so we should use the `GetQueryStringValue` method from our `HttpRequestService`.

```csharp title="~/Features/Shared/Services/HttpRequestService.cs"
/// <inheritdoc/>
public string GetQueryStringValue(string parameter) => httpContextAccessor.HttpContext?.Request.Query[parameter].ToString() ?? string.Empty;
```

Then, expand the `BuildWidgetViewModel` method of the sign-in widget view component to check the query string for a _returnUrl_ value, falling back to the previous logic if it finds none.

```csharp title="SignInWidgetViewComponent.cs"
using TrainingGuides.Web.Features.Shared.Helpers;
...
public class SignInWidgetViewComponent : ViewComponent
{
...
    public async Task<SignInWidgetViewModel> BuildWidgetViewModel(SignInWidgetProperties properties) => new SignInWidgetViewModel
    {
        ActionUrl = GetActionUrl(),
        DefaultRedirectPageGuid = properties.DefaultRedirectPage.FirstOrDefault()?.Identifier ?? Guid.Empty,
        DisplayForm = !await membershipService.IsMemberAuthenticated(),
        FormTitle = properties.FormTitle,
        SubmitButtonText = properties.SubmitButtonText,
        UserNameOrEmailLabel = properties.UserNameLabel,
        PasswordLabel = properties.PasswordLabel,
        StaySignedInLabel = properties.StaySignedInLabel
    };

    private string GetActionUrl()
    {
        // New code: retrieve and use the return URL if it exists
        string? returnUrl = GetReturnUrlFromQueryString();
        QueryString? queryString = string.IsNullOrWhiteSpace(returnUrl) ? null : QueryString.Create(ApplicationConstants.RETURN_URL_PARAMETER, returnUrl);

        return httpRequestService.GetAbsoluteUrlForPath(ApplicationConstants.AUTHENTICATE_ACTION_PATH, true, queryString);
    }

    // New code: use the new HttpRequestService method to retrieve the query string
    private string? GetReturnUrlFromQueryString()
    {
        string returnUrl = httpRequestService.GetQueryStringValue(ApplicationConstants.RETURN_URL_PARAMETER);

        // If there is no return URL or it is not a relative URL, return null
        if (string.IsNullOrWhiteSpace(returnUrl) || !returnUrl.StartsWith("/"))
        {
            return null;
        }

        return returnUrl;
    }
}
```

> **Info:** See [this section of the language selector guide](https://docs.kentico.com/guides/development/multilingual/implement-language-selector-for-your-website.md#handle-urls-and-routing) or [the Training guides repository](https://github.com/Kentico/xperience-by-kentico-training-guides/blob/finished/src/TrainingGuides.Web/Features/Shared/Services/HttpRequestService.cs) for details about how to implement the `GetBaseUrlWithLanguage` method.

> **Tip:** If you ensure the return URL only works with relative paths, you can prevent attackers from exploiting the query string to redirect members to phishing sites after successful authentication on your site.

## See the redirect in action

Now the sign in form should correctly redirect you to the _return URL_ after you authenticate.

🎬 [Video](https://docs.kentico.com/docsassets/modules/expand-restricted-content-and-sign-in-functionality/RedirectToLoginReturn.mp4)

However, you may notice that even if you followed along [earlier](https://docs.kentico.com/modules/members/set-up-restricted-pages.md#restrict-a-reusable-item-in-the-content-hub) and set a reusable item to require authentication, it is still visible to signed out users on the page.

![Screenshot of the 'About frogs' article displaying on the site even though the underlying reusable item is secured](https://docs.kentico.com/docsassets/modules/expand-restricted-content-and-sign-in-functionality/AboutFrogsUnsecured.png "Screenshot of the 'About frogs' article displaying on the site even though the underlying reusable item is secured")

This happens because the web page that references this reusable article is not secured. The _content tree-based router_ only knows to check security of the pages it is retrieving, and not any reusable content they might reference.

## Handle secured reusable content

Ideally, content editors should mark all pages that reference secured items to require authentication, but we shouldn't assume that they will never make mistakes.

When pages display reusable content, you need one of the following approaches for security:

1. Set the query that retrieves the content to [filter out secured items](https://docs.kentico.com/documentation/developers-and-admins/development/content-retrieval/retrieve-page-content.md#page-security-configuration).
2. Programmatically check whether the current user can access each linked item, and react accordingly. For example, you can redirect or display an error message.

In this case, let's use the latter approach. We'll add a new method, `CanCurrentUserAccessContentItem`, to the membership service. It relies on the [`HasAccess` extension method](https://docs.kentico.com/documentation/developers-and-admins/development/content-retrieval/retrieve-content-items.md#check-content-access-with-hasaccess), which is available out of the box on each content item and returns `true` when the current user has access, including role-based checks.

We'll then centralize access checks in the article page service and return _403 Forbidden_ when the current visitor cannot access the page or its referenced items.

Add the following method to the membership service so access checks are reusable wherever you need to evaluate secured content.

```csharp title="IMembershipService.cs"
/// <summary>
/// Checks whether the current user can access the provided content item.
/// </summary>
/// <param name="contentItem">The content item to evaluate.</param>
/// <returns>True when access is allowed for the current user; otherwise false.</returns>
bool CanCurrentUserAccessContentItem(IContentItemFieldsSource? contentItem);
```

```csharp title="MembershipService.cs"
/// <inheritdoc />
public bool CanCurrentUserAccessContentItem(IContentItemFieldsSource? contentItem) =>
    contentItem?.HasAccess(contextAccessor.HttpContext?.User) ?? false;
```

Go to the _\~/Features/Articles/Services_ folder and add a new method to the article page service that checks whether the current user can access the article page and at least one referenced article item.

```csharp title="IArticlePageService.cs"
...
/// <summary>
/// Determines whether the current user can access the article page and its referenced article content.
/// </summary>
/// <param name="articlePage">The article page.</param>
/// <returns>True if the current user can access the page and at least one referenced article item.</returns>
bool CanCurrentUserAccessArticlePage(ArticlePage articlePage);
...
```

```csharp title="ArticlePageService.cs"
...
/// <inheritdoc/>
public bool CanCurrentUserAccessArticlePage(ArticlePage articlePage)
{
    bool pageAccessible = membershipService.CanCurrentUserAccessContentItem(articlePage);

    if (!pageAccessible)
        return false;

    bool oldArticleAccessible = articlePage.ArticlePageContent
        .Any(article => membershipService.CanCurrentUserAccessContentItem(article));

    bool newArticleAccessible = articlePage.ArticlePageArticleContent
        .OfType<IContentItemFieldsSource>()
        .Any(article => membershipService.CanCurrentUserAccessContentItem(article));

    bool articleAccessible = oldArticleAccessible || newArticleAccessible;

    return pageAccessible && articleAccessible;
}
...
```

> **Note:** Depending on whether you've followed along with the [Advanced content](https://docs.kentico.com/guides/development/advanced-content.md) series, your page may use legacy (`ArticlePageContent`) or newer (`ArticlePageArticleContent`) fields. The access check should account for both.

Then, use your new method in the article page controller, returning the _403 Forbidden_ status code when the current visitor lacks access.

```csharp title="ArticlePageController.cs" highlight="23-27"
using Kentico.Content.Web.Mvc.Routing;
using Kentico.PageBuilder.Web.Mvc.PageTemplates;
using Microsoft.AspNetCore.Mvc;
using TrainingGuides;
using TrainingGuides.Web.Features.Articles.Services;
using TrainingGuides.Web.Features.Shared.Services;

[assembly: RegisterWebPageRoute(
    contentTypeName: ArticlePage.CONTENT_TYPE_NAME,
    controllerType: typeof(TrainingGuides.Web.Features.Articles.ArticlePageController))]

namespace TrainingGuides.Web.Features.Articles;

public class ArticlePageController(
    IContentItemRetrieverService contentItemRetrieverService,
    IArticlePageService articlePageService) : Controller
{

    public async Task<IActionResult> Index()
    {
        var articlePage = await contentItemRetrieverService.RetrieveCurrentPage<ArticlePage>(2);

        if (articlePage is not null
            && !articlePageService.CanCurrentUserAccessArticlePage(articlePage))
        {
            return Forbid();
        }

        var model = articlePageService.GetArticlePageViewModel(articlePage);
        return new TemplateResult(model);
    }
}
```

## Handle signed-in visitors without access in listing UI

With membership and roles in place, each item can end up in one of three states:

1. Items the current visitor can access.
2. Items that require sign-in.
3. Items the visitor still cannot access even when signed in.

This is especially visible in listings, such as the _ArticleList_ widget.

To account for all three states, we need to distinguish an unauthenticated visitor from an authenticated member who still lacks the required member role.

To handle these states consistently, we'll extend our `ArticlePageViewModel` with two boolean properties that carry this state: `Restricted` (the item can't be viewed in the current context) and `RequiresSignIn` (the current visitor is not authenticated).

Using these two fields, you can build the final per-item presentation in listings, for example by setting `CTAText` to either a default article CTA or a sign-in prompt and rendering matching locked-content messaging.

```csharp title="ArticlePageViewModel.cs" highlight="15-16"
using Microsoft.AspNetCore.Html;
using TrainingGuides.Web.Features.Shared.Models;

namespace TrainingGuides.Web.Features.Articles;

public class ArticlePageViewModel
{
    public string Title { get; set; } = string.Empty;
    public HtmlString SummaryHtml { get; set; } = HtmlString.Empty;
    public HtmlString TextHtml { get; set; } = HtmlString.Empty;
    public AssetViewModel? TeaserImage { get; set; } = null;
    public DateTime CreatedOn { get; set; }
    public List<ArticlePageViewModel> RelatedNews { get; set; } = [];
    public string Url { get; set; } = string.Empty;
    public bool Restricted { get; set; } = false;
    public bool RequiresSignIn { get; set; } = false;
    public string CTAText { get; set; } = string.Empty;
}
```

Then populate these fields in `GetArticlePageViewModel`. This method maps both reusable-schema and legacy article data, then sets security flags using `CanCurrentUserAccessArticlePage`.

```csharp title="ArticlePageService.cs" highlight="24-25,40-41"
...
public ArticlePageViewModel GetArticlePageViewModel(ArticlePage? articlePage)
{
    if (articlePage == null)
    {
        return new ArticlePageViewModel();
    }

    string articleUrl = GetArticlePageRelativeUrl(articlePage);
    var articleSchema = articlePage.ArticlePageArticleContent.FirstOrDefault();

    if (articleSchema != null)
    {
        var articleSchemaTeaserImage = articleSchema.ArticleSchemaTeaser.FirstOrDefault();

        return new ArticlePageViewModel
        {
            Title = articleSchema.ArticleSchemaTitle,
            SummaryHtml = new HtmlString(articleSchema?.ArticleSchemaSummary),
            TextHtml = new HtmlString(articleSchema?.ArticleSchemaText),
            CreatedOn = articlePage.ArticlePagePublishDate,
            TeaserImage = AssetViewModel.GetViewModel(articleSchemaTeaserImage),
            Url = articleUrl,
            Restricted = !CanCurrentUserAccessArticlePage(articlePage),
            RequiresSignIn = false
        };
    }

    var article = articlePage.ArticlePageContent.FirstOrDefault();
    var articleTeaserImage = article?.ArticleTeaser.FirstOrDefault();

    return new ArticlePageViewModel
    {
        Title = article?.ArticleTitle ?? string.Empty,
        SummaryHtml = new HtmlString(article?.ArticleSummary),
        TextHtml = new HtmlString(article?.ArticleText),
        CreatedOn = articlePage.ArticlePagePublishDate,
        TeaserImage = AssetViewModel.GetViewModel(articleTeaserImage),
        Url = articleUrl,
        Restricted = !CanCurrentUserAccessArticlePage(articlePage),
        RequiresSignIn = false
    };
}
...
```

In listing scenarios, such as the _ArticleList_ widget, use `GetArticlePageViewModelWithSecurity` to produce the final per-item output for the current visitor. That method builds on `GetArticlePageViewModel` and adjusts item messaging and links based on authentication and access state.

```csharp title="ArticlePageService.cs" highlight="48-49,67-68"
...
public async Task<ArticlePageViewModel> GetArticlePageViewModelWithSecurity(ArticlePage? articlePage)
{
    var originalViewModel = GetArticlePageViewModel(articlePage);

    if (articlePage is null)
    {
        return originalViewModel;
    }

    bool userHasAccess = CanCurrentUserAccessArticlePage(articlePage);

    if (userHasAccess)
    {
        return originalViewModel;
    }

    bool isAuthenticated = await membershipService.IsMemberAuthenticated();

    // If not authenticated, show sign-in prompt
    if (!isAuthenticated)
    {
        string language = preferredLanguageRetriever.Get();
        string signInUrl = await membershipService.GetSignInUrl(language);
        string relativePath = originalViewModel.Url.TrimStart('~');

        string baseUrl = httpRequestService.GetBaseUrl();

        var signInUri = new UriBuilder(baseUrl)
        {
            Path = signInUrl.TrimStart('~'),
            Query = QueryString.Create(ApplicationConstants.RETURN_URL_PARAMETER, relativePath).ToString()
        };

        string messageWithLinkString = $"<a href=\"{signInUri}\">{stringLocalizer["Sign in"]}</a> {stringLocalizer["to view this content."]}";

        var message = new HtmlString(stringLocalizer["Sign in to view this content."]);
        var messageWithLink = new HtmlString(messageWithLinkString);

        return new ArticlePageViewModel
        {
            Title = $"{stringLocalizer["(🔒 Locked)"]} {originalViewModel.Title}",
            SummaryHtml = message,
            TextHtml = messageWithLink,
            CreatedOn = articlePage.ArticlePagePublishDate,
            TeaserImage = originalViewModel.TeaserImage,
            Url = signInUri.ToString(),
            Restricted = true,
            RequiresSignIn = true
        };
    }
    // If authenticated but no access, show access denied message
    else
    {
        string deniedReturnPath = originalViewModel.Url.TrimStart('~');
        string accessDeniedUrl = $"{ApplicationConstants.ACCESS_DENIED_ACTION_PATH}{QueryString.Create(ApplicationConstants.RETURN_URL_PARAMETER, deniedReturnPath)}";

        var accessDeniedMessage = new HtmlString(stringLocalizer["You do not have permission to access this content. Upgrade to our higher tier."]);
        return new ArticlePageViewModel
        {
            Title = $"{stringLocalizer["(🔒 Locked)"]} {originalViewModel.Title}",
            SummaryHtml = accessDeniedMessage,
            TextHtml = accessDeniedMessage,
            CreatedOn = articlePage.ArticlePagePublishDate,
            TeaserImage = originalViewModel.TeaserImage,
            Url = accessDeniedUrl,
            Restricted = true,
            RequiresSignIn = false
        };
    }
}
...
```

## See the results

Now, if you try to access a secured article, Identity should handle the 403 status through the same redirect handler it uses for secured page requests.

🎬 [Video](https://docs.kentico.com/docsassets/modules/expand-restricted-content-and-sign-in-functionality/RedirectToLoginForReusable.mp4)

If you sign in as a user with a role different from _Basic_ (for example, _Enthusiast_), you should still see access denied even after signing in.

![Screenshot of a signed-in user seeing access denied when opening a secured article](https://docs.kentico.com/docsassets/modules/expand-restricted-content-and-sign-in-functionality/InsufficientPermissions.jpg "Screenshot of a signed-in user seeing access denied when opening a secured article")

> **Info:** As a reminder, this guide focuses on access-control principles. The _ArticleList_ widget in the Training guides finished branch demonstrates the full UI behavior and the three secured-item display modes:
>
> - `IncludeEverything` (Include everything)
> - `PromptForLogin` (Prompt for login)
> - `HideSecuredItems` (Hide secured items)
>
> Use that implementation as a reference when applying these concepts to your project.

## Convert member roles from the XperienceCommunity.MemberRoles package (optional)

> **Info:** _XperienceCommunity.MemberRoles_ is a community-maintained package created outside Kentico, and it is not developed or supported by Kentico. This section shows one possible way to map common concepts to built-in member roles, which you should adapt to your own solution.

Before member roles existed out of the box in Xperience by Kentico, Kentico MVP [\*Trevor Fayas\*](https://www.linkedin.com/in/trevor-fayas-a0628a32/) built a [community package](https://github.com/KenticoDevTrev/MembershipRoles_Temp) that introduced similar functionality.

If you used the XperienceCommunity.MemberRoles package, this section provides information about how to migrate to the newer built-in feature.

### Compare the features

Let's examine the differences in the architecture between Trevor's community package and the native Xperience functionality:

| XperienceCommunity.MemberRoles package                                                                                                                                                                                | Built-in Xperience member roles                                                                                                                                                                                                                                         |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The **Member Permission Configuration** schema (`IXperienceCommunityMemberPermissionConfiguration`) determines whether member role filtering can apply to items of a content type.                                    | Security and role access can apply to any content item without the need for a dedicated schema.                                                                                                                                                                         |
| A taxonomy tag (`TagInfo`) object represents each role.                                                                                                                                                               | A `MemberRoleInfo` object represents each role.                                                                                                                                                                                                                         |
| The _Member Permission Configuration_ schema allows you to apply language-specific configurations on the **Content** tab. These configurations are subject to the publishing workflow.                                | Security configurations that differ by language for the same item are not possible out of the box.                                                                                                                                                                      |
| `ContentFolderMemberPermissionSettingInfo` stores authentication requirements and permission inheritance for content folders.                                                                                         | Content folders do not have security configuration that applies to all of their items. You must apply security _directly to reusable items_.                                                                                                                            |
| `ContentFolderRoleTagInfo` represents the relationship between a role (`TagInfo`) and a content folder to which its members have access.                                                                              |                                                                                                                                                                                                                                                                         |
| `WebPageItemMemberPermissionSettingInfo` holds configuration for web pages' authentication requirements and permission inheritance.                                                                                   | Content items directly store configuration for whether they require authentication.<br>Web page role permissions automatically apply to child web pages. Children can break inheritance, but the parent permissions overwrite them the next time you update the parent. |
| There is no analog to `WebPageItemMemberPermissionSettingInfo` for reusable content items. Only the schema fields, which are subject to language versions and publishing workflows, apply directly to reusable items. |                                                                                                                                                                                                                                                                         |
| `WebPageItemRoleTagInfo`represents the relationship between member roles and the web pages their members can access.                                                                                                  | `ContentItemMemberRoleInfo` represents the relationship between member roles and the content items (both web page items and reusable content) their members can access.                                                                                                 |
| There is no analog to `WebPageItemRoleTagInfo` exist for reusable content items. Only the schema fields, which are subject to language versions and publishing workflows, apply directly to reusable items.           |                                                                                                                                                                                                                                                                         |
| `MemberRoleTagInfo` designates members as part of a role.                                                                                                                                                             | `MemberRoleMemberInfo` designates members as part of a role.                                                                                                                                                                                                            |

### Create a script

In order to convert roles and permissions from the community module to the built-in functionality, create a C# script to run one-time code. You can see a breakdown of options in our [guide about remodeling existing content](https://docs.kentico.com/guides/development/advanced-content/convert-content-to-reusable-schemas.md#execute-the-conversion) into new content types.

1. [Create member roles](https://docs.kentico.com/documentation/developers-and-admins/development/registration-and-authentication/member-roles.md#manage-member-roles) corresponding to the `TagInfo` taxonomy tags that formerly represented them. Try using the same code name, or some deterministic modification, so you can easily tell which objects relate.
2. Compile roles and security status for each item that implements the `Member Permission Configuration` schema from the XperienceCommunity.MemberRoles package. Make sure to consider the following for conversion:
   - Security, roles, and inheritance applied to **language variants** on the **Content** tab, through the reusable field schema.
   - Security, roles, and inheritance applied to **pages**.
     - `WebPageItemRoleTagInfo` bindings that point to the page.
     - Security and permission inheritance status applied via `WebPageItemMemberPermissionSettingInfo`.
     > **Warning:** Order your conversion by `WebPageItemTreePath`, so parents are processed first. **Allowed role changes to parent pages will overwrite children.**
   - Security, roles, and inheritance applied to **content folders**.
     - `ContentFolderRoleTagInfo` relating to the content folder containing the item
     - Security and permission inheritance status applied via `ContentFolderMemberPermissionSettingInfo`
     > **Note:** Content folder permissions are not supported in the Xperience by Kentico implementation, so you need to apply them **directly to the content items** that inhabit those folders.
   - Interactions between multiple levels of configuration.
     > **Tip:** If there are differences between languages, or between permissions applied on the **Content** tab and those applied at a higher level, implement logic to reconcile these settings during migration, or flag them for manual adjustment.
3. Apply the assigned roles and security status to the items using `ContentItemMemberRoleManager` (Use `ContentAccessSettings.SecuredWithRoles(roleIds)` for secured items restricted to specific member roles).
4. Use `UserManager<ApplicationUser>` to [assign roles to your members](https://docs.kentico.com/documentation/developers-and-admins/development/registration-and-authentication/member-roles.md#assign-member-roles-to-members), recreating the relationships from the community module's `MemberRoleTagInfo` objects.

After you've recreated permissions for the built-in feature, you can adjust code across your project to use the new feature. You can use `RetrievePagesParameters.IncludeSecuredItems` to determine whether to include secured items in an `IContentRetriever` query, and the `HasAccess` extension method on `IContentItemFieldsSource` to check whether an item is accessible to the current visitor.

## What's next?

The [finished branch](https://github.com/Kentico/xperience-by-kentico-training-guides/tree/finished) of the _Training guides_ repository contains a demonstration of two approaches to handle secured content in listings:

- Display a message explaining that the item is locked and linking to the sign-in page.
- Hide secured items completely from the listing by filtering them out of the query.

If you're looking to expand upon the lesson we've just covered, try to implement this functionality in your own project.

Specifically, check out:

- The [\*ArticleList\* widget](https://github.com/Kentico/xperience-by-kentico-training-guides/blob/finished/src/TrainingGuides.Web/Features/Articles/Widgets/ArticleList).
- The [Article page service](https://github.com/Kentico/xperience-by-kentico-training-guides/blob/finished/src/TrainingGuides.Web/Features/Articles/Services/ArticlePageService.cs).
- The [Content item retriever service](https://github.com/Kentico/xperience-by-kentico-training-guides/blob/finished/src/TrainingGuides.Web/Features/Shared/Services/ContentItemRetrieverService.cs).

The branch also contains other useful features for your reference, like [password reset functionality](https://github.com/Kentico/xperience-by-kentico-training-guides/blob/finished/src/TrainingGuides.Web/Features/Membership/Widgets/ResetPassword) and a [dynamic widget](https://github.com/Kentico/xperience-by-kentico-training-guides/blob/finished/src/TrainingGuides.Web/Features/Membership/Widgets/LinkOrSignOut) that shows a sign-out button or a link depending on whether or not the current visitor is authenticated.
