Search code examples
c#azureasp.net-corenext.jsnext-auth

Azure AD Authentication, Next-Auth JWT and .NET Core Web API "invalid_token"


We have a Next.js app that authenticates to Azure AD B2C using Next-Auth. This successfully returns a JWT token. However, when I open Swagger and use the id_token, I'm getting a 401 Unauthorized error, with the www-authenticate header returning:

www-authenticate: Bearer error="invalid_token"

info: Microsoft.IdentityModel.LoggingExtensions.IdentityLoggerAdapter[0]
      IDX10242: Security token: '[PII of type 'Microsoft.IdentityModel.JsonWebTokens.JsonWebToken' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]' has a valid signature.
info: Microsoft.IdentityModel.LoggingExtensions.IdentityLoggerAdapter[0]
      IDX10239: Lifetime of the token is valid.
info: Microsoft.IdentityModel.LoggingExtensions.IdentityLoggerAdapter[0]
      IDX10234: Audience Validated.Audience: '4a61b4f1-8e3f-488a-ad74-a491b569b0eb'
info: Microsoft.IdentityModel.LoggingExtensions.IdentityLoggerAdapter[0]
      IDX10245: Creating claims identity from the validated token: '[PII of type 'Microsoft.IdentityModel.JsonWebTokens.JsonWebToken' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.

It says token is valid and yet it returns invalid_token, why?

import type { NextAuthOptions } from 'next-auth';
import AzureADB2CProvider from 'next-auth/providers/azure-ad-b2c';

const options: NextAuthOptions = {
    providers: [
        AzureADB2CProvider({
            tenantId: process.env.AZURE_AD_B2C_TENANT_NAME,
            clientId: process.env.AZURE_AD_B2C_CLIENT_ID || '',
            clientSecret: process.env.AZURE_AD_B2C_CLIENT_SECRET || '',
            primaryUserFlow: process.env.AZURE_AD_B2C_PRIMARY_USER_FLOW,
            checks: ['pkce'],
            client: {
                token_endpoint_auth_method: 'none',
                client_secret: process.env.AZURE_AD_B2C_CLIENT_SECRET
            },
            authorization: {
                params: {
                    scope: 'offline_access openid'
                }
            }
        })
    ],
    session: {
        strategy: 'jwt' // Use JWTs instead of session cookies
    },
    secret: process.env.NEXTAUTH_SECRET,
    callbacks: {
        jwt({ token, account }) {
            const result = token;
            // If it's the first time signing in, persist the access token from the provider
            if (account) {
                result.accessToken = account.id_token;
            }
            return result;
        }
    }
};

export default options;
"AzureAdB2C": {
  "Instance": "hidden",
  "Domain": "hidden.onmicrosoft.com",
  "TenantId": "hidden",
  "ClientId": "hidden",
  "ClientSecret": "hidden",
  "SignUpSignInPolicyId": "hidden",
  "CallbackPath": "/signin-oidc",
  "Audience": "hidden",
  "SignedOutCallbackPath": "/signout-callback-oidc"
}
using System.Reflection;
using Asp.Versioning;
using FluentValidation;
using MediatR;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Web;
using Swashbuckle.AspNetCore.SwaggerGen;

var builder = WebApplication.CreateBuilder(args);

// Azure
builder.AddAzureAppConfiguration(StartupLogger.Current);
builder.Services.AddApplicationInsightsTelemetry(options => options.EnableAdaptiveSampling = false);

// Common
builder.Services
    .AddPersistence(StartupLogger.Current)
    .AddInfrastructure(builder.Environment, builder.Configuration);

// Host
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C"));
builder.Services.AddAuthorization();

builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());

builder.Services.AddValidatorsFromAssemblyContaining<Program>();

builder.Services.AddMediatR(configure =>
{
    configure.RegisterServicesFromAssemblyContaining<Program>();
    configure.AddBehavior(typeof(IPipelineBehavior<,>), typeof(MetricsBehavior<,>));
    configure.AddBehavior(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>));
    configure.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
});

builder.Services.AddFeatures(builder.Configuration);

// See: https://github.com/dotnet/aspnet-api-versioning/tree/main/examples/AspNetCore/WebApi/MinimalOpenApiExample
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiVersioning(options =>
    {
        options.DefaultApiVersion = new ApiVersion(1);
        options.ReportApiVersions = true;
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
    })
    .AddApiExplorer(options =>
    {
        options.GroupNameFormat = "'v'V";
        options.SubstituteApiVersionInUrl = true;
    });
builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen(x => x.OperationFilter<SwaggerDefaultValues>());

var app = builder.Build();

var apiVersionSet = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1))
    .ReportApiVersions()
    .Build();

app.UseSwagger();
app.UseSwaggerUI(options =>
{
    var descriptions = app.DescribeApiVersions();

    // Build a swagger endpoint for each discovered API version
    foreach (var description in descriptions)
    {
        var url = $"/swagger/{description.GroupName}/swagger.json";
        var name = description.GroupName.ToUpperInvariant();
        options.SwaggerEndpoint(url, name);
    }

    // RoutePrefix is set to an empty string to serve Swagger UI at the application's root instead of /swagger.
    options.RoutePrefix = string.Empty;
});

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

var group = app.MapGroup("api/v{version:apiVersion}")
    .WithApiVersionSet(apiVersionSet);

group.MapEndpoints();

app.Run();

public partial class Program;

Solution

  • Identity tokens and access tokens are different tokens. You should use the access token in

    result.accessToken = account.access_token;
    

    P.s. I am not familiar with Next-Auth. My point is to use the access token instead of identity token.