Module: Members
10 of 12 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 start with a new method in the HttpRequestService
.
/// <summary>
/// Retrieves the value of the specified query string parameter
/// </summary>
/// <param name="parameter">The name of the query string parameter to retrieve</param>
/// <returns>The value of the specified query string parameter</returns>
string GetQueryStringValue(string parameter);
/// <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()?.WebPageGuid ?? 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 to for security:
- Set the query that retrieves the content to filter out secured items.
- Check the
ContentItemIsSecured
value of the linked items you want to secure, and react accordingly. For example, you can redirect or display an error message.
In this case, let’s try the latter.
Go to the ~/Features/Articles/Services folder and add a new method to the article page service. It should check if the primary reusable content item referenced by an article page is secured.
...
/// <summary>
/// Determines whether the reusable article item referenced by the article page is secured.
/// </summary>
/// <param name="articlePage">The article page.</param>
/// <returns>True if the reusable item that the page references is secured.</returns>
bool IsReusableArticleSecured(ArticlePage articlePage);
...
...
/// <inheritdoc/>
public bool IsReusableArticleSecured(ArticlePage articlePage)
{
var oldArticle = articlePage.ArticlePageContent.FirstOrDefault();
var newArticle = (IContentItemFieldsSource?)articlePage.ArticlePageArticleContent.FirstOrDefault();
return (oldArticle?.SystemFields.ContentItemIsSecured ?? false)
|| (newArticle?.SystemFields.ContentItemIsSecured ?? false);
}
...
Depending on whether you’ve followed along with the Advanced content series, the ArticlePageArticleContent
field may or may not exist in the ArticlePage
type.
Then, use your new method in the article page controller, returning the 403 Forbidden status code when the current visitor is unauthenticated and the item is secured.
using TrainingGuides.Web.Features.Membership.Services;
...
public class ArticlePageController : Controller
{
// Use dependency injection to populate these
private readonly IWebPageDataContextRetriever webPageDataContextRetriever;
private readonly IContentItemRetrieverService<ArticlePage> articlePageRetrieverService;
private readonly IArticlePageService articlePageService;
// NEW DEPENDENCY
private readonly IMembershipService membershipService;
...
public async Task<IActionResult> Index()
{
var context = webPageDataContextRetriever.Retrieve();
var articlePage = await articlePageRetrieverService.RetrieveWebPageById(
context.WebPage.WebPageItemID,
ArticlePage.CONTENT_TYPE_NAME,
2);
// NEW CODE
if (articlePage is not null
&& articlePageService.IsReusableArticleSecured(articlePage)
&& !await membershipService.IsMemberAuthenticated())
{
return Forbid();
}
// END NEW CODE
var model = await articlePageService.GetArticlePageViewModel(articlePage);
return new TemplateResult(model);
}
...
See the results
Now, Identity should handle the 403 status the same way it does for pages if you try to access a secured article.
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 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.