Skip to content
This repository was archived by the owner on Feb 11, 2025. It is now read-only.

Commit

Permalink
Implement all properties for optional claims from the spec (RFC 7662)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xced committed Oct 9, 2024
1 parent 9c805a5 commit 7f53cf1
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 1 deletion.
80 changes: 80 additions & 0 deletions src/Client/Messages/TokenIntrospectionResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,34 @@ namespace IdentityModel.Client;
/// <seealso cref="IdentityModel.Client.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)
Expand Down Expand Up @@ -97,6 +113,38 @@ 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>
Expand All @@ -121,6 +169,38 @@ protected override Task InitializeAsync(object? initializationData = null)
/// </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
37 changes: 36 additions & 1 deletion test/UnitTests/HttpClientExtensions/TokenIntrospectionTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using FluentAssertions;
Expand Down Expand Up @@ -89,9 +89,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 @@ -129,9 +136,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 @@ -172,9 +186,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 @@ -199,9 +220,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 @@ -309,9 +337,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

0 comments on commit 7f53cf1

Please sign in to comment.