Module: Members
10 of 11 Pages
Expand restricted content and sign-in functionality
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.
/// <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.
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 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.
/// <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);
/// <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.
...
/// <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);
...
...
/// <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.
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.
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.
...
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.
...
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.
What’s next?
The finished branch 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.
- The Article page service.
- The Content item retriever service.
The branch also contains other useful features for your reference, like password reset functionality and a dynamic widget that shows a sign-out button or a link depending on whether or not the current visitor is authenticated.