Module: Members
10 of 11 Pages
Expand restricted content and sign-in functionality
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.
C#~/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.
C#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;
}
}See this section of the language selector guide or the Training guides repository (opens in a new tab) for details about how to implement the GetBaseUrlWithLanguage method.
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.
However, you may notice that even if you followed along earlier and set a reusable item to require authentication, it is still visible to signed out users on the page.
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:
- Set the query that retrieves the content to filter out secured items.
- 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, 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.
C#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);C#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.
C#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);
...C#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;
}
...Depending on whether you've followed along with the Advanced content 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.
C#ArticlePageController.cs
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:
- Items the current visitor can access.
- Items that require sign-in.
- 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.
C#ArticlePageViewModel.cs
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.
C#ArticlePageService.cs
...
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.
C#ArticlePageService.cs
...
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.
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.
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)
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* (opens in a new tab) built a community package (opens in a new tab) 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 ( | Security and role access can apply to any content item without the need for a dedicated schema. |
A taxonomy tag ( | A |
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. |
| Content folders do not have security configuration that applies to all of their items. You must apply security directly to reusable items. |
| |
| Content items directly store configuration for whether they require authentication. 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 | |
|
|
There is no analog to | |
|
|
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 into new content types.
- Create member roles corresponding to the
TagInfotaxonomy tags that formerly represented them. Try using the same code name, or some deterministic modification, so you can easily tell which objects relate. - Compile roles and security status for each item that implements the
Member Permission Configurationschema 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.
WebPageItemRoleTagInfobindings that point to the page.- Security and permission inheritance status applied via
WebPageItemMemberPermissionSettingInfo.
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.
ContentFolderRoleTagInforelating to the content folder containing the item- Security and permission inheritance status applied via
ContentFolderMemberPermissionSettingInfo
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.
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.
- Apply the assigned roles and security status to the items using
ContentItemMemberRoleManager(UseContentAccessSettings.SecuredWithRoles(roleIds)for secured items restricted to specific member roles). - Use
UserManager<ApplicationUser>to assign roles to your members, recreating the relationships from the community module'sMemberRoleTagInfoobjects.
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 (opens in a new tab) 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 (opens in a new tab).
- The Article page service (opens in a new tab).
- The Content item retriever service (opens in a new tab).
The branch also contains other useful features for your reference, like password reset functionality (opens in a new tab) and a dynamic widget (opens in a new tab) that shows a sign-out button or a link depending on whether or not the current visitor is authenticated.