diff --git a/.gitignore b/.gitignore
index 252a5b2..8281713 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ x64/
build/
[Bb]in/
[Oo]bj/
+.build/
# Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets
!packages/*/build/
diff --git a/src/AspNet.Security.OAuth.Introspection/AspNet.Security.OAuth.Introspection.xproj b/src/AspNet.Security.OAuth.Introspection/AspNet.Security.OAuth.Introspection.xproj
index c7a1b36..229ac6f 100644
--- a/src/AspNet.Security.OAuth.Introspection/AspNet.Security.OAuth.Introspection.xproj
+++ b/src/AspNet.Security.OAuth.Introspection/AspNet.Security.OAuth.Introspection.xproj
@@ -4,17 +4,15 @@
14.0
$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
-
a8569260-142c-427a-8b14-a8df56cc15b7
- AspNet.Security.OpenIdConnect.Introspection
+ AspNet.Security.OAuth.Introspection
..\..\artifacts\obj\$(MSBuildProjectName)
..\..\artifacts\bin\$(MSBuildProjectName)\
-
2.0
-
+
\ No newline at end of file
diff --git a/src/AspNet.Security.OAuth.Introspection/Events/BaseIntrospectionContext.cs b/src/AspNet.Security.OAuth.Introspection/Events/BaseIntrospectionContext.cs
new file mode 100644
index 0000000..f4258f2
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Introspection/Events/BaseIntrospectionContext.cs
@@ -0,0 +1,33 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+
+namespace AspNet.Security.OAuth.Introspection {
+ ///
+ /// Base class for all introspection events that holds common properties.
+ ///
+ public abstract class BaseIntrospectionContext : BaseContext {
+ public BaseIntrospectionContext(
+ [NotNull]HttpContext context,
+ [NotNull]OAuthIntrospectionOptions options)
+ : base(context) {
+ Options = options;
+ }
+
+ ///
+ /// Indicates the application has handled the event process.
+ ///
+ internal bool Handled { get; set; }
+
+ ///
+ /// The middleware Options.
+ ///
+ public OAuthIntrospectionOptions Options { get; }
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Introspection/Events/CreateTicketContext.cs b/src/AspNet.Security.OAuth.Introspection/Events/CreateTicketContext.cs
new file mode 100644
index 0000000..3a9b38d
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Introspection/Events/CreateTicketContext.cs
@@ -0,0 +1,48 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+using Newtonsoft.Json.Linq;
+
+namespace AspNet.Security.OAuth.Introspection {
+ ///
+ /// Allows interception of the AuthenticationTicket creation process.
+ ///
+ public class CreateTicketContext : BaseIntrospectionContext {
+ public CreateTicketContext(
+ [NotNull]HttpContext context,
+ [NotNull]OAuthIntrospectionOptions options,
+ [NotNull]JObject payload)
+ : base(context, options) {
+ Payload = payload;
+ }
+
+ ///
+ /// The payload from the introspection request to the authorization server.
+ ///
+ public JObject Payload { get; }
+
+ private AuthenticationTicket _ticket { get; set; }
+
+ ///
+ /// An created by the application.
+ ///
+ /// Set this property to indicate that the application has handled the creation of the
+ /// ticket. Set this property to null to instruct the middleware there was a failure
+ /// during ticket creation.
+ ///
+ ///
+ public AuthenticationTicket Ticket {
+ get { return _ticket; }
+ set {
+ Handled = true;
+ _ticket = value;
+ }
+ }
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Introspection/Events/IOAuthIntrospectionEvents.cs b/src/AspNet.Security.OAuth.Introspection/Events/IOAuthIntrospectionEvents.cs
new file mode 100644
index 0000000..76a610e
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Introspection/Events/IOAuthIntrospectionEvents.cs
@@ -0,0 +1,34 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using System.Threading.Tasks;
+
+namespace AspNet.Security.OAuth.Introspection {
+ ///
+ /// Allows customization of introspection handling within the middleware.
+ ///
+ public interface IOAuthIntrospectionEvents {
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ Task ParseAccessToken(ParseAccessTokenContext context);
+
+ ///
+ /// Invoked when a ticket is to be created from an introspection response.
+ ///
+ Task CreateTicket(CreateTicketContext context);
+
+ ///
+ /// Invoked when a token is to be sent to the authorization server for introspection.
+ ///
+ Task RequestTokenIntrospection(RequestTokenIntrospectionContext context);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ Task ValidateToken(ValidateTokenContext context);
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Introspection/Events/OAuthIntrospectionEvents.cs b/src/AspNet.Security.OAuth.Introspection/Events/OAuthIntrospectionEvents.cs
new file mode 100644
index 0000000..bc55cc1
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Introspection/Events/OAuthIntrospectionEvents.cs
@@ -0,0 +1,55 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using System;
+using System.Threading.Tasks;
+
+namespace AspNet.Security.OAuth.Introspection {
+ ///
+ /// Allows customization of introspection handling within the middleware.
+ ///
+ public class OAuthIntrospectionEvents : IOAuthIntrospectionEvents {
+ ///
+ /// Invoked when a ticket is to be created from an introspection response.
+ ///
+ public Func OnCreateTicket { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ public Func OnParseAccessToken { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a token is to be sent to the authorization server for introspection.
+ ///
+ public Func OnRequestTokenIntrospection { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ public Func OnValidateToken { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a ticket is to be created from an introspection response.
+ ///
+ public virtual Task CreateTicket(CreateTicketContext context) => OnCreateTicket(context);
+
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ public virtual Task ParseAccessToken(ParseAccessTokenContext context) => OnParseAccessToken(context);
+
+ ///
+ /// Invoked when a token is to be sent to the authorization server for introspection.
+ ///
+ public virtual Task RequestTokenIntrospection(RequestTokenIntrospectionContext context) => OnRequestTokenIntrospection(context);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ public virtual Task ValidateToken(ValidateTokenContext context) => OnValidateToken(context);
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Introspection/Events/ParseAccessTokenContext.cs b/src/AspNet.Security.OAuth.Introspection/Events/ParseAccessTokenContext.cs
new file mode 100644
index 0000000..f9d0c13
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Introspection/Events/ParseAccessTokenContext.cs
@@ -0,0 +1,38 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Http;
+
+namespace AspNet.Security.OAuth.Introspection {
+ ///
+ /// Allows custom parsing of access tokens from requests.
+ ///
+ public class ParseAccessTokenContext : BaseIntrospectionContext {
+ public ParseAccessTokenContext(
+ [NotNull]HttpContext context,
+ [NotNull]OAuthIntrospectionOptions options)
+ : base(context, options) {
+ }
+
+ private string _token { get; set; }
+
+ ///
+ /// Gets or sets the access token.
+ ///
+ /// Setting this property indicates to the middleware that the request has been processed
+ /// and a token extracted. Setting this to null will invalidate the token.
+ ///
+ ///
+ public string Token {
+ get { return _token; }
+ set {
+ Handled = true;
+ _token = value;
+ }
+ }
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Introspection/Events/RequestTokenIntrospectionContext.cs b/src/AspNet.Security.OAuth.Introspection/Events/RequestTokenIntrospectionContext.cs
new file mode 100644
index 0000000..382d258
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Introspection/Events/RequestTokenIntrospectionContext.cs
@@ -0,0 +1,53 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using System.Net.Http;
+using Microsoft.AspNetCore.Http;
+using Newtonsoft.Json.Linq;
+using JetBrains.Annotations;
+
+namespace AspNet.Security.OAuth.Introspection {
+ ///
+ /// Allows for custom handling of the call to the Authorization Server's Introspection endpoint.
+ ///
+ public class RequestTokenIntrospectionContext : BaseIntrospectionContext {
+ public RequestTokenIntrospectionContext(
+ [NotNull]HttpContext context,
+ [NotNull]OAuthIntrospectionOptions options,
+ [NotNull]string token)
+ : base(context, options) {
+ Token = token;
+ }
+
+ ///
+ /// An for use by the application to call the authorization server.
+ ///
+ public HttpClient Client => Options.HttpClient;
+
+ ///
+ /// The access token parsed from the client request.
+ ///
+ public string Token { get; }
+
+ private JObject _payload { get; set; }
+
+ ///
+ /// The data retrieved from the call to the introspection endpoint on the authorization server.
+ ///
+ /// Set this property to indicate that the introspection call was handled
+ /// by the application. Set this property to null to instruct the middleware
+ /// to indicate a failure.
+ ///
+ ///
+ public JObject Payload {
+ get { return _payload; }
+ set {
+ Handled = true;
+ Payload = value;
+ }
+ }
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Introspection/Events/ValidateTokenContext.cs b/src/AspNet.Security.OAuth.Introspection/Events/ValidateTokenContext.cs
new file mode 100644
index 0000000..ac011a2
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Introspection/Events/ValidateTokenContext.cs
@@ -0,0 +1,46 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+
+namespace AspNet.Security.OAuth.Introspection {
+ ///
+ /// Allows customization of the token validation logic.
+ ///
+ public class ValidateTokenContext : BaseIntrospectionContext {
+ public ValidateTokenContext(
+ [NotNull]HttpContext context,
+ [NotNull]OAuthIntrospectionOptions options,
+ [NotNull]AuthenticationTicket ticket)
+ : base(context, options) {
+ Ticket = ticket;
+ }
+
+ ///
+ /// The created from the introspection data.
+ ///
+ public AuthenticationTicket Ticket { get; }
+
+ private bool _isValid { get; set; } = true;
+
+ ///
+ /// Indicates the ticket is valid.
+ ///
+ /// Setting this property indicates to the middleware that token validation
+ /// has been handled by the application.
+ ///
+ ///
+ public bool IsValid {
+ get { return _isValid; }
+ set {
+ Handled = true;
+ _isValid = value;
+ }
+ }
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionConstants.cs b/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionConstants.cs
index 853b7c5..56a8c02 100644
--- a/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionConstants.cs
+++ b/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionConstants.cs
@@ -27,6 +27,10 @@ public static class Parameters {
public const string TokenTypeHint = "token_type_hint";
}
+ public static class Properties {
+ public const string Audiences = ".audiences";
+ }
+
public static class TokenTypes {
public const string AccessToken = "access_token";
}
diff --git a/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionHandler.cs b/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionHandler.cs
index c5764c7..b31c004 100644
--- a/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionHandler.cs
+++ b/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionHandler.cs
@@ -23,55 +23,94 @@
namespace AspNet.Security.OAuth.Introspection {
public class OAuthIntrospectionHandler : AuthenticationHandler {
protected override async Task HandleAuthenticateAsync() {
- string header = Request.Headers[HeaderNames.Authorization];
- if (string.IsNullOrEmpty(header)) {
- Logger.LogInformation("Authentication was skipped because no bearer token was received.");
-
- return AuthenticateResult.Skip();
- }
+ // Give the application an opportunity to parse the token from a different location, adjust, or reject token
+ var parseAccessTokenContext = new ParseAccessTokenContext(Context, Options);
+ await Options.Events.ParseAccessToken(parseAccessTokenContext);
+
+ // Initialize the token from the event in case it was set during the event.
+ string token = parseAccessTokenContext.Token;
+
+ // Bypass the default processing if the event handled the request parsing.
+ if (!parseAccessTokenContext.Handled) {
+ string header = Request.Headers[HeaderNames.Authorization];
+ if (string.IsNullOrWhiteSpace(header)) {
+ return AuthenticateResult.Fail("Authentication failed because the bearer token " +
+ "was missing from the 'Authorization' header.");
+ }
- // Ensure that the authorization header contains the mandatory "Bearer" scheme.
- // See https://tools.ietf.org/html/rfc6750#section-2.1
- if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) {
- Logger.LogInformation("Authentication was skipped because an incompatible " +
- "scheme was used in the 'Authorization' header.");
+ // Ensure that the authorization header contains the mandatory "Bearer" scheme.
+ // See https://tools.ietf.org/html/rfc6750#section-2.1
+ if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) {
+ return AuthenticateResult.Fail("Authentication failed because an invalid scheme " +
+ "was used in the 'Authorization' header.");
+ }
- return AuthenticateResult.Skip();
+ token = header.Substring("Bearer ".Length);
}
- var token = header.Substring("Bearer ".Length);
if (string.IsNullOrWhiteSpace(token)) {
return AuthenticateResult.Fail("Authentication failed because the bearer token " +
- "was missing from the 'Authorization' header.");
+ "was missing from the request. The default location" +
+ "is in the 'Authorization' header.");
}
// Try to resolve the authentication ticket from the distributed cache. If none
// can be found, a new introspection request is sent to the authorization server.
var ticket = await RetrieveTicketAsync(token);
if (ticket == null) {
+ // Allow interception of the introspection retrieval process via events
+ var requestTokenIntrospectionContext = new RequestTokenIntrospectionContext(Context, Options, token);
+ await Options.Events.RequestTokenIntrospection(requestTokenIntrospectionContext);
+
+ // Flow the payload from the application possibly handling the introspection request
+ var payload = requestTokenIntrospectionContext.Payload;
+
+ if (!requestTokenIntrospectionContext.Handled) {
+ // Set the payload using the default introspection behavior
+ payload = await GetIntrospectionPayloadAsync(token);
+ }
+
// Return a failed authentication result if the introspection
// request failed or if the "active" claim was false.
- var payload = await GetIntrospectionPayloadAsync(token);
if (payload == null || !payload.Value(OAuthIntrospectionConstants.Claims.Active)) {
return AuthenticateResult.Fail("Authentication failed because the authorization " +
"server rejected the access token.");
}
- // Ensure that the access token was issued
- // to be used with this resource server.
- if (!await ValidateAudienceAsync(payload)) {
- return AuthenticateResult.Fail("Authentication failed because the access token " +
- "was not valid for this resource server.");
- }
+ // Allow interception of the ticket creation process via events
+ var createTicketContext = new CreateTicketContext(Context, Options, payload);
+ await Options.Events.CreateTicket(createTicketContext);
// Create a new authentication ticket from the introspection
- // response returned by the authorization server.
- ticket = await CreateTicketAsync(payload);
+ // response returned by the authorization server if the application
+ // has not handled the ticket creation process.
+ ticket = createTicketContext.Handled ? createTicketContext.Ticket : await CreateTicketAsync(payload);
Debug.Assert(ticket != null);
await StoreTicketAsync(token, ticket);
}
+ // Allow for interception and handling of the token validated event.
+ var validateTokenContext = new ValidateTokenContext(Context, Options, ticket);
+ await Options.Events.ValidateToken(validateTokenContext);
+
+ if (validateTokenContext.Handled) {
+ // Return a result based on how the validation was handled.
+ return validateTokenContext.IsValid ?
+ AuthenticateResult.Success(validateTokenContext.Ticket) :
+ AuthenticateResult.Fail("Authentication failed because the access token was not valid.");
+ }
+
+ // Flow any changes that were made to the ticket during the validation event.
+ ticket = validateTokenContext.Ticket;
+
+ // Ensure that the access token was issued
+ // to be used with this resource server.
+ if (!await ValidateAudienceAsync(ticket)) {
+ return AuthenticateResult.Fail("Authentication failed because the access token " +
+ "was not valid for this resource server.");
+ }
+
// Ensure that the authentication ticket is still valid.
if (ticket.Properties.ExpiresUtc.HasValue &&
ticket.Properties.ExpiresUtc.Value < Options.SystemClock.UtcNow) {
@@ -114,11 +153,11 @@ protected virtual async Task ResolveIntrospectionEndpointAsync(string is
protected virtual async Task GetIntrospectionPayloadAsync(string token) {
// Note: updating the options during a request is not thread safe but is harmless in this case:
// in the worst case, it will only send multiple configuration requests to the authorization server.
- if (string.IsNullOrEmpty(Options.IntrospectionEndpoint)) {
+ if (string.IsNullOrWhiteSpace(Options.IntrospectionEndpoint)) {
Options.IntrospectionEndpoint = await ResolveIntrospectionEndpointAsync(Options.Authority);
}
- if (string.IsNullOrEmpty(Options.IntrospectionEndpoint)) {
+ if (string.IsNullOrWhiteSpace(Options.IntrospectionEndpoint)) {
throw new InvalidOperationException("The OAuth2 introspection middleware was unable to retrieve " +
"the provider configuration from the OAuth2 authorization server.");
}
@@ -149,48 +188,25 @@ protected virtual async Task GetIntrospectionPayloadAsync(string token)
return JObject.Parse(await response.Content.ReadAsStringAsync());
}
- protected virtual Task ValidateAudienceAsync(JObject payload) {
+ protected virtual Task ValidateAudienceAsync(AuthenticationTicket ticket) {
// If no explicit audience has been configured,
// skip the default audience validation.
if (Options.Audiences.Count == 0) {
return Task.FromResult(true);
}
- // If no "aud" claim was returned by the authorization server,
- // assume the access token was not specific enough and reject it.
- if (payload[OAuthIntrospectionConstants.Claims.Audience] == null) {
+ // Extract the audiences from the authentication ticket.
+ string audiences;
+ if (!ticket.Properties.Items.TryGetValue(OAuthIntrospectionConstants.Properties.Audiences, out audiences)) {
return Task.FromResult(false);
}
- // Note: the "aud" claim can be either a string or an array.
- // See https://tools.ietf.org/html/rfc7662#section-2.2
- switch (payload[OAuthIntrospectionConstants.Claims.Audience].Type) {
- case JTokenType.Array: {
- // When the "aud" claim is an array, at least one value must correspond
- // to the audience registered in the introspection middleware options.
- var audiences = payload.Value(OAuthIntrospectionConstants.Claims.Audience)
- .Select(audience => audience.Value());
- if (audiences.Intersect(Options.Audiences, StringComparer.Ordinal).Any()) {
- return Task.FromResult(true);
- }
-
- return Task.FromResult(false);
- }
-
- case JTokenType.String: {
- // When the "aud" claim is a string, it must exactly match the
- // audience registered in the introspection middleware options.
- var audience = payload.Value(OAuthIntrospectionConstants.Claims.Audience);
- if (Options.Audiences.Contains(audience, StringComparer.Ordinal)) {
- return Task.FromResult(true);
- }
-
- return Task.FromResult(false);
- }
-
- default:
- return Task.FromResult(false);
+ // Ensure that the authentication ticket contains the registered audience.
+ if (!audiences.Split(' ').Intersect(Options.Audiences, StringComparer.Ordinal).Any()) {
+ return Task.FromResult(false);
}
+
+ return Task.FromResult(true);
}
protected virtual Task CreateTicketAsync(JObject payload) {
@@ -254,6 +270,24 @@ protected virtual Task CreateTicketAsync(JObject payload)
continue;
}
+
+ // Store the audience or audiences in the ticket properties.
+ case OAuthIntrospectionConstants.Claims.Audience: {
+ if(property.Value.Type == JTokenType.Array) {
+ var array = (JArray)property.Value;
+ if(array == null) {
+ continue;
+ }
+ var audiences = string.Join(" ", array.Select(item => item.Value()));
+ properties.Items[OAuthIntrospectionConstants.Properties.Audiences] = audiences;
+ }
+
+ else if(property.Value.Type == JTokenType.String) {
+ properties.Items[OAuthIntrospectionConstants.Properties.Audiences] = (string)property.Value;
+ }
+
+ continue;
+ }
}
switch (property.Value.Type) {
diff --git a/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionOptions.cs b/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionOptions.cs
index 883dcc2..6282d0c 100644
--- a/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionOptions.cs
+++ b/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionOptions.cs
@@ -51,6 +51,13 @@ public OAuthIntrospectionOptions() {
///
public IDistributedCache Cache { get; set; }
+ ///
+ /// The object provided by the application to process events raised by the bearer authentication middleware.
+ /// The application may implement the interface fully, or it may create an instance of OAuthIntrospectionEvents
+ /// and assign delegates only to the events it wants to process.
+ ///
+ public IOAuthIntrospectionEvents Events { get; set; } = new OAuthIntrospectionEvents();
+
///
/// Gets or sets the HTTP client used to communicate
/// with the remote OAuth2/OpenID Connect server.
diff --git a/src/AspNet.Security.OAuth.Validation/Events/BaseValidationContext.cs b/src/AspNet.Security.OAuth.Validation/Events/BaseValidationContext.cs
new file mode 100644
index 0000000..e488f31
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Validation/Events/BaseValidationContext.cs
@@ -0,0 +1,33 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+
+namespace AspNet.Security.OAuth.Validation {
+ ///
+ /// Base class for all validation events that holds common properties.
+ ///
+ public abstract class BaseValidationContext : BaseContext {
+ public BaseValidationContext(
+ [NotNull]HttpContext context,
+ [NotNull]OAuthValidationOptions options)
+ : base(context) {
+ Options = options;
+ }
+
+ ///
+ /// Indicates the application has handled the event process.
+ ///
+ internal bool Handled { get; set; }
+
+ ///
+ /// The middleware Options.
+ ///
+ public OAuthValidationOptions Options { get; }
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Validation/Events/IOAuthValidationEvents.cs b/src/AspNet.Security.OAuth.Validation/Events/IOAuthValidationEvents.cs
new file mode 100644
index 0000000..203f480
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Validation/Events/IOAuthValidationEvents.cs
@@ -0,0 +1,24 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using System.Threading.Tasks;
+
+namespace AspNet.Security.OAuth.Validation {
+ ///
+ /// Allows customization of validation handling within the middleware.
+ ///
+ public interface IOAuthValidationEvents {
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ Task ParseAccessToken(ParseAccessTokenContext context);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ Task ValidateToken(ValidateTokenContext context);
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Validation/Events/OAuthValidationEvents.cs b/src/AspNet.Security.OAuth.Validation/Events/OAuthValidationEvents.cs
new file mode 100644
index 0000000..d6f5ffb
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Validation/Events/OAuthValidationEvents.cs
@@ -0,0 +1,35 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using System;
+using System.Threading.Tasks;
+
+namespace AspNet.Security.OAuth.Validation {
+ ///
+ /// Allows customization of validation handling within the middleware.
+ ///
+ public class OAuthValidationEvents : IOAuthValidationEvents {
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ public Func OnParseAccessToken { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ public Func OnValidateToken { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ public virtual Task ParseAccessToken(ParseAccessTokenContext context) => OnParseAccessToken(context);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ public virtual Task ValidateToken(ValidateTokenContext context) => OnValidateToken(context);
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Validation/Events/ParseAccessTokenContext.cs b/src/AspNet.Security.OAuth.Validation/Events/ParseAccessTokenContext.cs
new file mode 100644
index 0000000..f416181
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Validation/Events/ParseAccessTokenContext.cs
@@ -0,0 +1,38 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Http;
+
+namespace AspNet.Security.OAuth.Validation {
+ ///
+ /// Allows custom parsing of access tokens from requests.
+ ///
+ public class ParseAccessTokenContext : BaseValidationContext {
+ public ParseAccessTokenContext(
+ [NotNull]HttpContext context,
+ [NotNull]OAuthValidationOptions options)
+ : base(context, options) {
+ }
+
+ private string _token { get; set; }
+
+ ///
+ /// Gets or sets the access token.
+ ///
+ /// Setting this property indicates to the middleware that the request has been processed
+ /// and a token extracted. Setting this to null will invalidate the token.
+ ///
+ ///
+ public string Token {
+ get { return _token; }
+ set {
+ Handled = true;
+ _token = value;
+ }
+ }
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Validation/Events/ValidateTokenContext.cs b/src/AspNet.Security.OAuth.Validation/Events/ValidateTokenContext.cs
new file mode 100644
index 0000000..d91aa38
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Validation/Events/ValidateTokenContext.cs
@@ -0,0 +1,46 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+
+namespace AspNet.Security.OAuth.Validation {
+ ///
+ /// Allows customization of the token validation logic.
+ ///
+ public class ValidateTokenContext : BaseValidationContext {
+ public ValidateTokenContext(
+ [NotNull]HttpContext context,
+ [NotNull]OAuthValidationOptions options,
+ [NotNull]AuthenticationTicket ticket)
+ : base(context, options) {
+ Ticket = ticket;
+ }
+
+ ///
+ /// The created from the introspection data.
+ ///
+ public AuthenticationTicket Ticket { get; }
+
+ private bool _isValid { get; set; } = true;
+
+ ///
+ /// Indicates the ticket is valid.
+ ///
+ /// Setting this property indicates to the middleware that token validation
+ /// has been handled by the application.
+ ///
+ ///
+ public bool IsValid {
+ get { return _isValid; }
+ set {
+ Handled = true;
+ _isValid = value;
+ }
+ }
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Validation/OAuthValidationHandler.cs b/src/AspNet.Security.OAuth.Validation/OAuthValidationHandler.cs
index ad6fa66..cb1d9ec 100644
--- a/src/AspNet.Security.OAuth.Validation/OAuthValidationHandler.cs
+++ b/src/AspNet.Security.OAuth.Validation/OAuthValidationHandler.cs
@@ -8,32 +8,40 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
-using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace AspNet.Security.OAuth.Validation {
public class OAuthValidationHandler : AuthenticationHandler {
protected override async Task HandleAuthenticateAsync() {
- string header = Request.Headers[HeaderNames.Authorization];
- if (string.IsNullOrEmpty(header)) {
- Logger.LogInformation("Authentication was skipped because no bearer token was received.");
+ // Give the application an opportunity to parse the token from a different location, adjust, or reject token
+ var parseAccessTokenContext = new ParseAccessTokenContext(Context, Options);
+ await Options.Events.ParseAccessToken(parseAccessTokenContext);
- return AuthenticateResult.Skip();
- }
+ // Initialize the token from the event in case it was set during the event.
+ string token = parseAccessTokenContext.Token;
+
+ // Bypass the default processing if the event handled the request parsing.
+ if (!parseAccessTokenContext.Handled) {
+ string header = Request.Headers[HeaderNames.Authorization];
+ if (string.IsNullOrWhiteSpace(header)) {
+ return AuthenticateResult.Fail("Authentication failed because the bearer token " +
+ "was missing from the 'Authorization' header.");
+ }
- // Ensure that the authorization header contains the mandatory "Bearer" scheme.
- // See https://tools.ietf.org/html/rfc6750#section-2.1
- if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) {
- Logger.LogInformation("Authentication was skipped because an incompatible " +
- "scheme was used in the 'Authorization' header.");
+ // Ensure that the authorization header contains the mandatory "Bearer" scheme.
+ // See https://tools.ietf.org/html/rfc6750#section-2.1
+ if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) {
+ return AuthenticateResult.Fail("Authentication failed because an invalid scheme " +
+ "was used in the 'Authorization' header.");
+ }
- return AuthenticateResult.Skip();
+ token = header.Substring("Bearer ".Length);
}
- var token = header.Substring("Bearer ".Length);
if (string.IsNullOrWhiteSpace(token)) {
return AuthenticateResult.Fail("Authentication failed because the bearer token " +
- "was missing from the 'Authorization' header.");
+ "was missing from the request. The default location" +
+ "is in the 'Authorization' header.");
}
// Try to unprotect the token and return an error
@@ -43,6 +51,20 @@ protected override async Task HandleAuthenticateAsync() {
return AuthenticateResult.Fail("Authentication failed because the access token was invalid.");
}
+ // Allow for interception and handling of the token validated event.
+ var validateTokenContext = new ValidateTokenContext(Context, Options, ticket);
+ await Options.Events.ValidateToken(validateTokenContext);
+
+ if (validateTokenContext.Handled) {
+ // Return a result based on how the validation was handled.
+ return validateTokenContext.IsValid ?
+ AuthenticateResult.Success(validateTokenContext.Ticket) :
+ AuthenticateResult.Fail("Authentication failed because the access token was not valid.");
+ }
+
+ // Flow any changes that were made to the ticket during the validation event.
+ ticket = validateTokenContext.Ticket;
+
// Ensure that the access token was issued
// to be used with this resource server.
if (!await ValidateAudienceAsync(ticket)) {
diff --git a/src/AspNet.Security.OAuth.Validation/OAuthValidationOptions.cs b/src/AspNet.Security.OAuth.Validation/OAuthValidationOptions.cs
index d334d62..3e928f8 100644
--- a/src/AspNet.Security.OAuth.Validation/OAuthValidationOptions.cs
+++ b/src/AspNet.Security.OAuth.Validation/OAuthValidationOptions.cs
@@ -9,9 +9,12 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
-namespace AspNet.Security.OAuth.Validation {
- public class OAuthValidationOptions : AuthenticationOptions {
- public OAuthValidationOptions() {
+namespace AspNet.Security.OAuth.Validation
+{
+ public class OAuthValidationOptions : AuthenticationOptions
+ {
+ public OAuthValidationOptions()
+ {
AuthenticationScheme = OAuthValidationDefaults.AuthenticationScheme;
}
@@ -40,5 +43,12 @@ public OAuthValidationOptions() {
/// is directly retrieved from the dependency injection container.
///
public IDataProtectionProvider DataProtectionProvider { get; set; }
+
+ ///
+ /// The object provided by the application to process events raised by the validation authentication middleware.
+ /// The application may implement the interface fully, or it may create an instance of
+ /// and assign delegates only to the events it wants to process.
+ ///
+ public IOAuthValidationEvents Events { get; set; } = new OAuthValidationEvents();
}
}
diff --git a/src/Owin.Security.OAuth.Introspection/Events/BaseIntrospectionContext.cs b/src/Owin.Security.OAuth.Introspection/Events/BaseIntrospectionContext.cs
new file mode 100644
index 0000000..6a647d0
--- /dev/null
+++ b/src/Owin.Security.OAuth.Introspection/Events/BaseIntrospectionContext.cs
@@ -0,0 +1,28 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.Owin;
+using Microsoft.Owin.Security.Provider;
+
+namespace Owin.Security.OAuth.Introspection
+{
+ ///
+ /// Base class for all introspection events that holds common properties.
+ ///
+ public abstract class BaseIntrospectionContext : BaseContext {
+ public BaseIntrospectionContext(
+ [NotNull]IOwinContext context,
+ [NotNull]OAuthIntrospectionOptions options)
+ : base(context, options) {
+ }
+
+ ///
+ /// Indicates the application has handled the event process.
+ ///
+ internal bool Handled { get; set; }
+ }
+}
diff --git a/src/Owin.Security.OAuth.Introspection/Events/CreateTicketContext.cs b/src/Owin.Security.OAuth.Introspection/Events/CreateTicketContext.cs
new file mode 100644
index 0000000..09a896a
--- /dev/null
+++ b/src/Owin.Security.OAuth.Introspection/Events/CreateTicketContext.cs
@@ -0,0 +1,48 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.Owin;
+using Microsoft.Owin.Security;
+using Newtonsoft.Json.Linq;
+
+namespace Owin.Security.OAuth.Introspection {
+ ///
+ /// Allows interception of the AuthenticationTicket creation process.
+ ///
+ public class CreateTicketContext : BaseIntrospectionContext {
+ public CreateTicketContext(
+ [NotNull]IOwinContext context,
+ [NotNull]OAuthIntrospectionOptions options,
+ [NotNull]JObject payload)
+ : base(context, options) {
+ Payload = payload;
+ }
+
+ ///
+ /// The payload from the introspection request to the authorization server.
+ ///
+ public JObject Payload { get; }
+
+ private AuthenticationTicket _ticket { get; set; }
+
+ ///
+ /// An created by the application.
+ ///
+ /// Set this property to indicate that the application has handled the creation of the
+ /// ticket. Set this property to null to instruct the middleware there was a failure
+ /// during ticket creation.
+ ///
+ ///
+ public AuthenticationTicket Ticket {
+ get { return _ticket; }
+ set {
+ Handled = true;
+ _ticket = value;
+ }
+ }
+ }
+}
diff --git a/src/Owin.Security.OAuth.Introspection/Events/IOAuthIntrospectionEvents.cs b/src/Owin.Security.OAuth.Introspection/Events/IOAuthIntrospectionEvents.cs
new file mode 100644
index 0000000..a431db1
--- /dev/null
+++ b/src/Owin.Security.OAuth.Introspection/Events/IOAuthIntrospectionEvents.cs
@@ -0,0 +1,34 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using System.Threading.Tasks;
+
+namespace Owin.Security.OAuth.Introspection {
+ ///
+ /// Allows customization of introspection handling within the middleware.
+ ///
+ public interface IOAuthIntrospectionEvents {
+ ///
+ /// Invoked when a ticket is to be created from an introspection response.
+ ///
+ Task CreateTicket(CreateTicketContext context);
+
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ Task ParseAccessToken(ParseAccessTokenContext context);
+
+ ///
+ /// Invoked when a token is to be sent to the authorization server for introspection.
+ ///
+ Task RequestTokenIntrospection(RequestTokenIntrospectionContext context);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ Task ValidateToken(ValidateTokenContext context);
+ }
+}
diff --git a/src/Owin.Security.OAuth.Introspection/Events/OAuthIntrospectionEvents.cs b/src/Owin.Security.OAuth.Introspection/Events/OAuthIntrospectionEvents.cs
new file mode 100644
index 0000000..85cbb6f
--- /dev/null
+++ b/src/Owin.Security.OAuth.Introspection/Events/OAuthIntrospectionEvents.cs
@@ -0,0 +1,55 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using System;
+using System.Threading.Tasks;
+
+namespace Owin.Security.OAuth.Introspection {
+ ///
+ /// Allows customization of introspection handling within the middleware.
+ ///
+ public class OAuthIntrospectionEvents : IOAuthIntrospectionEvents {
+ ///
+ /// Invoked when a ticket is to be created from an introspection response.
+ ///
+ public Func OnCreateTicket { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ public Func OnParseAccessToken { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a token is to be sent to the authorization server for introspection.
+ ///
+ public Func OnRequestTokenIntrospection { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ public Func OnValidateToken { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a ticket is to be created from an introspection response.
+ ///
+ public virtual Task CreateTicket(CreateTicketContext context) => OnCreateTicket(context);
+
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ public virtual Task ParseAccessToken(ParseAccessTokenContext context) => OnParseAccessToken(context);
+
+ ///
+ /// Invoked when a token is to be sent to the authorization server for introspection.
+ ///
+ public virtual Task RequestTokenIntrospection(RequestTokenIntrospectionContext context) => OnRequestTokenIntrospection(context);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ public virtual Task ValidateToken(ValidateTokenContext context) => OnValidateToken(context);
+ }
+}
diff --git a/src/Owin.Security.OAuth.Introspection/Events/ParseAccessTokenContext.cs b/src/Owin.Security.OAuth.Introspection/Events/ParseAccessTokenContext.cs
new file mode 100644
index 0000000..281e017
--- /dev/null
+++ b/src/Owin.Security.OAuth.Introspection/Events/ParseAccessTokenContext.cs
@@ -0,0 +1,38 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.Owin;
+
+namespace Owin.Security.OAuth.Introspection {
+ ///
+ /// Allows custom parsing of access tokens from requests.
+ ///
+ public class ParseAccessTokenContext : BaseIntrospectionContext {
+ public ParseAccessTokenContext(
+ [NotNull]IOwinContext context,
+ [NotNull]OAuthIntrospectionOptions options)
+ : base(context, options) {
+ }
+
+ private string _token { get; set; }
+
+ ///
+ /// Gets or sets the access token.
+ ///
+ /// Setting this property indicates to the middleware that the request has been processed
+ /// and a token extracted. Setting this to null will invalidate the token.
+ ///
+ ///
+ public string Token {
+ get { return _token; }
+ set {
+ Handled = true;
+ _token = value;
+ }
+ }
+ }
+}
diff --git a/src/Owin.Security.OAuth.Introspection/Events/RequestTokenIntrospectionContext.cs b/src/Owin.Security.OAuth.Introspection/Events/RequestTokenIntrospectionContext.cs
new file mode 100644
index 0000000..bd26830
--- /dev/null
+++ b/src/Owin.Security.OAuth.Introspection/Events/RequestTokenIntrospectionContext.cs
@@ -0,0 +1,53 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using System.Net.Http;
+using JetBrains.Annotations;
+using Newtonsoft.Json.Linq;
+using Microsoft.Owin;
+
+namespace Owin.Security.OAuth.Introspection {
+ ///
+ /// Allows for custom handling of the call to the Authorization Server's Introspection endpoint.
+ ///
+ public class RequestTokenIntrospectionContext : BaseIntrospectionContext {
+ public RequestTokenIntrospectionContext(
+ [NotNull]IOwinContext context,
+ [NotNull]OAuthIntrospectionOptions options,
+ [NotNull]string token)
+ : base(context, options) {
+ Token = token;
+ }
+
+ ///
+ /// An for use by the application to call the authorization server.
+ ///
+ public HttpClient Client => Options.HttpClient;
+
+ ///
+ /// The access token parsed from the client request.
+ ///
+ public string Token { get; }
+
+ private JObject _payload { get; set; }
+
+ ///
+ /// The data retrieved from the call to the introspection endpoint on the authorization server.
+ ///
+ /// Set this property to indicate that the introspection call was handled
+ /// by the application. Set this property to null to instruct the middleware
+ /// to indicate a failure.
+ ///
+ ///
+ public JObject Payload {
+ get { return _payload; }
+ set {
+ Handled = true;
+ Payload = value;
+ }
+ }
+ }
+}
diff --git a/src/Owin.Security.OAuth.Introspection/Events/ValidateTokenContext.cs b/src/Owin.Security.OAuth.Introspection/Events/ValidateTokenContext.cs
new file mode 100644
index 0000000..7bb446c
--- /dev/null
+++ b/src/Owin.Security.OAuth.Introspection/Events/ValidateTokenContext.cs
@@ -0,0 +1,46 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.Owin;
+using Microsoft.Owin.Security;
+
+namespace Owin.Security.OAuth.Introspection {
+ ///
+ /// Allows customization of the token validation logic.
+ ///
+ public class ValidateTokenContext : BaseIntrospectionContext {
+ public ValidateTokenContext(
+ [NotNull]IOwinContext context,
+ [NotNull]OAuthIntrospectionOptions options,
+ [NotNull]AuthenticationTicket ticket)
+ : base(context, options) {
+ Ticket = ticket;
+ }
+
+ ///
+ /// The created from the introspection data.
+ ///
+ public AuthenticationTicket Ticket { get; }
+
+ private bool _isValid { get; set; } = true;
+
+ ///
+ /// Indicates the ticket is valid.
+ ///
+ /// Setting this property indicates to the middleware that token validation
+ /// has been handled by the application.
+ ///
+ ///
+ public bool IsValid {
+ get { return _isValid; }
+ set {
+ Handled = true;
+ _isValid = value;
+ }
+ }
+ }
+}
diff --git a/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionConstants.cs b/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionConstants.cs
index 9441d14..5b0cc45 100644
--- a/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionConstants.cs
+++ b/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionConstants.cs
@@ -18,6 +18,10 @@ public static class Claims {
public const string Username = "username";
}
+ public static class HeaderNames {
+ public const string Authorization = "Authorization";
+ }
+
public static class Metadata {
public const string IntrospectionEndpoint = "introspection_endpoint";
}
@@ -27,6 +31,10 @@ public static class Parameters {
public const string TokenTypeHint = "token_type_hint";
}
+ public static class Properties {
+ public const string Audiences = ".audiences";
+ }
+
public static class TokenTypes {
public const string AccessToken = "access_token";
}
diff --git a/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionHandler.cs b/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionHandler.cs
index 9879896..015c831 100644
--- a/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionHandler.cs
+++ b/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionHandler.cs
@@ -21,27 +21,44 @@
namespace Owin.Security.OAuth.Introspection {
public class OAuthIntrospectionHandler : AuthenticationHandler {
- protected override async Task AuthenticateCoreAsync() {
- var header = Request.Headers.Get("Authorization");
- if (string.IsNullOrEmpty(header)) {
- Options.Logger.LogInformation("Authentication was skipped because no bearer token was received.");
+ protected override async Task AuthenticateCoreAsync()
+ {// Give the application an opportunity to parse the token from a different location, adjust, or reject token
+ var parseAccessTokenContext = new ParseAccessTokenContext(Context, Options);
+ await Options.Events.ParseAccessToken(parseAccessTokenContext);
+
+ // Initialize the token from the event in case it was set during the event.
+ string token = parseAccessTokenContext.Token;
+
+ // Bypass the default processing if the event handled the request parsing.
+ if (!parseAccessTokenContext.Handled)
+ {
+ string header = Request.Headers[OAuthIntrospectionConstants.HeaderNames.Authorization];
+ if (string.IsNullOrWhiteSpace(header))
+ {
+ Options.Logger.LogError("Authentication failed because the bearer token " +
+ "was missing from the 'Authorization' header.");
- return null;
- }
+ return null;
+ }
- // Ensure that the authorization header contains the mandatory "Bearer" scheme.
- // See https://tools.ietf.org/html/rfc6750#section-2.1
- if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) {
- Options.Logger.LogInformation("Authentication was skipped because an incompatible " +
- "scheme was used in the 'Authorization' header.");
+ // Ensure that the authorization header contains the mandatory "Bearer" scheme.
+ // See https://tools.ietf.org/html/rfc6750#section-2.1
+ if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
+ {
+ Options.Logger.LogError("Authentication failed because an invalid scheme " +
+ "was used in the 'Authorization' header.");
- return null;
+ return null;
+ }
+
+ token = header.Substring("Bearer ".Length);
}
- var token = header.Substring("Bearer ".Length);
- if (string.IsNullOrWhiteSpace(token)) {
+ if (string.IsNullOrWhiteSpace(token))
+ {
Options.Logger.LogError("Authentication failed because the bearer token " +
- "was missing from the 'Authorization' header.");
+ "was missing from the request. The default location" +
+ "is in the 'Authorization' header.");
return null;
}
@@ -49,37 +66,77 @@ protected override async Task AuthenticateCoreAsync() {
// Try to resolve the authentication ticket from the distributed cache. If none
// can be found, a new introspection request is sent to the authorization server.
var ticket = await RetrieveTicketAsync(token);
- if (ticket == null) {
+ if (ticket == null)
+ {
+ // Allow interception of the introspection retrieval process via events
+ var requestTokenIntrospectionContext = new RequestTokenIntrospectionContext(Context, Options, token);
+ await Options.Events.RequestTokenIntrospection(requestTokenIntrospectionContext);
+
+ // Flow the payload from the application possibly handling the introspection request
+ var payload = requestTokenIntrospectionContext.Payload;
+
+ if (!requestTokenIntrospectionContext.Handled)
+ {
+ // Set the payload using the default introspection behavior
+ payload = await GetIntrospectionPayloadAsync(token);
+ }
+
// Return a failed authentication result if the introspection
// request failed or if the "active" claim was false.
- var payload = await GetIntrospectionPayloadAsync(token);
- if (payload == null || !payload.Value(OAuthIntrospectionConstants.Claims.Active)) {
+ if (payload == null || !payload.Value(OAuthIntrospectionConstants.Claims.Active))
+ {
Options.Logger.LogError("Authentication failed because the authorization " +
"server rejected the access token.");
return null;
}
- // Ensure that the access token was issued
- // to be used with this resource server.
- if (!await ValidateAudienceAsync(payload)) {
- Options.Logger.LogError("Authentication failed because the access token " +
- "was not valid for this resource server.");
-
- return null;
- }
+ // Allow interception of the ticket creation process via events
+ var createTicketContext = new CreateTicketContext(Context, Options, payload);
+ await Options.Events.CreateTicket(createTicketContext);
// Create a new authentication ticket from the introspection
- // response returned by the authorization server.
- ticket = await CreateTicketAsync(payload);
+ // response returned by the authorization server if the application
+ // has not handled the ticket creation process.
+ ticket = createTicketContext.Handled ? createTicketContext.Ticket : await CreateTicketAsync(payload);
Debug.Assert(ticket != null);
await StoreTicketAsync(token, ticket);
}
+ // Allow for interception and handling of the token validated event.
+ var validateTokenContext = new ValidateTokenContext(Context, Options, ticket);
+ await Options.Events.ValidateToken(validateTokenContext);
+
+ if (validateTokenContext.Handled)
+ {
+ // Return a result based on how the validation was handled.
+ if (validateTokenContext.IsValid)
+ {
+ return validateTokenContext.Ticket;
+ }
+
+ Options.Logger.LogError("Authentication failed because the access token was not valid.");
+ return null;
+ }
+
+ // Flow any changes that were made to the ticket during the validation event.
+ ticket = validateTokenContext.Ticket;
+
+ // Ensure that the access token was issued
+ // to be used with this resource server.
+ if (!await ValidateAudienceAsync(ticket))
+ {
+ Options.Logger.LogError("Authentication failed because the access token " +
+ "was not valid for this resource server.");
+
+ return null;
+ }
+
// Ensure that the authentication ticket is still valid.
if (ticket.Properties.ExpiresUtc.HasValue &&
- ticket.Properties.ExpiresUtc.Value < Options.SystemClock.UtcNow) {
+ ticket.Properties.ExpiresUtc.Value < Options.SystemClock.UtcNow)
+ {
Options.Logger.LogError("Authentication failed because the access token was expired.");
return null;
@@ -121,11 +178,11 @@ protected virtual async Task ResolveIntrospectionEndpointAsync(string is
protected virtual async Task GetIntrospectionPayloadAsync(string token) {
// Note: updating the options during a request is not thread safe but is harmless in this case:
// in the worst case, it will only send multiple configuration requests to the authorization server.
- if (string.IsNullOrEmpty(Options.IntrospectionEndpoint)) {
+ if (string.IsNullOrWhiteSpace(Options.IntrospectionEndpoint)) {
Options.IntrospectionEndpoint = await ResolveIntrospectionEndpointAsync(Options.Authority);
}
- if (string.IsNullOrEmpty(Options.IntrospectionEndpoint)) {
+ if (string.IsNullOrWhiteSpace(Options.IntrospectionEndpoint)) {
throw new InvalidOperationException("The OAuth2 introspection middleware was unable to retrieve " +
"the provider configuration from the OAuth2 authorization server.");
}
@@ -156,48 +213,29 @@ protected virtual async Task GetIntrospectionPayloadAsync(string token)
return JObject.Parse(await response.Content.ReadAsStringAsync());
}
- protected virtual Task ValidateAudienceAsync(JObject payload) {
+ protected virtual Task ValidateAudienceAsync(AuthenticationTicket ticket)
+ {
// If no explicit audience has been configured,
// skip the default audience validation.
- if (Options.Audiences.Count == 0) {
+ if (Options.Audiences.Count == 0)
+ {
return Task.FromResult(true);
}
- // If no "aud" claim was returned by the authorization server,
- // assume the access token was not specific enough and reject it.
- if (payload[OAuthIntrospectionConstants.Claims.Audience] == null) {
+ // Extract the audiences from the authentication ticket.
+ string audiences;
+ if (!ticket.Properties.Dictionary.TryGetValue(OAuthIntrospectionConstants.Properties.Audiences, out audiences))
+ {
return Task.FromResult(false);
}
- // Note: the "aud" claim can be either a string or an array.
- // See https://tools.ietf.org/html/rfc7662#section-2.2
- switch (payload[OAuthIntrospectionConstants.Claims.Audience].Type) {
- case JTokenType.Array: {
- // When the "aud" claim is an array, at least one value must correspond
- // to the audience registered in the introspection middleware options.
- var audiences = payload.Value(OAuthIntrospectionConstants.Claims.Audience)
- .Select(audience => audience.Value());
- if (audiences.Intersect(Options.Audiences, StringComparer.Ordinal).Any()) {
- return Task.FromResult(true);
- }
-
- return Task.FromResult(false);
- }
-
- case JTokenType.String: {
- // When the "aud" claim is a string, it must exactly match the
- // audience registered in the introspection middleware options.
- var audience = payload.Value(OAuthIntrospectionConstants.Claims.Audience);
- if (Options.Audiences.Contains(audience, StringComparer.Ordinal)) {
- return Task.FromResult(true);
- }
-
- return Task.FromResult(false);
- }
-
- default:
- return Task.FromResult(false);
+ // Ensure that the authentication ticket contains the registered audience.
+ if (!audiences.Split(' ').Intersect(Options.Audiences, StringComparer.Ordinal).Any())
+ {
+ return Task.FromResult(false);
}
+
+ return Task.FromResult(true);
}
protected virtual Task CreateTicketAsync(JObject payload) {
@@ -250,6 +288,24 @@ protected virtual Task CreateTicketAsync(JObject payload)
continue;
}
+
+ // Store the audience or audiences in the ticket properties.
+ case OAuthIntrospectionConstants.Claims.Audience: {
+ if (property.Value.Type == JTokenType.Array) {
+ var array = (JArray)property.Value;
+ if (array == null) {
+ continue;
+ }
+ var audiences = string.Join(" ", array.Select(item => item.Value()));
+ properties.Dictionary[OAuthIntrospectionConstants.Properties.Audiences] = audiences;
+ }
+
+ else if (property.Value.Type == JTokenType.String) {
+ properties.Dictionary[OAuthIntrospectionConstants.Properties.Audiences] = (string)property.Value;
+ }
+
+ continue;
+ }
}
switch (property.Value.Type) {
diff --git a/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionOptions.cs b/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionOptions.cs
index b458e6d..88e0659 100644
--- a/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionOptions.cs
+++ b/src/Owin.Security.OAuth.Introspection/OAuthIntrospectionOptions.cs
@@ -52,6 +52,13 @@ public OAuthIntrospectionOptions()
///
public IDistributedCache Cache { get; set; }
+ ///
+ /// The object provided by the application to process events raised by the bearer authentication middleware.
+ /// The application may implement the interface fully, or it may create an instance of OAuthIntrospectionEvents
+ /// and assign delegates only to the events it wants to process.
+ ///
+ public IOAuthIntrospectionEvents Events { get; set; } = new OAuthIntrospectionEvents();
+
///
/// Gets or sets the HTTP client used to communicate
/// with the remote OAuth2/OpenID Connect server.
diff --git a/src/Owin.Security.OAuth.Introspection/Owin.Security.OAuth.Introspection.xproj b/src/Owin.Security.OAuth.Introspection/Owin.Security.OAuth.Introspection.xproj
index fd3f228..67da38f 100644
--- a/src/Owin.Security.OAuth.Introspection/Owin.Security.OAuth.Introspection.xproj
+++ b/src/Owin.Security.OAuth.Introspection/Owin.Security.OAuth.Introspection.xproj
@@ -7,7 +7,7 @@
f9031f69-74bf-4321-88d5-2a606d3da4e4
- AspNet.Security.OpenIdConnect.Introspection
+ Owin.Security.OAuth.Introspection
..\..\artifacts\obj\$(MSBuildProjectName)
..\..\artifacts\bin\$(MSBuildProjectName)\
diff --git a/src/Owin.Security.OAuth.Validation/Events/BaseValidationContext.cs b/src/Owin.Security.OAuth.Validation/Events/BaseValidationContext.cs
new file mode 100644
index 0000000..d643909
--- /dev/null
+++ b/src/Owin.Security.OAuth.Validation/Events/BaseValidationContext.cs
@@ -0,0 +1,27 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.Owin;
+using Microsoft.Owin.Security.Provider;
+
+namespace Owin.Security.OAuth.Validation {
+ ///
+ /// Base class for all validation events that holds common properties.
+ ///
+ public abstract class BaseValidationContext : BaseContext {
+ public BaseValidationContext(
+ [NotNull]IOwinContext context,
+ [NotNull]OAuthValidationOptions options)
+ : base(context, options) {
+ }
+
+ ///
+ /// Indicates the application has handled the event process.
+ ///
+ internal bool Handled { get; set; }
+ }
+}
diff --git a/src/Owin.Security.OAuth.Validation/Events/IOAuthValidationEvents.cs b/src/Owin.Security.OAuth.Validation/Events/IOAuthValidationEvents.cs
new file mode 100644
index 0000000..e392bf1
--- /dev/null
+++ b/src/Owin.Security.OAuth.Validation/Events/IOAuthValidationEvents.cs
@@ -0,0 +1,24 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using System.Threading.Tasks;
+
+namespace Owin.Security.OAuth.Validation {
+ ///
+ /// Allows customization of validation handling within the middleware.
+ ///
+ public interface IOAuthValidationEvents {
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ Task ParseAccessToken(ParseAccessTokenContext context);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ Task ValidateToken(ValidateTokenContext context);
+ }
+}
diff --git a/src/Owin.Security.OAuth.Validation/Events/OAuthValidationEvents.cs b/src/Owin.Security.OAuth.Validation/Events/OAuthValidationEvents.cs
new file mode 100644
index 0000000..193961d
--- /dev/null
+++ b/src/Owin.Security.OAuth.Validation/Events/OAuthValidationEvents.cs
@@ -0,0 +1,35 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using System;
+using System.Threading.Tasks;
+
+namespace Owin.Security.OAuth.Validation {
+ ///
+ /// Allows customization of validation handling within the middleware.
+ ///
+ public class OAuthValidationEvents : IOAuthValidationEvents {
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ public Func OnParseAccessToken { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ public Func OnValidateToken { get; set; } = context => Task.FromResult(0);
+
+ ///
+ /// Invoked when a token is to be parsed from a newly-received request.
+ ///
+ public virtual Task ParseAccessToken(ParseAccessTokenContext context) => OnParseAccessToken(context);
+
+ ///
+ /// Invoked when a token is to be validated, before final processing.
+ ///
+ public virtual Task ValidateToken(ValidateTokenContext context) => OnValidateToken(context);
+ }
+}
diff --git a/src/Owin.Security.OAuth.Validation/Events/ParseAccessTokenContext.cs b/src/Owin.Security.OAuth.Validation/Events/ParseAccessTokenContext.cs
new file mode 100644
index 0000000..7b822db
--- /dev/null
+++ b/src/Owin.Security.OAuth.Validation/Events/ParseAccessTokenContext.cs
@@ -0,0 +1,38 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.Owin;
+
+namespace Owin.Security.OAuth.Validation {
+ ///
+ /// Allows custom parsing of access tokens from requests.
+ ///
+ public class ParseAccessTokenContext : BaseValidationContext {
+ public ParseAccessTokenContext(
+ [NotNull]IOwinContext context,
+ [NotNull]OAuthValidationOptions options)
+ : base(context, options) {
+ }
+
+ private string _token { get; set; }
+
+ ///
+ /// Gets or sets the access token.
+ ///
+ /// Setting this property indicates to the middleware that the request has been processed
+ /// and a token extracted. Setting this to null will invalidate the token.
+ ///
+ ///
+ public string Token {
+ get { return _token; }
+ set {
+ Handled = true;
+ _token = value;
+ }
+ }
+ }
+}
diff --git a/src/Owin.Security.OAuth.Validation/Events/ValidateTokenContext.cs b/src/Owin.Security.OAuth.Validation/Events/ValidateTokenContext.cs
new file mode 100644
index 0000000..8503d44
--- /dev/null
+++ b/src/Owin.Security.OAuth.Validation/Events/ValidateTokenContext.cs
@@ -0,0 +1,46 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
+ * concerning the license and the contributors participating to this project.
+ */
+
+using JetBrains.Annotations;
+using Microsoft.Owin;
+using Microsoft.Owin.Security;
+
+namespace Owin.Security.OAuth.Validation {
+ ///
+ /// Allows customization of the token validation logic.
+ ///
+ public class ValidateTokenContext : BaseValidationContext {
+ public ValidateTokenContext(
+ [NotNull]IOwinContext context,
+ [NotNull]OAuthValidationOptions options,
+ [NotNull]AuthenticationTicket ticket)
+ : base(context, options) {
+ Ticket = ticket;
+ }
+
+ ///
+ /// The created from the introspection data.
+ ///
+ public AuthenticationTicket Ticket { get; }
+
+ private bool _isValid { get; set; } = true;
+
+ ///
+ /// Indicates the ticket is valid.
+ ///
+ /// Setting this property indicates to the middleware that token validation
+ /// has been handled by the application.
+ ///
+ ///
+ public bool IsValid {
+ get { return _isValid; }
+ set {
+ Handled = true;
+ _isValid = value;
+ }
+ }
+ }
+}
diff --git a/src/Owin.Security.OAuth.Validation/OAuthValidationConstants.cs b/src/Owin.Security.OAuth.Validation/OAuthValidationConstants.cs
index 695b6fe..75d6d49 100644
--- a/src/Owin.Security.OAuth.Validation/OAuthValidationConstants.cs
+++ b/src/Owin.Security.OAuth.Validation/OAuthValidationConstants.cs
@@ -6,6 +6,10 @@
namespace Owin.Security.OAuth.Validation {
public static class OAuthValidationConstants {
+ public static class HeaderNames {
+ public const string Authorization = "Authorization";
+ }
+
public static class Properties {
public const string Audiences = ".audiences";
}
diff --git a/src/Owin.Security.OAuth.Validation/OAuthValidationHandler.cs b/src/Owin.Security.OAuth.Validation/OAuthValidationHandler.cs
index d6a4fa4..7ef7fb2 100644
--- a/src/Owin.Security.OAuth.Validation/OAuthValidationHandler.cs
+++ b/src/Owin.Security.OAuth.Validation/OAuthValidationHandler.cs
@@ -14,26 +14,39 @@
namespace Owin.Security.OAuth.Validation {
public class OAuthValidationHandler : AuthenticationHandler {
protected override async Task AuthenticateCoreAsync() {
- var header = Request.Headers.Get("Authorization");
- if (string.IsNullOrEmpty(header)) {
- Options.Logger.LogInformation("Authentication was skipped because no bearer token was received.");
+ // Give the application an opportunity to parse the token from a different location, adjust, or reject token
+ var parseAccessTokenContext = new ParseAccessTokenContext(Context, Options);
+ await Options.Events.ParseAccessToken(parseAccessTokenContext);
- return null;
- }
+ // Initialize the token from the event in case it was set during the event.
+ string token = parseAccessTokenContext.Token;
- // Ensure that the authorization header contains the mandatory "Bearer" scheme.
- // See https://tools.ietf.org/html/rfc6750#section-2.1
- if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) {
- Options.Logger.LogInformation("Authentication was skipped because an incompatible " +
- "scheme was used in the 'Authorization' header.");
+ // Bypass the default processing if the event handled the request parsing.
+ if (!parseAccessTokenContext.Handled) {
+ string header = Request.Headers[OAuthValidationConstants.HeaderNames.Authorization];
+ if (string.IsNullOrWhiteSpace(header)) {
+ Options.Logger.LogInformation("Authentication failed because the bearer token " +
+ "was missing from the 'Authorization' header.");
- return null;
+ return null;
+ }
+
+ // Ensure that the authorization header contains the mandatory "Bearer" scheme.
+ // See https://tools.ietf.org/html/rfc6750#section-2.1
+ if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) {
+ Options.Logger.LogInformation("Authentication failed because an invalid scheme " +
+ "was used in the 'Authorization' header.");
+
+ return null;
+ }
+
+ token = header.Substring("Bearer ".Length);
}
- var token = header.Substring("Bearer ".Length);
if (string.IsNullOrWhiteSpace(token)) {
- Options.Logger.LogError("Authentication failed because the bearer token " +
- "was missing from the 'Authorization' header.");
+ Options.Logger.LogInformation("Authentication failed because the bearer token " +
+ "was missing from the request. The default location" +
+ "is in the 'Authorization' header.");
return null;
}
@@ -47,6 +60,23 @@ protected override async Task AuthenticateCoreAsync() {
return null;
}
+ // Allow for interception and handling of the token validated event.
+ var validateTokenContext = new ValidateTokenContext(Context, Options, ticket);
+ await Options.Events.ValidateToken(validateTokenContext);
+
+ if (validateTokenContext.Handled) {
+ // Return a result based on how the validation was handled.
+ if (validateTokenContext.IsValid) {
+ return validateTokenContext.Ticket;
+ }
+
+ Options.Logger.LogInformation("Authentication failed because the access token was not valid.");
+ return null;
+ }
+
+ // Flow any changes that were made to the ticket during the validation event.
+ ticket = validateTokenContext.Ticket;
+
// Ensure that the access token was issued
// to be used with this resource server.
if (!await ValidateAudienceAsync(ticket)) {
diff --git a/src/Owin.Security.OAuth.Validation/OAuthValidationOptions.cs b/src/Owin.Security.OAuth.Validation/OAuthValidationOptions.cs
index eae9cc5..dbcbb07 100644
--- a/src/Owin.Security.OAuth.Validation/OAuthValidationOptions.cs
+++ b/src/Owin.Security.OAuth.Validation/OAuthValidationOptions.cs
@@ -10,10 +10,13 @@
using Microsoft.Owin.Infrastructure;
using Microsoft.Owin.Security;
-namespace Owin.Security.OAuth.Validation {
- public class OAuthValidationOptions : AuthenticationOptions {
+namespace Owin.Security.OAuth.Validation
+{
+ public class OAuthValidationOptions : AuthenticationOptions
+ {
public OAuthValidationOptions()
- : base(OAuthValidationDefaults.AuthenticationScheme) {
+ : base(OAuthValidationDefaults.AuthenticationScheme)
+ {
}
///
@@ -45,5 +48,12 @@ public OAuthValidationOptions()
/// data protector used by .
///
public IDataProtectionProvider DataProtectionProvider { get; set; }
+
+ ///
+ /// The object provided by the application to process events raised by the bearer authentication middleware.
+ /// The application may implement the interface fully, or it may create an instance of OAuthValidationEvents
+ /// and assign delegates only to the events it wants to process.
+ ///
+ public IOAuthValidationEvents Events { get; set; } = new OAuthValidationEvents();
}
}
diff --git a/src/Owin.Security.OAuth.Validation/Owin.Security.OAuth.Validation.xproj b/src/Owin.Security.OAuth.Validation/Owin.Security.OAuth.Validation.xproj
index a7353f9..e96587d 100644
--- a/src/Owin.Security.OAuth.Validation/Owin.Security.OAuth.Validation.xproj
+++ b/src/Owin.Security.OAuth.Validation/Owin.Security.OAuth.Validation.xproj
@@ -7,7 +7,7 @@
719af040-773b-4da5-b53e-28b26d1121ff
- AspNet.Security.OAuth.Validation
+ Owin.Security.OAuth.Validation
..\..\artifacts\obj\$(MSBuildProjectName)
..\..\artifacts\bin\$(MSBuildProjectName)\
diff --git a/test/AspNet.Security.OAuth.Introspection.Tests/OAuthIntrospectionMiddlewareTests.cs b/test/AspNet.Security.OAuth.Introspection.Tests/OAuthIntrospectionMiddlewareTests.cs
index 90dac89..97066c4 100644
--- a/test/AspNet.Security.OAuth.Introspection.Tests/OAuthIntrospectionMiddlewareTests.cs
+++ b/test/AspNet.Security.OAuth.Introspection.Tests/OAuthIntrospectionMiddlewareTests.cs
@@ -76,7 +76,7 @@ public async Task InvalidTokenCausesInvalidAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "invalid-token");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Invalid);
// Act
var response = await client.SendAsync(request);
@@ -96,7 +96,7 @@ public async Task ValidTokenAllowsSuccessfulAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
// Act
var response = await client.SendAsync(request);
@@ -118,7 +118,7 @@ public async Task MissingAudienceCausesInvalidAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
// Act
var response = await client.SendAsync(request);
@@ -139,8 +139,7 @@ public async Task InvalidAudienceCausesInvalidAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue(
- "Bearer", "valid-token-with-single-audience");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.SingleAudience);
// Act
var response = await client.SendAsync(request);
@@ -162,8 +161,7 @@ public async Task AnyMatchingAudienceCausesSuccessfulAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue(
- "Bearer", "valid-token-with-single-audience");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.SingleAudience);
// Act
var response = await client.SendAsync(request);
@@ -185,8 +183,7 @@ public async Task ValidAudienceAllowsSuccessfulAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue(
- "Bearer", "valid-token-with-multiple-audiences");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.MultipleAudiences);
// Act
var response = await client.SendAsync(request);
@@ -209,8 +206,7 @@ public async Task MultipleMatchingAudienceCausesSuccessfulAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue(
- "Bearer", "valid-token-with-multiple-audiences");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.MultipleAudiences);
// Act
var response = await client.SendAsync(request);
@@ -231,7 +227,7 @@ public async Task ExpiredTicketCausesInvalidAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "expired-token");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Expired);
// Act
var response = await client.SendAsync(request);
@@ -240,6 +236,146 @@ public async Task ExpiredTicketCausesInvalidAuthentication() {
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
+ [Fact]
+ public async Task TokenSetToNullDuringParseAccessTokenEventCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.ClientId = "Fabrikam";
+ options.ClientSecret = "B4657E03-D619";
+ options.Events = new OAuthIntrospectionEvents {
+ OnParseAccessToken = context => {
+ context.Token = null;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task TokenSetToInvalidTokenDuringParseAccessTokenEventCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.ClientId = "Fabrikam";
+ options.ClientSecret = "B4657E03-D619";
+ options.Events = new OAuthIntrospectionEvents {
+ OnParseAccessToken = context => {
+ context.Token = Tokens.Invalid;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task TokenSetToValidTokenDuringParseAccessTokenEventCausesSuccessfulAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.ClientId = "Fabrikam";
+ options.ClientSecret = "B4657E03-D619";
+ options.Events = new OAuthIntrospectionEvents {
+ OnParseAccessToken = context => {
+ context.Token = Tokens.Valid;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Invalid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task TokenValidatedEventSettingIsValidAsTrueCausesSuccessfulAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.ClientId = "Fabrikam";
+ options.ClientSecret = "B4657E03-D619";
+ options.Events = new OAuthIntrospectionEvents {
+ OnValidateToken = context => {
+ context.IsValid = true;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task TokenValidatedEventSettingIsValidAsFalseCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.ClientId = "Fabrikam";
+ options.ClientSecret = "B4657E03-D619";
+ options.Events = new OAuthIntrospectionEvents {
+ OnValidateToken = context => {
+ context.IsValid = false;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ private static class Tokens {
+ public const string Invalid = "invalid-token";
+ public const string Valid = "valid-token";
+ public const string SingleAudience = "valid-token-with-single-audience";
+ public const string MultipleAudiences = "valid-token-with-multiple-audiences";
+ public const string Expired = "expired-token";
+ }
+
private static TestServer CreateResourceServer(Action configuration) {
var server = CreateAuthorizationServer();
@@ -317,13 +453,13 @@ private static TestServer CreateAuthorizationServer() {
var form = await context.Request.ReadFormAsync();
switch (form[OAuthIntrospectionConstants.Parameters.Token]) {
- case "invalid-token": {
+ case Tokens.Invalid: {
payload[OAuthIntrospectionConstants.Claims.Active] = false;
break;
}
- case "expired-token": {
+ case Tokens.Expired: {
payload[OAuthIntrospectionConstants.Claims.Active] = true;
payload[OAuthIntrospectionConstants.Claims.Subject] = "Fabrikam";
@@ -333,14 +469,14 @@ private static TestServer CreateAuthorizationServer() {
break;
}
- case "valid-token": {
+ case Tokens.Valid: {
payload[OAuthIntrospectionConstants.Claims.Active] = true;
payload[OAuthIntrospectionConstants.Claims.Subject] = "Fabrikam";
break;
}
- case "valid-token-with-single-audience": {
+ case Tokens.SingleAudience: {
payload[OAuthIntrospectionConstants.Claims.Active] = true;
payload[OAuthIntrospectionConstants.Claims.Subject] = "Fabrikam";
payload[OAuthIntrospectionConstants.Claims.Audience] = "http://www.google.com/";
@@ -348,7 +484,7 @@ private static TestServer CreateAuthorizationServer() {
break;
}
- case "valid-token-with-multiple-audiences": {
+ case Tokens.MultipleAudiences: {
payload[OAuthIntrospectionConstants.Claims.Active] = true;
payload[OAuthIntrospectionConstants.Claims.Subject] = "Fabrikam";
payload[OAuthIntrospectionConstants.Claims.Audience] = JArray.FromObject(new[] {
diff --git a/test/AspNet.Security.OAuth.Validation.Tests/OAuthValidationMiddlewareTests.cs b/test/AspNet.Security.OAuth.Validation.Tests/OAuthValidationMiddlewareTests.cs
index 642580d..675bcbb 100644
--- a/test/AspNet.Security.OAuth.Validation.Tests/OAuthValidationMiddlewareTests.cs
+++ b/test/AspNet.Security.OAuth.Validation.Tests/OAuthValidationMiddlewareTests.cs
@@ -32,7 +32,7 @@ public async Task InvalidTokenCausesInvalidAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "invalid-token");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Invalid);
// Act
var response = await client.SendAsync(request);
@@ -49,7 +49,7 @@ public async Task ValidTokenAllowsSuccessfulAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-1");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
// Act
var response = await client.SendAsync(request);
@@ -69,7 +69,7 @@ public async Task MissingAudienceCausesInvalidAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-1");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
// Act
var response = await client.SendAsync(request);
@@ -88,7 +88,7 @@ public async Task InvalidAudienceCausesInvalidAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-2");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.SingleAudience);
// Act
var response = await client.SendAsync(request);
@@ -107,7 +107,7 @@ public async Task ValidAudienceAllowsSuccessfulAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-3");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.MultipleAudiences);
// Act
var response = await client.SendAsync(request);
@@ -128,7 +128,7 @@ public async Task AnyMatchingAudienceCausesSuccessfulAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-2");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.SingleAudience);
// Act
var response = await client.SendAsync(request);
@@ -149,7 +149,7 @@ public async Task MultipleMatchingAudienceCausesSuccessfulAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-3");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.MultipleAudiences);
// Act
var response = await client.SendAsync(request);
@@ -167,7 +167,7 @@ public async Task ExpiredTicketCausesInvalidAuthentication() {
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-4");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Expired);
// Act
var response = await client.SendAsync(request);
@@ -176,15 +176,145 @@ public async Task ExpiredTicketCausesInvalidAuthentication() {
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
+ [Fact]
+ public async Task TokenSetToNullDuringParseAccessTokenEventCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.Events = new OAuthValidationEvents {
+ OnParseAccessToken = context => {
+ context.Token = null;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task TokenSetToInvalidValueDuringParseAccessTokenEventCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.Events = new OAuthValidationEvents {
+ OnParseAccessToken = context => {
+ context.Token = Tokens.Invalid;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task TokenSetToValidValueDuringParseAccessTokenEventAllowsSuccessfulAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.Events = new OAuthValidationEvents {
+ OnParseAccessToken = context => {
+ context.Token = Tokens.Valid;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Invalid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task TokenValidatedEventSettingIsValidAsTrueCausesSuccessfulAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.Events = new OAuthValidationEvents {
+ OnValidateToken = context => {
+ context.IsValid = true;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task TokenValidatedEventSettingIsValidAsFalseCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.Events = new OAuthValidationEvents {
+ OnValidateToken = context => {
+ context.IsValid = false;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ private static class Tokens {
+ public const string Invalid = "invalid-token";
+ public const string Valid = "valid-token";
+ public const string SingleAudience = "valid-token-with-single-audience";
+ public const string MultipleAudiences = "valid-token-with-multiple-audiences";
+ public const string Expired = "expired-token";
+ }
+
private static TestServer CreateResourceServer(Action configuration = null) {
var builder = new WebHostBuilder();
- var format = new Mock>();
+ var format = new Mock>(MockBehavior.Strict);
- format.Setup(mock => mock.Unprotect(It.Is(token => token == "invalid-token")))
+ format.Setup(mock => mock.Unprotect(It.Is(token => token == Tokens.Invalid)))
.Returns(value: null);
- format.Setup(mock => mock.Unprotect(It.Is(token => token == "token-1")))
+ format.Setup(mock => mock.Unprotect(It.Is(token => token == Tokens.Valid)))
.Returns(delegate {
var identity = new ClaimsIdentity(OAuthValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Fabrikam"));
@@ -195,7 +325,7 @@ private static TestServer CreateResourceServer(Action co
properties, OAuthValidationDefaults.AuthenticationScheme);
});
- format.Setup(mock => mock.Unprotect(It.Is(token => token == "token-2")))
+ format.Setup(mock => mock.Unprotect(It.Is(token => token == Tokens.SingleAudience)))
.Returns(delegate {
var identity = new ClaimsIdentity(OAuthValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Fabrikam"));
@@ -208,7 +338,7 @@ private static TestServer CreateResourceServer(Action co
properties, OAuthValidationDefaults.AuthenticationScheme);
});
- format.Setup(mock => mock.Unprotect(It.Is(token => token == "token-3")))
+ format.Setup(mock => mock.Unprotect(It.Is(token => token == Tokens.MultipleAudiences)))
.Returns(delegate {
var identity = new ClaimsIdentity(OAuthValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Fabrikam"));
@@ -221,7 +351,7 @@ private static TestServer CreateResourceServer(Action co
properties, OAuthValidationDefaults.AuthenticationScheme);
});
- format.Setup(mock => mock.Unprotect(It.Is(token => token == "token-4")))
+ format.Setup(mock => mock.Unprotect(It.Is(token => token == Tokens.Expired)))
.Returns(delegate {
var identity = new ClaimsIdentity(OAuthValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Fabrikam"));
diff --git a/test/Owin.Security.OAuth.Introspection.Tests/OAuthIntrospectionMiddlewareTests.cs b/test/Owin.Security.OAuth.Introspection.Tests/OAuthIntrospectionMiddlewareTests.cs
index 23a3755..6683f58 100644
--- a/test/Owin.Security.OAuth.Introspection.Tests/OAuthIntrospectionMiddlewareTests.cs
+++ b/test/Owin.Security.OAuth.Introspection.Tests/OAuthIntrospectionMiddlewareTests.cs
@@ -239,6 +239,146 @@ public async Task ExpiredTicketCausesInvalidAuthentication() {
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
+ [Fact]
+ public async Task TokenSetToNullDuringParseAccessTokenEventCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.ClientId = "Fabrikam";
+ options.ClientSecret = "B4657E03-D619";
+ options.Events = new OAuthIntrospectionEvents {
+ OnParseAccessToken = context => {
+ context.Token = null;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.HttpClient;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task TokenSetToInvalidTokenDuringParseAccessTokenEventCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.ClientId = "Fabrikam";
+ options.ClientSecret = "B4657E03-D619";
+ options.Events = new OAuthIntrospectionEvents {
+ OnParseAccessToken = context => {
+ context.Token = Tokens.Invalid;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.HttpClient;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task TokenSetToValidTokenDuringParseAccessTokenEventAllowsSuccessfulAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.ClientId = "Fabrikam";
+ options.ClientSecret = "B4657E03-D619";
+ options.Events = new OAuthIntrospectionEvents {
+ OnParseAccessToken = context => {
+ context.Token = Tokens.Valid;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.HttpClient;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Invalid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task TokenValidatedEventSettingIsValidAsTrueCausesSuccessfulAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.ClientId = "Fabrikam";
+ options.ClientSecret = "B4657E03-D619";
+ options.Events = new OAuthIntrospectionEvents {
+ OnValidateToken = context => {
+ context.IsValid = true;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.HttpClient;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task TokenValidatedEventSettingIsValidAsFalseCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.ClientId = "Fabrikam";
+ options.ClientSecret = "B4657E03-D619";
+ options.Events = new OAuthIntrospectionEvents {
+ OnValidateToken = context => {
+ context.IsValid = false;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.HttpClient;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ private static class Tokens {
+ public const string Invalid = "invalid-token";
+ public const string Valid = "valid-token";
+ public const string SingleAudience = "valid-token-with-single-audience";
+ public const string MultipleAudiences = "valid-token-with-multiple-audiences";
+ public const string Expired = "expired-token";
+ }
+
private static TestServer CreateResourceServer(Action configuration) {
var server = CreateAuthorizationServer();
@@ -294,13 +434,13 @@ private static TestServer CreateAuthorizationServer() {
var form = await context.Request.ReadFormAsync();
switch (form[OAuthIntrospectionConstants.Parameters.Token]) {
- case "invalid-token": {
+ case Tokens.Invalid: {
payload[OAuthIntrospectionConstants.Claims.Active] = false;
break;
}
- case "expired-token": {
+ case Tokens.Expired: {
payload[OAuthIntrospectionConstants.Claims.Active] = true;
payload[OAuthIntrospectionConstants.Claims.Subject] = "Fabrikam";
@@ -310,14 +450,14 @@ private static TestServer CreateAuthorizationServer() {
break;
}
- case "valid-token": {
+ case Tokens.Valid: {
payload[OAuthIntrospectionConstants.Claims.Active] = true;
payload[OAuthIntrospectionConstants.Claims.Subject] = "Fabrikam";
break;
}
- case "valid-token-with-single-audience": {
+ case Tokens.SingleAudience: {
payload[OAuthIntrospectionConstants.Claims.Active] = true;
payload[OAuthIntrospectionConstants.Claims.Subject] = "Fabrikam";
payload[OAuthIntrospectionConstants.Claims.Audience] = "http://www.google.com/";
@@ -325,7 +465,7 @@ private static TestServer CreateAuthorizationServer() {
break;
}
- case "valid-token-with-multiple-audiences": {
+ case Tokens.MultipleAudiences: {
payload[OAuthIntrospectionConstants.Claims.Active] = true;
payload[OAuthIntrospectionConstants.Claims.Subject] = "Fabrikam";
payload[OAuthIntrospectionConstants.Claims.Audience] = JArray.FromObject(new[] {
diff --git a/test/Owin.Security.OAuth.Validation.Tests/OAuthValidationMiddlewareTests.cs b/test/Owin.Security.OAuth.Validation.Tests/OAuthValidationMiddlewareTests.cs
index 578b98e..e551bf7 100644
--- a/test/Owin.Security.OAuth.Validation.Tests/OAuthValidationMiddlewareTests.cs
+++ b/test/Owin.Security.OAuth.Validation.Tests/OAuthValidationMiddlewareTests.cs
@@ -27,7 +27,7 @@ public async Task InvalidTokenCausesInvalidAuthentication() {
var client = server.HttpClient;
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "invalid-token");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Invalid);
// Act
var response = await client.SendAsync(request);
@@ -44,7 +44,7 @@ public async Task ValidTokenAllowsSuccessfulAuthentication() {
var client = server.HttpClient;
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-1");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
// Act
var response = await client.SendAsync(request);
@@ -64,7 +64,7 @@ public async Task MissingAudienceCausesInvalidAuthentication() {
var client = server.HttpClient;
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-1");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
// Act
var response = await client.SendAsync(request);
@@ -83,7 +83,7 @@ public async Task InvalidAudienceCausesInvalidAuthentication() {
var client = server.HttpClient;
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-2");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.SingleAudience);
// Act
var response = await client.SendAsync(request);
@@ -102,7 +102,7 @@ public async Task ValidAudienceAllowsSuccessfulAuthentication() {
var client = server.HttpClient;
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-3");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.MultipleAudiences);
// Act
var response = await client.SendAsync(request);
@@ -123,7 +123,7 @@ public async Task AnyMatchingAudienceCausesSuccessfulAuthentication() {
var client = server.HttpClient;
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-2");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.SingleAudience);
// Act
var response = await client.SendAsync(request);
@@ -144,7 +144,7 @@ public async Task MultipleMatchingAudienceCausesSuccessfulAuthentication() {
var client = server.HttpClient;
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-3");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.MultipleAudiences);
// Act
var response = await client.SendAsync(request);
@@ -162,7 +162,7 @@ public async Task ExpiredTicketCausesInvalidAuthentication() {
var client = server.HttpClient;
var request = new HttpRequestMessage(HttpMethod.Get, "/");
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token-4");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Expired);
// Act
var response = await client.SendAsync(request);
@@ -171,13 +171,143 @@ public async Task ExpiredTicketCausesInvalidAuthentication() {
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
+ [Fact]
+ public async Task TokenSetToNullDuringParseAccessTokenEventCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.Events = new OAuthValidationEvents {
+ OnParseAccessToken = context => {
+ context.Token = null;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.HttpClient;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task TokenSetToInvalidValueDuringParseAccessTokenEventCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.Events = new OAuthValidationEvents {
+ OnParseAccessToken = context => {
+ context.Token = Tokens.Invalid;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.HttpClient;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task TokenSetToValidValueDuringParseAccessTokenEventAllowsSuccessfulAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.Events = new OAuthValidationEvents {
+ OnParseAccessToken = context => {
+ context.Token = Tokens.Valid;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.HttpClient;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Invalid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task TokenValidatedEventSettingIsValidAsTrueCausesSuccessfulAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.Events = new OAuthValidationEvents {
+ OnValidateToken = context => {
+ context.IsValid = true;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.HttpClient;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task TokenValidatedEventSettingIsValidAsFalseCausesInvalidAuthentication() {
+ // Arrange
+ var server = CreateResourceServer(options => {
+ options.Events = new OAuthValidationEvents {
+ OnValidateToken = context => {
+ context.IsValid = false;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var client = server.HttpClient;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Tokens.Valid);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ private static class Tokens {
+ public const string Invalid = "invalid-token";
+ public const string Valid = "valid-token";
+ public const string SingleAudience = "valid-token-with-single-audience";
+ public const string MultipleAudiences = "valid-token-with-multiple-audiences";
+ public const string Expired = "expired-token";
+ }
+
private static TestServer CreateResourceServer(Action configuration = null) {
var format = new Mock>();
- format.Setup(mock => mock.Unprotect(It.Is(token => token == "invalid-token")))
+ format.Setup(mock => mock.Unprotect(It.Is(token => token == Tokens.Invalid)))
.Returns(value: null);
- format.Setup(mock => mock.Unprotect(It.Is(token => token == "token-1")))
+ format.Setup(mock => mock.Unprotect(It.Is(token => token == Tokens.Valid)))
.Returns(delegate {
var identity = new ClaimsIdentity(OAuthValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Fabrikam"));
@@ -185,7 +315,7 @@ private static TestServer CreateResourceServer(Action co
return new AuthenticationTicket(identity, new AuthenticationProperties());
});
- format.Setup(mock => mock.Unprotect(It.Is(token => token == "token-2")))
+ format.Setup(mock => mock.Unprotect(It.Is(token => token == Tokens.SingleAudience)))
.Returns(delegate {
var identity = new ClaimsIdentity(OAuthValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Fabrikam"));
@@ -197,7 +327,7 @@ private static TestServer CreateResourceServer(Action co
return new AuthenticationTicket(identity, properties);
});
- format.Setup(mock => mock.Unprotect(It.Is(token => token == "token-3")))
+ format.Setup(mock => mock.Unprotect(It.Is(token => token == Tokens.MultipleAudiences)))
.Returns(delegate {
var identity = new ClaimsIdentity(OAuthValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Fabrikam"));
@@ -209,7 +339,7 @@ private static TestServer CreateResourceServer(Action co
return new AuthenticationTicket(identity, properties);
});
- format.Setup(mock => mock.Unprotect(It.Is(token => token == "token-4")))
+ format.Setup(mock => mock.Unprotect(It.Is(token => token == Tokens.Expired)))
.Returns(delegate {
var identity = new ClaimsIdentity(OAuthValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Fabrikam"));