Skip to content
This repository was archived by the owner on Dec 24, 2020. It is now read-only.

Commit a120c10

Browse files
committed
Update the introspection middleware to support the "token_usage" claim
1 parent 7dd09ff commit a120c10

File tree

6 files changed

+293
-42
lines changed

6 files changed

+293
-42
lines changed

src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionConstants.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public static class Claims
2121
public const string Scope = "scope";
2222
public const string Subject = "sub";
2323
public const string TokenType = "token_type";
24+
public const string TokenUsage = "token_usage";
2425
public const string Username = "username";
2526
}
2627

@@ -64,15 +65,16 @@ public static class Parameters
6465

6566
public static class Properties
6667
{
68+
public const string AccessToken = "access_token";
6769
public const string Audiences = ".audiences";
6870
public const string Error = ".error";
6971
public const string ErrorDescription = ".error_description";
7072
public const string ErrorUri = ".error_uri";
7173
public const string Realm = ".realm";
7274
public const string Scope = ".scope";
7375
public const string Scopes = ".scopes";
74-
public const string TicketId = ".ticket_id";
75-
public const string Token = "access_token";
76+
public const string TokenId = ".token_id";
77+
public const string TokenUsage = ".token_usage";
7678
}
7779

7880
public static class Schemes
@@ -90,5 +92,10 @@ public static class TokenTypeHints
9092
{
9193
public const string AccessToken = "access_token";
9294
}
95+
96+
public static class TokenUsages
97+
{
98+
public const string AccessToken = "access_token";
99+
}
93100
}
94101
}

src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionHandler.cs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,21 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
120120
await StoreTicketAsync(token, ticket);
121121
}
122122

123+
// Ensure that the token can be used as an access token.
124+
if (!ValidateTokenUsage(ticket))
125+
{
126+
Context.Features.Set(new OAuthIntrospectionFeature
127+
{
128+
Error = new OAuthIntrospectionError
129+
{
130+
Error = OAuthIntrospectionConstants.Errors.InvalidToken,
131+
ErrorDescription = "The access token is not valid."
132+
}
133+
});
134+
135+
return AuthenticateResult.Fail("Authentication failed because the token was not an access token.");
136+
}
137+
123138
// Ensure that the authentication ticket is still valid.
124139
if (ticket.Properties.ExpiresUtc.HasValue &&
125140
ticket.Properties.ExpiresUtc.Value < Options.SystemClock.UtcNow)
@@ -484,6 +499,21 @@ exception is JsonReaderException ||
484499
}
485500
}
486501

502+
private bool ValidateTokenUsage(AuthenticationTicket ticket)
503+
{
504+
// Try to extract the "token_usage" resolved from the introspection response.
505+
// If this non-standard claim was not returned by the authorization server,
506+
// assume the validated token can be used as an access token.
507+
var usage = ticket.Properties.GetProperty(OAuthIntrospectionConstants.Properties.TokenUsage);
508+
if (string.IsNullOrEmpty(usage))
509+
{
510+
return true;
511+
}
512+
513+
// If the "token_usage" claim was returned, it must be equal to "access_token".
514+
return string.Equals(usage, OAuthIntrospectionConstants.TokenUsages.AccessToken, StringComparison.OrdinalIgnoreCase);
515+
}
516+
487517
private bool ValidateAudience(AuthenticationTicket ticket)
488518
{
489519
// If no explicit audience has been configured,
@@ -522,7 +552,7 @@ private async Task<AuthenticationTicket> CreateTicketAsync(string token, JObject
522552
// Store the access token in the authentication ticket.
523553
properties.StoreTokens(new[]
524554
{
525-
new AuthenticationToken { Name = OAuthIntrospectionConstants.Properties.Token, Value = token }
555+
new AuthenticationToken { Name = OAuthIntrospectionConstants.Properties.AccessToken, Value = token }
526556
});
527557
}
528558

@@ -546,6 +576,20 @@ private async Task<AuthenticationTicket> CreateTicketAsync(string token, JObject
546576
case OAuthIntrospectionConstants.Claims.NotBefore:
547577
continue;
548578

579+
case OAuthIntrospectionConstants.Claims.TokenUsage:
580+
{
581+
if (property.Value.Type != JTokenType.String)
582+
{
583+
Logger.LogWarning("The 'token_usage' claim was ignored because it was not a string value.");
584+
585+
continue;
586+
}
587+
588+
properties.Items[OAuthIntrospectionConstants.Properties.TokenUsage] = (string) property.Value;
589+
590+
continue;
591+
}
592+
549593
case OAuthIntrospectionConstants.Claims.IssuedAt:
550594
{
551595
// Note: the iat claim must be a numeric date value.
@@ -594,7 +638,7 @@ private async Task<AuthenticationTicket> CreateTicketAsync(string token, JObject
594638
continue;
595639
}
596640

597-
properties.Items[OAuthIntrospectionConstants.Properties.TicketId] = (string) property;
641+
properties.Items[OAuthIntrospectionConstants.Properties.TokenId] = (string) property;
598642

599643
continue;
600644
}

src/Owin.Security.OAuth.Introspection/OAuthIntrospectionConstants.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public static class Claims
2121
public const string Scope = "scope";
2222
public const string Subject = "sub";
2323
public const string TokenType = "token_type";
24+
public const string TokenUsage = "token_usage";
2425
public const string Username = "username";
2526
}
2627

@@ -70,15 +71,16 @@ public static class Parameters
7071

7172
public static class Properties
7273
{
74+
public const string AccessToken = "access_token";
7375
public const string Audiences = ".audiences";
7476
public const string Error = ".error";
7577
public const string ErrorDescription = ".error_description";
7678
public const string ErrorUri = ".error_uri";
7779
public const string Realm = ".realm";
7880
public const string Scope = ".scope";
7981
public const string Scopes = ".scopes";
80-
public const string TicketId = ".ticket_id";
81-
public const string Token = "access_token";
82+
public const string TokenId = ".token_id";
83+
public const string TokenUsage = ".token_usage";
8284
}
8385

8486
public static class Schemes
@@ -96,5 +98,10 @@ public static class TokenTypeHints
9698
{
9799
public const string AccessToken = "access_token";
98100
}
101+
102+
public static class TokenUsages
103+
{
104+
public const string AccessToken = "access_token";
105+
}
99106
}
100107
}

src/Owin.Security.OAuth.Introspection/OAuthIntrospectionHandler.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
118118
await StoreTicketAsync(token, ticket);
119119
}
120120

121+
// Ensure that the token can be used as an access token.
122+
if (!ValidateTokenUsage(ticket))
123+
{
124+
Logger.LogError("Authentication failed because the token was not an access token.");
125+
126+
Context.Set(typeof(OAuthIntrospectionError).FullName, new OAuthIntrospectionError
127+
{
128+
Error = OAuthIntrospectionConstants.Errors.InvalidToken,
129+
ErrorDescription = "The access token is not valid."
130+
});
131+
132+
return null;
133+
}
134+
121135
// Ensure that the authentication ticket is still valid.
122136
if (ticket.Properties.ExpiresUtc.HasValue &&
123137
ticket.Properties.ExpiresUtc.Value < Options.SystemClock.UtcNow)
@@ -474,6 +488,21 @@ exception is JsonReaderException ||
474488
}
475489
}
476490

491+
private bool ValidateTokenUsage(AuthenticationTicket ticket)
492+
{
493+
// Try to extract the "token_usage" resolved from the introspection response.
494+
// If this non-standard claim was not returned by the authorization server,
495+
// assume the validated token can be used as an access token.
496+
var usage = ticket.Properties.GetProperty(OAuthIntrospectionConstants.Properties.TokenUsage);
497+
if (string.IsNullOrEmpty(usage))
498+
{
499+
return true;
500+
}
501+
502+
// If the "token_usage" claim was returned, it must be equal to "access_token".
503+
return string.Equals(usage, OAuthIntrospectionConstants.TokenUsages.AccessToken, StringComparison.OrdinalIgnoreCase);
504+
}
505+
477506
private bool ValidateAudience(AuthenticationTicket ticket)
478507
{
479508
// If no explicit audience has been configured,
@@ -510,7 +539,7 @@ private async Task<AuthenticationTicket> CreateTicketAsync(string token, JObject
510539
if (Options.SaveToken)
511540
{
512541
// Store the access token in the authentication ticket.
513-
properties.Dictionary[OAuthIntrospectionConstants.Properties.Token] = token;
542+
properties.Dictionary[OAuthIntrospectionConstants.Properties.AccessToken] = token;
514543
}
515544

516545
foreach (var property in payload.Properties())
@@ -533,6 +562,20 @@ private async Task<AuthenticationTicket> CreateTicketAsync(string token, JObject
533562
case OAuthIntrospectionConstants.Claims.NotBefore:
534563
continue;
535564

565+
case OAuthIntrospectionConstants.Claims.TokenUsage:
566+
{
567+
if (property.Value.Type != JTokenType.String)
568+
{
569+
Logger.LogWarning("The 'token_usage' claim was ignored because it was not a string value.");
570+
571+
continue;
572+
}
573+
574+
properties.Dictionary[OAuthIntrospectionConstants.Properties.TokenUsage] = (string) property.Value;
575+
576+
continue;
577+
}
578+
536579
case OAuthIntrospectionConstants.Claims.IssuedAt:
537580
{
538581
// Note: the iat claim must be a numeric date value.
@@ -581,7 +624,7 @@ private async Task<AuthenticationTicket> CreateTicketAsync(string token, JObject
581624
continue;
582625
}
583626

584-
properties.Dictionary[OAuthIntrospectionConstants.Properties.TicketId] = (string) property;
627+
properties.Dictionary[OAuthIntrospectionConstants.Properties.TokenId] = (string) property;
585628

586629
continue;
587630
}

test/AspNet.Security.OAuth.Introspection.Tests/OAuthIntrospectionHandlerTests.cs

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,77 @@ public async Task HandleAuthenticateAsync_ValidTokenAllowsSuccessfulAuthenticati
8282
Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
8383
}
8484

85+
[Fact]
86+
public async Task HandleAuthenticateAsync_MissingTokenUsageAllowsSuccessfulAuthentication()
87+
{
88+
// Arrange
89+
var server = CreateResourceServer();
90+
var client = server.CreateClient();
91+
92+
var request = new HttpRequestMessage(HttpMethod.Get, "/");
93+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token-without-usage");
94+
95+
// Act
96+
var response = await client.SendAsync(request);
97+
98+
// Assert
99+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
100+
Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
101+
}
102+
103+
[Fact]
104+
public async Task HandleAuthenticateAsync_InvalidTokenUsageCausesInvalidAuthentication()
105+
{
106+
// Arrange
107+
var server = CreateResourceServer();
108+
var client = server.CreateClient();
109+
110+
var request = new HttpRequestMessage(HttpMethod.Get, "/");
111+
request.Headers.Authorization = new AuthenticationHeaderValue(
112+
"Bearer", "valid-token-with-invalid-usage");
113+
114+
// Act
115+
var response = await client.SendAsync(request);
116+
117+
// Assert
118+
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
119+
}
120+
121+
[Fact]
122+
public async Task HandleAuthenticateAsync_ValidTokenUsageAllowsSuccessfulAuthentication()
123+
{
124+
// Arrange
125+
var server = CreateResourceServer();
126+
var client = server.CreateClient();
127+
128+
var request = new HttpRequestMessage(HttpMethod.Get, "/");
129+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token");
130+
131+
// Act
132+
var response = await client.SendAsync(request);
133+
134+
// Assert
135+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
136+
Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
137+
}
138+
139+
[Fact]
140+
public async Task HandleAuthenticateAsync_ExpiredTicketCausesInvalidAuthentication()
141+
{
142+
// Arrange
143+
var server = CreateResourceServer();
144+
var client = server.CreateClient();
145+
146+
var request = new HttpRequestMessage(HttpMethod.Get, "/");
147+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "expired-token");
148+
149+
// Act
150+
var response = await client.SendAsync(request);
151+
152+
// Assert
153+
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
154+
}
155+
85156
[Fact]
86157
public async Task HandleAuthenticateAsync_MissingAudienceCausesInvalidAuthentication()
87158
{
@@ -196,23 +267,6 @@ public async Task HandleAuthenticateAsync_MultipleMatchingAudienceCausesSuccessf
196267
Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
197268
}
198269

199-
[Fact]
200-
public async Task HandleAuthenticateAsync_ExpiredTicketCausesInvalidAuthentication()
201-
{
202-
// Arrange
203-
var server = CreateResourceServer();
204-
var client = server.CreateClient();
205-
206-
var request = new HttpRequestMessage(HttpMethod.Get, "/");
207-
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "expired-token");
208-
209-
// Act
210-
var response = await client.SendAsync(request);
211-
212-
// Assert
213-
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
214-
}
215-
216270
[Fact]
217271
public async Task HandleAuthenticateAsync_AuthenticationTicketContainsRequiredClaims()
218272
{
@@ -846,6 +900,27 @@ private static TestServer CreateAuthorizationServer()
846900
payload[OAuthIntrospectionConstants.Claims.Active] = true;
847901
payload[OAuthIntrospectionConstants.Claims.JwtId] = "jwt-token-identifier";
848902
payload[OAuthIntrospectionConstants.Claims.Subject] = "Fabrikam";
903+
payload[OAuthIntrospectionConstants.Claims.TokenUsage] =
904+
OAuthIntrospectionConstants.TokenUsages.AccessToken;
905+
906+
break;
907+
}
908+
909+
case "valid-token-without-usage":
910+
{
911+
payload[OAuthIntrospectionConstants.Claims.Active] = true;
912+
payload[OAuthIntrospectionConstants.Claims.JwtId] = "jwt-token-identifier";
913+
payload[OAuthIntrospectionConstants.Claims.Subject] = "Fabrikam";
914+
915+
break;
916+
}
917+
918+
case "valid-token-with-invalid-usage":
919+
{
920+
payload[OAuthIntrospectionConstants.Claims.Active] = true;
921+
payload[OAuthIntrospectionConstants.Claims.JwtId] = "jwt-token-identifier";
922+
payload[OAuthIntrospectionConstants.Claims.Subject] = "Fabrikam";
923+
payload[OAuthIntrospectionConstants.Claims.TokenUsage] = "refresh_token";
849924

850925
break;
851926
}

0 commit comments

Comments
 (0)