简体   繁体   中英

401 Unauthorized with Azure b2c on Xamarin.Forms

I have a Xamarin.Forms application that I'm using to connect to an App Service backend, and I'm attempting to authenticate using Auzre B2C JWT tokens.

Through various tutorials I have managed to get B2C setup using microsoft accounts, and I am able to create users, change passwords, and generate access tokens.

My next step was to add the [Authorize] attribute to my controller and attempt to pass that token to my app service and authorize users, but no matter what I try I get a 401 Unauthorized response from my service.

  • I'm adding the JWT token to the Authorization header of my HttpClient, and it's getting to the service.
  • I can paste my token into https://jwt.ms/ , and it correctly tells me what's in my token.
  • I've implemented this code in an attempt to figure out what's wrong.

ConfigureServices in startup.cs looks like this:

public void ConfigureServices(IServiceCollection services) {

            services.AddAuthentication(options => {                
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
                .AddJwtBearer(options => {

                    options.Audience = Configuration["Authentication:AzureAd:ClientId"];

                    options.Events = new JwtBearerEvents {
                        OnAuthenticationFailed = AuthenticationFailed
                    };

                    options.Authority = $"https://{tenant name}.b2clogin.com/{tenant id}/{Configuration["Authentication:AzureAd:Policy"]}";

                    options.Events = new JwtBearerEvents {

                        OnAuthenticationFailed = ctx =>
                        {
                            ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
                            message += "From OnAuthenticationFailed:\n";
                            message += FlattenException(ctx.Exception);
                            return Task.CompletedTask;
                        },
                        OnChallenge = ctx =>
                        {
                            message += "From OnChallenge:\n";
                            ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
                            ctx.Response.ContentType = "text/plain";
                            return ctx.Response.WriteAsync(message);
                        },
                        OnMessageReceived = ctx =>
                        {
                            message = "From OnMessageReceived:\n";
                            ctx.Request.Headers.TryGetValue("Authorization", out var BearerToken);
                            if (BearerToken.Count == 0)
                                BearerToken = "no Bearer token sent\n";
                            message += "Authorization Header sent: " + BearerToken + "\n";
                            return Task.CompletedTask;
                        },
                        OnTokenValidated = ctx =>
                        {
                            Debug.WriteLine("token: " + ctx.SecurityToken.ToString());
                            return Task.CompletedTask;
                        }
                    };

                });

            services.AddMvc();
        }

Configure looks like this:

        public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
            if (env.IsDevelopment()) {
                app.UseDeveloperExceptionPage();

                IdentityModelEventSource.ShowPII = true;
            } else {
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseAuthentication();
            app.UseMvc();
        }

And I've also added this call to AuthenticationFailed, so I'll know if my authentication is working or not:


        Task AuthenticationFailed(AuthenticationFailedContext arg) {
            Console.WriteLine(arg.Exception.Message);
            return Task.FromResult(0);
        }

With my current setup I'm getting a 401 error from the server, and that's right after it hits the OnChallenge event wired up in Startup.cs. According to the link above, that's what gets called right before it returns a 401 to the user, so it seems like the service is receiving the proper token, and authenticating, but maybe I don't have the correct rights set up?

I'm not sure where to go from here, but any guidance would be appreciated.

Edit:

As mentioned in a comment below, I was able to curl my website using the access token generated after logging in through my app like this:

curl https://mywebsite.azurewebsites.net/api/Values -i --header "Authorization: Bearer [TOKEN]"

And that seems to work with no issue, so it seems like it's something with how I'm making a call to the controller through my app, not the authentication itself.

Edit 2 (solution):

So, as per Edit 1, I was correct in that it was just how I was adding the token to the authorization header. It wasn't my brightest moment, but I wasn't calling .Value on the claim that contained my Access Token. I was only calling .ToString() on the claim itself, so the "token" was actually the entire claim text "Access Token: ". I didn't think much of it at the time when I was debugging my service, because I didn't realize it shouldn't have that text there.

Once I corrected that issue the service started working as expected.

So, in the end, I guess it was all working as expected. I was, in fact, not sending the expected token, so I was ... unauthorized.

As requested the line of code that I had to change was this:

So, this won't be 100% applicable to most because I'm using a business library called CSLA, but the idea is the same regardless.

After my b2c call returns the token I store it in the ApplicationContext.User.Identity that's built into the CSLA library. That allows me to get the access token claim later. The important part to take away from this is that I'm storing the token some place that I can access it later when I want to add it to the authorization header.

Later, when I'm making the call with my httpclient I need to get that token, so originally, I was doing this:

httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ((ClaimsIdentity)ApplicationContext.User.Identity).Claims.FirstOrDefault(c => c.Type == "AccessToken") .ToString() );

This isn't correct. This was sending the "token" as with value "Access Token: [token value]. Essentially, it was adding the words "Access Token" to the token I needed to authenticate, and that was failing, because the words "Access Token" are not actually supposed to be part of the token you use to authenticate.

After I changed my call to this:

httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ((ClaimsIdentity)ApplicationContext.User.Identity).Claims.FirstOrDefault(c => c.Type == "AccessToken") .Value );

It started getting only the token value, and when that was added to the authorization header, it worked just fine.

Edit 2 explains the answer to my problem.

I wasn't adding the token correctly to the authorization header, so the service wasn't able to authenticate the token, or rather, it saw the token as invalid.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM