User is authenticated after OpenId Connect (AAD) but Unable to find access token - authentication

I am trying to find the access token from AAD after user is authenticated from OpenId Connect. It is a web application integrated with AAD OpenId Connect. I need to get the access token to call another API that uses the same AAD. Here's what I've tried:
Clone this sample code.
In Startup.cs file, add the following block of code:
public void ConfigureServices(IServiceCollection services) {
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5000";
options.ClientId = "<<client-id>>";
options.SignInScheme = "cookie";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = false;
})
.AddCookie();
services.AddMvc();}
In HomeController class, I added a private variable called httpContextAccessor and also set it in the constructor.
private IHttpContextAccessor _httpContextAccessor;
public HomeController(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
In the HomeController class, I added some code to access the access token.
public IActionResult Index()
{
if (User.Identity.IsAuthenticated)
{
var attempt1 = Request.Headers["Authorization"];
var attempt2 = HttpContext.GetTokenAsync("access_token");
var attempt3 = _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
var attempt4 = _httpContextAccessor.HttpContext.Request.Headers["Authorziation"];
}
return View();
}
But all of them return either empty or null. Did I miss anything?
I've looked at this following posts for reference:
How to refresh access token
How to get access token from HttpContext in .Net core 2.0

You need to set SaveTokens to true in OpenID Connect configuration:
Clone that code sample
Keep the Startup.cs , you don't need to add .AddOpenIdConnect part , AddAzureAd extension method would help add Azure Active Directory Authentication to your application.
Modify the AzureAdAuthenticationBuilderExtensions.cs in Extensions folder :
public void Configure(string name, OpenIdConnectOptions options)
{
options.ClientId = _azureOptions.ClientId;
options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
options.UseTokenLifetime = true;
options.CallbackPath = _azureOptions.CallbackPath;
options.RequireHttpsMetadata = false;
options.SaveTokens = true; // set to true
}
Then you can get the ID token from httpContextAccessor:
var idToken = _httpContextAccessor.HttpContext.GetTokenAsync("id_token");
But the access token is still null . The sample shows how to use the OpenID Connect ASP.NET Core middleware to sign-in users from a single Azure AD tenant , that means you can get the ID Token which is sent to the client application as part of an OpenID Connect flow and is used by the client to authenticate the user. Please refer to document : ID tokens .
While Access tokens enable clients to securely call APIs protected by Azure . Please refer to document : Azure Active Directory access tokens .
If you want to get the access token for accessing resource which protected by Azure AD , you should use ADAL(Azure AD V1.0 endpoint) to obtain the token , see code sample(especially use OnAuthorizationCodeReceived to acquire access token):
https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-openidconnect-aspnetcore
Or use MSAL if you are using Azure AD V2.0 endpoint .

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.

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.

Hangfire Dashboard in .Net Core Web API Project

We have a .Net Core 2.0 Web API project. I have added the hangfire there. We don't have any web page in the project and I use JWT for authorization. So I'm not able to do the authorization for hangfire using the Authorize(DashboardContext context). Is there any way we can pass some sort of API key on the url to authorize the user for dashboard?
Thanks
Yes, you can do that by using cookies, I will explain the idea to you with some code...
Firstly once the user login (you generate the token) you must store the token you generated to cookies in the browser and then when you want to access to Hangfire dashboard you must read the token from cookies and then check the roles...
the code to store the token in cookies:
`
httpContext.Response.Cookies.Append("token", userToken.AccessToken,
new Microsoft.AspNetCore.Http.CookieOptions { Expires = DateTime.Now.AddMinutes(6000) });
`
make sure you enabled cookies by:
`
AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie()
.AddJwtBearer(cfg => your configs);
`
then the authorize method will be like this:
`
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
var jwtToken = string.Empty;
if (httpContext.Request.Cookies.ContainsKey("token"))
{
httpContext.Request.Cookies.TryGetValue("token", out jwtToken);
}
else
return false;
if (string.IsNullOrEmpty(jwtToken))
{
return false;
}
var handler = new JwtSecurityTokenHandler();
try
{
var claim = _tokenService.GetClaimsPrincipal(jwtToken);
return claim != null && claim.IsInRole(RolesConstants.ADMIN);
}
catch (Exception exception)
{
throw exception;
}
}
`
Implement an IAuthorizationDashboardFilter class.
Try this post here.
You need to pass JWT token with each request, try storing token in a cookie or as a query string parameter. Then you can pull that from request context and decide whether user is authorized
public class MyAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
var token = context.Request.Cookies["access-token"];
// check token validity and set authentication accordingly.
return httpContext.User.Identity.IsAuthenticated;
}
}
Register the filter in your OWIN pipeline like this, after whatever authentication method you are using. Then the logged in user claim will be available in the filter
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(...); // Authentication - first
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new [] { new MyAuthorizationFilter() }
}); // Hangfire - last
}

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

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