Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have a Blazor Server Side app configured with B2C auth. This app will call a webapi to do any of the data exchanges with my service. B2C auth works fine, and straight from template the config B2C auth is:

services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
            .AddAzureADB2C(options => { Configuration.Bind("AzureAdB2C", options); });

The claims only has the claims I'm returning from my signin policy, without any access tokens I can use for auth on behalf to my Web api (Also secured with same B2C tenant).

I've read about 100 different docs, but it seems that nothing makes sense in the context of blazor. Is there anyone that has done this before that could shed some light?

First prize would be to request an access token once the user auths to B2C the first time, and then keep the token in cache to use in the blazor app for any api calls while the session / browser is open or the access token is valid.

It seems that this is the right path: https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi/blob/master/TaskWebApp/Controllers/TasksController.cs but what I'm not understanding is:

  • Is this the right approach for Blazor?
  • How can I trigger it to get the access token requested on auth of the user. It's unclear to me on how to override the B2C auth constructors.
  • Could I add the access and refresh tokens in the claims of the current user so that I can work with the httpcontext objects within my app globally to get the tokens required to do my api call?
  • The code in the doc above is of course in a controller. Would love to have this just form as part as the auth flow of the user.
  • It seems like the configuration of B2C auth is now very templated... For example, where would I be able to customize the routes for auth? I'd like to rather direct the user to /auth or /login than /AzureB2C/Login (To obscure the obvious auth provider url. I know it shows up for the user in the address bar... but hey... Any help will be greatly appreciated that would be specific on how to handle this in Blazor Server Side.

Thanks!

like image 896
Tjopsta Avatar asked Nov 21 '19 23:11

Tjopsta


2 Answers

I have created a sample application with OpenId Connect and Blazor (Server) for you here https://github.com/yberstad/BlazorAuth. It is uses a SameSiteCookie and OpenId Connect.

  1. In the appsettings.json, fill in your Authority, ClientId and ClientSecret.
  2. Configure the redirect URi in Azure to http://localhost:62438/signin-oidc/
  3. Start up the app in debug mode
  4. Got to http://localhost:62438/api/openidconnect/login
  5. Login with Azure
  6. Got to http://localhost:62438/api/openidconnect/user, having a break point in the OpenIdConnectController.GetUser action, there you can see how to getting hold of the access token.

How to get access and refresh token:

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

SameSiteCookie Info: https://brockallen.com/2019/01/11/same-site-cookies-asp-net-core-and-external-authentication-providers/

Getting Access Token: http://docs.identityserver.io/en/latest/quickstarts/5_hybrid_and_api_access.html#using-the-access-token

Having the tokens stored in a SameSiteCookie makes it only visible for the server, hence not saving and exposing it in an unsafe environment on the client. A SameSiteCookie is also safe for XSS.

Hope this helps.

like image 88
Oyvind Habberstad Avatar answered Sep 21 '22 12:09

Oyvind Habberstad


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)
                {


                }

            };





            });
like image 25
Tjopsta Avatar answered Sep 21 '22 12:09

Tjopsta