Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 44 additions & 12 deletions src/IdentityServer/Endpoints/IntrospectionEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Duende.IdentityServer.Validation;
using Duende.IdentityServer.Extensions;
using System.IO;
using Duende.IdentityServer.Models;

namespace Duende.IdentityServer.Endpoints;

Expand All @@ -28,23 +29,27 @@ internal class IntrospectionEndpoint : IEndpointHandler
private readonly ILogger _logger;
private readonly IIntrospectionRequestValidator _requestValidator;
private readonly IApiSecretValidator _apiSecretValidator;
private readonly IClientSecretValidator _clientValidator;

/// <summary>
/// Initializes a new instance of the <see cref="IntrospectionEndpoint" /> class.
/// </summary>
/// <param name="apiSecretValidator">The API secret validator.</param>
/// <param name="clientValidator"></param>
/// <param name="requestValidator">The request validator.</param>
/// <param name="responseGenerator">The generator.</param>
/// <param name="events">The events.</param>
/// <param name="logger">The logger.</param>
public IntrospectionEndpoint(
IApiSecretValidator apiSecretValidator,
IClientSecretValidator clientValidator,
IIntrospectionRequestValidator requestValidator,
IIntrospectionResponseGenerator responseGenerator,
IEventService events,
ILogger<IntrospectionEndpoint> logger)
{
_apiSecretValidator = apiSecretValidator;
_clientValidator = clientValidator;
_requestValidator = requestValidator;
_responseGenerator = responseGenerator;
_events = events;
Expand Down Expand Up @@ -91,29 +96,56 @@ private async Task<IEndpointResult> ProcessIntrospectionRequestAsync(HttpContext
_logger.LogDebug("Starting introspection request.");

// caller validation
ClientSecretValidationResult clientResult = null;

ApiResource api = null;
Client client = null;

var apiResult = await _apiSecretValidator.ValidateAsync(context);
if (apiResult.Resource == null)
if (apiResult.IsError)
{
_logger.LogError("API unauthorized to call introspection endpoint. aborting.");
return new StatusCodeResult(HttpStatusCode.Unauthorized);
clientResult = await _clientValidator.ValidateAsync(context);
if (clientResult.IsError)
{
_logger.LogError("Unauthorized call introspection endpoint. aborting.");
return new StatusCodeResult(HttpStatusCode.Unauthorized);
}
else
{
client = clientResult.Client;
_logger.LogDebug("Client making introspection request: {clientId}", client.ClientId);
}
}
else
{
api = apiResult.Resource;
_logger.LogDebug("ApiResource making introspection request: {apiId}", api.Name);
}

var callerName = api?.Name ?? client.ClientId;

var body = await context.Request.ReadFormAsync();
if (body == null)
{
_logger.LogError("Malformed request body. aborting.");
await _events.RaiseAsync(new TokenIntrospectionFailureEvent(apiResult.Resource.Name, "Malformed request body"));
await _events.RaiseAsync(new TokenIntrospectionFailureEvent(callerName, "Malformed request body"));

return new StatusCodeResult(HttpStatusCode.BadRequest);
}

// request validation
_logger.LogTrace("Calling into introspection request validator: {type}", _requestValidator.GetType().FullName);
var validationResult = await _requestValidator.ValidateAsync(body.AsNameValueCollection(), apiResult.Resource);
var validationRequest = new IntrospectionRequestValidationContext
{
Parameters = body.AsNameValueCollection(),
Api = api,
Client = client,
};
var validationResult = await _requestValidator.ValidateAsync(validationRequest);
if (validationResult.IsError)
{
LogFailure(validationResult.Error, apiResult.Resource.Name);
await _events.RaiseAsync(new TokenIntrospectionFailureEvent(apiResult.Resource.Name, validationResult.Error));
LogFailure(validationResult.Error, callerName);
await _events.RaiseAsync(new TokenIntrospectionFailureEvent(callerName, validationResult.Error));

return new BadRequestResult(validationResult.Error);
}
Expand All @@ -123,17 +155,17 @@ private async Task<IEndpointResult> ProcessIntrospectionRequestAsync(HttpContext
var response = await _responseGenerator.ProcessAsync(validationResult);

// render result
LogSuccess(validationResult.IsActive, validationResult.Api.Name);
LogSuccess(validationResult.IsActive, callerName);
return new IntrospectionResult(response);
}

private void LogSuccess(bool tokenActive, string apiName)
private void LogSuccess(bool tokenActive, string callerName)
{
_logger.LogInformation("Success token introspection. Token active: {tokenActive}, for API name: {apiName}", tokenActive, apiName);
_logger.LogInformation("Success token introspection. Token active: {tokenActive}, for caller: {callerName}", tokenActive, callerName);
}

private void LogFailure(string error, string apiName)
private void LogFailure(string error, string callerName)
{
_logger.LogError("Failed token introspection: {error}, for API name: {apiName}", error, apiName);
_logger.LogError("Failed token introspection: {error}, for caller: {callerName}", error, callerName);
}
}
11 changes: 10 additions & 1 deletion src/IdentityServer/Events/TokenIntrospectionSuccessEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public TokenIntrospectionSuccessEvent(IntrospectionRequestValidationResult resul
EventTypes.Success,
EventIds.TokenIntrospectionSuccess)
{
ApiName = result.Api.Name;
ApiName = result.Api?.Name;
ClientName = result.Client?.ClientName;
IsActive = result.IsActive;

if (result.Token.IsPresent())
Expand All @@ -47,6 +48,14 @@ public TokenIntrospectionSuccessEvent(IntrospectionRequestValidationResult resul
/// The name of the API.
/// </value>
public string ApiName { get; set; }

/// <summary>
/// Gets or sets the name of the client.
/// </summary>
/// <value>
/// The name of the client.
/// </value>
public string ClientName { get; set; }

/// <summary>
/// Gets or sets a value indicating whether this instance is active.
Expand Down
2 changes: 1 addition & 1 deletion src/IdentityServer/Extensions/DateTimeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Duende Software. All rights reserved.
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,31 @@ public virtual async Task<Dictionary<string, object>> ProcessAsync(Introspection
return response;
}

// expected scope not present
if (await AreExpectedScopesPresentAsync(validationResult) == false)
// client can see all their own scopes
var scopes = validationResult.Claims.Where(c => c.Type == JwtClaimTypes.Scope).Select(x => x.Value);

if (validationResult.Api != null)
{
return response;
// expected scope not present
if (await AreExpectedScopesPresentAsync(validationResult) == false)
{
return response;
}

// calculate scopes the API is allowed to see
var allowedScopes = validationResult.Api.Scopes;
scopes = scopes.Where(x => allowedScopes.Contains(x));
}

Logger.LogDebug("Creating introspection response for active token.");

// get all claims (without scopes)
response = validationResult.Claims.Where(c => c.Type != JwtClaimTypes.Scope).ToClaimsDictionary();

// add active flag
response.Add("active", true);

// calculate scopes the caller is allowed to see
var allowedScopes = validationResult.Api.Scopes;
var scopes = validationResult.Claims.Where(c => c.Type == JwtClaimTypes.Scope).Select(x => x.Value);
scopes = scopes.Where(x => allowedScopes.Contains(x));
// add scopes
response.Add("scope", scopes.ToSpaceSeparatedString());

await Events.RaiseAsync(new TokenIntrospectionSuccessEvent(validationResult));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

#nullable enable


using Duende.IdentityServer.Models;
using System.Collections.Specialized;

namespace Duende.IdentityServer.Validation;

/// <summary>
/// Context for validating an introspection request.
/// </summary>
public class IntrospectionRequestValidationContext
{
/// <summary>
/// The request parameters
/// </summary>
public NameValueCollection Parameters { get; set; } = default!;

/// <summary>
/// The ApiResource that is making the request
/// </summary>
public ApiResource? Api { get; set; }

/// <summary>
/// The Client that is making the request
/// </summary>
public Client? Client { get; set; }
}
Loading