OpenIddict 3.0 beta6 is out

What changed?

All the changes introduced in this release can be found on GitHub, but here's a recap of the most important ones:

OpenIddict now supports response type permissions

In previous versions, the response_type values an application was allowed to use were always inferred from the grant types. For instance, if a client was granted the authorization_code grant type permission, it was automatically allowed to use response_type=code (that corresponds to the "pure" authorization code). If it was granted the implicit permission, it was also allowed to use response_type=id_token and depending on its type (confidential or public), response_type=id_token token and response_type=token.

While convenient, this approach lacked granularity. In OpenIddict 3.0 beta6, this mechanism is replaced by explicit response type permissions. Response type permissions are granted exactly the same way as the other permissions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ConsentType = ConsentTypes.Explicit,
DisplayName = "MVC client application",
DisplayNames =
{
[CultureInfo.GetCultureInfo("fr-FR")] = "Application cliente MVC"
},
PostLogoutRedirectUris =
{
new Uri("https://localhost:44381/signout-callback-oidc")
},
RedirectUris =
{
new Uri("https://localhost:44381/signin-oidc")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code, // New permission
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "demo_api"
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});

Introducing response type permissions was also a good opportunity for removing the OpenIddict-specific hybrid client type, that was used by confidential clients that also wanted to retrieve an access token directly from the authorization endpoint. In beta6, the hybrid type is no longer supported and must be replaced by explicit Permissions.ResponseTypes.CodeIdTokenToken or Permissions.ResponseTypes.CodeToken permissions.

Users who have many clients to migrate can use this script to infer the "best" response type permissions based on the already allowed grant types:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace MigrationScript
{
public static class Program
{
public static async Task Main(string[] args)
{
var services = new ServiceCollection();
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
});
services.AddDbContext<DbContext>(options =>
{
options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=aspnet5-openiddict-sample-12340be6-0442-4622-b782-a7412bb7d045;Trusted_Connection=True;MultipleActiveResultSets=true");
options.UseOpenIddict();
});
using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
foreach (var application in await GetApplicationsAsync(manager))
{
// Ignore applications that already have at least one response type permission.
var permissions = new HashSet<string>(await manager.GetPermissionsAsync(application), StringComparer.Ordinal);
if (permissions.Any(permission => permission.StartsWith(Permissions.Prefixes.ResponseType)))
{
continue;
}
// If a application was granted the authorization code grant permission, allow it to use the code response type.
if (await manager.HasPermissionAsync(application, Permissions.GrantTypes.AuthorizationCode))
{
permissions.Add(Permissions.ResponseTypes.Code);
}
if (await manager.HasPermissionAsync(application, Permissions.GrantTypes.Implicit))
{
// Allow all applications that were granted the implicit
// grant permission to use the "id_token" response types.
permissions.Add(Permissions.ResponseTypes.IdToken);
// Applications that were also granted the code grant type permission
// are allowed to use the "code id_token" response type permission.
if (await manager.HasPermissionAsync(application, Permissions.GrantTypes.AuthorizationCode))
{
permissions.Add(Permissions.ResponseTypes.CodeIdToken);
}
// Allow public and hybrid clients to retrieve an access token directly from the
// authorization endpoint by granting them the "id_token token" and "token" permissions.
if (await manager.HasClientTypeAsync(application, ClientTypes.Public) ||
await manager.HasClientTypeAsync(application, "hybrid"))
{
permissions.Add(Permissions.ResponseTypes.IdTokenToken);
permissions.Add(Permissions.ResponseTypes.Token);
}
// Hybrid applications that were also granted the code grant type permission
// are allowed to use the "code id_token token" and "code token" types.
if (await manager.HasClientTypeAsync(application, "hybrid") &&
await manager.HasPermissionAsync(application, Permissions.GrantTypes.AuthorizationCode))
{
permissions.Add(Permissions.ResponseTypes.CodeIdTokenToken);
permissions.Add(Permissions.ResponseTypes.CodeToken);
}
}
var descriptor = new OpenIddictApplicationDescriptor();
await manager.PopulateAsync(descriptor, application);
// If the application is an hybrid client, replace it by a confidential client.
// Hybrid client types were removed in OpenIddict 3.0 beta6 and replaced by
// response type permissions, that allow defining whether a confidential client
// is allowed to retrieved an access token from the authorization endpoint
// (i.e without having to send its client credentials to retrieve an access token).
if (await manager.HasClientTypeAsync(application, "hybrid"))
{
descriptor.Type = ClientTypes.Confidential;
}
// Replace the permissions on the descriptor.
descriptor.Permissions.Clear();
descriptor.Permissions.UnionWith(permissions);
// Update the application.
await manager.PopulateAsync(application, descriptor);
await manager.UpdateAsync(application);
}
static async Task<List<object>> GetApplicationsAsync(IOpenIddictApplicationManager manager)
{
var applications = new List<object>();
await foreach (var application in manager.ListAsync())
{
applications.Add(application);
}
return applications;
}
}
}
}

The hybrid flow is no longer enabled when enabling both the authorization code and implicit flows

In previous versions, the hybrid flow was automatically enabled when enabling both the authorization code and implicit flows. In beta6, it now must be enabled explicitly, like the other flows:

1
2
3
4
5
services.AddOpenIddict()
.AddServer(options =>
{
options.AllowHybridFlow();
});

Rolling refresh tokens are now enabled by default and allow for a configurable leeway

In 3.0 beta6, refresh tokens were revamped and the following changes were made:

  • The rolling refresh tokens mechanism now allows for a small leeway to avoid revoking entire token chains when concurrent requests are received (by default, it is set to 15 seconds but can be changed using OpenIddictServerBuilder.SetRefreshTokenReuseLeeway()).

  • The rolling refresh tokens mechanism no longer revokes previously issued tokens, it just marks refresh tokens as redeemed when they are used.

  • Rolling refresh tokens are now enabled by default to make OpenIddict compliant with the OAuth 2.0 best current practices, that require implementing either rotation or sender-constrained tokens. Developers who prefer disabling rolling refresh tokens can do so by calling OpenIddictServerBuilder.DisableRollingRefreshTokens().

  • The refresh token lifetime extension mechanism was removed: even when rolling refresh tokens are disabled, a new refresh token will now be generated and returned for each refresh token request, which guarantees that the client application always gets a refresh tokens (and subsequent access tokens) containing the latest claims.

The EF Core and EF 6 entities now use DateTime instead of DateTimeOffset

Due to incomplete support in the Oracle MySQL and Npgsql Entity Framework providers, the EF Core and EF 6 entities no longer use DateTimeOffset. Developers migrating to OpenIddict 3.0 beta6 will need to add a migration to move from DateTimeOffset to DateTime.

Proof Key for Code Exchange (PKCE) can now be enforced globally

Previous versions of OpenIddict 3.0 already allowed making PKCE mandatory per-client, but in beta6, there's now a global option:

1
2
3
4
5
6
services.AddOpenIddict()
.AddServer(options =>
{
// Force all client applications to use Proof Key for Code Exchange (PKCE).
options.RequireProofKeyForCodeExchange();
});

Tokens no longer contain a jti claim

To make tokens lighter, a jti claim is no longer generated and persisted in the tokens generated by OpenIddict 3.0 beta6.

Special thanks to Johann Wimmer for his great contribution to the tests projects, that are now compiled with nullable reference types enabled!