Browse Source

Port OpenIddict to the new ASP.NET Core 2.0 authentication stack

pull/415/head
Kévin Chalet 9 years ago
parent
commit
5e7a5c103b
  1. 2
      .travis.yml
  2. 1
      NuGet.config
  3. 6
      build/common.props
  4. 19
      build/dependencies.props
  5. 4
      build/version.props
  6. 18
      samples/Mvc.Client/Controllers/AuthenticationController.cs
  7. 3
      samples/Mvc.Client/Controllers/HomeController.cs
  8. 4
      samples/Mvc.Client/Mvc.Client.csproj
  9. 61
      samples/Mvc.Client/Startup.cs
  10. 49
      samples/Mvc.Server/Controllers/AuthorizationController.cs
  11. 2
      samples/Mvc.Server/Controllers/ManageController.cs
  12. 2
      samples/Mvc.Server/Controllers/ResourceController.cs
  13. 2
      samples/Mvc.Server/Controllers/UserinfoController.cs
  14. 49
      samples/Mvc.Server/Extensions/AppBuilderExtensions.cs
  15. 2
      samples/Mvc.Server/Models/ApplicationUser.cs
  16. 4
      samples/Mvc.Server/Mvc.Server.csproj
  17. 79
      samples/Mvc.Server/Startup.cs
  18. 4
      samples/Mvc.Server/ViewModels/Manage/ManageLoginsViewModel.cs
  19. 4
      samples/Mvc.Server/Views/Account/Login.cshtml
  20. 2
      samples/Mvc.Server/Views/Manage/ManageLogins.cshtml
  21. 7
      src/OpenIddict.Core/OpenIddict.Core.csproj
  22. 17
      src/OpenIddict.Core/OpenIddictExtensions.cs
  23. 6
      src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj
  24. 5
      src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs
  25. 12
      src/OpenIddict.EntityFrameworkCore/OpenIddictExtension.cs
  26. 2
      src/OpenIddict.Mvc/OpenIddict.Mvc.csproj
  27. 3
      src/OpenIddict/OpenIddict.csproj
  28. 135
      src/OpenIddict/OpenIddictExtensions.cs
  29. 23
      src/OpenIddict/OpenIddictHandler.cs
  30. 101
      src/OpenIddict/OpenIddictInitializer.cs
  31. 6
      src/OpenIddict/OpenIddictOptions.cs
  32. 83
      src/OpenIddict/OpenIddictProvider.Authentication.cs
  33. 16
      src/OpenIddict/OpenIddictProvider.Discovery.cs
  34. 57
      src/OpenIddict/OpenIddictProvider.Exchange.cs
  35. 30
      src/OpenIddict/OpenIddictProvider.Introspection.cs
  36. 36
      src/OpenIddict/OpenIddictProvider.Revocation.cs
  37. 34
      src/OpenIddict/OpenIddictProvider.Serialization.cs
  38. 40
      src/OpenIddict/OpenIddictProvider.Session.cs
  39. 2
      src/OpenIddict/OpenIddictProvider.Userinfo.cs
  40. 41
      src/OpenIddict/OpenIddictProvider.cs
  41. 4
      test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj
  42. 4
      test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj
  43. 4
      test/OpenIddict.Mvc.Tests/OpenIddict.Mvc.Tests.csproj
  44. 6
      test/OpenIddict.Tests/OpenIddict.Tests.csproj
  45. 459
      test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
  46. 164
      test/OpenIddict.Tests/OpenIddictInitializerTests.cs
  47. 14
      test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs
  48. 1
      test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs
  49. 1
      test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs
  50. 1
      test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs
  51. 3
      test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs
  52. 93
      test/OpenIddict.Tests/OpenIddictProviderTests.cs

2
.travis.yml

@ -14,8 +14,6 @@ env:
global:
- DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
- DOTNET_CLI_TELEMETRY_OPTOUT: 1
- KOREBUILD_DOTNET_VERSION: 2.0.0-preview1-005418
- KOREBUILD_DOTNET_SHARED_RUNTIME_VERSION: 2.0.0-preview1-001919-00
mono: none
os:
- linux

1
NuGet.config

@ -2,6 +2,7 @@
<configuration>
<packageSources>
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
<add key="aspnetcore" value="https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json" />
<add key="aspnet-contrib" value="https://www.myget.org/F/aspnet-contrib/api/v3/index.json" />
</packageSources>
</configuration>

6
build/common.props

@ -12,4 +12,10 @@
<DebugType Condition=" '$(OS)' != 'Windows_NT' ">portable</DebugType>
</PropertyGroup>
<Target Name="WorkaroundAppConfigPathTooLong" BeforeTargets="GenerateBindingRedirects">
<PropertyGroup>
<_GenerateBindingRedirectsIntermediateAppConfig>$(IntermediateOutputPath)$(TargetFileName).config</_GenerateBindingRedirectsIntermediateAppConfig>
</PropertyGroup>
</Target>
</Project>

19
build/dependencies.props

@ -1,16 +1,19 @@
<Project>
<PropertyGroup>
<AspNetCoreVersion>1.0.0</AspNetCoreVersion>
<AspNetContribOpenIdExtensionsVersion>1.0.0</AspNetContribOpenIdExtensionsVersion>
<AspNetContribOpenIdServerVersion>1.0.0</AspNetContribOpenIdServerVersion>
<AspNetContribOpenIdExtensionsVersion>2.0.0-*</AspNetContribOpenIdExtensionsVersion>
<AspNetContribOpenIdServerVersion>2.0.0-*</AspNetContribOpenIdServerVersion>
<AspNetCoreVersion>2.0.0-*</AspNetCoreVersion>
<CoreFxVersion>4.4.0-*</CoreFxVersion>
<CryptoHelperVersion>2.0.0</CryptoHelperVersion>
<JetBrainsVersion>10.3.0</JetBrainsVersion>
<NetStandardImplicitPackageVersion>1.6.0</NetStandardImplicitPackageVersion>
<MoqVersion>4.7.8</MoqVersion>
<RuntimeFrameworkVersion>1.0.0</RuntimeFrameworkVersion>
<TestSdkVersion>15.0.0</TestSdkVersion>
<XunitVersion>2.2.0</XunitVersion>
<JsonNetBsonVersion>1.0.1</JsonNetBsonVersion>
<MoqVersion>4.7.63</MoqVersion>
<NetStandardImplicitPackageVersion>2.0.0-*</NetStandardImplicitPackageVersion>
<NetStandardLibraryNetFrameworkVersion>2.0.0-*</NetStandardLibraryNetFrameworkVersion>
<RuntimeFrameworkVersion>2.0.0-*</RuntimeFrameworkVersion>
<TestSdkVersion>15.3.0-*</TestSdkVersion>
<XunitVersion>2.3.0-*</XunitVersion>
</PropertyGroup>
</Project>

4
build/version.props

@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>beta2</VersionSuffix>
<VersionPrefix>2.0.0</VersionPrefix>
<VersionSuffix>preview1</VersionSuffix>
<VersionSuffix Condition=" '$(VersionSuffix)' != '' AND '$(BuildNumber)' != '' ">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
</PropertyGroup>

18
samples/Mvc.Client/Controllers/AuthenticationController.cs

@ -1,7 +1,6 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Mvc;
namespace Mvc.Client.Controllers
@ -13,21 +12,16 @@ namespace Mvc.Client.Controllers
{
// Instruct the OIDC client middleware to redirect the user agent to the identity provider.
// Note: the authenticationType parameter must match the value configured in Startup.cs
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties
{
RedirectUri = "/"
});
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme);
}
[HttpGet("~/signout"), HttpPost("~/signout")]
public async Task SignOut()
public ActionResult SignOut()
{
// Instruct the cookies middleware to delete the local cookie created when the user agent
// is redirected from the identity provider after a successful authorization flow.
await HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// Instruct the OpenID Connect middleware to redirect the user agent to the identity provider to sign out.
await HttpContext.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
// is redirected from the identity provider after a successful authorization flow and
// to redirect the user agent to the identity provider to sign out.
return SignOut(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme);
}
}
}

3
samples/Mvc.Client/Controllers/HomeController.cs

@ -4,6 +4,7 @@ using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
@ -28,7 +29,7 @@ namespace Mvc.Client.Controllers
[Authorize, HttpPost("~/")]
public async Task<ActionResult> Index(CancellationToken cancellationToken)
{
var token = await HttpContext.Authentication.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
var token = await HttpContext.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectParameterNames.AccessToken);
if (string.IsNullOrEmpty(token))
{
throw new InvalidOperationException("The access token cannot be found in the authentication ticket. " +

4
samples/Mvc.Client/Mvc.Client.csproj

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="..\..\build\dependencies.props" />
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<TargetFrameworks>net451;netcoreapp1.0</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

61
samples/Mvc.Client/Startup.cs

@ -14,53 +14,52 @@ namespace Mvc.Client
{
services.AddAuthentication(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
services.AddMvc();
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
services.AddSingleton<HttpClient>();
}
public void Configure(IApplicationBuilder app)
{
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
// Insert a new cookies middleware in the pipeline to store the user
// identity after he has been redirected from the identity provider.
app.UseCookieAuthentication(new CookieAuthenticationOptions
.AddCookie(options =>
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
LoginPath = new PathString("/signin")
});
options.LoginPath = new PathString("/signin");
})
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
.AddOpenIdConnect(options =>
{
// Note: these settings must match the application details
// inserted in the database at the server level.
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
PostLogoutRedirectUri = "http://localhost:53507/",
options.ClientId = "mvc";
options.ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654";
RequireHttpsMetadata = false,
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
// Use the authorization code flow.
ResponseType = OpenIdConnectResponseType.Code,
AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet,
options.ResponseType = OpenIdConnectResponseType.Code;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
// Note: setting the Authority allows the OIDC client middleware to automatically
// retrieve the identity provider's configuration and spare you from setting
// the different endpoints URIs or the token validation parameters explicitly.
Authority = "http://localhost:54540/",
options.Authority = "http://localhost:54540/";
Scope = { "email", "roles", "offline_access" }
options.Scope.Add("email");
options.Scope.Add("roles");
options.Scope.Add("offline_access");
});
services.AddMvc();
services.AddSingleton<HttpClient>();
}
public void Configure(IApplicationBuilder app)
{
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc();
}
}

49
samples/Mvc.Server/Controllers/AuthorizationController.cs

@ -14,7 +14,6 @@ using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@ -168,44 +167,10 @@ namespace Mvc.Server
});
}
// Ensure the user is allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Reject the token request if two-factor authentication has been enabled by the user.
if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Ensure the user is not already locked out.
if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password))
// Validate the username/password parameters and ensure the account is not locked out.
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);
if (!result.Succeeded)
{
if (_userManager.SupportsUserLockout)
{
await _userManager.AccessFailedAsync(user);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
@ -213,11 +178,6 @@ namespace Mvc.Server
});
}
if (_userManager.SupportsUserLockout)
{
await _userManager.ResetAccessFailedCountAsync(user);
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
@ -227,8 +187,7 @@ namespace Mvc.Server
else if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/refresh token.
var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(
OpenIdConnectServerDefaults.AuthenticationScheme);
var info = await HttpContext.AuthenticateAsync(OpenIdConnectServerDefaults.AuthenticationScheme);
// Retrieve the user profile corresponding to the authorization code/refresh token.
// Note: if you want to automatically invalidate the authorization code/refresh token

2
samples/Mvc.Server/Controllers/ManageController.cs

@ -272,7 +272,7 @@ namespace Mvc.Server.Controllers
return View("Error");
}
var userLogins = await _userManager.GetLoginsAsync(user);
var otherLogins = _signInManager.GetExternalAuthenticationSchemes().Where(auth => userLogins.All(ul => auth.AuthenticationScheme != ul.LoginProvider)).ToList();
var otherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).Where(auth => userLogins.All(ul => auth.Name != ul.LoginProvider)).ToList();
ViewData["ShowRemoveButton"] = user.PasswordHash != null || userLogins.Count > 1;
return View(new ManageLoginsViewModel
{

2
samples/Mvc.Server/Controllers/ResourceController.cs

@ -17,7 +17,7 @@ namespace Mvc.Server.Controllers
_userManager = userManager;
}
[Authorize(ActiveAuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)]
[Authorize(AuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)]
[HttpGet("message")]
public async Task<IActionResult> GetMessage()
{

2
samples/Mvc.Server/Controllers/UserinfoController.cs

@ -22,7 +22,7 @@ namespace Mvc.Server.Controllers
//
// GET: /api/userinfo
[Authorize(ActiveAuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)]
[Authorize(AuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)]
[HttpGet("userinfo"), Produces("application/json")]
public async Task<IActionResult> Userinfo()
{

49
samples/Mvc.Server/Extensions/AppBuilderExtensions.cs

@ -1,49 +0,0 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
namespace Mvc.Server.Extensions
{
public static class AppBuilderExtensions
{
public static IApplicationBuilder UseWhen(this IApplicationBuilder app,
Func<HttpContext, bool> condition, Action<IApplicationBuilder> configuration)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (condition == null)
{
throw new ArgumentNullException(nameof(condition));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
var builder = app.New();
configuration(builder);
return app.Use(next =>
{
builder.Run(next);
var branch = builder.Build();
return context =>
{
if (condition(context))
{
return branch(context);
}
return next(context);
};
});
}
}
}

2
samples/Mvc.Server/Models/ApplicationUser.cs

@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
namespace Mvc.Server.Models
{

4
samples/Mvc.Server/Mvc.Server.csproj

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="..\..\build\dependencies.props" />
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<TargetFrameworks>net451;netcoreapp1.0</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>

79
samples/Mvc.Server/Startup.cs

@ -3,11 +3,10 @@ using System.Threading;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Mvc.Server.Extensions;
using Mvc.Server.Models;
using Mvc.Server.Services;
using OpenIddict.Core;
@ -52,6 +51,21 @@ namespace Mvc.Server
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com";
options.ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f";
})
.AddTwitter(options =>
{
options.ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g";
options.ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI";
})
.AddOAuthValidation();
// Register the OpenIddict services.
services.AddOpenIddict(options =>
{
@ -105,64 +119,9 @@ namespace Mvc.Server
app.UseStaticFiles();
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), branch =>
{
// Add a middleware used to validate access
// tokens and protect the API endpoints.
branch.UseOAuthValidation();
// If you prefer using JWT, don't forget to disable the automatic
// JWT -> WS-Federation claims mapping used by the JWT middleware:
//
// JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
// JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
//
// branch.UseJwtBearerAuthentication(new JwtBearerOptions
// {
// Authority = "http://localhost:54540/",
// Audience = "resource_server",
// RequireHttpsMetadata = false,
// TokenValidationParameters = new TokenValidationParameters
// {
// NameClaimType = OpenIdConnectConstants.Claims.Subject,
// RoleClaimType = OpenIdConnectConstants.Claims.Role
// }
// });
// Alternatively, you can also use the introspection middleware.
// Using it is recommended if your resource server is in a
// different application/separated from the authorization server.
//
// branch.UseOAuthIntrospection(options =>
// {
// options.Authority = new Uri("http://localhost:54540/");
// options.Audiences.Add("resource_server");
// options.ClientId = "resource_server";
// options.ClientSecret = "875sqd4s5d748z78z7ds1ff8zz8814ff88ed8ea4z4zzd";
// options.RequireHttpsMetadata = false;
// });
});
app.UseWhen(context => !context.Request.Path.StartsWithSegments("/api"), branch =>
{
branch.UseStatusCodePagesWithReExecute("/error");
branch.UseIdentity();
branch.UseGoogleAuthentication(new GoogleOptions
{
ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com",
ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"
});
branch.UseTwitterAuthentication(new TwitterOptions
{
ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g",
ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI"
});
});
app.UseStatusCodePagesWithReExecute("/error");
app.UseOpenIddict();
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
@ -187,7 +146,7 @@ namespace Mvc.Server
{
ClientId = "mvc",
DisplayName = "MVC client application",
LogoutRedirectUri = "http://localhost:53507/",
LogoutRedirectUri = "http://localhost:53507/signout-callback-oidc",
RedirectUri = "http://localhost:53507/signin-oidc"
};

4
samples/Mvc.Server/ViewModels/Manage/ManageLoginsViewModel.cs

@ -1,5 +1,5 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
namespace Mvc.Server.ViewModels.Manage
@ -8,6 +8,6 @@ namespace Mvc.Server.ViewModels.Manage
{
public IList<UserLoginInfo> CurrentLogins { get; set; }
public IList<AuthenticationDescription> OtherLogins { get; set; }
public IList<AuthenticationScheme> OtherLogins { get; set; }
}
}

4
samples/Mvc.Server/Views/Account/Login.cshtml

@ -57,7 +57,7 @@
<h4>Use another service to log in.</h4>
<hr />
@{
var loginProviders = SignInManager.GetExternalAuthenticationSchemes().ToList();
var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList();
if (loginProviders.Count == 0)
{
<div>
@ -74,7 +74,7 @@
<p>
@foreach (var provider in loginProviders)
{
<button type="submit" class="btn btn-default" name="provider" value="@provider.AuthenticationScheme" title="Log in using your @provider.DisplayName account">@provider.AuthenticationScheme</button>
<button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.Name</button>
}
</p>
</div>

2
samples/Mvc.Server/Views/Manage/ManageLogins.cshtml

@ -46,7 +46,7 @@
<p>
@foreach (var provider in Model.OtherLogins)
{
<button type="submit" class="btn btn-default" name="provider" value="@provider.AuthenticationScheme" title="Log in using your @provider.DisplayName account">@provider.AuthenticationScheme</button>
<button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.Name</button>
}
</p>
</div>

7
src/OpenIddict.Core/OpenIddict.Core.csproj

@ -3,7 +3,7 @@
<Import Project="..\..\build\packages.props" />
<PropertyGroup>
<TargetFrameworks>net451;netstandard1.3</TargetFrameworks>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
@ -25,9 +25,4 @@
<PackageReference Include="Microsoft.Extensions.Options" Version="$(AspNetCoreVersion)" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.1.0" />
<PackageReference Include="System.Security.Claims" Version="4.0.1" />
</ItemGroup>
</Project>

17
src/OpenIddict.Core/OpenIddictExtensions.cs

@ -66,22 +66,19 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddOptions();
var builder = new OpenIddictBuilder(services)
// Register the OpenIddict core services in the DI container.
services.TryAddScoped<OpenIddictApplicationManager<TApplication>>();
services.TryAddScoped<OpenIddictAuthorizationManager<TAuthorization>>();
services.TryAddScoped<OpenIddictScopeManager<TScope>>();
services.TryAddScoped<OpenIddictTokenManager<TToken>>();
return new OpenIddictBuilder(services)
{
ApplicationType = typeof(TApplication),
AuthorizationType = typeof(TAuthorization),
ScopeType = typeof(TScope),
TokenType = typeof(TToken)
};
// Register the OpenIddict core services in the DI container.
builder.Services.TryAddSingleton(builder);
builder.Services.TryAddScoped<OpenIddictApplicationManager<TApplication>>();
builder.Services.TryAddScoped<OpenIddictAuthorizationManager<TAuthorization>>();
builder.Services.TryAddScoped<OpenIddictScopeManager<TScope>>();
builder.Services.TryAddScoped<OpenIddictTokenManager<TToken>>();
return builder;
}
/// <summary>

6
src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj

@ -3,7 +3,7 @@
<Import Project="..\..\build\packages.props" />
<PropertyGroup>
<TargetFrameworks>net451;netstandard1.3</TargetFrameworks>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
@ -21,8 +21,4 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="$(AspNetCoreVersion)" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
<PackageReference Include="System.ComponentModel.TypeConverter" Version="4.1.0" />
</ItemGroup>
</Project>

5
src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs

@ -24,6 +24,11 @@ namespace OpenIddict.EntityFrameworkCore
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TKey : IEquatable<TKey>
{
public OpenIddictCustomizer([NotNull] ModelCustomizerDependencies dependencies)
: base(dependencies)
{
}
public override void Customize([NotNull] ModelBuilder builder, [NotNull] DbContext context)
{
if (builder == null)

12
src/OpenIddict.EntityFrameworkCore/OpenIddictExtension.cs

@ -19,7 +19,9 @@ namespace OpenIddict.EntityFrameworkCore
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TKey : IEquatable<TKey>
{
public void ApplyServices([NotNull] IServiceCollection services)
public string LogFragment => null;
public bool ApplyServices([NotNull] IServiceCollection services)
{
if (services == null)
{
@ -27,6 +29,14 @@ namespace OpenIddict.EntityFrameworkCore
}
services.AddSingleton<IModelCustomizer, OpenIddictCustomizer<TApplication, TAuthorization, TScope, TToken, TKey>>();
// Return false to indicate that no database
// provider was registered by this extension.
return false;
}
public long GetServiceProviderHashCode() => 0;
public void Validate([NotNull] IDbContextOptions options) { }
}
}

2
src/OpenIddict.Mvc/OpenIddict.Mvc.csproj

@ -3,7 +3,7 @@
<Import Project="..\..\build\packages.props" />
<PropertyGroup>
<TargetFrameworks>net451;netstandard1.6</TargetFrameworks>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>

3
src/OpenIddict/OpenIddict.csproj

@ -3,7 +3,7 @@
<Import Project="..\..\build\packages.props" />
<PropertyGroup>
<TargetFrameworks>net451;netstandard1.4</TargetFrameworks>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
@ -21,6 +21,7 @@
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.Abstractions" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Newtonsoft.Json.Bson" Version="$(JsonNetBsonVersion)" />
</ItemGroup>
</Project>

135
src/OpenIddict/OpenIddictExtensions.cs

@ -8,16 +8,16 @@ using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict;
@ -26,98 +26,6 @@ namespace Microsoft.AspNetCore.Builder
{
public static class OpenIddictExtensions
{
/// <summary>
/// Registers OpenIddict in the ASP.NET Core pipeline.
/// </summary>
/// <param name="app">The application builder used to register middleware instances.</param>
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
public static IApplicationBuilder UseOpenIddict([NotNull] this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
// Resolve the OpenIddict builder from the DI container.
// If it cannot be found, throw an invalid operation exception.
var builder = app.ApplicationServices.GetService<OpenIddictBuilder>();
if (builder == null)
{
throw new InvalidOperationException("The OpenIddict services cannot be resolved from the dependency injection container. " +
"Make sure 'services.AddOpenIddict()' is correctly called from 'ConfigureServices()'.");
}
// Resolve the OpenIddict options from the DI container.
var options = app.ApplicationServices.GetRequiredService<IOptions<OpenIddictOptions>>().Value;
// When no authorization provider has been registered in the options,
// create a new OpenIddictProvider instance using the specified entities.
if (options.Provider == null)
{
options.Provider = (OpenIdConnectServerProvider) Activator.CreateInstance(
typeof(OpenIddictProvider<,,,>).MakeGenericType(
/* TApplication: */ builder.ApplicationType,
/* TAuthorization: */ builder.AuthorizationType,
/* TScope: */ builder.ScopeType,
/* TToken: */ builder.TokenType));
}
// When no distributed cache has been registered in the options,
// try to resolve it from the dependency injection container.
if (options.Cache == null)
{
options.Cache = app.ApplicationServices.GetService<IDistributedCache>();
if (options.EnableRequestCaching && options.Cache == null)
{
throw new InvalidOperationException("A distributed cache implementation must be registered in the OpenIddict options " +
"or in the dependency injection container when enabling request caching support.");
}
}
// Ensure at least one flow has been enabled.
if (options.GrantTypes.Count == 0)
{
throw new InvalidOperationException("At least one OAuth2/OpenID Connect flow must be enabled.");
}
// Ensure the authorization endpoint has been enabled when
// the authorization code or implicit grants are supported.
if (!options.AuthorizationEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) ||
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)))
{
throw new InvalidOperationException("The authorization endpoint must be enabled to use " +
"the authorization code and implicit flows.");
}
// Ensure the token endpoint has been enabled when the authorization code,
// client credentials, password or refresh token grants are supported.
if (!options.TokenEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) ||
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials) ||
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Password) ||
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)))
{
throw new InvalidOperationException("The token endpoint must be enabled to use the authorization code, " +
"client credentials, password and refresh token flows.");
}
if (options.RevocationEndpointPath.HasValue && options.DisableTokenRevocation)
{
throw new InvalidOperationException("The revocation endpoint cannot be enabled when token revocation is disabled.");
}
// Ensure at least one asymmetric signing certificate/key was registered if the implicit flow was enabled.
if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey) &&
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))
{
throw new InvalidOperationException("At least one asymmetric signing key must be registered when enabling the implicit flow. " +
"Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " +
"or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.");
}
return app.UseOpenIdConnectServer(options);
}
/// <summary>
/// Amends the default OpenIddict configuration.
/// </summary>
@ -139,7 +47,44 @@ namespace Microsoft.AspNetCore.Builder
throw new ArgumentNullException(nameof(configuration));
}
builder.Services.Configure(configuration);
// Register the OpenIddict handler/provider.
builder.Services.TryAddScoped<OpenIddictHandler>();
builder.Services.TryAddScoped(
typeof(OpenIdConnectServerProvider),
typeof(OpenIddictProvider<,,,>).MakeGenericType(
/* TApplication: */ builder.ApplicationType,
/* TAuthorization: */ builder.AuthorizationType,
/* TScope: */ builder.ScopeType,
/* TToken: */ builder.TokenType));
// Register the options initializers used by the OpenID Connect server handler and OpenIddict.
// Note: TryAddEnumerable() is used here to ensure the initializers are only registered once.
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictOptions>,
OpenIdConnectServerInitializer>());
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictOptions>,
OpenIddictInitializer>());
// Register the OpenID Connect server handler in the authentication options,
// so it can be discovered by the default authentication handler provider.
builder.Services.Configure<AuthenticationOptions>(options =>
{
// Note: similarly to Identity, OpenIddict should be registered only once.
// To prevent multiple schemes from being registered, a check is made here.
if (options.SchemeMap.ContainsKey(OpenIdConnectServerDefaults.AuthenticationScheme))
{
return;
}
options.AddScheme(OpenIdConnectServerDefaults.AuthenticationScheme, scheme =>
{
scheme.HandlerType = typeof(OpenIddictHandler);
});
});
builder.Services.Configure(OpenIdConnectServerDefaults.AuthenticationScheme, configuration);
return builder;
}

23
src/OpenIddict/OpenIddictHandler.cs

@ -0,0 +1,23 @@
using System.ComponentModel;
using System.Text.Encodings.Web;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace OpenIddict
{
[EditorBrowsable(EditorBrowsableState.Never)]
public class OpenIddictHandler : OpenIdConnectServerHandler
{
public OpenIddictHandler(
[NotNull] IOptionsMonitor<OpenIddictOptions> options,
[NotNull] ILoggerFactory logger,
[NotNull] UrlEncoder encoder,
[NotNull] ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
}
}

101
src/OpenIddict/OpenIddictInitializer.cs

@ -0,0 +1,101 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.ComponentModel;
using System.Linq;
using AspNet.Security.OpenIdConnect.Primitives;
using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict
{
/// <summary>
/// Contains the methods required to ensure that the configuration
/// used by OpenIddict is in a consistent and valid state.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public class OpenIddictInitializer : IPostConfigureOptions<OpenIddictOptions>
{
private readonly IDistributedCache _cache;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictInitializer"/> class.
/// </summary>
public OpenIddictInitializer([NotNull] IDistributedCache cache)
{
_cache = cache;
}
/// <summary>
/// Populates the default OpenID Connect server options and ensure
/// that the configuration is in a consistent and valid state.
/// </summary>
/// <param name="name">The authentication scheme associated with the handler instance.</param>
/// <param name="options">The options instance to initialize.</param>
public void PostConfigure([NotNull] string name, [NotNull] OpenIddictOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The options instance name cannot be null or empty.", nameof(name));
}
// When no distributed cache has been registered in the options,
// try to resolve it from the dependency injection container.
if (options.Cache == null)
{
options.Cache = _cache;
}
// Ensure at least one flow has been enabled.
if (options.GrantTypes.Count == 0)
{
throw new InvalidOperationException("At least one OAuth2/OpenID Connect flow must be enabled.");
}
// Ensure the authorization endpoint has been enabled when
// the authorization code or implicit grants are supported.
if (!options.AuthorizationEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) ||
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)))
{
throw new InvalidOperationException("The authorization endpoint must be enabled to use " +
"the authorization code and implicit flows.");
}
// Ensure the token endpoint has been enabled when the authorization code,
// client credentials, password or refresh token grants are supported.
if (!options.TokenEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) ||
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials) ||
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Password) ||
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)))
{
throw new InvalidOperationException("The token endpoint must be enabled to use the authorization code, " +
"client credentials, password and refresh token flows.");
}
if (options.RevocationEndpointPath.HasValue && options.DisableTokenRevocation)
{
throw new InvalidOperationException("The revocation endpoint cannot be enabled when token revocation is disabled.");
}
// Ensure at least one asymmetric signing certificate/key was registered if the implicit flow was enabled.
if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey) &&
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))
{
throw new InvalidOperationException("At least one asymmetric signing key must be registered when enabling the implicit flow. " +
"Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " +
"or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.");
}
}
}
}

6
src/OpenIddict/OpenIddictOptions.cs

@ -16,9 +16,15 @@ namespace OpenIddict
/// </summary>
public class OpenIddictOptions : OpenIdConnectServerOptions
{
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictOptions"/> class.
/// </summary>
public OpenIddictOptions()
{
// Note: OpenIdConnectServerProvider is automatically mapped
// to the generic OpenIddictProvider class by the DI container.
Provider = null;
ProviderType = typeof(OpenIdConnectServerProvider);
}
/// <summary>

83
src/OpenIddict/OpenIddictProvider.Authentication.cs

@ -14,9 +14,7 @@ using JetBrains.Annotations;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;
@ -29,13 +27,12 @@ namespace OpenIddict
{
public override async Task ExtractAuthorizationRequest([NotNull] ExtractAuthorizationRequestContext context)
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// Reject requests using the unsupported request parameter.
if (!string.IsNullOrEmpty(context.Request.Request))
{
logger.LogError("The authorization request was rejected because it contained " +
Logger.LogError("The authorization request was rejected because it contained " +
"an unsupported parameter: {Parameter}.", "request");
context.Reject(
@ -48,7 +45,7 @@ namespace OpenIddict
// Reject requests using the unsupported request_uri parameter.
if (!string.IsNullOrEmpty(context.Request.RequestUri))
{
logger.LogError("The authorization request was rejected because it contained " +
Logger.LogError("The authorization request was rejected because it contained " +
"an unsupported parameter: {Parameter}.", "request_uri");
context.Reject(
@ -63,9 +60,9 @@ namespace OpenIddict
if (!string.IsNullOrEmpty(context.Request.RequestId))
{
// Return an error if request caching support was not enabled.
if (!options.Value.EnableRequestCaching)
if (!options.EnableRequestCaching)
{
logger.LogError("The authorization request was rejected because " +
Logger.LogError("The authorization request was rejected because " +
"request caching support was not enabled.");
context.Reject(
@ -79,10 +76,10 @@ namespace OpenIddict
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId;
var payload = await options.Value.Cache.GetAsync(key);
var payload = await options.Cache.GetAsync(key);
if (payload == null)
{
logger.LogError("The authorization request was rejected because an unknown " +
Logger.LogError("The authorization request was rejected because an unknown " +
"or invalid request_id parameter was specified.");
context.Reject(
@ -93,7 +90,7 @@ namespace OpenIddict
}
// Restore the authorization request parameters from the serialized payload.
using (var reader = new BsonReader(new MemoryStream(payload)))
using (var reader = new BsonDataReader(new MemoryStream(payload)))
{
foreach (var parameter in JObject.Load(reader))
{
@ -111,16 +108,14 @@ namespace OpenIddict
public override async Task ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context)
{
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// Note: the OpenID Connect server middleware supports authorization code, implicit, hybrid,
// none and custom flows but OpenIddict uses a stricter policy rejecting unknown flows.
if (!context.Request.IsAuthorizationCodeFlow() && !context.Request.IsHybridFlow() &&
!context.Request.IsImplicitFlow() && !context.Request.IsNoneFlow())
{
logger.LogError("The authorization request was rejected because the '{ResponseType}' " +
Logger.LogError("The authorization request was rejected because the '{ResponseType}' " +
"response type is not supported.", context.Request.ResponseType);
context.Reject(
@ -132,9 +127,9 @@ namespace OpenIddict
// Reject code flow authorization requests if the authorization code flow is not enabled.
if (context.Request.IsAuthorizationCodeFlow() &&
!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode))
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode))
{
logger.LogError("The authorization request was rejected because " +
Logger.LogError("The authorization request was rejected because " +
"the authorization code flow was not enabled.");
context.Reject(
@ -145,9 +140,9 @@ namespace OpenIddict
}
// Reject implicit flow authorization requests if the implicit flow is not enabled.
if (context.Request.IsImplicitFlow() && !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))
if (context.Request.IsImplicitFlow() && !options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))
{
logger.LogError("The authorization request was rejected because the implicit flow was not enabled.");
Logger.LogError("The authorization request was rejected because the implicit flow was not enabled.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedResponseType,
@ -157,10 +152,10 @@ namespace OpenIddict
}
// Reject hybrid flow authorization requests if the authorization code or the implicit flows are not enabled.
if (context.Request.IsHybridFlow() && (!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) ||
!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)))
if (context.Request.IsHybridFlow() && (!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) ||
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)))
{
logger.LogError("The authorization request was rejected because the " +
Logger.LogError("The authorization request was rejected because the " +
"authorization code flow or the implicit flow was not enabled.");
context.Reject(
@ -172,7 +167,7 @@ namespace OpenIddict
// Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled.
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
@ -188,7 +183,7 @@ namespace OpenIddict
!context.Request.IsFragmentResponseMode() &&
!context.Request.IsQueryResponseMode())
{
logger.LogError("The authorization request was rejected because the '{ResponseMode}' " +
Logger.LogError("The authorization request was rejected because the '{ResponseMode}' " +
"response mode is not supported.", context.Request.ResponseMode);
context.Reject(
@ -219,7 +214,7 @@ namespace OpenIddict
// reject the authorization request if the code_challenge_method is missing.
if (string.IsNullOrEmpty(context.Request.CodeChallengeMethod))
{
logger.LogError("The authorization request was rejected because the " +
Logger.LogError("The authorization request was rejected because the " +
"required 'code_challenge_method' parameter was missing.");
context.Reject(
@ -233,7 +228,7 @@ namespace OpenIddict
// See https://tools.ietf.org/html/rfc7636#section-7.2 for more information.
if (string.Equals(context.Request.CodeChallengeMethod, OpenIdConnectConstants.CodeChallengeMethods.Plain))
{
logger.LogError("The authorization request was rejected because the " +
Logger.LogError("The authorization request was rejected because the " +
"'code_challenge_method' parameter was set to 'plain'.");
context.Reject(
@ -246,7 +241,7 @@ namespace OpenIddict
// Reject authorization requests that contain response_type=token when a code_challenge is specified.
if (context.Request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token))
{
logger.LogError("The authorization request was rejected because the " +
Logger.LogError("The authorization request was rejected because the " +
"specified response type was not compatible with PKCE.");
context.Reject(
@ -258,10 +253,10 @@ namespace OpenIddict
}
// Retrieve the application details corresponding to the requested client_id.
var application = await applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted);
var application = await Applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted);
if (application == null)
{
logger.LogError("The authorization request was rejected because the client " +
Logger.LogError("The authorization request was rejected because the client " +
"application was not found: '{ClientId}'.", context.ClientId);
context.Reject(
@ -272,9 +267,9 @@ namespace OpenIddict
}
// Ensure a redirect_uri was associated with the application.
if (!await applications.HasRedirectUriAsync(application, context.HttpContext.RequestAborted))
if (!await Applications.HasRedirectUriAsync(application, context.HttpContext.RequestAborted))
{
logger.LogError("The authorization request was rejected because no redirect_uri " +
Logger.LogError("The authorization request was rejected because no redirect_uri " +
"was registered with the application '{ClientId}'.", context.ClientId);
context.Reject(
@ -285,9 +280,9 @@ namespace OpenIddict
}
// Ensure the redirect_uri is valid.
if (!await applications.ValidateRedirectUriAsync(application, context.RedirectUri, context.HttpContext.RequestAborted))
if (!await Applications.ValidateRedirectUriAsync(application, context.RedirectUri, context.HttpContext.RequestAborted))
{
logger.LogError("The authorization request was rejected because the redirect_uri " +
Logger.LogError("The authorization request was rejected because the redirect_uri " +
"was invalid: '{RedirectUri}'.", context.RedirectUri);
context.Reject(
@ -301,7 +296,7 @@ namespace OpenIddict
// from the authorization endpoint are rejected if the client_id corresponds to a confidential application.
// Note: when using the authorization code grant, ValidateTokenRequest is responsible of rejecting
// the token request if the client_id corresponds to an unauthenticated confidential client.
if (await applications.IsConfidentialAsync(application, context.HttpContext.RequestAborted) &&
if (await Applications.IsConfidentialAsync(application, context.HttpContext.RequestAborted) &&
context.Request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token))
{
context.Reject(
@ -317,12 +312,12 @@ namespace OpenIddict
public override async Task HandleAuthorizationRequest([NotNull] HandleAuthorizationRequestContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// If no request_id parameter can be found in the current request, assume the OpenID Connect request
// was not serialized yet and store the entire payload in the distributed cache to make it easier
// to flow across requests and internal/external authentication/registration workflows.
if (options.Value.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId))
if (options.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId))
{
// Generate a request identifier. Note: using a crypto-secure
// random number generator is not necessary in this case.
@ -330,7 +325,7 @@ namespace OpenIddict
// Store the serialized authorization request parameters in the distributed cache.
var stream = new MemoryStream();
using (var writer = new BsonWriter(stream))
using (var writer = new BsonDataWriter(stream))
{
writer.CloseOutput = false;
@ -342,7 +337,7 @@ namespace OpenIddict
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId;
await options.Value.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions
await options.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions
{
AbsoluteExpiration = context.Options.SystemClock.UtcNow + TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10)
@ -363,15 +358,15 @@ namespace OpenIddict
return;
}
context.SkipToNextMiddleware();
context.SkipHandler();
}
public override async Task ApplyAuthorizationResponse([NotNull] ApplyAuthorizationResponseContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// Remove the authorization request from the distributed cache.
if (options.Value.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId))
if (options.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId))
{
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached requests.
@ -380,11 +375,11 @@ namespace OpenIddict
// Note: the ApplyAuthorizationResponse event is called for both successful
// and errored authorization responses but discrimination is not necessary here,
// as the authorization request must be removed from the distributed cache in both cases.
await options.Value.Cache.RemoveAsync(key);
await options.Cache.RemoveAsync(key);
}
if (!options.Value.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) &&
string.IsNullOrEmpty(context.RedirectUri))
if (!options.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) &&
string.IsNullOrEmpty(context.RedirectUri))
{
// Determine if the status code pages middleware has been enabled for this request.
// If it was not registered or enabled, let the OpenID Connect server middleware render

16
src/OpenIddict/OpenIddictProvider.Discovery.cs

@ -9,8 +9,8 @@ using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using OpenIddict.Core;
@ -19,9 +19,9 @@ namespace OpenIddict
public partial class OpenIddictProvider<TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider
where TApplication : class where TAuthorization : class where TScope : class where TToken : class
{
public override Task HandleConfigurationRequest([NotNull] HandleConfigurationRequestContext context)
public override async Task HandleConfigurationRequest([NotNull] HandleConfigurationRequestContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// Note: though it's natively supported by the OpenID Connect server middleware,
// OpenIddict disallows the use of the unsecure code_challenge_method=plain method,
@ -32,7 +32,7 @@ namespace OpenIddict
// Note: the OpenID Connect server middleware automatically populates grant_types_supported
// by determining whether the authorization and token endpoints are enabled or not but
// OpenIddict uses a different approach and relies on a configurable "grants list".
context.GrantTypes.IntersectWith(options.Value.GrantTypes);
context.GrantTypes.IntersectWith(options.GrantTypes);
// Note: the "openid" scope is automatically
// added by the OpenID Connect server middleware.
@ -47,12 +47,12 @@ namespace OpenIddict
context.Scopes.Add(OpenIdConnectConstants.Scopes.OfflineAccess);
}
var schemes = context.HttpContext.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>();
context.Metadata[OpenIddictConstants.Metadata.ExternalProvidersSupported] = new JArray(
from provider in context.HttpContext.Authentication.GetAuthenticationSchemes()
from provider in await schemes.GetAllSchemesAsync()
where !string.IsNullOrEmpty(provider.DisplayName)
select provider.AuthenticationScheme);
return Task.FromResult(0);
select provider.Name);
}
}
}

57
src/OpenIddict/OpenIddictProvider.Exchange.cs

@ -10,10 +10,7 @@ using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
namespace OpenIddict
{
@ -22,14 +19,12 @@ namespace OpenIddict
{
public override async Task ValidateTokenRequest([NotNull] ValidateTokenRequestContext context)
{
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// Reject token requests that don't specify a supported grant type.
if (!options.Value.GrantTypes.Contains(context.Request.GrantType))
if (!options.GrantTypes.Contains(context.Request.GrantType))
{
logger.LogError("The token request was rejected because the '{Grant}' " +
Logger.LogError("The token request was rejected because the '{Grant}' " +
"grant is not supported.", context.Request.GrantType);
context.Reject(
@ -41,7 +36,7 @@ namespace OpenIddict
// Reject token requests that specify scope=offline_access if the refresh token flow is not enabled.
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
@ -100,7 +95,7 @@ namespace OpenIddict
if (string.IsNullOrEmpty(context.ClientId))
{
// Reject the request if client identification is mandatory.
if (options.Value.RequireClientIdentification)
if (options.RequireClientIdentification)
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
@ -109,7 +104,7 @@ namespace OpenIddict
return;
}
logger.LogDebug("The token request validation process was partially skipped " +
Logger.LogDebug("The token request validation process was partially skipped " +
"because the 'client_id' parameter was missing or empty.");
context.Skip();
@ -118,10 +113,10 @@ namespace OpenIddict
}
// Retrieve the application details corresponding to the requested client_id.
var application = await applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted);
var application = await Applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted);
if (application == null)
{
logger.LogError("The token request was rejected because the client " +
Logger.LogError("The token request was rejected because the client " +
"application was not found: '{ClientId}'.", context.ClientId);
context.Reject(
@ -131,12 +126,12 @@ namespace OpenIddict
return;
}
if (await applications.IsPublicAsync(application, context.HttpContext.RequestAborted))
if (await Applications.IsPublicAsync(application, context.HttpContext.RequestAborted))
{
// Note: public applications are not allowed to use the client credentials grant.
if (context.Request.IsClientCredentialsGrantType())
{
logger.LogError("The token request was rejected because the public client application '{ClientId}' " +
Logger.LogError("The token request was rejected because the public client application '{ClientId}' " +
"was not allowed to use the client credentials grant.", context.Request.ClientId);
context.Reject(
@ -149,7 +144,7 @@ namespace OpenIddict
// Reject tokens requests containing a client_secret when the client is a public application.
if (!string.IsNullOrEmpty(context.ClientSecret))
{
logger.LogError("The token request was rejected because the public application '{ClientId}' " +
Logger.LogError("The token request was rejected because the public application '{ClientId}' " +
"was not allowed to send a client secret.", context.ClientId);
context.Reject(
@ -159,7 +154,7 @@ namespace OpenIddict
return;
}
logger.LogInformation("The token request validation process was not fully validated because " +
Logger.LogInformation("The token request validation process was not fully validated because " +
"the client '{ClientId}' was a public application.", context.ClientId);
// If client authentication cannot be enforced, call context.Skip() to inform
@ -173,7 +168,7 @@ namespace OpenIddict
// to protect them from impersonation attacks.
if (string.IsNullOrEmpty(context.ClientSecret))
{
logger.LogError("The token request was rejected because the confidential application " +
Logger.LogError("The token request was rejected because the confidential application " +
"'{ClientId}' didn't specify a client secret.", context.ClientId);
context.Reject(
@ -183,9 +178,9 @@ namespace OpenIddict
return;
}
if (!await applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted))
if (!await Applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted))
{
logger.LogError("The token request was rejected because the confidential application " +
Logger.LogError("The token request was rejected because the confidential application " +
"'{ClientId}' didn't specify valid client credentials.", context.ClientId);
context.Reject(
@ -200,12 +195,10 @@ namespace OpenIddict
public override async Task HandleTokenRequest([NotNull] HandleTokenRequestContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var tokens = context.HttpContext.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
var options = (OpenIddictOptions) context.Options;
if (!options.Value.DisableTokenRevocation && (context.Request.IsAuthorizationCodeGrantType() ||
context.Request.IsRefreshTokenGrantType()))
if (!options.DisableTokenRevocation && (context.Request.IsAuthorizationCodeGrantType() ||
context.Request.IsRefreshTokenGrantType()))
{
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
@ -216,10 +209,10 @@ namespace OpenIddict
if (context.Request.IsAuthorizationCodeGrantType())
{
// Retrieve the token from the database and ensure it is still valid.
var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token == null)
{
logger.LogError("The token request was rejected because the authorization code was revoked.");
Logger.LogError("The token request was rejected because the authorization code was revoked.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
@ -229,16 +222,16 @@ namespace OpenIddict
}
// Revoke the authorization code to prevent token reuse.
await tokens.RevokeAsync(token, context.HttpContext.RequestAborted);
await Tokens.RevokeAsync(token, context.HttpContext.RequestAborted);
}
else if (context.Request.IsRefreshTokenGrantType())
{
// Retrieve the token from the database and ensure it is still valid.
var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token == null)
{
logger.LogError("The token request was rejected because the refresh token was revoked.");
Logger.LogError("The token request was rejected because the refresh token was revoked.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
@ -252,14 +245,14 @@ namespace OpenIddict
// See https://tools.ietf.org/html/rfc6749#section-6.
if (context.Options.UseSlidingExpiration)
{
await tokens.RevokeAsync(token, context.HttpContext.RequestAborted);
await Tokens.RevokeAsync(token, context.HttpContext.RequestAborted);
}
}
}
// Invoke the rest of the pipeline to allow
// the user code to handle the token request.
context.SkipToNextMiddleware();
context.SkipHandler();
}
}
}

30
src/OpenIddict/OpenIddictProvider.Introspection.cs

@ -11,10 +11,7 @@ using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
namespace OpenIddict
{
@ -39,9 +36,6 @@ namespace OpenIddict
public override async Task ValidateIntrospectionRequest([NotNull] ValidateIntrospectionRequestContext context)
{
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
// Note: the OpenID Connect server middleware supports unauthenticated introspection requests
// but OpenIddict uses a stricter policy preventing unauthenticated/public applications
// from using the introspection endpoint, as required by the specifications.
@ -56,10 +50,10 @@ namespace OpenIddict
}
// Retrieve the application details corresponding to the requested client_id.
var application = await applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted);
var application = await Applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted);
if (application == null)
{
logger.LogError("The introspection request was rejected because the client " +
Logger.LogError("The introspection request was rejected because the client " +
"application was not found: '{ClientId}'.", context.ClientId);
context.Reject(
@ -70,9 +64,9 @@ namespace OpenIddict
}
// Reject non-confidential applications.
if (!await applications.IsConfidentialAsync(application, context.HttpContext.RequestAborted))
if (!await Applications.IsConfidentialAsync(application, context.HttpContext.RequestAborted))
{
logger.LogError("The introspection request was rejected because the public application " +
Logger.LogError("The introspection request was rejected because the public application " +
"'{ClientId}' was not allowed to use this endpoint.", context.ClientId);
context.Reject(
@ -83,9 +77,9 @@ namespace OpenIddict
}
// Validate the client credentials.
if (!await applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted))
if (!await Applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted))
{
logger.LogError("The introspection request was rejected because the confidential application " +
Logger.LogError("The introspection request was rejected because the confidential application " +
"'{ClientId}' didn't specify valid client credentials.", context.ClientId);
context.Reject(
@ -100,9 +94,7 @@ namespace OpenIddict
public override async Task HandleIntrospectionRequest([NotNull] HandleIntrospectionRequestContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var tokens = context.HttpContext.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
var options = (OpenIddictOptions) context.Options;
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId), "The client_id parameter shouldn't be null.");
@ -115,7 +107,7 @@ namespace OpenIddict
// doesn't have any audience: in this case, the caller is allowed to introspect the token even if it's not listed as a valid audience.
if (context.Ticket.IsAccessToken() && context.Ticket.HasAudience() && !context.Ticket.HasAudience(context.Request.ClientId))
{
logger.LogWarning("The client application '{ClientId}' is not allowed to introspect the access " +
Logger.LogWarning("The client application '{ClientId}' is not allowed to introspect the access " +
"token '{Identifier}' because it's not listed as a valid audience.",
context.Request.ClientId, identifier);
@ -125,14 +117,14 @@ namespace OpenIddict
}
// When the received ticket is revocable, ensure it is still valid.
if (!options.Value.DisableTokenRevocation && (context.Ticket.IsAuthorizationCode() || context.Ticket.IsRefreshToken()))
if (!options.DisableTokenRevocation && (context.Ticket.IsAuthorizationCode() || context.Ticket.IsRefreshToken()))
{
// Retrieve the token from the database using the unique identifier stored in the authentication ticket:
// if the corresponding entry cannot be found, return Active = false to indicate that is is no longer valid.
var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token == null)
{
logger.LogInformation("The token {Identifier} was declared as inactive because " +
Logger.LogInformation("The token {Identifier} was declared as inactive because " +
"it was revoked.", identifier);
context.Active = false;

36
src/OpenIddict/OpenIddictProvider.Revocation.cs

@ -10,10 +10,7 @@ using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
namespace OpenIddict
{
@ -22,11 +19,9 @@ namespace OpenIddict
{
public override async Task ValidateRevocationRequest([NotNull] ValidateRevocationRequestContext context)
{
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
Debug.Assert(!options.Value.DisableTokenRevocation, "Token revocation support shouldn't be disabled at this stage.");
Debug.Assert(!options.DisableTokenRevocation, "Token revocation support shouldn't be disabled at this stage.");
// When token_type_hint is specified, reject the request if it doesn't correspond to a revocable token.
if (!string.IsNullOrEmpty(context.Request.TokenTypeHint) &&
@ -49,9 +44,9 @@ namespace OpenIddict
if (string.IsNullOrEmpty(context.ClientId))
{
// Reject the request if client identification is mandatory.
if (options.Value.RequireClientIdentification)
if (options.RequireClientIdentification)
{
logger.LogError("The revocation request was rejected becaused the " +
Logger.LogError("The revocation request was rejected becaused the " +
"mandatory client_id parameter was missing or empty.");
context.Reject(
@ -61,7 +56,7 @@ namespace OpenIddict
return;
}
logger.LogInformation("The revocation request validation process was skipped " +
Logger.LogInformation("The revocation request validation process was skipped " +
"because the client_id parameter was missing or empty.");
context.Skip();
@ -70,7 +65,7 @@ namespace OpenIddict
}
// Retrieve the application details corresponding to the requested client_id.
var application = await applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted);
var application = await Applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted);
if (application == null)
{
context.Reject(
@ -81,7 +76,7 @@ namespace OpenIddict
}
// Reject revocation requests containing a client_secret if the client application is not confidential.
if (await applications.IsPublicAsync(application, context.HttpContext.RequestAborted))
if (await Applications.IsPublicAsync(application, context.HttpContext.RequestAborted))
{
// Reject tokens requests containing a client_secret when the client is a public application.
if (!string.IsNullOrEmpty(context.ClientSecret))
@ -93,7 +88,7 @@ namespace OpenIddict
return;
}
logger.LogInformation("The revocation request validation process was not fully validated because " +
Logger.LogInformation("The revocation request validation process was not fully validated because " +
"the client '{ClientId}' was a public application.", context.ClientId);
// If client authentication cannot be enforced, call context.Skip() to inform
@ -114,7 +109,7 @@ namespace OpenIddict
return;
}
if (!await applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted))
if (!await Applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
@ -128,16 +123,13 @@ namespace OpenIddict
public override async Task HandleRevocationRequest([NotNull] HandleRevocationRequestContext context)
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var tokens = context.HttpContext.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
// If the received token is not an authorization code or a refresh token,
// return an error to indicate that the token cannot be revoked.
if (!context.Ticket.IsAuthorizationCode() && !context.Ticket.IsRefreshToken())
{
logger.LogError("The revocation request was rejected because the token was not revocable.");
Logger.LogError("The revocation request was rejected because the token was not revocable.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedTokenType,
@ -152,10 +144,10 @@ namespace OpenIddict
// Retrieve the token from the database. If the token cannot be found,
// assume it is invalid and consider the revocation as successful.
var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token == null)
{
logger.LogInformation("The token '{Identifier}' was already revoked.", identifier);
Logger.LogInformation("The token '{Identifier}' was already revoked.", identifier);
context.Revoked = true;
@ -163,9 +155,9 @@ namespace OpenIddict
}
// Revoke the token.
await tokens.RevokeAsync(token, context.HttpContext.RequestAborted);
await Tokens.RevokeAsync(token, context.HttpContext.RequestAborted);
logger.LogInformation("The token '{Identifier}' was successfully revoked.", identifier);
Logger.LogInformation("The token '{Identifier}' was successfully revoked.", identifier);
context.Revoked = true;
}

34
src/OpenIddict/OpenIddictProvider.Serialization.cs

@ -11,8 +11,6 @@ using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
namespace OpenIddict
@ -22,13 +20,11 @@ namespace OpenIddict
{
public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context)
{
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var tokens = context.HttpContext.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
var options = (OpenIddictOptions) context.Options;
Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId), "The client identifier shouldn't be null or empty.");
if (!options.Value.DisableTokenRevocation)
if (!options.DisableTokenRevocation)
{
// Resolve the subject from the authentication ticket. If it cannot be found, throw an exception.
var subject = context.Ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject);
@ -38,14 +34,14 @@ namespace OpenIddict
}
// If a null value was returned by CreateAsync, return immediately.
var token = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, subject, context.HttpContext.RequestAborted);
var token = await Tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, subject, context.HttpContext.RequestAborted);
if (token == null)
{
return;
}
// Throw an exception if the token identifier can't be resolved.
var identifier = await tokens.GetIdAsync(token, context.HttpContext.RequestAborted);
var identifier = await Tokens.GetIdAsync(token, context.HttpContext.RequestAborted);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The unique key associated with an authorization code cannot be null or empty.");
@ -56,30 +52,28 @@ namespace OpenIddict
// generated by the OpenID Connect server middleware.
context.Ticket.SetProperty(OpenIdConnectConstants.Properties.TokenId, identifier);
var application = await applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted);
var application = await Applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted);
if (application == null)
{
throw new InvalidOperationException("The client application cannot be retrieved from the database.");
}
await tokens.SetClientAsync(token, await applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted);
await Tokens.SetClientAsync(token, await Applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted);
// If an authorization identifier was specified, bind it to the token.
var authorization = context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId);
if (!string.IsNullOrEmpty(authorization))
{
await tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted);
await Tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted);
}
}
}
public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context)
{
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var tokens = context.HttpContext.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
var options = (OpenIddictOptions) context.Options;
if (!options.Value.DisableTokenRevocation)
if (!options.DisableTokenRevocation)
{
// Resolve the subject from the authentication ticket. If it cannot be found, throw an exception.
var subject = context.Ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject);
@ -89,14 +83,14 @@ namespace OpenIddict
}
// If a null value was returned by CreateAsync, return immediately.
var token = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, subject, context.HttpContext.RequestAborted);
var token = await Tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, subject, context.HttpContext.RequestAborted);
if (token == null)
{
return;
}
// Throw an exception if the token identifier can't be resolved.
var identifier = await tokens.GetIdAsync(token, context.HttpContext.RequestAborted);
var identifier = await Tokens.GetIdAsync(token, context.HttpContext.RequestAborted);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty.");
@ -110,20 +104,20 @@ namespace OpenIddict
// If the client application is known, associate it with the token.
if (!string.IsNullOrEmpty(context.Request.ClientId))
{
var application = await applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted);
var application = await Applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted);
if (application == null)
{
throw new InvalidOperationException("The client application cannot be retrieved from the database.");
}
await tokens.SetClientAsync(token, await applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted);
await Tokens.SetClientAsync(token, await Applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted);
}
// If an authorization identifier was specified, bind it to the token.
var authorization = context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId);
if (!string.IsNullOrEmpty(authorization))
{
await tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted);
await Tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted);
}
}
}

40
src/OpenIddict/OpenIddictProvider.Session.cs

@ -13,9 +13,7 @@ using JetBrains.Annotations;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;
@ -28,17 +26,16 @@ namespace OpenIddict
{
public override async Task ExtractLogoutRequest([NotNull] ExtractLogoutRequestContext context)
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// If a request_id parameter can be found in the logout request,
// restore the complete logout request from the distributed cache.
if (!string.IsNullOrEmpty(context.Request.RequestId))
{
// Return an error if request caching support was not enabled.
if (!options.Value.EnableRequestCaching)
if (!options.EnableRequestCaching)
{
logger.LogError("The logout request was rejected because " +
Logger.LogError("The logout request was rejected because " +
"request caching support was not enabled.");
context.Reject(
@ -52,10 +49,10 @@ namespace OpenIddict
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId;
var payload = await options.Value.Cache.GetAsync(key);
var payload = await options.Cache.GetAsync(key);
if (payload == null)
{
logger.LogError("The logout request was rejected because an unknown " +
Logger.LogError("The logout request was rejected because an unknown " +
"or invalid request_id parameter was specified.");
context.Reject(
@ -66,7 +63,7 @@ namespace OpenIddict
}
// Restore the logout request parameters from the serialized payload.
using (var reader = new BsonReader(new MemoryStream(payload)))
using (var reader = new BsonDataReader(new MemoryStream(payload)))
{
foreach (var parameter in JObject.Load(reader))
{
@ -84,16 +81,13 @@ namespace OpenIddict
public override async Task ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context)
{
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
// If an optional post_logout_redirect_uri was provided, validate it.
if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri))
{
var application = await applications.FindByLogoutRedirectUri(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted);
var application = await Applications.FindByLogoutRedirectUri(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted);
if (application == null)
{
logger.LogError("The logout request was rejected because the client application corresponding " +
Logger.LogError("The logout request was rejected because the client application corresponding " +
"to the specified post_logout_redirect_uri was not found in the database: " +
"'{PostLogoutRedirectUri}'.", context.PostLogoutRedirectUri);
@ -110,12 +104,12 @@ namespace OpenIddict
public override async Task HandleLogoutRequest([NotNull] HandleLogoutRequestContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// If no request_id parameter can be found in the current request, assume the OpenID Connect
// request was not serialized yet and store the entire payload in the distributed cache
// to make it easier to flow across requests and internal/external logout workflows.
if (options.Value.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId))
if (options.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId))
{
// Generate a request identifier. Note: using a crypto-secure
// random number generator is not necessary in this case.
@ -123,7 +117,7 @@ namespace OpenIddict
// Store the serialized logout request parameters in the distributed cache.
var stream = new MemoryStream();
using (var writer = new BsonWriter(stream))
using (var writer = new BsonDataWriter(stream))
{
writer.CloseOutput = false;
@ -135,7 +129,7 @@ namespace OpenIddict
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId;
await options.Value.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions
await options.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions
{
AbsoluteExpiration = context.Options.SystemClock.UtcNow + TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10)
@ -159,10 +153,10 @@ namespace OpenIddict
public override async Task ApplyLogoutResponse([NotNull] ApplyLogoutResponseContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// Remove the logout request from the distributed cache.
if (options.Value.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId))
if (options.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId))
{
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached requests.
@ -171,11 +165,11 @@ namespace OpenIddict
// Note: the ApplyLogoutResponse event is called for both successful
// and errored logout responses but discrimination is not necessary here,
// as the logout request must be removed from the distributed cache in both cases.
await options.Value.Cache.RemoveAsync(key);
await options.Cache.RemoveAsync(key);
}
if (!options.Value.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) &&
string.IsNullOrEmpty(context.PostLogoutRedirectUri))
if (!options.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) &&
string.IsNullOrEmpty(context.PostLogoutRedirectUri))
{
// Determine if the status code pages middleware has been enabled for this request.
// If it was not registered or enabled, let the OpenID Connect server middleware render

2
src/OpenIddict/OpenIddictProvider.Userinfo.cs

@ -23,7 +23,7 @@ namespace OpenIddict
// Invoke the rest of the pipeline to allow
// the user code to handle the userinfo request.
context.SkipToNextMiddleware();
context.SkipHandler();
return Task.FromResult(0);
}

41
src/OpenIddict/OpenIddictProvider.cs

@ -6,13 +6,52 @@
using System.ComponentModel;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using OpenIddict.Core;
namespace OpenIddict
{
/// <summary>
/// Provides the logic necessary to extract, validate and handle OpenID Connect requests.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public partial class OpenIddictProvider<TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider
where TApplication : class where TAuthorization : class where TScope : class where TToken : class
{
// Note: this class is split into specialized partial classes.
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictProvider{TApplication, TAuthorization, TScope, TToken}"/> class.
/// </summary>
public OpenIddictProvider(
[NotNull] ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>> logger,
[NotNull] OpenIddictApplicationManager<TApplication> applications,
[NotNull] OpenIddictAuthorizationManager<TAuthorization> authorizations,
[NotNull] OpenIddictTokenManager<TToken> tokens)
{
Applications = applications;
Authorizations = authorizations;
Logger = logger;
Tokens = tokens;
}
/// <summary>
/// Gets the applications manager.
/// </summary>
public OpenIddictApplicationManager<TApplication> Applications { get; }
/// <summary>
/// Gets the authorizations manager.
/// </summary>
public OpenIddictAuthorizationManager<TAuthorization> Authorizations { get; }
/// <summary>
/// Gets the logger associated with the current class.
/// </summary>
public ILogger Logger { get; }
/// <summary>
/// Gets the tokens manager.
/// </summary>
public OpenIddictTokenManager<TToken> Tokens { get; }
}
}

4
test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj

@ -3,8 +3,8 @@
<Import Project="..\..\build\tests.props" />
<PropertyGroup>
<TargetFrameworks>netcoreapp1.0;net452</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp1.0</TargetFrameworks>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

4
test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj

@ -3,8 +3,8 @@
<Import Project="..\..\build\tests.props" />
<PropertyGroup>
<TargetFrameworks>netcoreapp1.0;net452</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp1.0</TargetFrameworks>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

4
test/OpenIddict.Mvc.Tests/OpenIddict.Mvc.Tests.csproj

@ -3,8 +3,8 @@
<Import Project="..\..\build\tests.props" />
<PropertyGroup>
<TargetFrameworks>netcoreapp1.0;net452</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp1.0</TargetFrameworks>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

6
test/OpenIddict.Tests/OpenIddict.Tests.csproj

@ -3,8 +3,8 @@
<Import Project="..\..\build\tests.props" />
<PropertyGroup>
<TargetFrameworks>netcoreapp1.0;net452</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp1.0</TargetFrameworks>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
@ -34,7 +34,7 @@
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.0' ">
<PropertyGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">
<DefineConstants>$(DefineConstants);SUPPORTS_ECDSA</DefineConstants>
</PropertyGroup>

459
test/OpenIddict.Tests/OpenIddictExtensionsTests.cs

@ -2,11 +2,15 @@
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Builder.Internal;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Moq;
@ -16,199 +20,36 @@ namespace OpenIddict.Tests
{
public class OpenIddictExtensionsTests
{
[Fact]
public void UseOpenIddict_ThrowsAnExceptionWhenServicesAreNotRegistered()
{
// Arrange
var services = new ServiceCollection();
var builder = new ApplicationBuilder(services.BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.UseOpenIddict());
Assert.Equal("The OpenIddict services cannot be resolved from the dependency injection container. " +
"Make sure 'services.AddOpenIddict()' is correctly called from 'ConfigureServices()'.", exception.Message);
}
[Fact]
public void UseOpenIddict_ThrowsAnExceptionWhenNoDistributedCacheIsRegisteredIfRequestCachingIsEnabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOpenIddict()
.EnableRequestCaching();
var builder = new ApplicationBuilder(services.BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.UseOpenIddict());
Assert.Equal("A distributed cache implementation must be registered in the OpenIddict options " +
"or in the dependency injection container when enabling request caching support.", exception.Message);
}
[Fact]
public void UseOpenIddict_ThrowsAnExceptionWhenNoFlowIsEnabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOpenIddict();
var builder = new ApplicationBuilder(services.BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.UseOpenIddict());
Assert.Equal("At least one OAuth2/OpenID Connect flow must be enabled.", exception.Message);
}
[Theory]
[InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)]
[InlineData(OpenIdConnectConstants.GrantTypes.Implicit)]
public void UseOpenIddict_ThrowsAnExceptionWhenAuthorizationEndpointIsDisabled(string flow)
{
// Arrange
var services = new ServiceCollection();
services.AddOpenIddict()
.Configure(options => options.GrantTypes.Add(flow))
.Configure(options => options.AuthorizationEndpointPath = PathString.Empty);
var builder = new ApplicationBuilder(services.BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.UseOpenIddict());
Assert.Equal("The authorization endpoint must be enabled to use " +
"the authorization code and implicit flows.", exception.Message);
}
[Theory]
[InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)]
[InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)]
[InlineData(OpenIdConnectConstants.GrantTypes.Password)]
[InlineData(OpenIdConnectConstants.GrantTypes.RefreshToken)]
public void UseOpenIddict_ThrowsAnExceptionWhenTokenEndpointIsDisabled(string flow)
{
// Arrange
var services = new ServiceCollection();
services.AddOpenIddict()
.EnableAuthorizationEndpoint("/connect/authorize")
.Configure(options => options.GrantTypes.Add(flow))
.Configure(options => options.TokenEndpointPath = PathString.Empty);
var builder = new ApplicationBuilder(services.BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.UseOpenIddict());
Assert.Equal("The token endpoint must be enabled to use the authorization code, " +
"client credentials, password and refresh token flows.", exception.Message);
}
[Fact]
public void UseOpenIddict_ThrowsAnExceptionWhenTokenRevocationIsDisabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOpenIddict()
.EnableAuthorizationEndpoint("/connect/authorize")
.EnableRevocationEndpoint("/connect/revocation")
.AllowImplicitFlow()
.DisableTokenRevocation();
var builder = new ApplicationBuilder(services.BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.UseOpenIddict());
Assert.Equal("The revocation endpoint cannot be enabled when token revocation is disabled.", exception.Message);
}
[Fact]
public void UseOpenIddict_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfTheImplicitFlowIsEnabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOpenIddict()
.EnableAuthorizationEndpoint("/connect/authorize")
.AllowImplicitFlow();
var builder = new ApplicationBuilder(services.BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.UseOpenIddict());
Assert.Equal("At least one asymmetric signing key must be registered when enabling the implicit flow. " +
"Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " +
"or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message);
}
[Fact]
public void Configure_OptionsAreCorrectlyAmended()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.Configure(configuration => configuration.Description.DisplayName = "OpenIddict");
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
// Assert
Assert.Equal("OpenIddict", options.Value.Description.DisplayName);
}
[Fact]
public void UseOpenIddict_OpenIdConnectServerMiddlewareIsRegistered()
{
// Arrange
var services = new ServiceCollection();
builder.Configure(configuration => configuration.AccessTokenLifetime = TimeSpan.FromDays(1));
services.AddOpenIddict()
.AddSigningCertificate(
assembly: typeof(OpenIddictProviderTests).GetTypeInfo().Assembly,
resource: "OpenIddict.Tests.Certificate.pfx",
password: "OpenIddict")
.AllowImplicitFlow()
.EnableAuthorizationEndpoint("/connect/authorize");
var builder = new Mock<IApplicationBuilder>();
builder.SetupGet(mock => mock.ApplicationServices)
.Returns(services.BuildServiceProvider());
// Act
builder.Object.UseOpenIddict();
var options = GetOptions(services);
// Assert
builder.Verify(mock => mock.Use(It.IsAny<Func<RequestDelegate, RequestDelegate>>()), Times.Once());
Assert.Equal(TimeSpan.FromDays(1), options.AccessTokenLifetime);
}
[Fact]
public void AddEphemeralSigningKey_SigningKeyIsCorrectlyAdded()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.AddEphemeralSigningKey();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal(1, options.Value.SigningCredentials.Count);
Assert.Equal(1, options.SigningCredentials.Count);
}
[Theory]
@ -223,17 +64,14 @@ namespace OpenIddict.Tests
public void AddEphemeralSigningKey_SigningCredentialsUseSpecifiedAlgorithm(string algorithm)
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.AddEphemeralSigningKey(algorithm);
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var credentials = options.Value.SigningCredentials[0];
var options = GetOptions(services);
var credentials = options.SigningCredentials[0];
// Assert
Assert.Equal(algorithm, credentials.Algorithm);
@ -250,9 +88,7 @@ namespace OpenIddict.Tests
public void AddSigningKey_SigningKeyIsCorrectlyAdded(string algorithm)
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
var factory = Mock.Of<CryptoProviderFactory>(mock =>
@ -263,20 +99,17 @@ namespace OpenIddict.Tests
// Act
builder.AddSigningKey(key);
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Same(key, options.Value.SigningCredentials[0].Key);
Assert.Same(key, options.SigningCredentials[0].Key);
}
[Fact]
public void AddSigningCertificate_SigningKeyIsCorrectlyAdded()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
@ -285,486 +118,432 @@ namespace OpenIddict.Tests
resource: "OpenIddict.Tests.Certificate.pfx",
password: "OpenIddict");
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.IsType(typeof(X509SecurityKey), options.Value.SigningCredentials[0].Key);
Assert.IsType<X509SecurityKey>(options.SigningCredentials[0].Key);
}
[Fact]
public void AllowAuthorizationCodeFlow_CodeFlowIsAddedToGrantTypes()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.AllowAuthorizationCodeFlow();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode, options.Value.GrantTypes);
Assert.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode, options.GrantTypes);
}
[Fact]
public void AllowClientCredentialsFlow_ClientCredentialsFlowIsAddedToGrantTypes()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.AllowClientCredentialsFlow();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials, options.Value.GrantTypes);
Assert.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials, options.GrantTypes);
}
[Fact]
public void AllowCustomFlow_CustomFlowIsAddedToGrantTypes()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant");
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Contains("urn:ietf:params:oauth:grant-type:custom_grant", options.Value.GrantTypes);
Assert.Contains("urn:ietf:params:oauth:grant-type:custom_grant", options.GrantTypes);
}
[Fact]
public void AllowImplicitFlow_ImplicitFlowIsAddedToGrantTypes()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.AllowImplicitFlow();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Contains(OpenIdConnectConstants.GrantTypes.Implicit, options.Value.GrantTypes);
Assert.Contains(OpenIdConnectConstants.GrantTypes.Implicit, options.GrantTypes);
}
[Fact]
public void AllowPasswordFlow_PasswordFlowIsAddedToGrantTypes()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.AllowPasswordFlow();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Contains(OpenIdConnectConstants.GrantTypes.Password, options.Value.GrantTypes);
Assert.Contains(OpenIdConnectConstants.GrantTypes.Password, options.GrantTypes);
}
[Fact]
public void AllowRefreshTokenFlow_RefreshTokenFlowIsAddedToGrantTypes()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.AllowRefreshTokenFlow();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken, options.Value.GrantTypes);
Assert.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken, options.GrantTypes);
}
[Fact]
public void DisableConfigurationEndpoint_ConfigurationEndpointIsDisabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.DisableConfigurationEndpoint();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal(PathString.Empty, options.Value.ConfigurationEndpointPath);
Assert.Equal(PathString.Empty, options.ConfigurationEndpointPath);
}
[Fact]
public void DisableCryptographyEndpoint_CryptographyEndpointIsDisabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.DisableCryptographyEndpoint();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal(PathString.Empty, options.Value.CryptographyEndpointPath);
Assert.Equal(PathString.Empty, options.CryptographyEndpointPath);
}
[Fact]
public void DisableSlidingExpiration_SlidingExpirationIsDisabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.DisableSlidingExpiration();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.False(options.Value.UseSlidingExpiration);
Assert.False(options.UseSlidingExpiration);
}
[Fact]
public void DisableTokenRevocation_TokenRevocationIsDisabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.DisableTokenRevocation();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.True(options.Value.DisableTokenRevocation);
Assert.True(options.DisableTokenRevocation);
}
[Fact]
public void EnableAuthorizationEndpoint_AuthorizationEndpointIsEnabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.EnableAuthorizationEndpoint("/endpoint-path");
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal("/endpoint-path", options.Value.AuthorizationEndpointPath);
Assert.Equal("/endpoint-path", options.AuthorizationEndpointPath);
}
[Fact]
public void EnableIntrospectionEndpoint_IntrospectionEndpointIsEnabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.EnableIntrospectionEndpoint("/endpoint-path");
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal("/endpoint-path", options.Value.IntrospectionEndpointPath);
Assert.Equal("/endpoint-path", options.IntrospectionEndpointPath);
}
[Fact]
public void EnableLogoutEndpoint_LogoutEndpointIsEnabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.EnableLogoutEndpoint("/endpoint-path");
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal("/endpoint-path", options.Value.LogoutEndpointPath);
Assert.Equal("/endpoint-path", options.LogoutEndpointPath);
}
[Fact]
public void EnableRequestCaching_RequestCachingIsEnabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.EnableRequestCaching();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.True(options.Value.EnableRequestCaching);
Assert.True(options.EnableRequestCaching);
}
[Fact]
public void EnableRevocationEndpoint_RevocationEndpointIsEnabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.EnableRevocationEndpoint("/endpoint-path");
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal("/endpoint-path", options.Value.RevocationEndpointPath);
Assert.Equal("/endpoint-path", options.RevocationEndpointPath);
}
[Fact]
public void EnableTokenEndpoint_TokenEndpointIsEnabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.EnableTokenEndpoint("/endpoint-path");
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal("/endpoint-path", options.Value.TokenEndpointPath);
Assert.Equal("/endpoint-path", options.TokenEndpointPath);
}
[Fact]
public void EnableUserinfoEndpoint_UserinfoEndpointIsEnabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.EnableUserinfoEndpoint("/endpoint-path");
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal("/endpoint-path", options.Value.UserinfoEndpointPath);
Assert.Equal("/endpoint-path", options.UserinfoEndpointPath);
}
[Fact]
public void RequireClientIdentification_ClientIdentificationIsEnforced()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.RequireClientIdentification();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.True(options.Value.RequireClientIdentification);
Assert.True(options.RequireClientIdentification);
}
[Fact]
public void SetAccessTokenLifetime_DefaultAccessTokenLifetimeIsReplaced()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.SetAccessTokenLifetime(TimeSpan.FromMinutes(42));
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal(TimeSpan.FromMinutes(42), options.Value.AccessTokenLifetime);
Assert.Equal(TimeSpan.FromMinutes(42), options.AccessTokenLifetime);
}
[Fact]
public void SetAuthorizationCodeLifetime_DefaultAuthorizationCodeLifetimeIsReplaced()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.SetAuthorizationCodeLifetime(TimeSpan.FromMinutes(42));
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal(TimeSpan.FromMinutes(42), options.Value.AuthorizationCodeLifetime);
Assert.Equal(TimeSpan.FromMinutes(42), options.AuthorizationCodeLifetime);
}
[Fact]
public void SetIdentityTokenLifetime_DefaultIdentityTokenLifetimeIsReplaced()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.SetIdentityTokenLifetime(TimeSpan.FromMinutes(42));
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal(TimeSpan.FromMinutes(42), options.Value.IdentityTokenLifetime);
Assert.Equal(TimeSpan.FromMinutes(42), options.IdentityTokenLifetime);
}
[Fact]
public void SetRefreshTokenLifetime_DefaultRefreshTokenLifetimeIsReplaced()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.SetRefreshTokenLifetime(TimeSpan.FromMinutes(42));
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal(TimeSpan.FromMinutes(42), options.Value.RefreshTokenLifetime);
Assert.Equal(TimeSpan.FromMinutes(42), options.RefreshTokenLifetime);
}
[Fact]
public void SetIssuer_AddressIsReplaced()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.SetIssuer(new Uri("http://www.fabrikam.com/"));
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Value.Issuer);
Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Issuer);
}
[Fact]
public void UseDataProtectionProvider_DefaultProviderIsReplaced()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.UseDataProtectionProvider(new EphemeralDataProtectionProvider());
builder.UseDataProtectionProvider(new EphemeralDataProtectionProvider(new LoggerFactory()));
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.IsType(typeof(EphemeralDataProtectionProvider), options.Value.DataProtectionProvider);
Assert.IsType<EphemeralDataProtectionProvider>(options.DataProtectionProvider);
}
[Fact]
public void UseJsonWebTokens_AccessTokenHandlerIsCorrectlySet()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var services = CreateServices();
var builder = new OpenIddictBuilder(services);
// Act
builder.UseJsonWebTokens();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = GetOptions(services);
// Assert
Assert.IsType(typeof(JwtSecurityTokenHandler), options.Value.AccessTokenHandler);
Assert.IsType<JwtSecurityTokenHandler>(options.AccessTokenHandler);
}
private static IServiceCollection CreateServices()
{
var services = new ServiceCollection();
services.AddAuthentication();
services.AddDistributedMemoryCache();
services.AddLogging();
services.AddSingleton<IHostingEnvironment, HostingEnvironment>();
return services;
}
private static OpenIddictOptions GetOptions(IServiceCollection services)
{
services.RemoveAll<IPostConfigureOptions<OpenIdConnectServerOptions>>();
services.RemoveAll<IPostConfigureOptions<OpenIddictOptions>>();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptionsSnapshot<OpenIddictOptions>>();
return options.Get(OpenIdConnectServerDefaults.AuthenticationScheme);
}
}
}

164
test/OpenIddict.Tests/OpenIddictInitializerTests.cs

@ -0,0 +1,164 @@
using System;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Client;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
namespace OpenIddict.Tests
{
public class OpenIddictInitializerTests
{
[Fact]
public async Task PostConfigure_ThrowsAnExceptionWhenNoFlowIsEnabled()
{
// Arrange
var server = CreateAuthorizationServer(builder =>
{
builder.Configure(options => { });
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(delegate
{
return client.GetAsync("/");
});
// Assert
Assert.Equal("At least one OAuth2/OpenID Connect flow must be enabled.", exception.Message);
}
[Theory]
[InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)]
[InlineData(OpenIdConnectConstants.GrantTypes.Implicit)]
public async Task PostConfigure_ThrowsAnExceptionWhenAuthorizationEndpointIsDisabled(string flow)
{
// Arrange
var server = CreateAuthorizationServer(builder =>
{
builder.Configure(options => options.GrantTypes.Add(flow))
.Configure(options => options.AuthorizationEndpointPath = PathString.Empty);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(delegate
{
return client.GetAsync("/");
});
Assert.Equal("The authorization endpoint must be enabled to use " +
"the authorization code and implicit flows.", exception.Message);
}
[Theory]
[InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)]
[InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)]
[InlineData(OpenIdConnectConstants.GrantTypes.Password)]
[InlineData(OpenIdConnectConstants.GrantTypes.RefreshToken)]
public async Task PostConfigure_ThrowsAnExceptionWhenTokenEndpointIsDisabled(string flow)
{
// Arrange
var server = CreateAuthorizationServer(builder =>
{
builder.EnableAuthorizationEndpoint("/connect/authorize")
.Configure(options => options.GrantTypes.Add(flow))
.Configure(options => options.TokenEndpointPath = PathString.Empty);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(delegate
{
return client.GetAsync("/");
});
Assert.Equal("The token endpoint must be enabled to use the authorization code, " +
"client credentials, password and refresh token flows.", exception.Message);
}
[Fact]
public async Task PostConfigure_ThrowsAnExceptionWhenTokenRevocationIsDisabled()
{
// Arrange
var server = CreateAuthorizationServer(builder =>
{
builder.EnableAuthorizationEndpoint("/connect/authorize")
.EnableRevocationEndpoint("/connect/revocation")
.AllowImplicitFlow()
.DisableTokenRevocation();
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(delegate
{
return client.GetAsync("/");
});
Assert.Equal("The revocation endpoint cannot be enabled when token revocation is disabled.", exception.Message);
}
[Fact]
public async Task PostConfigure_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfTheImplicitFlowIsEnabled()
{
// Arrange
var server = CreateAuthorizationServer(builder =>
{
builder.EnableAuthorizationEndpoint("/connect/authorize")
.AllowImplicitFlow();
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(delegate
{
return client.GetAsync("/");
});
Assert.Equal("At least one asymmetric signing key must be registered when enabling the implicit flow. " +
"Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " +
"or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message);
}
private static TestServer CreateAuthorizationServer(Action<OpenIddictBuilder> configuration = null)
{
var builder = new WebHostBuilder();
builder.UseEnvironment("Testing");
builder.ConfigureLogging(options => options.AddDebug());
builder.ConfigureServices(services =>
{
services.AddAuthentication();
services.AddOptions();
services.AddDistributedMemoryCache();
services.AddOpenIddict(options => configuration?.Invoke(options));
});
builder.Configure(app =>
{
app.UseAuthentication();
app.Run(context => context.ChallengeAsync(OpenIdConnectServerDefaults.AuthenticationScheme));
});
return new TestServer(builder);
}
}
}

14
test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs

@ -521,7 +521,8 @@ namespace OpenIddict.Tests
cache.Verify(mock => mock.SetAsync(
OpenIddictConstants.Environment.AuthorizationRequest + identifier,
It.IsAny<byte[]>(),
It.IsAny<DistributedCacheEntryOptions>()), Times.Once());
It.IsAny<DistributedCacheEntryOptions>(),
It.IsAny<CancellationToken>()), Times.Once());
}
[Theory]
@ -596,7 +597,7 @@ namespace OpenIddict.Tests
};
var stream = new MemoryStream();
using (var writer = new BsonWriter(stream))
using (var writer = new BsonDataWriter(stream))
{
writer.CloseOutput = false;
@ -606,8 +607,9 @@ namespace OpenIddict.Tests
var cache = new Mock<IDistributedCache>();
cache.Setup(mock => mock.GetAsync(OpenIddictConstants.Environment.AuthorizationRequest +
"b2ee7815-5579-4ff7-86b0-ba671b939d96"))
cache.Setup(mock => mock.GetAsync(
OpenIddictConstants.Environment.AuthorizationRequest + "b2ee7815-5579-4ff7-86b0-ba671b939d96",
It.IsAny<CancellationToken>()))
.ReturnsAsync(stream.ToArray());
var server = CreateAuthorizationServer(builder =>
@ -646,8 +648,8 @@ namespace OpenIddict.Tests
Assert.NotNull(response.AccessToken);
cache.Verify(mock => mock.RemoveAsync(
OpenIddictConstants.Environment.AuthorizationRequest +
"b2ee7815-5579-4ff7-86b0-ba671b939d96"), Times.Once());
OpenIddictConstants.Environment.AuthorizationRequest + "b2ee7815-5579-4ff7-86b0-ba671b939d96",
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]

1
test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs

@ -8,7 +8,6 @@ using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using OpenIddict.Core;

1
test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs

@ -9,7 +9,6 @@ using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using OpenIddict.Core;

1
test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs

@ -10,7 +10,6 @@ using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Moq;

3
test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs

@ -126,7 +126,8 @@ namespace OpenIddict.Tests
cache.Verify(mock => mock.SetAsync(
OpenIddictConstants.Environment.LogoutRequest + identifier,
It.IsAny<byte[]>(),
It.IsAny<DistributedCacheEntryOptions>()), Times.Once());
It.IsAny<DistributedCacheEntryOptions>(),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]

93
test/OpenIddict.Tests/OpenIddictProviderTests.cs

@ -12,7 +12,6 @@ using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -44,46 +43,66 @@ namespace OpenIddict.Tests
builder.ConfigureServices(services =>
{
services.AddAuthentication();
services.AddOptions();
services.AddDistributedMemoryCache();
var instance = services.AddOpenIddict()
// Note: the following client_id/client_secret are fake and are only
// used to test the metadata returned by the discovery endpoint.
services.AddAuthentication()
.AddFacebook(options =>
{
options.ClientId = "16018790-E88E-4553-8036-BB342579FF19";
options.ClientSecret = "3D6499AF-5607-489B-815A-F3ACF1617296";
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddGoogle(options =>
{
options.ClientId = "BAF437A5-87FA-4D06-8EFD-F9BA96CCEDC4";
options.ClientSecret = "27DF07D3-6B03-4EE0-95CD-3AC16782216B";
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
// Replace the default OpenIddict managers.
services.AddSingleton(CreateApplicationManager());
services.AddSingleton(CreateAuthorizationManager());
services.AddSingleton(CreateTokenManager());
services.AddOpenIddict(options =>
{
// Disable the transport security requirement during testing.
.DisableHttpsRequirement()
options.DisableHttpsRequirement();
// Enable the tested endpoints.
.EnableAuthorizationEndpoint(AuthorizationEndpoint)
.EnableIntrospectionEndpoint(IntrospectionEndpoint)
.EnableLogoutEndpoint(LogoutEndpoint)
.EnableRevocationEndpoint(RevocationEndpoint)
.EnableTokenEndpoint(TokenEndpoint)
.EnableUserinfoEndpoint(UserinfoEndpoint)
options.EnableAuthorizationEndpoint(AuthorizationEndpoint)
.EnableIntrospectionEndpoint(IntrospectionEndpoint)
.EnableLogoutEndpoint(LogoutEndpoint)
.EnableRevocationEndpoint(RevocationEndpoint)
.EnableTokenEndpoint(TokenEndpoint)
.EnableUserinfoEndpoint(UserinfoEndpoint);
// Enable the tested flows.
.AllowAuthorizationCodeFlow()
.AllowClientCredentialsFlow()
.AllowImplicitFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
options.AllowAuthorizationCodeFlow()
.AllowClientCredentialsFlow()
.AllowImplicitFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
// Register the X.509 certificate used to sign the identity tokens.
.AddSigningCertificate(
options.AddSigningCertificate(
assembly: typeof(OpenIddictProviderTests).GetTypeInfo().Assembly,
resource: "OpenIddict.Tests.Certificate.pfx",
password: "OpenIddict")
password: "OpenIddict");
// Note: overriding the default data protection provider is not necessary for the tests to pass,
// but is useful to ensure unnecessary keys are not persisted in testing environments, which also
// helps make the unit tests run faster, as no registry or disk access is required in this case.
.UseDataProtectionProvider(new EphemeralDataProtectionProvider());
options.UseDataProtectionProvider(new EphemeralDataProtectionProvider(new LoggerFactory()));
// Replace the default application/token managers.
services.AddSingleton(CreateApplicationManager());
services.AddSingleton(CreateTokenManager());
// Run the configuration delegate
// registered by the unit tests.
configuration?.Invoke(instance);
// Run the configuration delegate
// registered by the unit tests.
configuration?.Invoke(options);
});
});
builder.Configure(app =>
@ -110,25 +129,7 @@ namespace OpenIddict.Tests
return next(context);
});
app.UseCookieAuthentication();
// Note: the following client_id/client_secret are fake and are only
// used to test the metadata returned by the discovery endpoint.
app.UseFacebookAuthentication(new FacebookOptions
{
ClientId = "16018790-E88E-4553-8036-BB342579FF19",
ClientSecret = "3D6499AF-5607-489B-815A-F3ACF1617296",
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme
});
app.UseGoogleAuthentication(new GoogleOptions
{
ClientId = "BAF437A5-87FA-4D06-8EFD-F9BA96CCEDC4",
ClientSecret = "27DF07D3-6B03-4EE0-95CD-3AC16782216B",
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme
});
app.UseOpenIddict();
app.UseAuthentication();
app.Run(context =>
{
@ -147,12 +148,12 @@ namespace OpenIddict.Tests
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70");
return context.Authentication.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties);
return context.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties);
}
else if (request.IsLogoutRequest())
{
return context.Authentication.SignOutAsync(OpenIdConnectServerDefaults.AuthenticationScheme);
return context.SignOutAsync(OpenIdConnectServerDefaults.AuthenticationScheme);
}
else if (request.IsUserinfoRequest())

Loading…
Cancel
Save