Caching Issue: ASP.NET Core, failed to get access token - asp.net-core

GetUserAccessTokenAsync by using this method I am trying to get the Token, The same code working fine in POC App, where as in the real application I am getting TokenNotFound.
User is authenticated and able to get the userId too, at the end I am getting account as null, it leads to the main error.
public async Task<string> GetUserAccessTokenAsync(string userId)
{
var account = await _app.GetAccountAsync(userId);
if (account == null) throw new ServiceException(new Error
{
Code = "TokenNotFound",
Message = "User not found in token cache. Maybe the server was restarted."
});
try
{
var result = await _app.AcquireTokenSilent(_scopes, account).ExecuteAsync();
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (Exception)
{
throw new ServiceException(new Error
{
Code = GraphErrorCode.AuthenticationFailure.ToString(),
Message = "Caller needs to authenticate. Unable to retrieve the access token silently."
});
}
}
Note: The same code working fine in POC application, I tried with same app resource and created new app resource too.
Please help me what exactly I am missing...

I have a sample with asp.net core mvc, which integrate MSAL to obtain access token for calling Ms graph api. I don't know what is POC App actually, but per my searching, AcquireTokenSilent may from MSAL.js
Here's the detail.
Adding configurations in appsetting file about azure ad:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "your_tenant_name.onmicrosoft.com",
"TenantId": "tenant_id",
"ClientId": "azure_ad_client_id",
"ClientSecret": "client_secret",
"CallbackPath": "/signin-oidc",//don't forget to set redirect url in azure portal
"SignedOutCallbackPath ": "/signout-callback-oidc"
}
installing packages:
<PackageReference Include="Microsoft.Identity.Web" Version="1.23.1" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.23.1" />
StartUp.cs, don't forget to add app.UseAuthentication();app.UseAuthorization(); in Configure method:
public void ConfigureServices(IServiceCollection services)
{
services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "user.read" })
.AddInMemoryTokenCaches();
// Require authentication
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
// Add the Microsoft Identity UI pages for signin/out
.AddMicrosoftIdentityUI();
}
And my test Controller:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web;
using System.Diagnostics;
using System.Threading.Tasks;
using WebAppMvc.Models;
namespace WebAppMvc.Controllers
{
[Authorize]
public class HomeController : Controller
{
private readonly ITokenAcquisition _tokenAcquisition;
public HomeController(ITokenAcquisition tokenAcquisition)
{
_tokenAcquisition = tokenAcquisition;
}
public async Task<IActionResult> IndexAsync()
{
string[] scopes = new string[] { "user.read" };
string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
return View();
}
}
}
If the redirection url is wrong after signing in, just changing the url in browser manually to localhost:xxx/home

At Finally I have followed below link it helped to get rid of my all queries for graph API;
https://github.com/microsoftgraph/msgraph-training-aspnet-core

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.

Custom Login Path for ASP.NET CORE 6 and Azure AD

I currently have a page in "/login/index" with my logo and a form/button on it which on POST will initiate the challenge for MS Azure AD login to authenticate the user. The user is then redirected back to the home page after login. However, currently with the default setup for Azure AD authentication a user never sees this "/login/index" page because they are forced to MS Azures ADs login page for all request paths if not authenticated. Is there a way to force users to this initial login page I setup so that they can click the button to go authenticate?
My program.cs is as follows:
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using Project.Models;
using DocumentFormat.OpenXml.Office2016.Drawing.ChartDrawing;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToFolder("/Login");
options.Conventions.AuthorizeFolder("/");
options.Conventions.AuthorizeFolder("/files");
});
//authentication pipline
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
builder.Services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
//We are using this so we can find the modified date later on. If we move to Box or Onedrive we may not need this.
var RootPath = builder.Environment.ContentRootPath;
var WebPath = builder.Environment.WebRootPath;
var fileDirectory = Path.Combine(Directory.GetParent(RootPath).Parent.ToString(), "files");
IFileProvider physicalProvider = new PhysicalFileProvider(fileDirectory);
builder.Services.AddSingleton<IFileProvider>(physicalProvider);
//Not needed. We are not using this level of abstraction but may move towards it one day so possibly keep.
var connectionString = builder.Configuration.GetConnectionString("DBContext");
builder.Services.AddDbContext<DbContext>(options => options.UseSqlServer(connectionString));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
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();
}
else
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
//We are making it so armsfiles are not accessible outside of arms so if we move to box or onedrive then the parameter may need to be removed.
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = physicalProvider,
RequestPath = "/files"
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
app.Run();
Before using Azure AD for authentication I would use this cookie policy in my startup class (I have since converted to minimal hosting model in program.cs) to force users to the login page (which although different now but still similar concept to what I am trying to achieve):
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(cookieOptions =>
{
cookieOptions.Cookie.Name = "UserLoginCookie";
cookieOptions.LoginPath = "/Login/";
cookieOptions.ExpireTimeSpan = TimeSpan.FromMinutes(30);
cookieOptions.SlidingExpiration = true;
});
I follow the official doc about Quickstart: Add sign-in with Microsoft to a web app. And I downloaded the repo and test it.
You can check the test result first, please confirm if this is what you want?
Test Result
What I changed in the sample project
Change the appsettings.json file in project.
Copy the code from Home/Index method, and create a new page LoginSuccess then paste the code to Home/LoginSuccess. And modify the attribute, you can copy my sample code directly.
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Identity.Web;
using Microsoft.Graph;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using active_directory_aspnetcore_webapp_openidconnect_v2.Models;
namespace active_directory_aspnetcore_webapp_openidconnect_v2.Controllers
{
[Authorize]
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly GraphServiceClient _graphServiceClient;
public HomeController(ILogger<HomeController> logger,
GraphServiceClient graphServiceClient)
{
_logger = logger;
_graphServiceClient = graphServiceClient;
}
[AllowAnonymous]
public async Task<IActionResult> Index()
{
//var user = await _graphServiceClient.Me.Request().GetAsync();
ViewData["ApiResult"] = "Demo";//user.DisplayName;
return View();
}
[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
public async Task<IActionResult> LoginSuccess()
{
var user = await _graphServiceClient.Me.Request().GetAsync();
ViewData["ApiResult"] = user.DisplayName;
return View();
}
[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
public IActionResult Privacy()
{
return View();
}
[AllowAnonymous]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
Create LoginSuccess page(copy from index page).

Getting 401 from custom Api when using Microsoft.Identity.platform to protect api

I am following the tutorial from Microsfot.document for how to protect api using Azure AD (Microsoft Identity).
The steps I took are following: Sorry I tried to put information that might be helpful but too much to get to the issue most of the time contributors ask for screenshot or the code.
I followed several documents and video tutorials but here is the link for one of them: https://learn.microsoft.com/en-us/learn/modules/identity-secure-custom-api/2-secure-api-microsoft-identity
WebApi.
Created a webapi using core 5. Register it in Azure AD.
Created single scope 'check' and allowed permission to user and admin.
Webapp
Created webapp using .net(classic) Note that webapi is core 5.
Created a webapp and register it in Azure AD.
Setup the authentication and created a OnAuthorizationCodeReceived to get the access token to call the api.
Configuration:
1.From Azure AD->Registration for Webapi-> selected application(web app created above) and give permission to the scope.
2. For Azure AD->Registration for webapp-> Access permission->delegate->selected the scope.
Test:
1.Run the test. At this point; I do not have [Authorization] on the api method which I am calling.
2. Webapp successfully able to get the string which is returned by the api. Somewhat I get the idea that plumbing was right.
Added [Authorize] filter on the app method.
Result 401 unauthorized.
I have checked multiple times and looked at multiple tutorial and rewrote my code, watched several videos and updated my code but I am always getting 401 error.
Below is the code;
Api controller:
namespace Utility.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class AzAdUtility : ControllerBase
{
// GET: api/<AzAdUtility>
[HttpGet]
public string Get()
{
//HttpContext.VerifyUserHasAnyAcceptedScope(new string[] {"check"});
var name = "Vaqas";
return name;
}
}
}
Api startup :
public class Startup
{
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)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "GlobalNetApiUtility", Version = "v1" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Utility v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Api Appsettings:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "myorg.onmicrosoft.com",
"ClientId": "abcd.............................",
"TenantId": "dabcd.............................."
},
Webapp startup:
Only adding startup page because at first all I am doing getting some data for testing purpose in the OnAuthorizationCodeReceived.
public class Startup
{
// The Client ID is used by the application to uniquely identify itself to Azure AD.
static string clientId = System.Configuration.ConfigurationManager.AppSettings["ClientId"];
// RedirectUri is the URL where the user will be redirected to after they sign in.
string redirectUri = System.Configuration.ConfigurationManager.AppSettings["RedirectUri"];
// Tenant is the tenant ID (e.g. contoso.onmicrosoft.com, or 'common' for multi-tenant)
static string tenant = System.Configuration.ConfigurationManager.AppSettings["Tenant"];
// Authority is the URL for authority, composed by Microsoft identity platform endpoint and the tenant name (e.g. https://login.microsoftonline.com/contoso.onmicrosoft.com/v2.0)
string authority = String.Format(System.Globalization.CultureInfo.InvariantCulture, System.Configuration.ConfigurationManager.AppSettings["Authority"], tenant);
//string authority = "https://login.microsoftonline.com/" + tenant + "/adminconsent?client_id=" + clientId;
string clientSecret = System.Configuration.ConfigurationManager.AppSettings["ClientSecret"];
/// <summary>
/// Configure OWIN to use OpenIdConnect
/// </summary>
/// <param name="app"></param>
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Sets the ClientId, authority, RedirectUri as obtained from web.config
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
// PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it is using the home page
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
// ResponseType is set to request the code id_token - which contains basic information about the signed-in user
//ResponseType = OpenIdConnectResponseType.CodeIdToken,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed
}
}
);
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
notification.HandleCodeRedemption();
var idClient = ConfidentialClientApplicationBuilder.Create(clientId)
.WithRedirectUri(redirectUri)
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.Build();
try
{
var apiScope = "api://28......................../check2 api://28................/check api://28...........................1d/AzAdUtility.Get";
string[] scopes = apiScope.Split(' ');
//gettig the token no issues.
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
var myurl = "https://localhost:99356/api/AzAdUtility";
var client = new HttpClient();
// var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(Constants.ProductCatalogAPI.SCOPES);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken); //accessToken
var json = await client.GetStringAsync(myurl);
var serializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
//getting 401 response
var checkResponse = JsonSerializer.Deserialize(json, typeof(string), serializerOptions) as string;
}
catch (Exception ex)
{
string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
notification.HandleResponse();
notification.Response.Redirect($"/Home/Error?message={message}&debug={ex.Message}");
}
}
/// <summary>
/// Handle failed authentication requests by redirecting the user to the home page with an error in the query string
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
context.HandleResponse();
context.Response.Redirect("Error/AccessDenied/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
}
In Api startup class I was missing app.UseAuthentication().
I never really thought that would be an issue. Once I added this code. I got the expected response rather than 401 Unauthorized error

.Net Core / Identity Server - Is it possible to AllowAnonymous but only from my client?

I have a REST API and an IdentityServer set up. I would like to be able to display items in my client from the API without having to sign in. However, I would also like to protect my API from external clients that don't belong to me. Is it possible to AllowAnonymous but only from my client?
[HttpGet]
[AllowAnonymous]
public List<Item> GetItems()
{
return new List<Item> { "item1", "item2" };
}
Edit w/ Solution
As mentioned by Tore Nestenius, I changed the grant types from Code to CodeAndClientCredentials and added the Authorize attribute to my controller so that only my client can access it.
Controller:
[HttpGet]
[Authorize]
public List<Item> GetItems()
{
return new List<Item> { "item1", "item2" };
Identity Server 4 Config File:
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "postman-api",
AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
}
};
}
CORS only works for requests from browsers, if a non browser application makes a request, then CORS will not be involved.
if you use [AllowAnonymous], then any client can access that API endpoint. Either you create separate client for the general things, perhaps using the Client Credentials flow, so that the client can authenticate, get its own token without any user involved.
Turns out this is handled by CORS.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddDefaultPolicy(
builder => builder
.WithOrigins("yourURL")
.AllowAnyMethod());
})
}

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