How to Get the Access Token from a Blazor (Server-Side) Web App

How do I get the access token from a blazor (server-side) web app?

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

Refreshing tokens Blazor server side openId connect

I solved this by updating the tokens stored in the HttpContext instead.
First I was trying to do this with a newly created razor page, but ran into some issues with redirecting the user afterwards.

Eventually I placed the logic for updating the tokens directly in the _Host.cshtml.

EDIT:

Inside of _Host.cshtml I check if the token has expired:

@{
Layout = "_Layout";

var tokenExpiry = await HttpContext.GetTokenAsync("expires_at");
DateTimeOffset.TryParse(tokenExpiry, out var expiresAt);
var accessToken = await HttpContext.GetTokenAsync("access_token");

var tokenShouldBeRefreshed = accessToken != null && expiresAt < DateTime.UtcNow.AddMinutes(20);
if (tokenShouldBeRefreshed)
{
await RefreshAccessTokenAsync();
}

accessToken = await HttpContext.GetTokenAsync("access_token");

var refreshToken = await HttpContext.GetTokenAsync("refresh_token");
}

<component type="typeof(App)" param-InitialAccessToken="accessToken" param-InitialRefreshToken="refreshToken" render-mode="Server" />

The method RefreshAccessTokenAsync() looks something like this:

 async Task RefreshAccessTokenAsync()
{
var auth = await HttpContext.AuthenticateAsync();

if (!auth.Succeeded)
{
await HttpContext.SignOutAsync();
return;
}

var injectedIOptionsHere= injectedIOptionsHere.Value;
var refreshToken = await HttpContext.GetTokenAsync("refresh_token");

if (refreshToken == null)
{
await HttpContext.SignOutAsync();
return;
}

var httpClient = HttpClientFactory.CreateClient();
var refreshTokenUrl = $"{injectedIOptionsHere.Authority}/common/oauth/tokens?";

var postData = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("client_id", injectedIOptionsHere.ClientId!),
new KeyValuePair<string, string>("client_secret", injectedIOptionsHere.ClientSecret!),
new KeyValuePair<string, string>("refresh_token", refreshToken!),
new KeyValuePair<string, string>("redirect_url", injectedIOptionsHere.RedirectUrl!),
};

var content = new FormUrlEncodedContent(postData);
HttpResponseMessage? responseMessage = await httpClient.PostAsync(refreshTokenUrl, content);
responseMessage.EnsureSuccessStatusCode();
var responseJson = await responseMessage.Content.ReadAsStringAsync();
var responseJObject = JObject.Parse(responseJson);
var newAccessToken = responseJObject.GetStringValue("access_token");
var expiresInSeconds = responseJObject.GetIntValue("expires_in");
var newExpiryTime = DateTime.UtcNow.AddSeconds(expiresInSeconds).ToString(CultureInfo.InvariantCulture);

var expiresAtTokenUpdated = auth.Properties!.UpdateTokenValue("expires_at", newExpiryTime);
var accessTokenUpdated = auth.Properties!.UpdateTokenValue("access_token", newAccessToken);

var tokensUpdatedCorrectly = expiresAtTokenUpdated && accessTokenUpdated;

if (tokensUpdatedCorrectly)
{
await HttpContext.SignInAsync(auth.Principal, auth.Properties);
}
}

Hope this helps!

Confused on how to get access tokens from B2C in Blazor App

I was able to solve this myself. My AcquireTokenSilent call was failling because there was no users in the cache when I call it, so I had to make sure to add first entry to the cache when my user logs in. I was able to achieve this by configuring my auth as follows:

services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = AzureADB2CDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = AzureADB2CDefaults.OpenIdScheme;
})
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options))
.AddCookie();

services.Configure<OpenIdConnectOptions>(AzureADB2CDefaults.OpenIdScheme, options =>
{
//Configuration.Bind("AzureAdB2C", options);
options.ResponseType = Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectResponseType.CodeIdToken;
options.Scope.Add("offline_access");
options.Scope.Add("https://mytenant.onmicrosoft.com/api/api.read.write");

options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;

options.Events.OnAuthorizationCodeReceived = async context =>
{
AzureADB2COptions opt = new AzureADB2COptions();
Configuration.Bind("AzureAdB2C", opt);
// As AcquireTokenByAuthorizationCodeAsync is asynchronous we want to tell ASP.NET core that we are handing the code
// even if it's not done yet, so that it does not concurrently call the Token endpoint. (otherwise there will be a
// race condition ending-up in an error from Azure AD telling "code already redeemed")
context.HandleCodeRedemption();

var code = context.ProtocolMessage.Code;
string signedInUserID = context.Principal.FindFirst(ClaimTypes.NameIdentifier).Value;

IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder.Create(opt.ClientId)
.WithB2CAuthority(opt.Authority)
.WithRedirectUri(opt.RedirectUri)
.WithClientSecret(opt.ClientSecret)
.WithClientName("myWebapp")
.WithClientVersion("0.0.0.1")
.Build();
new MSALStaticCache(signedInUserID, context.HttpContext).EnablePersistence(cca.UserTokenCache);

try
{
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCode(opt.ApiScopes.Split(' '), code)
.ExecuteAsync();
context.HandleCodeRedemption(result.AccessToken, result.IdToken);

}
catch (Exception)
{

}

};

});

Why is the returned access token null in a Blazor Server-Side application?

If you want to request a token for your downstream API, TokenAcquisition is the way to go:

using Microsoft.Identity.Web;
//...
string token = await TokenAcquisition.GetAccessTokenForUserAsync(new string[] {apiScope});

Combined with IDownstreamWebApi you don't even have to save the token as a string, it will get assigned when the api is called:

protected readonly IDownstreamWebApi _webApi; // Injected
protected readonly ITokenAcquisition TokenAcquisition; // Injected
// ...
public async Task CallApi(){
await TokenAcquisition.GetAccessTokenForUserAsync(new string[] {apiScope});
var response = await _webApi.CallWebApiForUserAsync("apiName", options => ... );
}

And appsettings structure:

"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "<AD domain>",
"TenantId": "<TenantId>",
"ClientId": "<ClientId of this registered app>",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath": "/signout-callback-oidc",

"ClientSecret": "<Secret of registered API>"
},
"API": {
"APIScope": "api://<APIAppID>/access_as_user",
"Scopes": "api://<APIAppID/access_as_user",
"BaseUrl": "https://localhost:7003/"
}

Blazor server - get AAD access token from current logged in user

I found the solution to this issue.
I needed to make use of the "Microsoft.Identity.Web" & "Microsoft.Identity.Web.UI" nuget packages to be able to retrieve the tokens to call the API.

I followed the solution explained in following sample: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/b07a9e06206f7274fdcadc34a50b8bebf9666fcf/4-WebApp-your-API/4-1-MyOrg#step-2-register-the-sample-with-your-azure-active-directory-tenant

I did encounter some issues with the consent mechanism that is needed the first time a user logs in. This is solved by adding the "Microsoft Identity consent and conditional access handler service" which is explained here: https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access

So case closed :)

Hope this helps someone in the future as well.

For completeness, I added the changes I did in my code:

Startup.cs

services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();


services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { Configuration["DashboardAPI:ApiScope"] })
.AddInMemoryTokenCaches();

services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();

API controller class

Injected "ITokenAcquisition" class in my constructor and assigned it to the "_tokenAcquisition" var
Each time I call the API, I execute the "PrepareAuthenticatedClient" method first

private async Task PrepareAuthenticatedClient()
{
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { _dashboardAPIScope });
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}

In the view where I call the API controller, I added a try-catch block for the consent handling and inject the consenthandler

[Inject]
MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler { get; set; }

try
{
// Call to my API controller
}
catch(Exception ex)
{
ConsentHandler.HandleException(ex);
}

how to take token from http client with signalr in blazor server app?

Please, do not use HttpContext in Server Blazor App. It is not available. Server-side Blazor is WebSocket-based, not HTTP requests based. Just don't do that.

Here are links to my answer how to implement OpenID Connect, and others, where you'll find answers to your questions.

See these:
how to jwt authentication blazor server without microsoft identity?
How do I get the access token from a blazor (server-side) web app?
How to add OpenIdConnect via IdentityServer4 to ASP.NET Core ServerSide Blazor web app?
How to get JWT authenticated sign-in user from identity claims in asp.net web api
How to use the HttpContext object in server-side Blazor to retrieve information about the user, user agent
Blazor Adding HttpClientHandler to add Jwt to HTTP Header on requests

Blazor: Redirect to Login page when there is no session / JWT token?

Note: There are more answers related to Jwt,OpenID Connect, etc. You just have to look for them

How to handle user OIDC tokens in Blazor Server when the browser is refreshed and the cookie’s tokens are invalid?

Edit 20220715: After some feedback on our approach from Dominic Baier we removed our Scoped UserSubProvider service in favour of using AuthenticationStateProvider instead. This has simplified our approach. I have edited the following answer to reflect this change.


This approach combines advice from Microsoft on how to pass tokens to a Blazor Server app (here), with server side storage of tokens in a Singleton service for all users (inspired by Dominick Baier’s Blazor Server sample project on GitHub here).

Instead of capturing the tokens in the _Host.cshtml file and storing them in a Scoped service (like Microsoft do in their example), we use the OnTokenValidated event in a similar way to Dominick Baier’s sample, storing the tokens in a Singleton service that holds tokens for all Users, we call this service ServerSideTokenStore.

When we use our HttpClient to call an API and it needs an access_token (or refresh_token), then it retrieves the User’s sub from an injected AuthenticationStateProvider, uses it to call ServerSideTokenStore.GetTokensAsync(), which returns a UserTokenProvider (similar to Microsoft’s TokenProvider) containing the tokens. If the HttpClient needs to refresh the tokens then it populates a UserTokenProvider and saves it by calling ServerSideTokenStore.SetTokensAsync().

Another issue we had was if a separate instance of the web browser is open while the app restarts (and therefore loses the data held in ServerSideTokenStore) the user would still be authenticated using the cookie, but we’ve lost the access_token and refresh_token. This could happen in production if the application is restarted, but happens a lot more frequently in a dev environment. We work around this by handling OnValidatePrincipal and calling RejectPrincipal() if we cannot get a suitable access_token. This forces a round trip to IdentityServer which provides a new access_token and refresh_token. This approach came from this stack overflow thread.

(For clarity/focus, some of the code that follows excludes some standard error handling, logging, etc.)

Getting the User sub claim from AuthenticationStateProvider

Our HttpClient gets the user's sub claim from an injected AuthenticationStateProvider. It uses the userSub string when calling ServerSideTokenStore.GetTokensAsync() and ServerSideTokenStore.SetTokensAsync().

    var state = await AuthenticationStateProvider.GetAuthenticationStateAsync();
string userSub = state.User.FindFirst("sub")?.Value;

UserTokenProvider

    public class UserTokenProvider
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTimeOffset Expiration { get; set; }
}

ServerSideTokenStore

    public class ServerSideTokenStore
{
private readonly ConcurrentDictionary<string, UserTokenProvider> UserTokenProviders = new();

public Task ClearTokensAsync(string userSub)
{
UserTokenProviders.TryRemove(userSub, out _);
return Task.CompletedTask;
}

public Task<UserTokenProvider> GetTokensAsync(string userSub)
{
UserTokenProviders.TryGetValue(userSub, out var value);
return Task.FromResult(value);
}

public Task StoreTokensAsync(string userSub, UserTokenProvider userTokenProvider)
{
UserTokenProviders[userSub] = userTokenProvider;
Return Task.CompletedTask;
}
}

Startup.cs ConfigureServices (or equivalent location if using .NET 6 / whatever)

    public void ConfigureServices(IServiceCollection services)
{
// …
services.AddAuthentication(…)
.AddCookie(“Cookies”, options =>
{
// …
options.Events.OnValidatePrincipal = async context =>
{
if (context.Principal.Identity.IsAuthenticated)
{
// get user sub
var userSub = context.Principal.FindFirst(“sub”).Value;
// get user's tokens from server side token store
var tokenStore =
context.HttpContext.RequestServices.GetRequiredService<IServerSideTokenStore>();
var tokens = await tokenStore.GetTokenAsync(userSub);
if (tokens?.AccessToken == null
|| tokens?.Expiration == null
|| tokens?.RefreshToken == null)
{
// if we lack either an access or refresh token,
// then reject the Principal (forcing a round trip to the id server)
context.RejectPrincipal();
return;
}
// if the access token has expired, attempt to refresh it
if (tokens.Expiration < DateTimeOffset.UtcNow)
{
// we have a custom API client that takes care of refreshing our tokens
// and storing them in ServerSideTokenStore, we call that here
// …
// check the tokens have been updated
var newTokens = await tokenStore.GetTokenAsync(userSubProvider.UserSub);
if (newTokens?.AccessToken == null
|| newTokens?.Expiration == null
|| newTokens.Expiration < DateTimeOffset.UtcNow)
{
// if we lack an access token or it was not successfully renewed,
// then reject the Principal (forcing a round trip to the id server)
context.RejectPrincipal();
return;
}
}
}
}
}
.AddOpenIdConnect(“oidc”, options =>
{
// …
options.Events.OnTokenValidated = async n =>
{
var svc = n.HttpContext.RequestServices.GetRequiredService<IServerSideTokenStore>();
var culture = new CultureInfo(“EN”) ;
var exp = DateTimeOffset
.UtcNow
.AddSeconds(double.Parse(n.TokenEndpointResponse !.ExpiresIn, culture));
var userTokenProvider = new UserTokenProvider()
{
AcessToken = n.TokenEndpointResponse.AccessToken,
Expiration = exp,
RefreshToken = n.TokenEndpointResponse.RefreshToken
}
await svc.StoreTokensAsync(n.Principal.FindFirst(“sub”).Value, userTokenProvider);
};
// …
});
// …
}


Related Topics



Leave a reply



Submit