diff --git a/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Introspection.cs b/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Introspection.cs index b4ad5fd3..f0e8c6fd 100644 --- a/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Introspection.cs +++ b/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Introspection.cs @@ -219,10 +219,42 @@ private async Task InvokeIntrospectionEndpointAsync() // See https://tools.ietf.org/html/rfc7662#section-2.1 if (ticket == null) { - ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? - await DeserializeAuthorizationCodeAsync(request.Token, request) ?? - await DeserializeIdentityTokenAsync(request.Token, request) ?? - await DeserializeRefreshTokenAsync(request.Token, request); + // To avoid calling the same deserialization methods twice, + // an additional check is made to exclude the corresponding + // method when an explicit token_type_hint was specified. + switch (request.TokenTypeHint) + { + case OpenIdConnectConstants.TokenTypeHints.AccessToken: + ticket = await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.AuthorizationCode: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.IdToken: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.RefreshToken: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request); + break; + + default: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + } } if (ticket == null) diff --git a/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Revocation.cs b/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Revocation.cs index 3aeb7d18..d0c10f69 100644 --- a/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Revocation.cs +++ b/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Revocation.cs @@ -204,10 +204,42 @@ private async Task InvokeRevocationEndpointAsync() // See https://tools.ietf.org/html/rfc7009#section-2.1 if (ticket == null) { - ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? - await DeserializeAuthorizationCodeAsync(request.Token, request) ?? - await DeserializeIdentityTokenAsync(request.Token, request) ?? - await DeserializeRefreshTokenAsync(request.Token, request); + // To avoid calling the same deserialization methods twice, + // an additional check is made to exclude the corresponding + // method when an explicit token_type_hint was specified. + switch (request.TokenTypeHint) + { + case OpenIdConnectConstants.TokenTypeHints.AccessToken: + ticket = await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.AuthorizationCode: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.IdToken: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.RefreshToken: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request); + break; + + default: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + } } if (ticket == null) diff --git a/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Introspection.cs b/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Introspection.cs index 9840a039..a848f959 100644 --- a/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Introspection.cs +++ b/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Introspection.cs @@ -219,10 +219,42 @@ private async Task InvokeIntrospectionEndpointAsync() // See https://tools.ietf.org/html/rfc7662#section-2.1 if (ticket == null) { - ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? - await DeserializeAuthorizationCodeAsync(request.Token, request) ?? - await DeserializeIdentityTokenAsync(request.Token, request) ?? - await DeserializeRefreshTokenAsync(request.Token, request); + // To avoid calling the same deserialization methods twice, + // an additional check is made to exclude the corresponding + // method when an explicit token_type_hint was specified. + switch (request.TokenTypeHint) + { + case OpenIdConnectConstants.TokenTypeHints.AccessToken: + ticket = await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.AuthorizationCode: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.IdToken: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.RefreshToken: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request); + break; + + default: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + } } if (ticket == null) diff --git a/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Revocation.cs b/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Revocation.cs index 15250823..3da7aa8d 100644 --- a/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Revocation.cs +++ b/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Revocation.cs @@ -204,10 +204,42 @@ private async Task InvokeRevocationEndpointAsync() // See https://tools.ietf.org/html/rfc7009#section-2.1 if (ticket == null) { - ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? - await DeserializeAuthorizationCodeAsync(request.Token, request) ?? - await DeserializeIdentityTokenAsync(request.Token, request) ?? - await DeserializeRefreshTokenAsync(request.Token, request); + // To avoid calling the same deserialization methods twice, + // an additional check is made to exclude the corresponding + // method when an explicit token_type_hint was specified. + switch (request.TokenTypeHint) + { + case OpenIdConnectConstants.TokenTypeHints.AccessToken: + ticket = await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.AuthorizationCode: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.IdToken: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + + case OpenIdConnectConstants.TokenTypeHints.RefreshToken: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request); + break; + + default: + ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? + await DeserializeAuthorizationCodeAsync(request.Token, request) ?? + await DeserializeIdentityTokenAsync(request.Token, request) ?? + await DeserializeRefreshTokenAsync(request.Token, request); + break; + } } if (ticket == null) diff --git a/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Introspection.cs b/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Introspection.cs index ff445369..523f3bde 100644 --- a/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Introspection.cs +++ b/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Introspection.cs @@ -356,6 +356,69 @@ public async Task InvokeIntrospectionEndpointAsync_InvalidTokenCausesAnError() Assert.False((bool) response[OpenIdConnectConstants.Parameters.Active]); } + [Theory] + [InlineData(OpenIdConnectConstants.TokenTypeHints.AccessToken)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.IdToken)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.RefreshToken)] + public async Task InvokeIntrospectionEndpointAsync_TokenIsNotDeserializedTwice(string hint) + { + // Arrange + var server = CreateAuthorizationServer(options => + { + options.Provider.OnDeserializeAccessToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeAccessToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeAccessToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeAuthorizationCode = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeAuthorizationCode))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeAuthorizationCode), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeIdentityToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeIdentityToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeIdentityToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeRefreshToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeRefreshToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeRefreshToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnValidateIntrospectionRequest = context => + { + context.Skip(); + + return Task.FromResult(0); + }; + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + Token = "SlAV32hkKG", + TokenTypeHint = hint + }); + + // Assert + Assert.False((bool) response[OpenIdConnectConstants.Parameters.Active]); + } + [Fact] public async Task InvokeIntrospectionEndpointAsync_ConfidentialTokenCausesAnErrorWhenValidationIsSkipped() { diff --git a/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Revocation.cs b/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Revocation.cs index cf9aaf32..24dad16b 100644 --- a/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Revocation.cs +++ b/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Revocation.cs @@ -327,8 +327,71 @@ public async Task InvokeRevocationEndpointAsync_ValidateRevocationRequest_Invali "client_id was specified by the client application.", exception.Message); } + [Theory] + [InlineData(OpenIdConnectConstants.TokenTypeHints.AccessToken)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.IdToken)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.RefreshToken)] + public async Task InvokeRevocationEndpointAsync_TokenIsNotDeserializedTwice(string hint) + { + // Arrange + var server = CreateAuthorizationServer(options => + { + options.Provider.OnDeserializeAccessToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeAccessToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeAccessToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeAuthorizationCode = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeAuthorizationCode))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeAuthorizationCode), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeIdentityToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeIdentityToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeIdentityToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeRefreshToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeRefreshToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeRefreshToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnValidateRevocationRequest = context => + { + context.Validate("Contoso"); + + return Task.FromResult(0); + }; + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest + { + Token = "SlAV32hkKG", + TokenTypeHint = hint + }); + + // Assert + Assert.Empty(response.GetParameters()); + } + [Fact] - public async Task InvokeRevocationEndpointAsync_ValidateRevocationRequest_ConfidentialTokenCausesAnErrorWhenValidationIsSkipped() + public async Task InvokeRevocationEndpointAsync_ConfidentialTokenCausesAnErrorWhenValidationIsSkipped() { // Arrange var server = CreateAuthorizationServer(options => @@ -688,6 +751,90 @@ public async Task InvokeRevocationEndpointAsync_HandleRevocationRequest_AllowsSk Assert.Equal("Bob le Magnifique", (string) response["name"]); } + [Fact] + public async Task InvokeRevocationEndpointAsync_EmptyResponseIsReturnedForRevokedToken() + { + // Arrange + var server = CreateAuthorizationServer(options => + { + options.Provider.OnDeserializeAuthorizationCode = context => + { + Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.AuthorizationCode); + + context.Ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + context.Options.AuthenticationScheme); + + return Task.FromResult(0); + }; + + options.Provider.OnValidateRevocationRequest = context => + { + context.Validate("Contoso"); + + return Task.FromResult(0); + }; + + options.Provider.OnHandleRevocationRequest = context => + { + context.Revoked = true; + + return Task.FromResult(0); + }; + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest + { + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Empty(response.GetParameters()); + } + + [Fact] + public async Task InvokeRevocationEndpointAsync_ErrorResponseIsReturnedForNonRevokedToken() + { + // Arrange + var server = CreateAuthorizationServer(options => + { + options.Provider.OnDeserializeAuthorizationCode = context => + { + Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.AuthorizationCode); + + context.Ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + context.Options.AuthenticationScheme); + + return Task.FromResult(0); + }; + + options.Provider.OnValidateRevocationRequest = context => + { + context.Validate("Contoso"); + + return Task.FromResult(0); + }; + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest + { + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedTokenType, response.Error); + Assert.Equal("The specified token cannot be revoked.", response.ErrorDescription); + } + [Fact] public async Task SendRevocationResponseAsync_ApplyRevocationResponse_AllowsHandlingResponse() { diff --git a/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Introspection.cs b/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Introspection.cs index faf718a7..e5062fb2 100644 --- a/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Introspection.cs +++ b/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Introspection.cs @@ -354,6 +354,69 @@ public async Task InvokeIntrospectionEndpointAsync_InvalidTokenCausesAnError() Assert.False((bool) response[OpenIdConnectConstants.Parameters.Active]); } + [Theory] + [InlineData(OpenIdConnectConstants.TokenTypeHints.AccessToken)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.IdToken)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.RefreshToken)] + public async Task InvokeIntrospectionEndpointAsync_TokenIsNotDeserializedTwice(string hint) + { + // Arrange + var server = CreateAuthorizationServer(options => + { + options.Provider.OnDeserializeAccessToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeAccessToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeAccessToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeAuthorizationCode = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeAuthorizationCode))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeAuthorizationCode), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeIdentityToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeIdentityToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeIdentityToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeRefreshToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeRefreshToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeRefreshToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnValidateIntrospectionRequest = context => + { + context.Skip(); + + return Task.FromResult(0); + }; + }); + + var client = new OpenIdConnectClient(server.HttpClient); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + Token = "SlAV32hkKG", + TokenTypeHint = hint + }); + + // Assert + Assert.False((bool) response[OpenIdConnectConstants.Parameters.Active]); + } + [Fact] public async Task InvokeIntrospectionEndpointAsync_ConfidentialTokenCausesAnErrorWhenValidationIsSkipped() { diff --git a/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Revocation.cs b/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Revocation.cs index 71324143..aea7b62b 100644 --- a/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Revocation.cs +++ b/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.Revocation.cs @@ -324,8 +324,71 @@ public async Task InvokeRevocationEndpointAsync_ValidateRevocationRequest_Invali "client_id was specified by the client application.", exception.Message); } + [Theory] + [InlineData(OpenIdConnectConstants.TokenTypeHints.AccessToken)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.IdToken)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.RefreshToken)] + public async Task InvokeRevocationEndpointAsync_TokenIsNotDeserializedTwice(string hint) + { + // Arrange + var server = CreateAuthorizationServer(options => + { + options.Provider.OnDeserializeAccessToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeAccessToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeAccessToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeAuthorizationCode = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeAuthorizationCode))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeAuthorizationCode), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeIdentityToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeIdentityToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeIdentityToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnDeserializeRefreshToken = context => + { + Assert.False(context.Request.HasProperty(nameof(options.Provider.OnDeserializeRefreshToken))); + context.Request.AddProperty(nameof(options.Provider.OnDeserializeRefreshToken), new object()); + + return Task.FromResult(0); + }; + + options.Provider.OnValidateRevocationRequest = context => + { + context.Validate("Contoso"); + + return Task.FromResult(0); + }; + }); + + var client = new OpenIdConnectClient(server.HttpClient); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest + { + Token = "SlAV32hkKG", + TokenTypeHint = hint + }); + + // Assert + Assert.Empty(response.GetParameters()); + } + [Fact] - public async Task InvokeRevocationEndpointAsync_ValidateRevocationRequest_ConfidentialTokenCausesAnErrorWhenValidationIsSkipped() + public async Task InvokeRevocationEndpointAsync_ConfidentialTokenCausesAnErrorWhenValidationIsSkipped() { // Arrange var server = CreateAuthorizationServer(options => @@ -677,6 +740,88 @@ public async Task InvokeRevocationEndpointAsync_HandleRevocationRequest_AllowsSk Assert.Equal("Bob le Magnifique", (string) response["name"]); } + [Fact] + public async Task InvokeRevocationEndpointAsync_EmptyResponseIsReturnedForRevokedToken() + { + // Arrange + var server = CreateAuthorizationServer(options => + { + options.Provider.OnDeserializeAuthorizationCode = context => + { + Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.AuthorizationCode); + + context.Ticket = new AuthenticationTicket( + new ClaimsIdentity(context.Options.AuthenticationType), + new AuthenticationProperties()); + + return Task.FromResult(0); + }; + + options.Provider.OnValidateRevocationRequest = context => + { + context.Validate("Contoso"); + + return Task.FromResult(0); + }; + + options.Provider.OnHandleRevocationRequest = context => + { + context.Revoked = true; + + return Task.FromResult(0); + }; + }); + + var client = new OpenIdConnectClient(server.HttpClient); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest + { + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Empty(response.GetParameters()); + } + + [Fact] + public async Task InvokeRevocationEndpointAsync_ErrorResponseIsReturnedForNonRevokedToken() + { + // Arrange + var server = CreateAuthorizationServer(options => + { + options.Provider.OnDeserializeAuthorizationCode = context => + { + Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.AuthorizationCode); + + context.Ticket = new AuthenticationTicket( + new ClaimsIdentity(context.Options.AuthenticationType), + new AuthenticationProperties()); + + return Task.FromResult(0); + }; + + options.Provider.OnValidateRevocationRequest = context => + { + context.Validate("Contoso"); + + return Task.FromResult(0); + }; + }); + + var client = new OpenIdConnectClient(server.HttpClient); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest + { + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedTokenType, response.Error); + Assert.Equal("The specified token cannot be revoked.", response.ErrorDescription); + } + [Fact] public async Task SendRevocationResponseAsync_ApplyRevocationResponse_AllowsHandlingResponse() {