IdentityServer4 Signing Certificate Reference tokens - asp.net-core

Getting errors when an api is trying to validate a reference token. Our identity server will serve reference tokens only. Why would a signing certificate be required. The error is keyset related.
System.InvalidOperationException: Policy error while contacting the discovery endpoint https://****.net/.well-known/openid-configuration: Keyset is missing
at IdentityModel.AspNetCore.OAuth2Introspection.PostConfigureOAuth2IntrospectionOptions.GetIntrospectionEndpointFromDiscoveryDocument(OAuth2IntrospectionOptions Options)
at IdentityModel.AspNetCore.OAuth2Introspection.PostConfigureOAuth2IntrospectionOptions.InitializeIntrospectionClient(OAuth2IntrospectionOptions Options)
at IdentityModel.AspNetCore.OAuth2Introspection.OAuth2IntrospectionHandler.LoadClaimsForToken(String token)
at IdentityModel.AspNetCore.OAuth2Introspection.OAuth2IntrospectionHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.AuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
at IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler.HandleAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.AuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware.Invoke(HttpContext context)
at Ips.Middleware.SerilogMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

You can stop the key information being return from the DiscoveryEndpoint by setting IdentityServerOptions in IdentityServer:
options.Discovery.ShowKeySet = false;
Looking the implementation of PostConfigureOAuth2IntrospectionOptions.InitializeIntrospectionClient:
private async Task<IntrospectionClient> InitializeIntrospectionClient(OAuth2IntrospectionOptions Options)
{
string endpoint;
if (Options.IntrospectionEndpoint.IsPresent())
{
endpoint = Options.IntrospectionEndpoint;
}
else
{
endpoint = await GetIntrospectionEndpointFromDiscoveryDocument(Options).ConfigureAwait(false);
Options.IntrospectionEndpoint = endpoint;
}
IntrospectionClient client;
if (Options.IntrospectionHttpHandler != null)
{
client = new IntrospectionClient(
endpoint,
headerStyle: Options.BasicAuthenticationHeaderStyle,
innerHttpMessageHandler: Options.IntrospectionHttpHandler);
}
else
{
client = new IntrospectionClient(endpoint);
}
client.Timeout = Options.DiscoveryTimeout;
return client;
}
You can avoid the call to GetIntrospectionEndpointFromDiscoveryDocument by setting the IntrospectionEndpoint property on OAuth2IntrospectionOptions

Found the solution. You do not need to make changes to identity. The changes are to the api.
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(
IdentityServerAuthenticationDefaults.AuthenticationScheme,
//Null if you do not want to support jwt bearer tokens
null,
options =>
{
options.Authority = "https://yourIdentityServer.com";
//This is the key
options.DiscoveryPolicy.RequireKeySet = false;
options.ClientId = "xxxx";
options.ClientSecret = "xxxx";
});

It looks like something is validating the discovery document to ensure it’s well formed. You could probably disable this validation by overriding the policy but since you’ll need a signing key for id_tokens anyway you might as well set up the signing and validation credentials.

Related

How to add JWT Bearer Token to ALL requests to an API

I'm in the process of trying to put together a small project which uses Asp.Net Core Identity, Identity Server 4 and a Web API project.
I've got my MVC project authenticating correctly with IdS4 from which I get a JWT which I can then add to the header of a request to my Web API project, this all works as expected.
The issue I have is how I'm actually adding the token to the HttpClient, basically I'm setting it up for every request which is obviously wrong otherwise I'd have seen other examples online, but I haven't been able to determine a good way to refactor this. I've read many articles and I have found very little information about this part of the flow, so I'm guessing it could be so simple that it's never detailed in guides, but I still don't know!
Here is an example MVC action that calls my API:
[HttpGet]
[Authorize]
public async Task<IActionResult> GetFromApi()
{
var client = await GetHttpClient();
string testUri = "https://localhost:44308/api/TestItems";
var response = await client.GetAsync(testUri, HttpCompletionOption.ResponseHeadersRead);
var data = await response.Content.ReadAsStringAsync();
GetFromApiViewModel vm = new GetFromApiViewModel()
{
Output = data
};
return View(vm);
}
And here is the GetHttpClient() method which I call (currently residing in the same controller):
private async Task<HttpClient> GetHttpClient()
{
var client = new HttpClient();
var expat = HttpContext.GetTokenAsync("expires_at").Result;
var dataExp = DateTime.Parse(expat, null, DateTimeStyles.RoundtripKind);
if ((dataExp - DateTime.Now).TotalMinutes < 10)
{
//SNIP GETTING A NEW TOKEN IF ITS ABOUT TO EXPIRE
}
var accessToken = await HttpContext.GetTokenAsync("access_token");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return client;
}
My StartUp classes are pretty standard from what I gather, but if they could be useful, then I'll add them in.
I've read many articles and I have found very little information about this part of the flow, so I'm guessing it could be so simple that it's never detailed in guides, but I still don't know!
The problem is that the docs are really spread all over, so it's hard to get a big picture of all the best practices. I'm planning a blog series on "Modern HTTP API Clients" that will collect all these best practices.
First, I recommend you use HttpClientFactory rather than new-ing up an HttpClient.
Next, adding an authorization header is IMO best done by hooking into the HttpClient's pipeline of message handlers. A basic bearer-token authentication helper could look like this:
public sealed class BackendApiAuthenticationHttpClientHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _accessor;
public BackendApiAuthenticationHttpClientHandler(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var expat = await _accessor.HttpContext.GetTokenAsync("expires_at");
var dataExp = DateTime.Parse(expat, null, DateTimeStyles.RoundtripKind);
if ((dataExp - DateTime.Now).TotalMinutes < 10)
{
//SNIP GETTING A NEW TOKEN IF ITS ABOUT TO EXPIRE
}
var token = await _accessor.HttpContext.GetTokenAsync("access_token");
// Use the token to make the call.
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
This can be hooked up via DI:
services.AddTransient<BackendApiAuthenticationHttpClientHandler>();
services.AddHttpClient<MyController>()
.ConfigureHttpClient((provider, c) => c.BaseAddress = new Uri("https://localhost:44308/api"))
.AddHttpMessageHandler<BackendApiAuthenticationHttpClientHandler>();
Then you can inject an HttpClient into your MyController, and it will magically use the auth tokens:
// _client is an HttpClient, initialized in the constructor
string testUri = "TestItems";
var response = await _client.GetAsync(testUri, HttpCompletionOption.ResponseHeadersRead);
var data = await response.Content.ReadAsStringAsync();
GetFromApiViewModel vm = new GetFromApiViewModel()
{
Output = data
};
return View(vm);
This pattern seems complex at first, but it separates the "how do I call this API" logic from "what is this action doing" logic. And it's easier to extend with retries / circuit breakers / etc, via Polly.
You can use HttpRequestMessage
// Create this instance once on stratup
// (preferably you want to keep an instance per base url to avoid waiting for socket fin)
HttpClient client = new HttpClient();
Then create an instance of HttpRequestMessage:
HttpRequestMessage request = new HttpRequestMessage(
HttpMethod.Get,
"https://localhost:44308/api/TestItems");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "ey..");
await client.SendAsync(request);

jwt refresh tokens without saving anywhere

I have implemented refresh tokens in ASP.NET framework like below. It does not require refresh token to be saved anywhere. Can i do the same in ASP.NET Core JWT? The thing I want is not to save the refresh token in the DB or anywhere else.
public class RefreshTokenProvider : IAuthenticationTokenProvider
{
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
Create(context);
}
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
Receive(context);
}
public void Create(AuthenticationTokenCreateContext context)
{
object inputs;
context.OwinContext.Environment.TryGetValue("Microsoft.Owin.Form#collection", out inputs);
var grantType = ((FormCollection)inputs)?.GetValues("grant_type");
var grant = grantType.FirstOrDefault();
if (grant == null || grant.Equals("refresh_token")) return;
context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(Constants.RefreshTokenExpiryInDays);
context.SetToken(context.SerializeTicket());
}
public void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
if (context.Ticket == null)
{
context.Response.StatusCode = 400;
context.Response.ContentType = "application/json";
context.Response.ReasonPhrase = "invalid token";
return;
}
if (context.Ticket.Properties.ExpiresUtc <= DateTime.UtcNow)
{
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
context.Response.ReasonPhrase = "unauthorized";
return;
}
context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(Constants.RefreshTokenExpiryInDays);
context.SetTicket(context.Ticket);
}
}
JWT tokens are automatically refreshed when using JWT middleware and they are saved in memory. From my understanding you won't need to write any kind of code. Just set up jwt as a service. If a JWT token is send from the client it will be refreshed on the server side.
JWT tokens info are stored in the memory not in any kind of database. After all you must know what tokens to accept and what to reject as it's impossible not to store anything anywhere. You wont need a seperate token. Simply JWT handles the refresh for you.

Making requests to Azure Management

I have completed the guide here to add Azure AD authentication to my application:
https://azure.microsoft.com/en-gb/resources/samples/active-directory-dotnet-webapp-openidconnect-aspnetcore/
and can log in successfully, have a service principal and everything works as expected.
I now want to make web requests as the user, but can't see how to get the authentication details to send in the request, I've tried looking through the ClaimsPrincipal.Current object, but there is nothing i can pass to a HTTP client to make the request.
The sample web app you refered to only signs the user in, but you need to get the access token on behalf of that user to access the api.
You can refer to this sample. This sample calls another webapi, you can ignore that part, just change the resource to https://management.core.windows.net/
public void Configure(string name, OpenIdConnectOptions options)
{
options.ClientId = _azureOptions.ClientId;
options.Authority = _azureOptions.Authority;
options.UseTokenLifetime = true;
options.CallbackPath = _azureOptions.CallbackPath;
options.RequireHttpsMetadata = false;
options.ClientSecret = _azureOptions.ClientSecret;
options.Resource = "https://management.core.windows.net/"; // management api
options.ResponseType = "id_token code";
// Subscribing to the OIDC events
options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
options.Events.OnAuthenticationFailed = OnAuthenticationFailed;
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
// Acquire a Token for the management API
string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
var authContext = new AuthenticationContext(context.Options.Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
var credential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);
var authResult = await authContext.AcquireTokenAsync(context.Options.Resource,credential);
// Notify the OIDC middleware that we already took care of code redemption.
context.HandleCodeRedemption(authResult.AccessToken, context.ProtocolMessage.IdToken);
}

Azure AD and OAUTH resources

I am writing a web application that needs to access both PowerBI and Microsoft Graph. I am new with OAUTH so I am not understanding how to request access to two different resources. This is my code to access one (PowerBI) resource. How do I modify it to also get access to Microsoft Graph?
class ConfigureAzureOptions : IConfigureNamedOptions<OpenIdConnectOptions>
{
private readonly PowerBiOptions _powerBiOptions;
private readonly IDistributedCache _distributedCache;
private readonly AzureADOptions _azureOptions;
public ConfigureAzureOptions(IOptions<AzureADOptions> azureOptions, IOptions<PowerBiOptions> powerBiOptions, IDistributedCache distributedCache)
{
_azureOptions = azureOptions.Value;
_powerBiOptions = powerBiOptions.Value;
_distributedCache = distributedCache;
}
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.ClientSecret = _azureOptions.ClientSecret;
options.Resource = _powerBiOptions.Resource;
// Without overriding the response type (which by default is id_token), the OnAuthorizationCodeReceived event is not called.
// but instead OnTokenValidated event is called. Here we request both so that OnTokenValidated is called first which
// ensures that context.Principal has a non-null value when OnAuthorizeationCodeReceived is called
options.ResponseType = "id_token code";
options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
options.Events.OnAuthenticationFailed = OnAuthenticationFailed;
}
public void Configure(OpenIdConnectOptions options)
{
Configure(Options.DefaultName, options);
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
string userObjectId = context.Principal.FindFirst(AccessTokenProvider.Identifier)?.Value;
var authContext = new AuthenticationContext(context.Options.Authority, new DistributedTokenCache(_distributedCache, userObjectId));
var credential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(context.TokenEndpointRequest.Code,
new Uri(context.TokenEndpointRequest.RedirectUri, UriKind.RelativeOrAbsolute), credential, context.Options.Resource);
context.HandleCodeRedemption(authResult.AccessToken, context.ProtocolMessage.IdToken);
}
private Task OnAuthenticationFailed(AuthenticationFailedContext context)
{
context.HandleResponse();
context.Response.Redirect("/Home/Error?message=" + context.Exception.Message);
return Task.FromResult(0);
}
}
You doesn't need to get each access token for different resource at the first sign-in process .
Suppose the first time you are acquiring PowerBI's access token in OnAuthorizationCodeReceived function , in controller , of course you can directly use that access token to call PowerBI's API since token is cached . Now you need to call Microsoft Graph , just try below codes :
string userObjectID = (User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
// Using ADAL.Net, get a bearer token to access the TodoListService
AuthenticationContext authContext = new AuthenticationContext(AzureAdOptions.Settings.Authority, new NaiveSessionCache(userObjectID, HttpContext.Session));
ClientCredential credential = new ClientCredential(AzureAdOptions.Settings.ClientId, AzureAdOptions.Settings.ClientSecret);
result = await authContext.AcquireTokenSilentAsync("https://graph.microsoft.com", credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
Just set the resource parameter of AcquireTokenSilentAsync function ,it will use refresh token to acquire access token for new resource .

Microsoft Graph: Current authenticated context is not valid for this request

I had an app that used MSAL and the v2.0 endpoint to sign in users and get token.
I recently changed it to ADAL and the normal AAD endpoint (also changing the app), and now when I try to use the GraphService I get the following error: Current authenticated context is not valid for this request
My user is admin
All permissions have been delegated
The token is successfully retrieved
Here is the code I use:
public static GraphServiceClient GetAuthenticatedClient()
{
GraphServiceClient graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
string accessToken = await SampleAuthProvider.Instance.GetUserAccessTokenAsync();
// Append the access token to the request.
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
}));
return graphClient;
}
Calling the method, where the actual error happens:
try
{
// Initialize the GraphServiceClient.
GraphServiceClient graphClient = SDKHelper.GetAuthenticatedClient();
// Get events.
items = await eventsService.GetMyEvents(graphClient);
}
catch (ServiceException se)
{
}
Getting the token:
public async Task<string> GetTokenAsync()
{
ClientCredential cc = new ClientCredential(appId, appSecret);
AuthenticationContext authContext = new AuthenticationContext("https://login.microsoftonline.com/tenant.onmicrosoft.com");
AuthenticationResult result = await authContext.AcquireTokenAsync("https://graph.microsoft.com", cc);
return result.AccessToken;
}
Can't find anything on this online so I am not sure how to continue.
Error:
This exception is caused by the token acquired using the client credentials flow. In this flow, there is no context for Me.
To fix this issue, you need to specify the whose event you want to get. Or you need to provide the delegate-token.
code for your reference:
//var envens=await graphClient.Me.Events.Request().GetAsync();
var envens = await graphClient.Users["xxx#xxx.onmicrosoft.com"].Events.Request().GetAsync();