RadzenSSRSViewer + Blazor Web Assembly ASP.NET Core Hosted + JWT Authorization

My objective of this post is to share with the RADZEN Blazor community the problem that I did encounter with RadzenSSRSViewer while utilizing the same in Blazor Web Assembly PWA and how I resolved\worked around the same blocker issue, so anyone else facing the issue can benefit from the solution\work-around.

This post is in continuation of my last following post:

After implementing the solution (mentioned in above post) the report preview within Blazor Web Assembly PWA was achieved successfully i.e. report was authenticated against the SSRS server using proxy active directory credentials passed through the report controller from ASP.NET Core Blazor server but the following security issue was also introduced with this step.

Security Issue:

Any Unauthenticated\Unauthorized user who tries to load the __SSRSPROXY URL (The URL located within the IFRAME tag of RadzenSSRSViewer) will successfully be able to view the SSRS reports.

Note: If we add JwtBearerDefaults AuthenticationScheme Authorize attribute to __ssrsreport and ssrsproxy endpoints located in Radzen’s ReportController class, then report preview will get blocked with not authorized response as the request call made by RadzenSSRSViewer links does not contain the JWT Authorization header Key and associated JWT bearer token.

Objective:

  1. Create a dummy request & response related to RadzenSSRSViewer end point URL & Save the Authorization token Value in the Cache under the response’s statusText
    a) Update the cache at Log-In\Log-out to add\delete token to\from cache
    i) Read the JWT token value stored in the Local Storage of browser
  2. Send the key value pair i.e. Authorization & JWT bearer token with request that are trying to communicate with Radzen’s ReportController class’s __ssrsreport and ssrsproxy end point
    a) Intercept the related request call via the service-worker.js and add the Authorization Key value pair to the header of the request
    i) Read the authorization token saved in the status text of cached response
  3. Add the Authorize attribute to the __ssrsreport for auto Authentication & Authorization
  4. Manually validate the JWT token under ssrsproxy end point
  5. Add JWT Token in the query string related to export report URL’s
    a) Only validate request containing report name or request related to report export and allow other request to bypass authentication.

Objective # 1 Solution:

  1. Create a new java script method with following code to save JWT token to cache
const authCacheName = 'Auth-Cache';

// Add SSRS report related URLs to Auth-Cache along with JWT authentication token
async function AddAuthTokenToCache() {
    var urlPrefix = window.location.origin + '/' + window.location.pathname.split('/')[1] + '/';
    var tokkenValue = ('bearer ' + localStorage.getItem("TOKENKEY"));
    var url1 = urlPrefix+'__ssrsreport';
    var request1 = new Request(url1, {});
    var myBlob = tokkenValue;
    var init1 = {
        'status': 200,
        'url': url1,
        'statusText': tokkenValue
    };
    var response1 = new Response(myBlob, init1);
    
    await caches.open(authCacheName).then(cache => cache.put(request1, response1));

    var url2 = urlPrefix +'ssrsproxy/https/REPORT-SERVER-NAME/443/ReportServer/Pages/ReportViewer.aspx';
    var request2 =  new Request(url2, {});

    var init2 = {
        'status': 200,
        'url': url2,
        'statusText': tokkenValue
    };
    var response2 = new Response(myBlob, init2);

    await caches.open(authCacheName).then(cache => cache.put(request2, response2));


    var url3 = urlPrefix +'ssrsproxy/https/REPORT-SERVER-NAME/443/ReportServer/Reserved.ReportViewerWebControl.axd';
    var request3 = new Request(url3, {});

    var init3 = {
        'status': 200,
        'url': url3,
        'statusText': tokkenValue
    };
    var response3 = new Response(myBlob, init3);

    await caches.open(authCacheName).then(cache => cache.put(request3, response3));
}
  1. Create a new java script method with following code to delete JWT token from cache
async function DeleteAuthTokenFromCache() {
    await caches.delete(authCacheName);
}

Note:
• Invoke the AddAuthTokenToCache at login event;
• Invoke the DeleteAuthTokenFromCache at log out\session expiration event.

Objective # 2 Solution:

  1. Append the following java script code in the service-worker.published.js > onFetch method
const authCacheName = 'Auth-Cache';
/// <summary>
    /// Customization for RADZEN SSRS Report Viewer
    /// Get the JWT Authorization header value saved in cache and use the same in new request header as value for Authorization key
    /// If request method equals to GET and url does include "__ssrsreport" then Create new request and Add\Append header with JWT Authentication Token
    /// If request method equals to POST and url does include "ssrsproxy, ReportViewer and Embed=true" then Create new request by adding\Appending headers with JWT Authentication Token and copy the body content from old request
    /// </summary>
    if (event.request.method === 'GET' && event.request.url.includes('__ssrsreport')) {
        //Create new Headers value object instance
        var headers = {};
        //Copy all headers from old request in to the new Headers value instance
        if (event.request.headers !== undefined && event.request.headers !== null) {
            for (var entry of event.request.headers.entries()) {
                headers[entry[0]] = entry[1];
            }
        }

        //Open Auth-Cache and get the JWT Authorization Token saved for request alike URL
        //When comparing cache URL with reqeust url, ignore check query string equality
        const authCacheOpen = await caches.open(authCacheName);
        var authCacheResponse = await authCacheOpen.match(event.request, {
            //Ignore checking query string equality
            ignoreSearch: true,
            //Ignore checking request method type i.e. GET, HEAD, POST etc...
            ignoreMethod: true,
            //Ignore checking request headers
            ignoreVary: true
        });

        var serialized = null;

        //Set Authorization Header key's value equal to JWT token value storged in request cache
        if (authCacheResponse !== undefined) {
            if (authCacheResponse.headers !== undefined) {
                headers['Authorization'] = authCacheResponse.statusText;
            }
        }

        //Copy request details to new request
        serialized = {
            url: event.request.url,
            headers: headers,
            method: event.request.method,
            mode: (event.request.url.includes('__ssrsreport') ? 'same-origin' : 'no-cors'),
            credentials: event.request.credentials,
            redirect: 'manual',
            referrer: event.request.referrer
        }

        return fetch(new Request(serialized.url, serialized));
    }
    else if (event.request.method === 'POST' && event.request.url.includes('ssrsproxy') && event.request.url.includes('ReportViewer') && event.request.url.includes('Embed=true')) {
        //Create new Headers value object instance
        var headers = {};
        //Copy all headers from old request in to the new Headers value instance
        if (event.request.headers !== undefined && event.request.headers !== null) {
            for (var entry of event.request.headers.entries()) {
                headers[entry[0]] = entry[1];
            }
        }

        //Open Auth-Cache and get the JWT Authorization Token saved for request alike URL
        //When comparing cache URL with reqeust url, ignore check query string equality
        const authCacheOpen = await caches.open(authCacheName);
        var authCacheResponse = await authCacheOpen.match(event.request, {
            //Ignore checking query string equality
            ignoreSearch: true,
            //Ignore checking request method type i.e. GET, HEAD, POST etc...
            ignoreMethod: true,
            //Ignore checking request headers
            ignoreVary: true
        });

        var serialized = null;

        //Set Authorization Header key's value equal to JWT token value storged in request cache
        if (authCacheResponse !== undefined) {
            if (authCacheResponse.headers !== undefined) {
                headers['Authorization'] = authCacheResponse.statusText;
            }
        }

        //Copy request details to new request
        serialized = {
            url: event.request.url,
            host: event.request.host,
            connection: event.request.connection,
            headers: headers,
            method: event.request.method,
            credentials: event.request.credentials,
            redirect: 'manual',
            referrer: event.request.referrer,
            accept: event.request.accept,
            origin: event.request.origin,
            site: event.request.site,
            mode: event.request.mode,
            dest: event.request.dest,
            cookie: event.request.cookie
        }

        //Read the reqeust's body content and copy the same to new request
        serialized.body = await event.request.clone().text().then(function (bodyVal) {
            return bodyVal;
        });

        return fetch(new Request(serialized.url, serialized));
    }
    else {
        return cachedResponse || fetch(event.request);
    }

Objective # 3 Solution:

  1. Go to Radzen’s ReportController.cs > __ssrsreport end point and add the following attribute

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

Objective # 4 Solution:

  1. Go to Radzen’s ReportController.cs > ssrsproxy end point and move the Radzen’s proxy code within the following if condition
public async Task Proxy()
{
    if (JWTTokenValidationRequired(Request))
    {
       //Radzen’s ssrsproxy source code……
    }
}
  1. Create the following private method for manually reading the JWT token from the request headers
/// <summary>
        /// Validate the JWT bearer token available in the Request header's Authorization key
        /// If HTTP request URL's query string contains the name of the report and Header or URL contains JWT Token then validate token else bypass validation
        /// </summary>
        /// <param name="request">HttpRequest</param>
        /// <returns>True if request validaiton passed or bypass allowed else false</returns>
        private bool JWTTokenValidationRequired(HttpRequest request)
        {
            if (request.QueryString.Value?.Trim()?.Length > 0)
            {
                foreach (string reportName in Enum.GetNames(typeof(SSRSReportName)))
                {
                    string queryStringValue = request.QueryString.Value ?? string.Empty;
                    if (queryStringValue.Contains(reportName,StringComparison.InvariantCultureIgnoreCase))
                    {
                        string urlValue = request.Path.Value ?? string.Empty;
                        string authToken = Request.Headers["Authorization"];
                        if (authToken?.Trim()?.Length > 0)
                        {
                            string validationResp = Authenticate(authToken.Replace("bearer ", ""));
                            return (validationResp?.Length > 0);
                        }
                        else if (request.Method == "GET" && urlValue.Contains("ReportViewerWebControl.axd",StringComparison.InvariantCultureIgnoreCase)&& queryStringValue.Contains("OpType=Export", StringComparison.InvariantCultureIgnoreCase))
                        {
                            string formatValue = request.Query["Format"];
                            string jwtToken = request.Query["SsrsKey"];
                            if (formatValue?.Length > 0 
                                && (formatValue == "PDF" || formatValue == "WORDOPENXML" || formatValue == "EXCELOPENXML" || formatValue == "PPTX" || formatValue == "IMAGE" || formatValue == "MHTML" || formatValue == "CSV" || formatValue == "XML" || formatValue == "ATOM")
                                && (jwtToken?.Length  > 0 && (Authenticate(jwtToken))?.Length  > 0)
                                )
                            {
                                return true;
                            }
                            else
                            {
                                return false;
                            }
                        }
                        else
                        {
                            return false;
                        }
                    }
                }
            }
            return true;
        }
  1. Create the following private method for manually validating the JWT token
/// <summary>
        /// JWT bearer toekn validaiton
        /// </summary>
        /// <param name="token">string: JWT Token</param>
        /// <returns>Email claim value if token is valid else blank</returns>
        /// <exception cref="Exception"></exception>
        private string Authenticate(string token)
        {
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["jwt:key"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            SecurityToken validatedToken;
            var validator = new JwtSecurityTokenHandler();

            // These need to match the values used to generate the token
            TokenValidationParameters validationParameters = new TokenValidationParameters();
            validationParameters.ValidateIssuer = false;
            validationParameters.ValidateAudience = false;
            validationParameters.ValidateLifetime = true;
            validationParameters.ValidateIssuerSigningKey = true;
            validationParameters.IssuerSigningKey = key;
            validationParameters.ClockSkew = TimeSpan.Zero;

            if (validator.CanReadToken(token))
            {
                ClaimsPrincipal principal;
                try
                {
                    // This line throws if invalid
                    principal = validator.ValidateToken(token, validationParameters, out validatedToken);

                    // If we got here then the token is valid
                    if (principal.HasClaim(c => c.Type == ClaimTypes.Email))
                    {
                        return principal.Claims.Where(c => c.Type == ClaimTypes.Email).First().Value;
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception(AdminResource.NotAuthorized);
                }
            }

            return String.Empty;
        }

Objective # 5 Solution:

  1. Go to Radzen’s ReportController.cs > WriteResponse private method and append the following code within:
    a) IF > WHILE loop and before the builder.Append
// Add Authorization token in all URL related export report 
                    string authToken = currentReqest.Headers["Authorization"];
                    if (authToken?.Trim()?.Length > 0)
                    {
                        string jwtToken = authToken.Replace("bearer ", "");
                        if (jwtToken?.Trim()?.Length > 0)
                        {
                            if (content.Contains("Reserved.ReportViewerWebControl.axd?ExecutionID=", StringComparison.InvariantCultureIgnoreCase))
                            {
                                content = content.Replace("Reserved.ReportViewerWebControl.axd?ExecutionID=", $"Reserved.ReportViewerWebControl.axd?SsrsKey={jwtToken}&ExecutionID=", StringComparison.InvariantCultureIgnoreCase);
                            }
                        }
                    }

b) ELSE

// Add Authorization token in all URL related export report 
                string authToken = currentReqest.Headers["Authorization"];
                if (authToken?.Trim()?.Length > 0)
                {
                    string jwtToken = authToken.Replace("bearer ", "");
                    if (jwtToken?.Trim()?.Length > 0)
                    {
                        if (result.Contains("Reserved.ReportViewerWebControl.axd?ExecutionID=", StringComparison.InvariantCultureIgnoreCase))
                        {
                            result = result.Replace("Reserved.ReportViewerWebControl.axd?ExecutionID=", $"Reserved.ReportViewerWebControl.axd?SsrsKey={jwtToken}&ExecutionID=", StringComparison.InvariantCultureIgnoreCase);
                        }
                    }
                }

Note:

  • I have re-used & customized the few solutions mentioned above as per my specific requirement that i picked up\found from microsoft docs, stackoverflow.com, radzen forum & developer mozilla org etc.. therefore due credit goes to respective folks for providing the original solutions\documentation.
  • Use the above solution at your own risk and testing.
  • If you find any improvements to the above solution, then kindly let me know as well.

Hope this helps.

Thank you.

1 Like