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.

C#
~/Features/Shared/Services/IHttpRequestService.cs

/// <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);
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()?.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.

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 to for security:

  1. Set the query that retrieves the content to filter out secured items.
  2. 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.

C#
IArticlePageService.cs

...
/// <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);
...
C#
ArticlePageService.cs

...
/// <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.

C#
ArticlePageController.cs

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.