4

After implementing openidconnect, where does blazor store the access token? how to retrieve it?


How to add OpenIdConnect via IdentityServer4 to ASP.NET Core ServerSide Blazor web app?

https://learn.microsoft.com/en-us/aspnet/core/security/blazor/?view=aspnetcore-3.1&tabs=visual-studio#customize-unauthorized-content-with-the-router-component

001
  • 62,807
  • 94
  • 230
  • 350
  • I'm still trying to come up with a solution for "how would you handle access token expiration?" It's immensely difficult... I'll answer your current question soon... Incidentally, what do you want the access token for ? – enet Jan 24 '20 at 07:37
  • I want to exchange the id token for a (client credential) access token (If I can not figure this out, I would have to do it in a different) – 001 Jan 24 '20 at 08:04
  • I can still recommend this blog: https://mcguirev10.com/2019/12/15/blazor-authentication-with-openid-connect.html. – Pascal R. Jan 24 '20 at 08:48
  • I fixed this issue by adding the code from this link: https://learn.microsoft.com/en-us/powerapps/developer/common-data-service/webapi/quick-start-blazor-server-app#make-a-call-to-the-web-api – Richard Oct 15 '20 at 00:06

3 Answers3

12

I resolved this issue by adding the code listed in the link below.

https://learn.microsoft.com/en-us/powerapps/developer/data-platform/webapi/quick-start-blazor-server-app#prepare-the-app-to-use-azure-ad-tokens

Note: The steps listed in the link above work fine, however, I made some small modifications that make more sense to me. I also modified the order that make more sense to me.

Steps:

1. Create the TokenProvider class

public class TokenProvider
{
    public string AccessToken { get; set; }
}

2. Update the _Host.cshtml file with the following:

@using Microsoft.AspNetCore.Authentication

@{
   
    var accessToken = await HttpContext.GetTokenAsync("access_token");
}

<body>
    <app>
        <component type="typeof(App)" param-AccessToken="accessToken" render-mode="ServerPrerendered" />
    </app>

3. Update StartUp.cs with DI:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();

        services.AddScoped<TokenProvider>();

4. Update App.razor with the following:

@inject TokenProvider TokenProvider

@code
{
    
    [Parameter]
    public string AccessToken { get; set; }

    protected override void OnInitialized()
    {
        //Accept the parameter from _Host.cshtml and move into the Token Provider 
        TokenProvider.AccessToken = AccessToken;
        base.OnInitialized();
    }
}

5. Create an instance of the _tokenProvider in the constructor and use it to get the Access Token

Note: Below I get the access token NOT in the @code block or the code behind of the Blazor page, but I am using a Service class. (The page calls the Service class). I hope this makes sense. Again, I would suggest reviewing the link above.

    private readonly TokenProvider _tokenProvider;

    //Create tokenProvider using constructor Dependency Injection
    public HttpClientUtility(TokenProvider tokenProvider)
    {
        _tokenProvider = tokenProvider;
    }

var accessToken = _tokenProvider.AccessToken;

if (accessToken != null)
{
    _httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
}

Hopefully the above steps will help someone else.

Richard
  • 1,054
  • 1
  • 19
  • 36
  • 1
    The provided link is the official way, it's fairly simple and should be the accepted answer ;) – Heinzlmaen Jan 28 '21 at 16:17
  • 1
    @Richard I have followed your steps.. and I am struck in step 5 which you mentioned `_tokenProvider` from constructor DI. Could you please elaborate this? – Balagurunathan Marimuthu Feb 22 '21 at 23:53
  • 1
    @Richard https://stackoverflow.com/q/66325590/4337436 I have raised my question here. If you can please assist me. – Balagurunathan Marimuthu Feb 23 '21 at 00:59
  • @BalagurunathanMarimuthu Sorry I didn't get this till recently. Reviewing your post above it looks as though you got though it. – Richard Feb 24 '21 at 17:11
  • @Richard I always receive `null` value when accessing `TokenProvider`. Please assist on this. – Balagurunathan Marimuthu Feb 25 '21 at 15:51
  • @BalagurunathanMarimuthu When you say TokenProvider receives null. Do you mean TokenProvider is null or AccessToken it returns is null? – Richard Feb 27 '21 at 18:55
  • @BalagurunathanMarimuthu You asked me to elaborate on Step 5 and the _tokenProvider. I don't really understand what you are asking but I will try my best. In Step 3 the DI (Dependence Injection) was set up. So, in Step 5 you would create an instance of the _tokenProvider in the constructor of the class which you need to us it in. If I can, I will edit the code example above. – Richard Feb 27 '21 at 19:11
  • @BalagurunathanMarimuthu I added more detail to Step 5. I hope it helps. Again review: https://learn.microsoft.com/en-us/powerapps/developer/data-platform/webapi/quick-start-blazor-server-app#prepare-the-app-to-use-azure-ad-tokens – Richard Feb 27 '21 at 19:41
  • Thank you so much!! Just one thing: in .AddOpenIdConnect you must set options.SaveTokens = true; otherwise it won't work. I searched a long time for this detail. I think, this was also the problem from Balagurunathan – Chris Berlin Sep 10 '21 at 09:29
11

The following code snippets provide a way to retrieve the access token issued when a user is authenticated with IdentityServer4 provider. In order to get the access token you can use the HttpContext object, but since Blazor is SignalR-based, you'll have to do it the only time the HttpContext object is available, when the connection to your application is an HTTP connection, and not a WebSocket connection.

After retrieving the access token, you need to pass it to your Blazor app, and store it in a local storage. My code also provide a way to parse the access token, if necessary.

  • Add a file to the Pages folder and name it _Host.cshtml.cs

  • Add this code to the file:

     public class HostAuthenticationModel : PageModel
     {
         public async Task<IActionResult> OnGet()
         {
             if (User.Identity.IsAuthenticated)
             {
                var token = await HttpContext.GetTokenAsync("access_token");
                AccessToken = token;
    
             }
        return Page();
     }
    
     public string AccessToken { get; set; }
     }
    

Note: I've name the the PageModel class: HostAuthenticationModel You'll need some of these: using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System; using System.Linq; using System.Threading.Tasks;

  • Next we have to pass the value stored in the AccessToken property to the Blazor App:

In the _Host.cshtml file add the model directive at the top portion of the file:

@model HostAuthenticationModel

Add a new attribute to the component Tag Helper like this:

param-AccessToken="Model.AccessToken"

Final result:

 <app>
        <component type="typeof(App)" render-mode="ServerPrerendered" 
                 param-AccessToken="Model.AccessToken"/>
 </app>

The param-AccessToken attribute requires you to define a property named AccessToken in the App component which will get the access token from the page model.

  • Next define the property which will receive the access token
  • And then override the OnAfterRenderAsync method from which we call a method to store the access token in the local storage.

    @code{
       [Parameter]
       public string AccessToken { get; set; }
    
       protected override async Task OnAfterRenderAsync(bool firstRender)
       {
           if (firstRender)
            {
                await tokenStorage.SetTokenAsync(AccessToken);
            }
       }
     }
    

Also place the following at the top of the App component:

@inject AccessTokenStorage tokenStorage
  • Next you'll have to create the AccessTokenStorage service like this:

    Create a class named AccessTokenStorage at the root of your app, and add the following code:

    public class AccessTokenStorage { private readonly IJSRuntime _jsRuntime;

    public AccessTokenStorage(IJSRuntime jsRuntime)
    {
        _jsRuntime = jsRuntime;
    }
    
    public async Task<string> GetTokenAsync()
        => await _jsRuntime.InvokeAsync<string>("localStorage.getItem", "accessToken");
    
    public async Task SetTokenAsync(string token)
    {
        if (token == null)
        {
            await _jsRuntime.InvokeAsync<object>("localStorage.removeItem", 
                                                            "accessToken");
        }
        else
        {
            await _jsRuntime.InvokeAsync<object>("localStorage.setItem", 
                                                   "accessToken", token);
        }
    
    
     }
    }
    

I guess no explanation is needed here... Here's some using directives you may need using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.JSInterop;

Add the following to the Startup.ConfigureServices

services.AddHttpClient(); services.AddScoped<AccessTokenStorage>();

Note: the above code should be used with the code I provide in my answer here

enet
  • 41,195
  • 5
  • 76
  • 113
  • I'm still working on a solution for "how would you handle access token expiration?" . Please post a question for this in a couple of days. – enet Jan 24 '20 at 18:12
  • wow, great work! by the way, are you doing all this to answer the question only or you also need to retrieve the access token for your own app? – 001 Jan 25 '20 at 11:07
  • Glad to be of help!... I'm doing it as part of my learning Blazor. Personally, I do not need it, but I augment my knowledge by solving a plethora of questions here. Would you mind marking it as the answer if it solved your problem, so others know it was useful. – enet Jan 25 '20 at 14:29
  • Tested it, it works! however where did you get to use this? services.AddHttpClient(); – 001 Jan 26 '20 at 00:37
  • why not just store the token via OnGet in _host.cshtml.cs? – 001 Jan 26 '20 at 02:14
  • store it where ? My code passes the access token to my Blazor Server App from which it is stored in a JavaScript local storage, and retrieved as necessary. – enet Jan 26 '20 at 18:23
  • When you use Blazor Server App it is recommended to use IHttpClientFactory...IHttpClientFactory can be registered by calling AddHttpClient...See basic usage: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#basic-usage – enet Jan 26 '20 at 18:29
  • Yes however, I don't see where httpclient is required. – 001 Jan 26 '20 at 21:02
  • 1
    I'm sorry... yes, httpclient is not required here. In my app, however, I use it to test http calls to a web api. Just delete it... – enet Jan 27 '20 at 06:56
  • @enet https://stackoverflow.com/q/66325590/4337436 Could you please assist me here? – Balagurunathan Marimuthu Feb 23 '21 at 01:04
1

I used the following way

Startup.cs

services.AddHttpContextAccessor();

Razor page

@using Microsoft.AspNetCore.Http
@using Microsoft.AspNetCore.Authentication
@inject IHttpContextAccessor httpContextAccessor


@code {    
    private async Task<string> GetToken()
            => await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
}
  • Why the -1? Lack of explanation? Doesn't work? Bad practice? – Heinzlmaen Nov 06 '20 at 13:03
  • -1 because of 2 reasons. 1. This didn't work for me. My OnPrem solution had the above code and it worked. However, when I deployed to Azure, this solution did not work. Above is a detailed post with how I got it to work in Azure. 2. I have read numerous post that stated getting the access token from the HttpContextAccessor / HttpContext is not a good practice. – Richard Feb 18 '21 at 15:37
  • 1
    I think that it is not safe to read HttpContext this way inside blazor page, because it runs on a "Circuit" and the Context might not be the Context that is pertinent to the actual Circuit. – Tony Aug 14 '22 at 15:56