I'm trying to implement custom authentication for Elmah.Mvc 2.0. I know there are two keys in my web.config (elmah.mvc.allowedRoles and elmah.mvc.allowedUsers) but it won't be enough for me.

I have a custom Forms Authentication method which adds some random salt in the cookie, so I don't have a clear username to put for elmah.mvc.allowedUsers value. Also, I have no roles implemented.

Is there any way to override ElmahController or some Elmah authentication classes/methods?

Thanks!

Elmah has a built in viewer exposed at ~/Elmah.axd and Elmah MVC has a built in viewer at exposed ~/Elmah, both with some preset configuration options.

As dove's answer outlines, if you need to extend beyond the built in configurations, you can wrap the Elmah view inside of your own controller action method and authorize access however you like.

One possible limitation is that rendering the view from Elmah via the Elmah.ErrorLogPageFactory() isn't super friendly to an MVC application with a master layout page.
<sup> *To be fair, this constraint applies to any out-of-the-box implementation as well.</sup>

Roll Your Own

But since you're writing your own custom code to route and handle the Error Log View anyway, it's not that much addition work to write the view components as well, instead of just wrapping the provided view. This approach by far provides the most control and granularity over not only who may view, but also what they may view as well.

Here's a quick and dirty implementation of a custom Controller, Actions, and Views that exposes the data stored in Elmah.ErrorLog.GetDefault().GetErrors(0, 10, errors).

Controllers/ElmahController.cs:

<!-- language: lang-cs -->
public class ElmahController : Controller
{
    [Authorize(Roles = "Admin")]
    public ActionResult Index(int pageNum = 1, int pageSize = 7)
    {
        var vm = new ElmahViewModel()
        {
            PageNum = pageNum,
            PageSize = pageSize
        };

        ErrorLog log = Elmah.ErrorLog.GetDefault(System.Web.HttpContext.Current);
        vm.TotalSize = log.GetErrors(vm.PageIndex, vm.PageSize, vm.Errors);

        return View(vm);
    }

    [Authorize(Roles = "Admin")]
    public ActionResult Details(string id)
    {
        ErrorLog log = Elmah.ErrorLog.GetDefault(System.Web.HttpContext.Current);
        ErrorLogEntry errDetail = log.GetError(id);

        return View(errDetail);
    }
}

public class ElmahViewModel
{
    public List<ErrorLogEntry> Errors { get; set; } = new List<ErrorLogEntry>();
    public int TotalSize { get; set; }
    public int PageSize { get; set; }
    public int PageNum { get; set; }
    public int PageIndex => Math.Max(0, PageNum - 1);
    public int TotalPages => (int)Math.Ceiling((double)TotalSize / PageSize);
}

This adds two actions. One to display a list of errors with some optional pagination inputs, and another that'll take in the error id and return just that error. Then we can add the following two views as well:

Views/Elmah/List.cshtml:

<!-- language: lang-html -->
@model CSHN.Controllers.ElmahViewModel

@{
    ViewBag.Title = "ELMAH (Error Logging Modules and Handlers)";
}

<table class="table table-condensed table-bordered table-striped table-accent">
    <thead>
        <tr>
            <th>
                Host Name
            </th>
            <th class="text-center" style="width: 85px">
                Status Code
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Errors.First().Error.Type)
            </th>
            <th style="min-width: 250px;">
                @Html.DisplayNameFor(model => model.Errors.First().Error.Message)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Errors.First().Error.User)
            </th>
            <th class="text-center" style="width: 160px">
                @Html.DisplayNameFor(model => model.Errors.First().Error.Time)
            </th>
            <th class="filter-false text-center" data-sorter="false" style="width: 75px">
                Actions
            </th>
        </tr>
    </thead>
    <tbody>
        @if (Model.Errors.Any())
        {
            foreach (var item in Model.Errors)
            {
                <tr>
                    <td>
                        @Html.DisplayFor(modelItem => item.Error.HostName)
                    </td>
                    <td class="text-center" style="width: 85px">
                        @Html.DisplayFor(modelItem => item.Error.StatusCode)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Error.Type)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Error.Message)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Error.User)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Error.Time)
                    </td>
                    <td class="disable-user-select hidden-print text-center" style="width: 75px">
                        <a href="@Url.Action("Details", "Elmah", new { id = item.Id})"
                           class="btn btn-xs btn-primary btn-muted">
                            <i class='fa fa-eye fa-fw'></i> Open
                        </a>
                    </td>
                </tr>

            }
        }
        else
        {
            <tr class="warning">
                <td colspan="7">There haven't been any errors since the last AppPool Restart.</td>
            </tr>
        }

    </tbody>
    @* We need a paginator if we have more records than currently returned *@
    @if (Model.TotalSize > Model.PageSize)
    {
        <tfoot>
            <tr>
                <th colspan="7" class="form-inline form-inline-xs">
                    <a href="@Url.Action("Index", new {pageNum = Model.PageNum - 1, pageSize = Model.PageSize})"
                       class="btn btn-default btn-sm prev @(Model.PageNum == 1?"disabled":"")">
                        <span class="fa fa-backward fa-fw"></span>
                    </a>
                    <span class="pagedisplay">
                        Page @Model.PageNum of @Model.TotalPages
                    </span>
                    <a href="@Url.Action("Index", new {pageNum = Model.PageNum + 1, pageSize = Model.PageSize})"
                       class="btn btn-default btn-sm next @(Model.PageNum == Model.TotalPages?"disabled":"")">
                        <span class="fa fa-forward fa-fw"></span>
                    </a>
                </th>
            </tr>
        </tfoot>
    }

</table>

<style>
    .table-accent thead tr {
        background: #0b6cce;
        color: white;
    }
    .pagedisplay {
        margin: 0 10px;
    }
</style>

Views/Elmah/Details.cshtml:

<!-- language: lang-html -->
@model Elmah.ErrorLogEntry

@{
    ViewBag.Title = $"Error Details on {Model.Error.Time}";
}

<a href="@Url.Action("Index", "Elmah")"
   class="btn btn-sm btn-default ">
    <i class='fa fa-th-list fa-fw'></i> Back to All Errors
</a>

<div class="form-horizontal">

    <h4 class="table-header"> General </h4>
    <table class="table table-condensed table-bordered table-striped table-fixed table-accent">
        <thead>
            <tr>
                <th>Name</th>
                <th>Value</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>Message</td>
                <td>@Model.Error.Message</td>
            </tr>
            <tr>
                <td>Type</td>
                <td>@Model.Error.Type</td>
            </tr>
            <tr>
                <td>Time</td>
                <td>@Model.Error.Time</td>
            </tr>
            <tr>
                <td>Detail</td>
                <td><pre class="code-block">@Model.Error.Detail</pre></td>
            </tr>

        </tbody>
    </table>


    <h4 class="table-header"> Cookies </h4>
    <table class="table table-condensed table-bordered table-striped table-fixed table-accent">
        <thead>
            <tr>
                <th >Name</th>
                <th>Value</th>
            </tr>
        </thead>
        @foreach (var cookieKey in Model.Error.Cookies.AllKeys)
        {
            <tr>
                <td>@cookieKey</td>
                <td>@Model.Error.Cookies[cookieKey]</td>
            </tr>
        }
    </table>
    

    <h4 class="table-header"> Server Variables </h4>
    <table class="table table-condensed table-bordered table-striped table-fixed table-accent">
        <thead>
            <tr>
                <th >Name</th>
                <th>Value</th>
            </tr>
        </thead>
        @foreach (var servKey in Model.Error.ServerVariables.AllKeys)
        {
            if (!string.IsNullOrWhiteSpace(Model.Error.ServerVariables[servKey]))
            {
                <tr>
                    <td>@servKey</td>
                    <td>@Html.Raw(Html.Encode(Model.Error.ServerVariables[servKey]).Replace(";", ";<br />"))</td>
                </tr>
            }
           
        }
    </table>

</div>

<style>

    .table-header {
        background: #16168c;
        color: white;
        margin-bottom: 0;
        padding: 5px;
    }

    .table-fixed {
        table-layout: fixed;
    }

    .table-fixed td,
    .table-fixed th {
        word-break: break-all;
    }

    .table-accent thead tr {
        background: #0b6cce;
        color: white;
    }

    .table-accent thead tr th:first-child {
        width: 250px;
    }
    .table-accent td:first-child {
        font-weight: bold;
    }


    .code-block {
        overflow-x: scroll;
        white-space: pre;
        background: #ffffcc;
    }

</style>

Another added benefit to this approach is we don't need to set allowRemoteAccess="yes", which can expose some security concerns like session hijacking by exposing the SessionId.

If this implementation isn't robust enough, you can always extend it and customize it till your heart's content. And if you want to leave the local option available, you can still expose that and provide a friendly link for admins on the machine by hiding it under HttpRequest.IsLocal