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

Commit 58a0c83

Browse files
committed
Dynamically determine the best client authentication method using introspection_endpoint_auth_methods_supported
1 parent 16b3995 commit 58a0c83

6 files changed

Lines changed: 96 additions & 14 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
using System;
8+
using System.Collections.Generic;
89
using System.Threading;
910
using System.Threading.Tasks;
1011
using JetBrains.Annotations;
@@ -42,6 +43,15 @@ public OAuthIntrospectionConfiguration([NotNull] string json)
4243
PropertyName = OAuthIntrospectionConstants.Metadata.IntrospectionEndpoint)]
4344
public string IntrospectionEndpoint { get; set; }
4445

46+
/// <summary>
47+
/// Gets the list of authentication methods supported by the introspection endpoint.
48+
/// </summary>
49+
[JsonProperty(
50+
DefaultValueHandling = DefaultValueHandling.Ignore,
51+
NullValueHandling = NullValueHandling.Ignore,
52+
PropertyName = OAuthIntrospectionConstants.Metadata.IntrospectionEndpointAuthMethodsSupported)]
53+
public ISet<string> IntrospectionEndpointAuthMethodsSupported { get; } = new HashSet<string>();
54+
4555
/// <summary>
4656
/// Represents a configuration retriever able to deserialize
4757
/// <see cref="OAuthIntrospectionConfiguration"/> instances.

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ public static class ClaimValueTypes
3030
public const string JsonArray = "JSON_ARRAY";
3131
}
3232

33+
public static class ClientAuthenticationMethods
34+
{
35+
public const string ClientSecretBasic = "client_secret_basic";
36+
public const string ClientSecretPost = "client_secret_post";
37+
}
38+
3339
public static class Errors
3440
{
3541
public const string InsufficientScope = "insufficient_scope";
@@ -40,10 +46,13 @@ public static class Errors
4046
public static class Metadata
4147
{
4248
public const string IntrospectionEndpoint = "introspection_endpoint";
49+
public const string IntrospectionEndpointAuthMethodsSupported = "introspection_endpoint_auth_methods_supported";
4350
}
4451

4552
public static class Parameters
4653
{
54+
public const string ClientId = "client_id";
55+
public const string ClientSecret = "client_secret";
4756
public const string Error = "error";
4857
public const string ErrorDescription = "error_description";
4958
public const string ErrorUri = "error_uri";
@@ -68,6 +77,7 @@ public static class Properties
6877

6978
public static class Schemes
7079
{
80+
public const string Basic = "Basic";
7181
public const string Bearer = "Bearer";
7282
}
7383

@@ -76,7 +86,7 @@ public static class Separators
7686
public static readonly char[] Space = { ' ' };
7787
}
7888

79-
public static class TokenTypes
89+
public static class TokenTypeHints
8090
{
8191
public const string AccessToken = "access_token";
8292
}

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -376,18 +376,39 @@ protected virtual async Task<JObject> GetIntrospectionPayloadAsync(string token)
376376
"the introspection endpoint address from the discovery document.");
377377
}
378378

379-
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Options.ClientId}:{Options.ClientSecret}"));
380-
381379
// Create a new introspection request containing the access token and the client credentials.
382380
var request = new HttpRequestMessage(HttpMethod.Post, configuration.IntrospectionEndpoint);
383381
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
384-
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
385382

386-
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
383+
// Note: always specify the token_type_hint to help
384+
// the authorization server make a faster token lookup.
385+
var parameters = new Dictionary<string, string>
387386
{
388387
[OAuthIntrospectionConstants.Parameters.Token] = token,
389-
[OAuthIntrospectionConstants.Parameters.TokenTypeHint] = OAuthIntrospectionConstants.TokenTypes.AccessToken
390-
});
388+
[OAuthIntrospectionConstants.Parameters.TokenTypeHint] = OAuthIntrospectionConstants.TokenTypeHints.AccessToken
389+
};
390+
391+
// If the introspection endpoint provided by the authorization server supports
392+
// client_secret_post, flow the client credentials as regular OAuth2 parameters.
393+
// See https://tools.ietf.org/html/draft-ietf-oauth-discovery-05#section-2
394+
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
395+
if (configuration.IntrospectionEndpointAuthMethodsSupported.Contains(OAuthIntrospectionConstants.ClientAuthenticationMethods.ClientSecretPost))
396+
{
397+
parameters[OAuthIntrospectionConstants.Parameters.ClientId] = Options.ClientId;
398+
parameters[OAuthIntrospectionConstants.Parameters.ClientSecret] = Options.ClientSecret;
399+
}
400+
401+
// Otherwise, assume the authorization server only supports basic authentication,
402+
// as it's the only authentication method required by the OAuth2 specification.
403+
// See https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
404+
else
405+
{
406+
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Options.ClientId}:{Options.ClientSecret}"));
407+
408+
request.Headers.Authorization = new AuthenticationHeaderValue(OAuthIntrospectionConstants.Schemes.Basic, credentials);
409+
}
410+
411+
request.Content = new FormUrlEncodedContent(parameters);
391412

392413
var notification = new RequestTokenIntrospectionContext(Context, Options, request, token);
393414
await Options.Events.RequestTokenIntrospection(notification);

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
using System;
8+
using System.Collections.Generic;
89
using System.Threading;
910
using System.Threading.Tasks;
1011
using JetBrains.Annotations;
@@ -44,6 +45,15 @@ public OAuthIntrospectionConfiguration([NotNull] string json)
4445
PropertyName = OAuthIntrospectionConstants.Metadata.IntrospectionEndpoint)]
4546
public string IntrospectionEndpoint { get; set; }
4647

48+
/// <summary>
49+
/// Gets the list of authentication methods supported by the introspection endpoint.
50+
/// </summary>
51+
[JsonProperty(
52+
DefaultValueHandling = DefaultValueHandling.Ignore,
53+
NullValueHandling = NullValueHandling.Ignore,
54+
PropertyName = OAuthIntrospectionConstants.Metadata.IntrospectionEndpointAuthMethodsSupported)]
55+
public ISet<string> IntrospectionEndpointAuthMethodsSupported { get; } = new HashSet<string>();
56+
4757
/// <summary>
4858
/// Represents a configuration retriever able to deserialize
4959
/// <see cref="OAuthIntrospectionConfiguration"/> instances.

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ public static class ClaimValueTypes
3030
public const string JsonArray = "JSON_ARRAY";
3131
}
3232

33+
public static class ClientAuthenticationMethods
34+
{
35+
public const string ClientSecretBasic = "client_secret_basic";
36+
public const string ClientSecretPost = "client_secret_post";
37+
}
38+
3339
public static class Errors
3440
{
3541
public const string InsufficientScope = "insufficient_scope";
@@ -46,10 +52,13 @@ public static class Headers
4652
public static class Metadata
4753
{
4854
public const string IntrospectionEndpoint = "introspection_endpoint";
55+
public const string IntrospectionEndpointAuthMethodsSupported = "introspection_endpoint_auth_methods_supported";
4956
}
5057

5158
public static class Parameters
5259
{
60+
public const string ClientId = "client_id";
61+
public const string ClientSecret = "client_secret";
5362
public const string Error = "error";
5463
public const string ErrorDescription = "error_description";
5564
public const string ErrorUri = "error_uri";
@@ -74,6 +83,7 @@ public static class Properties
7483

7584
public static class Schemes
7685
{
86+
public const string Basic = "Basic";
7787
public const string Bearer = "Bearer";
7888
}
7989

@@ -82,7 +92,7 @@ public static class Separators
8292
public static readonly char[] Space = { ' ' };
8393
}
8494

85-
public static class TokenTypes
95+
public static class TokenTypeHints
8696
{
8797
public const string AccessToken = "access_token";
8898
}

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -365,18 +365,39 @@ protected virtual async Task<JObject> GetIntrospectionPayloadAsync(string token)
365365
"the introspection endpoint address from the discovery document.");
366366
}
367367

368-
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Options.ClientId}:{Options.ClientSecret}"));
369-
370368
// Create a new introspection request containing the access token and the client credentials.
371369
var request = new HttpRequestMessage(HttpMethod.Post, configuration.IntrospectionEndpoint);
372370
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
373-
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
374371

375-
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
372+
// Note: always specify the token_type_hint to help
373+
// the authorization server make a faster token lookup.
374+
var parameters = new Dictionary<string, string>
376375
{
377376
[OAuthIntrospectionConstants.Parameters.Token] = token,
378-
[OAuthIntrospectionConstants.Parameters.TokenTypeHint] = OAuthIntrospectionConstants.TokenTypes.AccessToken
379-
});
377+
[OAuthIntrospectionConstants.Parameters.TokenTypeHint] = OAuthIntrospectionConstants.TokenTypeHints.AccessToken
378+
};
379+
380+
// If the introspection endpoint provided by the authorization server supports
381+
// client_secret_post, flow the client credentials as regular OAuth2 parameters.
382+
// See https://tools.ietf.org/html/draft-ietf-oauth-discovery-05#section-2
383+
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
384+
if (configuration.IntrospectionEndpointAuthMethodsSupported.Contains(OAuthIntrospectionConstants.ClientAuthenticationMethods.ClientSecretPost))
385+
{
386+
parameters[OAuthIntrospectionConstants.Parameters.ClientId] = Options.ClientId;
387+
parameters[OAuthIntrospectionConstants.Parameters.ClientSecret] = Options.ClientSecret;
388+
}
389+
390+
// Otherwise, assume the authorization server only supports basic authentication,
391+
// as it's the only authentication method required by the OAuth2 specification.
392+
// See https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
393+
else
394+
{
395+
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Options.ClientId}:{Options.ClientSecret}"));
396+
397+
request.Headers.Authorization = new AuthenticationHeaderValue(OAuthIntrospectionConstants.Schemes.Basic, credentials);
398+
}
399+
400+
request.Content = new FormUrlEncodedContent(parameters);
380401

381402
var notification = new RequestTokenIntrospectionContext(Context, Options, request, token);
382403
await Options.Events.RequestTokenIntrospection(notification);

0 commit comments

Comments
 (0)