Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement all properties for TokenIntrospectionResponse optional claims #55

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -1,17 +1,57 @@
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Globalization;
using System.Security.Claims;
using System.Text.Json;

namespace Duende.IdentityModel.Client;

/// <summary>
/// Models an OAuth 2.0 introspection response
/// Models an OAuth 2.0 introspection response as defined by <a href="https://datatracker.ietf.org/doc/html/rfc7662">RFC 7662 - OAuth 2.0 Token Introspection</a>
/// </summary>
/// <seealso cref="ProtocolResponse" />
public class TokenIntrospectionResponse : ProtocolResponse
{
private readonly Lazy<string[]> _scopes;
private readonly Lazy<string?> _clientId;
private readonly Lazy<string?> _userName;
private readonly Lazy<string?> _tokenType;
private readonly Lazy<DateTimeOffset?> _expiration;
private readonly Lazy<DateTimeOffset?> _issuedAt;
private readonly Lazy<DateTimeOffset?> _notBefore;
private readonly Lazy<string?> _subject;
private readonly Lazy<string[]> _audiences;
private readonly Lazy<string?> _issuer;
private readonly Lazy<string?> _jwtId;

/// <summary>
/// Initializes a new instance of the <see cref="TokenIntrospectionResponse"/> class.
/// </summary>
public TokenIntrospectionResponse()
{
_scopes = new Lazy<string[]>(() => Claims.Where(c => c.Type == JwtClaimTypes.Scope).Select(c => c.Value).ToArray());
_clientId = new Lazy<string?>(() => Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.ClientId)?.Value);
_userName = new Lazy<string?>(() => Claims.FirstOrDefault(c => c.Type == "username")?.Value);
_tokenType = new Lazy<string?>(() => Claims.FirstOrDefault(c => c.Type == "token_type")?.Value);
_expiration = new Lazy<DateTimeOffset?>(() => GetTime(JwtClaimTypes.Expiration));
_issuedAt = new Lazy<DateTimeOffset?>(() => GetTime(JwtClaimTypes.IssuedAt));
_notBefore = new Lazy<DateTimeOffset?>(() => GetTime(JwtClaimTypes.NotBefore));
_subject = new Lazy<string?>(() => Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Subject)?.Value);
_audiences = new Lazy<string[]>(() => Claims.Where(c => c.Type == JwtClaimTypes.Audience).Select(c => c.Value).ToArray());
_issuer = new Lazy<string?>(() => Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Issuer)?.Value);
_jwtId = new Lazy<string?>(() => Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.JwtId)?.Value);
}

private DateTimeOffset? GetTime(string claimType)
{
var claimValue = Claims.FirstOrDefault(e => e.Type == claimType)?.Value;
if (claimValue == null) return null;

var seconds = long.Parse(claimValue, NumberStyles.AllowLeadingSign, NumberFormatInfo.InvariantInfo);
return DateTimeOffset.FromUnixTimeSeconds(seconds);
}

/// <summary>
/// Allows to initialize instance specific data.
/// </summary>
Expand Down Expand Up @@ -69,6 +109,94 @@ protected override Task InitializeAsync(object? initializationData = null)
/// </value>
public bool IsActive => Json?.TryGetBoolean("active") ?? false;

/// <summary>
/// Gets the list of scopes associated to the token.
/// </summary>
/// <value>
/// The list of scopes associated to the token or an empty array if no <c>scope</c> claim is present.
/// </value>
public string[] Scopes => _scopes.Value;

/// <summary>
/// Gets the client identifier for the OAuth 2.0 client that requested the token.
/// </summary>
/// <value>
/// The client identifier for the OAuth 2.0 client that requested the token or null if the <c>client_id</c> claim is missing.
/// </value>
public string? ClientId => _clientId.Value;

/// <summary>
/// Gets the human-readable identifier for the resource owner who authorized the token.
/// </summary>
/// <value>
/// The human-readable identifier for the resource owner who authorized the token or null if the <c>username</c> claim is missing.
/// </value>
public string? UserName => _userName.Value;

/// <summary>
/// Gets the type of the token as defined in <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-5.1">section 5.1 of OAuth 2.0 (RFC6749)</a>.
/// </summary>
/// <value>
/// The type of the token as defined in <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-5.1">section 5.1 of OAuth 2.0 (RFC6749)</a> or null if the <c>token_type</c> claim is missing.
/// </value>
public string? TokenType => _tokenType.Value;

/// <summary>
/// Gets the time on or after which the token must not be accepted for processing.
/// </summary>
/// <value>
/// The expiration time of the token or null if the <c>exp</c> claim is missing.
/// </value>
public DateTimeOffset? Expiration => _expiration.Value;

/// <summary>
/// Gets the time when the token was issued.
/// </summary>
/// <value>
/// The issuance time of the token or null if the <c>iat</c> claim is missing.
/// </value>
public DateTimeOffset? IssuedAt => _issuedAt.Value;

/// <summary>
/// Gets the time before which the token must not be accepted for processing.
/// </summary>
/// <value>
/// The validity start time of the token or null if the <c>nbf</c> claim is missing.
/// </value>
public DateTimeOffset? NotBefore => _notBefore.Value;

/// <summary>
/// Gets the subject of the token. Usually a machine-readable identifier of the resource owner who authorized the token.
/// </summary>
/// <value>
/// The subject of the token or null if the <c>sub</c> claim is missing.
/// </value>
public string? Subject => _subject.Value;

/// <summary>
/// Gets the service-specific list of string identifiers representing the intended audience for the token.
/// </summary>
/// <value>
/// The service-specific list of string identifiers representing the intended audience for the token or an empty array if no <c>aud</c> claim is present.
/// </value>
public string[] Audiences => _audiences.Value;

/// <summary>
/// Gets the string representing the issuer of the token.
/// </summary>
/// <value>
/// The string representing the issuer of the token or null if the <c>iss</c> claim is missing.
/// </value>
public string? Issuer => _issuer.Value;

/// <summary>
/// Gets the string identifier for the token.
/// </summary>
/// <value>
/// The string identifier for the token or null if the <c>jti</c> claim is missing.
/// </value>
public string? JwtId => _jwtId.Value;

/// <summary>
/// Gets the claims.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Text.Json;
using Duende.IdentityModel.Client;
using Duende.IdentityModel.Infrastructure;
using FluentAssertions;
using FluentAssertions.Extensions;

namespace Duende.IdentityModel.HttpClientExtensions
{
Expand Down Expand Up @@ -84,6 +84,16 @@ public async Task Success_protocol_response_should_be_handled_correctly()
new Claim("scope", "api1", ClaimValueTypes.String, "https://idsvr4"),
new Claim("scope", "api2", ClaimValueTypes.String, "https://idsvr4"),
});
response.Scopes.Should().BeEquivalentTo("api1", "api2");
response.ClientId.Should().Be("client");
response.UserName.Should().BeNull();
response.IssuedAt.Should().BeNull();
response.NotBefore.Should().Be(7.October(2016).At(7, 21, 11).WithOffset(0.Hours()));
response.Expiration.Should().Be(7.October(2016).At(8, 21, 11).WithOffset(0.Hours()));
response.Subject.Should().Be("1");
response.Audiences.Should().BeEquivalentTo("https://idsvr4/resources", "api1");
response.Issuer.Should().Be("https://idsvr4");
response.JwtId.Should().BeNull();
}

[Fact]
Expand Down Expand Up @@ -121,6 +131,16 @@ public async Task Success_protocol_response_without_issuer_should_be_handled_cor
new Claim("scope", "api1", ClaimValueTypes.String, "LOCAL AUTHORITY"),
new Claim("scope", "api2", ClaimValueTypes.String, "LOCAL AUTHORITY"),
});
response.Scopes.Should().BeEquivalentTo("api1", "api2");
response.ClientId.Should().Be("client");
response.UserName.Should().BeNull();
response.IssuedAt.Should().BeNull();
response.NotBefore.Should().Be(7.October(2016).At(7, 21, 11).WithOffset(0.Hours()));
response.Expiration.Should().Be(7.October(2016).At(8, 21, 11).WithOffset(0.Hours()));
response.Subject.Should().Be("1");
response.Audiences.Should().BeEquivalentTo("https://idsvr4/resources", "api1");
response.Issuer.Should().BeNull();
response.JwtId.Should().BeNull();
}

[Fact]
Expand Down Expand Up @@ -161,6 +181,16 @@ public async Task Repeating_a_request_should_succeed()
new Claim("scope", "api1", ClaimValueTypes.String, "https://idsvr4"),
new Claim("scope", "api2", ClaimValueTypes.String, "https://idsvr4"),
});
response.Scopes.Should().BeEquivalentTo("api1", "api2");
response.ClientId.Should().Be("client");
response.UserName.Should().BeNull();
response.IssuedAt.Should().BeNull();
response.NotBefore.Should().Be(7.October(2016).At(7, 21, 11).WithOffset(0.Hours()));
response.Expiration.Should().Be(7.October(2016).At(8, 21, 11).WithOffset(0.Hours()));
response.Subject.Should().Be("1");
response.Audiences.Should().BeEquivalentTo("https://idsvr4/resources", "api1");
response.Issuer.Should().Be("https://idsvr4");
response.JwtId.Should().BeNull();

// repeat
response = await client.IntrospectTokenAsync(request);
Expand All @@ -185,6 +215,16 @@ public async Task Repeating_a_request_should_succeed()
new Claim("scope", "api1", ClaimValueTypes.String, "https://idsvr4"),
new Claim("scope", "api2", ClaimValueTypes.String, "https://idsvr4"),
});
response.Scopes.Should().BeEquivalentTo("api1", "api2");
response.ClientId.Should().Be("client");
response.UserName.Should().BeNull();
response.IssuedAt.Should().BeNull();
response.NotBefore.Should().Be(7.October(2016).At(7, 21, 11).WithOffset(0.Hours()));
response.Expiration.Should().Be(7.October(2016).At(8, 21, 11).WithOffset(0.Hours()));
response.Subject.Should().Be("1");
response.Audiences.Should().BeEquivalentTo("https://idsvr4/resources", "api1");
response.Issuer.Should().Be("https://idsvr4");
response.JwtId.Should().BeNull();
}

[Fact]
Expand Down Expand Up @@ -292,6 +332,16 @@ public async Task Legacy_protocol_response_should_be_handled_correctly()
new Claim("scope", "api1", ClaimValueTypes.String, "https://idsvr4"),
new Claim("scope", "api2", ClaimValueTypes.String, "https://idsvr4"),
});
response.Scopes.Should().BeEquivalentTo("api1", "api2");
response.ClientId.Should().Be("client");
response.UserName.Should().BeNull();
response.IssuedAt.Should().BeNull();
response.NotBefore.Should().Be(7.October(2016).At(7, 21, 11).WithOffset(0.Hours()));
response.Expiration.Should().Be(7.October(2016).At(8, 21, 11).WithOffset(0.Hours()));
response.Subject.Should().Be("1");
response.Audiences.Should().BeEquivalentTo("https://idsvr4/resources", "api1");
response.Issuer.Should().Be("https://idsvr4");
response.JwtId.Should().BeNull();
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1248,8 +1248,19 @@ namespace Duende.IdentityModel.Client
public class TokenIntrospectionResponse : Duende.IdentityModel.Client.ProtocolResponse
{
public TokenIntrospectionResponse() { }
public string[] Audiences { get; }
public System.Collections.Generic.IEnumerable<System.Security.Claims.Claim> Claims { get; set; }
public string? ClientId { get; }
public System.DateTimeOffset? Expiration { get; }
public bool IsActive { get; }
public System.DateTimeOffset? IssuedAt { get; }
public string? Issuer { get; }
public string? JwtId { get; }
public System.DateTimeOffset? NotBefore { get; }
public string[] Scopes { get; }
public string? Subject { get; }
public string? TokenType { get; }
public string? UserName { get; }
protected override System.Threading.Tasks.Task InitializeAsync(object? initializationData = null) { }
}
public class TokenRequest : Duende.IdentityModel.Client.ProtocolRequest
Expand Down