How to Consume/Validate Token Issued by AspNet.Security.OpenIdConnect.Server (RC1)? - authentication

I have followed everything I know from posts regarding how to implement AspNet.Security.OpenIdConnect.Server.
Pinpoint, do you hear me? ;)
I've managed to separate token issuing and token consumption. I won't show the "auth server side" because I think that part is all set, but I'll show how I built the authentication ticket inside my custom AuthorizationProvider:
public sealed class AuthorizationProvider : OpenIdConnectServerProvider
{
// The other overrides are not show. I've relaxed them to always validate.
public override async Task GrantResourceOwnerCredentials(GrantResourceOwnerCredentialsContext context)
{
// I'm using Microsoft.AspNet.Identity to validate user/password.
// So, let's say that I already have MyUser user from
//UserManager<MyUser> UM:
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
//identity.AddClaims(await UM.GetClaimsAsync(user));
identity.AddClaim(ClaimTypes.Name, user.UserName);
(await UM.GetRolesAsync(user)).ToList().ForEach(role => {
identity.AddClaim(ClaimTypes.Role, role);
});
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Some new stuff, per my latest research
ticket.SetResources(new[] { "my_resource_server" });
ticket.SetAudiences(new[] { "my_resource_server" });
ticket.SetScopes(new[] { "defaultscope" });
context.Validated(ticket);
}
}
And startup at the auth server:
using System;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.Data.Entity;
using Microsoft.Extensions.DependencyInjection;
using MyAuthServer.Providers;
namespace My.AuthServer
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication();
services.AddCaching();
services.AddMvc();
string connectionString = "there is actually one";
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<MyDbContext>(options => {
options.UseSqlServer(connectionString).UseRowNumberForPaging();
});
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<MyDbContext>().AddDefaultTokenProviders();
}
public void Configure(IApplicationBuilder app)
{
app.UseIISPlatformHandler();
app.UseOpenIdConnectServer(options => {
options.ApplicationCanDisplayErrors = true;
options.AllowInsecureHttp = true;
options.Provider = new AuthorizationProvider();
options.TokenEndpointPath = "/token";
options.AccessTokenLifetime = new TimeSpan(1, 0, 0, 0);
options.Issuer = new Uri("http://localhost:60556/");
});
app.UseMvc();
app.UseWelcomePage();
}
public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}
}
Sure enough, when I have this HTTP request, I do get an access token, but I'm not sure if that access token has all the data that the resource server expects.
POST /token HTTP/1.1
Host: localhost:60556
Content-Type: application/x-www-form-urlencoded
username=admin&password=pw&grant_type=password
Now, At the resource server side, I'm using JWT Bearer Authentication. On startup, I've got:
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.Data.Entity;
using Microsoft.Extensions.DependencyInjection;
namespace MyResourceServer
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
string connectionString = "there is actually one";
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<MyDbContext>(options => {
options.UseSqlServer(connectionString).UseRowNumberForPaging();
});
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<MyDbContext>().AddDefaultTokenProviders();
}
public void Configure(IApplicationBuilder app)
{
app.UseIISPlatformHandler();
app.UseMvc();
app.UseWelcomePage();
app.UseJwtBearerAuthentication(options => {
options.Audience = "my_resource_server";
options.Authority = "http://localhost:60556/";
options.AutomaticAuthenticate = true;
options.RequireHttpsMetadata = false;
});
}
public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}
}
When I make this HTTP request to the resource server, I get a 401 Unauthorized:
GET /api/user/myroles HTTP/1.1
Host: localhost:64539
Authorization: Bearer eyJhbGciOiJS...
Content-Type: application/json;charset=utf-8
The controller who has a route to /api/user/myroles is decorated with a plain [Authorize] with no parameters.
I feel like I'm missing something in both auth and resource servers, but don't know what they are.
The other questions that ask "how to validate token issued by AspNet.Security.OpenIdConnect.Server" don't have an answer. I would appreciate some help in this.
Also, I've noticed that there is OAuth Introspection commented out in the sample provider, and have read somewhere that Jwt is not going to be supported soon. I can't find the dependency that gives me the OAuth Instrospection.
UPDATE I've included both of my startup.cs, from each of auth and resource servers. Could there be anything wrong that would cause the resource server to always return a 401 for every request?
One thing I didn't really touch throughout this whole endeavor is signing. It seems to generate a signature for the JWT at the auth server, but the resource server (I guess) doesn't know the signing keys. Back in the OWIN projects, I had to create a machine key and put on the two servers.

Edit: the order of your middleware instances is not correct: the JWT bearer middleware must be registered before MVC:
app.UseIISPlatformHandler();
app.UseJwtBearerAuthentication(options => {
options.Audience = "my_resource_server";
options.Authority = "http://localhost:60556/";
options.AutomaticAuthenticate = true;
options.RequireHttpsMetadata = false;
});
app.UseMvc();
app.UseWelcomePage();
Sure enough, when I have this HTTP request, I do get an access token, but I'm not sure if that access token has all the data that the resource server expects.
Your authorization server and resource server configuration look fine, but you're not setting the "destination" when adding your claims (don't forget that to avoid leaking confidential data, AspNet.Security.OpenIdConnect.Server refuses to serialize the claims that don't explicitly specify a destination):
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(ClaimTypes.Name, user.UserName, destination: "id_token token");
(await UM.GetRolesAsync(user)).ToList().ForEach(role => {
identity.AddClaim(ClaimTypes.Role, role, destination: "id_token token");
});
Also, I've noticed that there is OAuth Introspection commented out in the sample provider, and have read somewhere that Jwt is not going to be supported soon. I can't find the dependency that gives me the OAuth Instrospection.
Starting with the next beta (ASOS beta5, not yet on NuGet.org when writing this answer), we'll stop using JWT as the default format for access tokens, but of course, JWT will still be supported OTB.
Tokens now being opaque by default, you'll have to use either the new validation middleware (inspired from Katana's OAuthBearerAuthenticationMiddleware) or the new standard introspection middleware, that implements the OAuth2 introspection RFC:
app.UseOAuthValidation();
// Alternatively, you can also use the introspection middleware.
// Using it is recommended if your resource server is in a
// different application/separated from the authorization server.
//
// app.UseOAuthIntrospection(options => {
// options.AutomaticAuthenticate = true;
// options.AutomaticChallenge = true;
// options.Authority = "http://localhost:54540/";
// options.Audience = "resource_server";
// options.ClientId = "resource_server";
// options.ClientSecret = "875sqd4s5d748z78z7ds1ff8zz8814ff88ed8ea4z4zzd";
// });
You can find more information about these 2 middleware here: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/issues/185

Related

Migrating to Openiddict 4.0.0 breaks HotChocolate GraphQL authorization for client credentials flow if token encryption is enabled

I've an ASP.NET Core project which hosts both an identity server using Openiddict and a resource server using HotChocolate GraphQL package.
Client-credentials flow is enabled in the system. The token is encrypted using RSA algorithm.
Till now, I had Openiddict v3.1.1 and everything used to work flawlessly. Recently, I've migrated to Openiddict v4.0.0. Following this, the authorization has stopped working. If I disable token encyption then authorization works as expected. On enabling token encyption, I saw in debugging, that claims are not being passed at all. I cannot switch off token encyption, as it is a business and security requirement. The Openiddict migration guidelines doesn't mention anything about any change related to encryption keys. I need help to make this work as Openiddict v3.1.1 is no longer supported for bug fixes.
The OpenIddict setup in ASP.NET Core pipeline:
public static void AddOpenIddict(this IServiceCollection services, IConfiguration configuration)
{
var openIddictOptions = configuration.GetSection("OpenIddict").Get<OpenIddictOptions>();
var encryptionKeyData = openIddictOptions.EncryptionKey.RSA;
var signingKeyData = openIddictOptions.SigningKey.RSA;
var encryptionKey = RSA.Create();
var signingKey = RSA.Create();
encryptionKey.ImportFromEncryptedPem(encryptionKeyData.ToCharArray(),
openIddictOptions.EncryptionKey.Passphrase.ToCharArray());
signingKey.ImportFromEncryptedPem(signingKeyData.ToCharArray(),
openIddictOptions.SigningKey.Passphrase.ToCharArray());
encryptionKey.ImportFromEncryptedPem(encryptionKeyData.ToCharArray(),
openIddictOptions.EncryptionKey.Passphrase.ToCharArray());
signingKey.ImportFromEncryptedPem(signingKeyData.ToCharArray(),
openIddictOptions.SigningKey.Passphrase.ToCharArray());
var sk = new RsaSecurityKey(signingKey);
var ek = new RsaSecurityKey(encryptionKey);
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<AuthDbContext>()
.ReplaceDefaultEntities<Guid>();
})
.AddServer(options =>
{
// https://documentation.openiddict.com/guides/migration/30-to-40.html#update-your-endpoint-uris
options.SetCryptographyEndpointUris("oauth2/.well-known/jwks");
options.SetConfigurationEndpointUris("oauth2/.well-known/openid-configuration");
options.SetTokenEndpointUris("oauth2/connect/token");
options.AllowClientCredentialsFlow();
options.SetUserinfoEndpointUris("oauth2/connect/userinfo");
options.SetIntrospectionEndpointUris("oauth2/connect/introspection");
options.AddSigningKey(sk);
options.AddEncryptionKey(ek);
//options.DisableAccessTokenEncryption(); // If this line is not commented, things work as expected
options.UseAspNetCore(o =>
{
// NOTE: disabled because by default OpenIddict accepts request from HTTPS endpoints only
o.DisableTransportSecurityRequirement();
o.EnableTokenEndpointPassthrough();
});
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
}
Authorization controller token get action:
[HttpPost("~/oauth2/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest();
if (request.IsClientCredentialsGrantType())
{
// Note: the client credentials are automatically validated by OpenIddict:
// if client_id or client_secret are invalid, this action won't be invoked.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null)
{
throw new InvalidOperationException("The application details cannot be found in the database.");
}
// Create a new ClaimsIdentity containing the claims that
// will be used to create an id_token, a token or a code.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: OpenIddictConstants.Claims.Name,
roleType: OpenIddictConstants.Claims.Role);
var clientId = await _applicationManager.GetClientIdAsync(application);
var organizationId = await _applicationManager.GetDisplayNameAsync(application);
// https://documentation.openiddict.com/guides/migration/30-to-40.html#remove-calls-to-addclaims-that-specify-a-list-of-destinations
identity.SetClaim(type: OpenIddictConstants.Claims.Subject, value: organizationId)
.SetClaim(type: OpenIddictConstants.Claims.ClientId, value: clientId)
.SetClaim(type: "organization_id", value: organizationId);
identity.SetDestinations(static claim => claim.Type switch
{
_ => new[] { OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken }
});
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new NotImplementedException("The specified grant type is not implemented.");
}
Resource controller (where Authorization is not working):
public class Query
{
private readonly IMapper _mapper;
public Query(IMapper mapper)
{
_mapper = mapper;
}
[HotChocolate.AspNetCore.Authorization.Authorize]
public async Task<Organization> GetMe(ClaimsPrincipal claimsPrincipal,
[Service] IDbContextFactory<DbContext> dbContextFactory,
CancellationToken ct)
{
var organizationId = Ulid.Parse(claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier));
... // further code removed for brevity
}
}
}
GraphQL setup in ASP.NET Core pipeline:
public static void AddGraphQL(this IServiceCollection services, IWebHostEnvironment webHostEnvironment)
{
services.AddGraphQLServer()
.AddAuthorization()
.AddQueryType<Query>()
}
Packages with versions:
OpenIddict (4.0.0)
OpenIddict.AspNetCore (4.0.0)
OpenIddict.EntityFrameworkCore (4.0.0)
HotChocolate.AspNetCore (12.13.2)
HotChocolate.AspNetCore.Authorization (12.13.2)
HotChocolate.Diagnostics (12.13.2)
I think you can look at the logs to figure out exactly what's going on:
Logging in .NET Core and ASP.NET Core.
Not sure if the Openiddict 4.0 release changed encryption key related configuration, but if you can't get encryption keys to work perhaps you can remove the OpenIddict validation part and configure the JWT bearer handler instance to use the appropriate encryption key , a configuration similar to this.
Of course, you can also open an issue in GitHub to ask #Kévin Chalet.

ASP.NET Core enrich IIdentity with custom profile

I am using Azure AD to authorize and authenticate the users.
All users have a profile in the database.
I would like on login to always "merge" the Azure user with my database user.
This is the code I am using to setup authentication in my web api.
public static partial class ServiceCollectionExtensions
{
public static IServiceCollection AddBearerAuthentication(this IServiceCollection services,
OpenIdConnectOptions openIdConnectOptions)
{
#if DEBUG
IdentityModelEventSource.ShowPII = true;
#endif
services
.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", o =>
{
o.Authority = openIdConnectOptions.Authority;
o.TokenValidationParameters.ValidIssuer = openIdConnectOptions.ValidIssuer;
o.TokenValidationParameters.ValidAudiences = openIdConnectOptions.ValidAudiences;
});
return services;
}
}
Can someone point me in the right direction?
Right now I am loading the user in all of my controllers, not pretty at all.
Not sure what do you mean by "merge" the user. But if it's just some logic you want to run for every incoming http request, you could just add a custom middleware
app.Use(async (context, next) =>
{
var user = await context.RequestServices
.GetRequiredService<DatabaseContext>()
.Users
.Where(....)
.SingleOrDefaultAsync();
...
await next(context);
});
Alternatively, if you want to couple your code with the authentication process very much, you could use the callback from JwtBearerOptions
.AddJwtBearer("Bearer", o =>
{
...
o.Events.OnTokenValidated = async context =>
{
var user = await context.HttpContext
.RequestServices
.GetRequiredService....
...
};
}
But personally, I think both approaches are bad. Going to the DB to get the user's credentials with every request is bad for performance. Also, it kinda defies the whole point of the JWT, which was designed specifically to not do that. The token should already contain all the claims inside. If it doesn't, I would suggest reconfiguring azure AD, or switch to self-issued tokens.

How to pass authorization token in every request

After the user logs in I verify their info and generate a JWT token.
Authentication process happens with Authentication (it's not my custom handler).
Where and how do I save this token so it will be sent along the http calls? I don't want to save it in the client side because of XSS attacks. The following doesn't seem to work either as I wont be in every request
HttpContext.Request.Headers.Append("Authorization", MyGeneratedJWTTokenAsString);
I have found answers that use HttpClient.Request but is there any other secure way of doing this?
When using HttpClient in a backend service, it is always good to use the IHttpClientFactory to generate clients.
So, what we are going to do is use this factory (in conjunction with IHttpContextAccessor) to produce HttpClient objects that have the current user's authorization scheme and token. So, add this to your ConfigureServices method in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddHttpClient("UserAuthorizedHttpClient", (sp, httpClient) =>
{
var accessor = sp.GetRequiredService<IHttpContextAccessor>();
if (accessor.HttpContext.Request.Headers.TryGetValue(
"Authorization", out var authHeaderValue) &&
AuthenticationHeaderValue.TryParse(
authHeaderValue, out var auth))
{
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(auth.Scheme, auth.Parameter);
}
else
{
// incase there is a value from a previous generation
if(httpClient.DefaultRequestHeaders.Contains("Authorization"))
{
httpClient.DefaultRequestHeaders.Remove("Authorization");
}
}
});
services.AddHttpContextAccessor();
// ...
}
In order to use these special clients, you simply inject IHttpClientFactory in to the service that needs to make the HTTP requests:
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace YouApplicationNamespace.Services
{
public interface IMyHttpRequesterService
{
Task DoSomethingCoolAsync();
}
public sealed class MyHttpRequesterService : IMyHttpRequesterService
{
private readonly IHttpClientFactory _httpClientFactory;
public MyHttpRequesterService(IHttpClientFactory httpClientFactory) =>
_httpClientFactory = httpClientFactory;
public async Task DoSomethingCoolAsync()
{
var authroizedHttpClient =
_httpClientFactory.CreateClient("UserAuthorizedHttpClient");
var resp = await authroizedHttpClient.GetAsync(new Uri("https://www.example.com/"));
// ...
}
}
}
As long as you use the same name, you will get a client that uses the AddHttpClient routine in your configuration.
(Please note: this code is not tested. It is more of a guideline)

ServiceStack ServiceClient stores wrong cookies after authentication

i have a strange problem with Servicestack Authentication.
I've developed an Asp .Net Core web app (.net core 3.1) in which is implemented a servicestack authentication with credentials auth provider. Everything work correctly if i authenticate with any browsers.
Instead if i try to authenticate from external application with JsonServiceClient pointing to servicestack /auth/{provider} api i've this problem:
authentication goes well but the JsonServiceClient object stores a SessionId in cookies (s-id/s-pid) different from the SessionId of AuthenticateResponse. Here my example.
Authenticate request = new Authenticate()
{
provider = "credentials",
UserName = username,
Password = password,
RememberMe = true
};
var client = new JsonServiceClient(webappUrl);
AuthenticateResponse response = await client.PostAsync(request);
var cookies = client.GetCookieValues();
If i check values in cookies variable i see that there are s-id and s-pid completely different from the sessionId of the response.
The other strange thing is that if i repeat the authentication a second time under those lines of code, now the s-pid cookie is equal to sessionId of response!
Why??
In the startup of web app i have these lines of code:
public new void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => options.EnableEndpointRouting = false);
// Per accedere all'httpcontext della request
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Per accedere alla request context della request
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
// Registro il json di configurazione (innietta l'appSettings)
services.AddSingleton(Configuration);
// Filters
services.AddSingleton<ModulePermissionFilter>();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
... other lines of code
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IBackgroundJobClient backgroundJobs)
{
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseServiceStack(new AppHost
{
AppSettings = new NetCoreAppSettings(Configuration)
});
}
public class AppHost : AppHostBase
{
public AppHost() : base("webapp", typeof(BaseServices).Assembly) { }
// Configure your AppHost with the necessary configuration and dependencies your App needs
public override void Configure(Container container)
{
SetConfig(new HostConfig
{
UseCamelCase = false,
WriteErrorsToResponse = true,
ReturnsInnerException = true,
AllowNonHttpOnlyCookies = false,
DebugMode = AppSettings.Get(nameof(HostConfig.DebugMode), HostingEnvironment.IsDevelopment()),
// Restrict cookies to domain level in order to support PflowV2
RestrictAllCookiesToDomain = !string.IsNullOrEmpty(AppSettings.Get("RestrictAllCookiesToDomain", "")) && AppSettings.Get("RestrictAllCookiesToDomain", "").ToLower() != "localhost" ? AppSettings.Get("RestrictAllCookiesToDomain", "") : null
});
// Create DBFactory for cache
var defaultConnection = appHost.AppSettings.Get<string>("ConnectionStrings:Webapp");
var dbFactory = new OrmLiteConnectionFactory(defaultConnection, SqlServerDialect.Provider);
// Register ormlite sql session and cache
appHost.Register<IDbConnectionFactory>(dbFactory);
appHost.RegisterAs<OrmLiteCacheClient, ICacheClient>();
appHost.Resolve<ICacheClient>().InitSchema();
appHost.Register<ISessionFactory>(new SessionFactory(appHost.Resolve<ICacheClient>()));
//Tell ServiceStack you want to persist User Auth Info in SQL Server
appHost.Register<IAuthRepository>(new OrmLiteAuthRepository(dbFactory));
appHost.Resolve<IAuthRepository>().InitSchema();
var sessionMinute = appHost.AppSettings.Get("SessionTimeoutMinute", 15);
// Adding custom usersession and custom auth provider
Plugins.Add(new AuthFeature(() => new CustomUserSession(), new IAuthProvider[] { new CustomCredentialsAuthProvider(), new ApiKeyAuthProvider() })
{
HtmlRedirect = "/Account/Login", // Redirect to login if session is expired
IncludeAssignRoleServices = false,
SessionExpiry = TimeSpan.FromHours(sessionMinute),
});
Plugins.Add(new SessionFeature());
}
}

Version problem using MSAL to access Azure AD and get back an auth id token

Asp.Net Core v2.2.0
Microsoft.AspNetCore.Authentication.AzureAD.UI v2.2.0
Microsoft.Identity.Client v4.2.1
I'm receiving the following error when logging into Azure AD and then requesting an auth id token:
While searching for a solution, the closest thing I've found is that there's an issue with using two different versions of the auth api. V2 uses login.microsoftonline.com and V1 uses sts.windows.net. The question I have is how to get everything in the MSAL library to use V2.
Here's my Startup class. It's based (largely copied) from the doc: Web app that calls web APIs - code configuration
public class Startup
{
private const string AzureAdConfigSectionName = "AzureAd";
private ConfidentialClientApplicationOptions applicationOptions;
private AzureADOptions azureAdOptions;
private MsalPerUserSessionTokenCacheProvider userTokenCacheProvider;
private MsalAppSessionTokenCacheProvider appTokenCacheProvider;
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
applicationOptions = new ConfidentialClientApplicationOptions();
Configuration.Bind(AzureAdConfigSectionName, applicationOptions);
azureAdOptions = new AzureADOptions();
Configuration.Bind(AzureAdConfigSectionName, azureAdOptions);
//services.AddOptions<AzureADOptions>();
var adOptionsMonitor = services.BuildServiceProvider().GetService<IOptionsMonitor<AzureADOptions>>();
userTokenCacheProvider = new MsalPerUserSessionTokenCacheProvider();
appTokenCacheProvider = new MsalAppSessionTokenCacheProvider(adOptionsMonitor);
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind(AzureAdConfigSectionName, options));
ConfigureSession(services);
ConfigureTokenHandling(services);
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
private void ConfigureSession(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
}
private void ConfigureTokenHandling(IServiceCollection services)
{
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
// Response type. We ask ASP.NET to request an Auth Code, and an IDToken
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
// This "offline_access" scope is needed to get a refresh token when users sign in with
// their Microsoft personal accounts
// (it's required by MSAL.NET and automatically provided by Azure AD when users
// sign in with work or school accounts, but not with their Microsoft personal accounts)
options.Scope.Add("offline_access");
options.Scope.Add("user.read"); // for instance
// Handling the auth redemption by MSAL.NET so that a token is available in the token cache
// where it will be usable from Controllers later (through the TokenAcquisition service)
var handler = options.Events.OnAuthorizationCodeReceived;
options.Events.OnAuthorizationCodeReceived = async context =>
{
// As AcquireTokenByAuthorizationCode 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.
context.HandleCodeRedemption();
// Call MSAL.NET AcquireTokenByAuthorizationCode
var application = BuildConfidentialClientApplication(context.HttpContext,
context.Principal);
var scopes = new [] { "user.read" };
var scopesRequestedByMsalNet = new[] { "openid", "profile", "offline_access" };
var result = await application
.AcquireTokenByAuthorizationCode(scopes.Except(scopesRequestedByMsalNet),
context.ProtocolMessage.Code)
.ExecuteAsync();
// Do not share the access token with ASP.NET Core otherwise ASP.NET will cache it
// and will not send the OAuth 2.0 request in case a further call to
// AcquireTokenByAuthorizationCodeAsync in the future for incremental consent
// (getting a code requesting more scopes)
// Share the ID Token so that the identity of the user is known in the application (in
// HttpContext.User)
context.HandleCodeRedemption(null, result.IdToken);
// Call the previous handler if any
await handler(context);
};
});
}
/// <summary>
/// Creates an MSAL Confidential client application
/// </summary>
/// <param name="httpContext">HttpContext associated with the OIDC response</param>
/// <param name="claimsPrincipal">Identity for the signed-in user</param>
/// <returns></returns>
private IConfidentialClientApplication BuildConfidentialClientApplication(HttpContext httpContext,
ClaimsPrincipal claimsPrincipal)
{
var request = httpContext.Request;
// Find the URI of the application)
var currentUri = UriHelper.BuildAbsolute(request.Scheme,
request.Host,
request.PathBase,
azureAdOptions.CallbackPath ?? String.Empty);
// Updates the authority from the instance (including national clouds) and the tenant
var authority = $"{azureAdOptions.Instance}{azureAdOptions.TenantId}/";
// Instantiates the application based on the application options (including the client secret)
var app = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(applicationOptions)
.WithRedirectUri(currentUri)
.WithAuthority(authority)
.Build();
// Initialize token cache providers. In the case of Web applications, there must be one
// token cache per user (here the key of the token cache is in the claimsPrincipal which
// contains the identity of the signed-in user)
userTokenCacheProvider?.Initialize(app.UserTokenCache, httpContext, claimsPrincipal);
appTokenCacheProvider?.Initialize(app.AppTokenCache, httpContext);
return app;
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
IdentityModelEventSource.ShowPII = true;
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios,
// see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSession();
app.UseAuthentication();
app.UseMvc();
}
}
The context received by the OnAuthorizationCodeReceived event, has the following:
JwtSecurityToken.Issuer = https://sts.windows.net
Not sure why, but that's where the issue is coming from.
appsettings.json
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "{domain}",
"TenantId": "{tenant id}",
"ClientId": "{client id}",
"CallbackPath": "/signin-oidc",
"ClientSecret": "{client secret}"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
The problem turned out to be I was using
AzureADDefaults.OpenIdScheme
instead of
AzureADDefaults.AuthenticationScheme (Default Azure AD scheme)
Which makes perfect sense, considering the problem.