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

Commit af5c754

Browse files
committed
Update Owin.Security.OAuth.Validation to use the ASP.NET Core Data Protection stack
1 parent 0813003 commit af5c754

File tree

9 files changed

+72
-192
lines changed

9 files changed

+72
-192
lines changed

src/AspNet.Security.OAuth.Validation/OAuthValidationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
3636

3737
// Try to unprotect the token and return an error
3838
// if the ticket can't be decrypted or validated.
39-
var ticket = Options.TicketFormat.Unprotect(token);
39+
var ticket = Options.AccessTokenFormat.Unprotect(token);
4040
if (ticket == null) {
4141
return AuthenticateResult.Fail("Authentication failed because the access token was invalid.");
4242
}

src/AspNet.Security.OAuth.Validation/OAuthValidationMiddleware.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ public OAuthValidationMiddleware(
2121
[NotNull] UrlEncoder encoder,
2222
[NotNull] IDataProtectionProvider dataProtectionProvider)
2323
: base(next, options, loggerFactory, encoder) {
24-
if (Options.TicketFormat == null) {
25-
// Note: the purposes of the default ticket
26-
// format must match the values used by ASOS.
27-
Options.TicketFormat = new TicketDataFormat(
28-
dataProtectionProvider.CreateProtector(
29-
"AspNet.Security.OpenIdConnect.Server.OpenIdConnectServerMiddleware",
30-
"ASOS", "Access_Token", "v1"));
24+
if (Options.DataProtectionProvider == null) {
25+
Options.DataProtectionProvider = dataProtectionProvider;
26+
}
27+
28+
if (Options.AccessTokenFormat == null) {
29+
// Note: the following purposes must match the values used by ASOS.
30+
var protector = Options.DataProtectionProvider.CreateProtector(
31+
"OpenIdConnectServerMiddleware", "ASOS", "Access_Token", "v1");
32+
33+
Options.AccessTokenFormat = new TicketDataFormat(protector);
3134
}
3235
}
3336

src/AspNet.Security.OAuth.Validation/OAuthValidationOptions.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Generic;
88
using Microsoft.AspNetCore.Authentication;
99
using Microsoft.AspNetCore.Builder;
10+
using Microsoft.AspNetCore.DataProtection;
1011

1112
namespace AspNet.Security.OAuth.Validation {
1213
public class OAuthValidationOptions : AuthenticationOptions {
@@ -28,8 +29,16 @@ public OAuthValidationOptions() {
2829

2930
/// <summary>
3031
/// Gets or sets the data format used to unprotect the
31-
/// authenticated tickets received by the validation middleware.
32+
/// access tokens received by the validation middleware.
3233
/// </summary>
33-
public ISecureDataFormat<AuthenticationTicket> TicketFormat { get; set; }
34+
public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; set; }
35+
36+
/// <summary>
37+
/// Gets or sets the data protection provider used to create the default
38+
/// data protectors used by <see cref="OAuthValidationMiddleware"/>.
39+
/// When this property is set to <c>null</c>, the data protection provider
40+
/// is directly retrieved from the dependency injection container.
41+
/// </summary>
42+
public IDataProtectionProvider DataProtectionProvider { get; set; }
3443
}
3544
}

src/Owin.Security.OAuth.Validation/OAuthValidationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync() {
4040

4141
// Try to unprotect the token and return an error
4242
// if the ticket can't be decrypted or validated.
43-
var ticket = Options.TicketFormat.Unprotect(token);
43+
var ticket = Options.AccessTokenFormat.Unprotect(token);
4444
if (ticket == null) {
4545
Options.Logger.WriteWarning("Authentication failed because the access token was invalid.");
4646

src/Owin.Security.OAuth.Validation/OAuthValidationMiddleware.cs

Lines changed: 34 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -5,199 +5,58 @@
55
*/
66

77
using System;
8-
using System.IdentityModel.Tokens;
9-
using System.IO;
10-
using System.Linq;
11-
using System.Security.Claims;
8+
using Microsoft.AspNetCore.DataProtection;
9+
using Microsoft.Extensions.DependencyInjection;
1210
using Microsoft.Owin;
11+
using Microsoft.Owin.BuilderProperties;
1312
using Microsoft.Owin.Logging;
14-
using Microsoft.Owin.Security;
15-
using Microsoft.Owin.Security.DataHandler;
16-
using Microsoft.Owin.Security.DataHandler.Encoder;
17-
using Microsoft.Owin.Security.DataHandler.Serializer;
18-
using Microsoft.Owin.Security.DataProtection;
1913
using Microsoft.Owin.Security.Infrastructure;
14+
using Microsoft.Owin.Security.Interop;
2015

2116
namespace Owin.Security.OAuth.Validation {
2217
public class OAuthValidationMiddleware : AuthenticationMiddleware<OAuthValidationOptions> {
2318
public OAuthValidationMiddleware(OwinMiddleware next, IAppBuilder app, OAuthValidationOptions options)
2419
: base(next, options) {
25-
if (options.TicketFormat == null) {
26-
// Note: the purposes of the default ticket
27-
// format must match the values used by ASOS.
28-
options.TicketFormat = new EnhancedTicketDataFormat(
29-
app.CreateDataProtector(
30-
"Owin.Security.OpenIdConnect.Server.OpenIdConnectServerMiddleware",
31-
"ASOS", "Access_Token", "v1"));
32-
}
33-
34-
if (options.Logger == null) {
35-
options.Logger = app.CreateLogger<OAuthValidationMiddleware>();
36-
}
37-
}
38-
39-
protected override AuthenticationHandler<OAuthValidationOptions> CreateHandler() {
40-
return new OAuthValidationHandler();
41-
}
42-
43-
internal sealed class EnhancedTicketDataFormat : SecureDataFormat<AuthenticationTicket> {
44-
private static readonly EnhancedTicketSerializer Serializer = new EnhancedTicketSerializer();
45-
46-
public EnhancedTicketDataFormat(IDataProtector protector)
47-
: base(Serializer, protector, TextEncodings.Base64Url) {
48-
}
49-
50-
private sealed class EnhancedTicketSerializer : IDataSerializer<AuthenticationTicket> {
51-
private const int FormatVersion = 3;
52-
53-
public byte[] Serialize(AuthenticationTicket model) {
54-
if (model == null) {
55-
throw new ArgumentNullException("model");
56-
}
57-
58-
using (var buffer = new MemoryStream())
59-
using (var writer = new BinaryWriter(buffer)) {
60-
writer.Write(FormatVersion);
61-
62-
WriteIdentity(writer, model.Identity);
63-
PropertiesSerializer.Write(writer, model.Properties);
64-
65-
return buffer.ToArray();
66-
}
67-
}
68-
69-
public AuthenticationTicket Deserialize(byte[] data) {
70-
if (data == null) {
71-
throw new ArgumentNullException("data");
72-
}
73-
74-
using (var buffer = new MemoryStream(data))
75-
using (var reader = new BinaryReader(buffer)) {
76-
if (reader.ReadInt32() != FormatVersion) {
77-
return null;
78-
}
79-
80-
var identity = ReadIdentity(reader);
81-
var properties = PropertiesSerializer.Read(reader);
82-
83-
return new AuthenticationTicket(identity, properties);
84-
}
85-
}
86-
87-
private static void WriteIdentity(BinaryWriter writer, ClaimsIdentity identity) {
88-
writer.Write(identity.AuthenticationType);
89-
WriteWithDefault(writer, identity.NameClaimType, DefaultValues.NameClaimType);
90-
WriteWithDefault(writer, identity.RoleClaimType, DefaultValues.RoleClaimType);
91-
writer.Write(identity.Claims.Count());
92-
93-
foreach (var claim in identity.Claims) {
94-
WriteClaim(writer, claim, identity.NameClaimType);
95-
}
96-
97-
var context = identity.BootstrapContext as BootstrapContext;
98-
if (context == null || string.IsNullOrEmpty(context.Token)) {
99-
writer.Write(0);
100-
}
20+
if (options.DataProtectionProvider == null) {
21+
// Create a new DI container and register
22+
// the data protection services.
23+
var services = new ServiceCollection();
10124

102-
else {
103-
writer.Write(context.Token.Length);
104-
writer.Write(context.Token);
105-
}
106-
107-
if (identity.Actor != null) {
108-
writer.Write(true);
109-
WriteIdentity(writer, identity.Actor);
110-
}
111-
112-
else {
113-
writer.Write(false);
114-
}
115-
}
116-
117-
private static ClaimsIdentity ReadIdentity(BinaryReader reader) {
118-
var authenticationType = reader.ReadString();
119-
var nameClaimType = ReadWithDefault(reader, DefaultValues.NameClaimType);
120-
var roleClaimType = ReadWithDefault(reader, DefaultValues.RoleClaimType);
121-
var count = reader.ReadInt32();
122-
123-
var claims = new Claim[count];
124-
125-
for (int index = 0; index != count; ++index) {
126-
claims[index] = ReadClaim(reader, nameClaimType);
127-
}
128-
129-
var identity = new ClaimsIdentity(claims, authenticationType, nameClaimType, roleClaimType);
130-
131-
int bootstrapContextSize = reader.ReadInt32();
132-
if (bootstrapContextSize > 0) {
133-
identity.BootstrapContext = new BootstrapContext(reader.ReadString());
134-
}
135-
136-
if (reader.ReadBoolean()) {
137-
identity.Actor = ReadIdentity(reader);
138-
}
139-
140-
return identity;
141-
}
142-
143-
private static void WriteClaim(BinaryWriter writer, Claim claim, string nameClaimType) {
144-
WriteWithDefault(writer, claim.Type, nameClaimType);
145-
writer.Write(claim.Value);
146-
WriteWithDefault(writer, claim.ValueType, DefaultValues.StringValueType);
147-
WriteWithDefault(writer, claim.Issuer, DefaultValues.LocalAuthority);
148-
WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer);
149-
writer.Write(claim.Properties.Count);
25+
services.AddDataProtection(configuration => {
26+
// Try to use the application name provided by
27+
// the OWIN host as the application discriminator.
28+
var discriminator = new AppProperties(app.Properties).AppName;
15029

151-
foreach (var property in claim.Properties) {
152-
writer.Write(property.Key);
153-
writer.Write(property.Value);
30+
// When an application discriminator cannot be resolved from
31+
// the OWIN host properties, generate a temporary identifier.
32+
if (string.IsNullOrEmpty(discriminator)) {
33+
discriminator = Guid.NewGuid().ToString();
15434
}
155-
}
15635

157-
private static Claim ReadClaim(BinaryReader reader, string nameClaimType) {
158-
var type = ReadWithDefault(reader, nameClaimType);
159-
var value = reader.ReadString();
160-
var valueType = ReadWithDefault(reader, DefaultValues.StringValueType);
161-
var issuer = ReadWithDefault(reader, DefaultValues.LocalAuthority);
162-
var originalIssuer = ReadWithDefault(reader, issuer);
163-
var count = reader.ReadInt32();
36+
configuration.ApplicationDiscriminator = discriminator;
37+
});
16438

165-
var claim = new Claim(type, value, valueType, issuer, originalIssuer);
39+
var container = services.BuildServiceProvider();
16640

167-
for (var index = 0; index != count; ++index) {
168-
claim.Properties.Add(key: reader.ReadString(), value: reader.ReadString());
169-
}
170-
171-
return claim;
172-
}
173-
174-
private static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) {
175-
if (string.Equals(value, defaultValue, StringComparison.Ordinal)) {
176-
writer.Write(DefaultValues.DefaultStringPlaceholder);
177-
}
178-
179-
else {
180-
writer.Write(value);
181-
}
182-
}
41+
// Resolve a data protection provider from the services container.
42+
options.DataProtectionProvider = container.GetRequiredService<IDataProtectionProvider>();
43+
}
18344

184-
private static string ReadWithDefault(BinaryReader reader, string defaultValue) {
185-
string value = reader.ReadString();
186-
if (string.Equals(value, DefaultValues.DefaultStringPlaceholder, StringComparison.Ordinal)) {
187-
return defaultValue;
188-
}
45+
if (options.AccessTokenFormat == null) {
46+
// Note: the following purposes must match the ones used by ASOS.
47+
var protector = options.DataProtectionProvider.CreateProtector(
48+
"OpenIdConnectServerMiddleware", "ASOS", "Access_Token", "v1");
18949

190-
return value;
191-
}
50+
options.AccessTokenFormat = new AspNetTicketDataFormat(new DataProtectorShim(protector));
51+
}
19252

193-
private static class DefaultValues {
194-
public const string DefaultStringPlaceholder = "\0";
195-
public const string NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name";
196-
public const string RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role";
197-
public const string LocalAuthority = "LOCAL AUTHORITY";
198-
public const string StringValueType = "http://www.w3.org/2001/XMLSchema#string";
199-
}
53+
if (options.Logger == null) {
54+
options.Logger = app.CreateLogger<OAuthValidationMiddleware>();
20055
}
20156
}
57+
58+
protected override AuthenticationHandler<OAuthValidationOptions> CreateHandler() {
59+
return new OAuthValidationHandler();
60+
}
20261
}
20362
}

src/Owin.Security.OAuth.Validation/OAuthValidationOptions.cs

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

77
using System.Collections.Generic;
8+
using Microsoft.AspNetCore.DataProtection;
89
using Microsoft.Owin.Infrastructure;
910
using Microsoft.Owin.Logging;
1011
using Microsoft.Owin.Security;
@@ -35,8 +36,14 @@ public OAuthValidationOptions()
3536

3637
/// <summary>
3738
/// Gets or sets the data format used to unprotect the
38-
/// authenticated tickets received by the validation middleware.
39+
/// access tokens received by the validation middleware.
3940
/// </summary>
40-
public ISecureDataFormat<AuthenticationTicket> TicketFormat { get; set; }
41+
public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; set; }
42+
43+
/// <summary>
44+
/// Gets or sets the data protection provider used to create the default
45+
/// data protector used by <see cref="OAuthValidationMiddleware"/>.
46+
/// </summary>
47+
public IDataProtectionProvider DataProtectionProvider { get; set; }
4148
}
4249
}

src/Owin.Security.OAuth.Validation/project.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@
2929

3030
"dependencies": {
3131
"JetBrains.Annotations": { "type": "build", "version": "10.1.2-eap" },
32-
"Microsoft.Owin.Security": "3.0.1"
32+
"Microsoft.Owin.Security.Interop": "1.0.0-*"
3333
},
3434

3535
"frameworks": {
3636
"net451": {
3737
"frameworkAssemblies": {
38-
"System.IdentityModel": "4.0.0.0"
38+
"System.ComponentModel": { "type": "build" },
39+
"System.IdentityModel": "4.0.0.0",
40+
"System.Runtime": { "type": "build" }
3941
}
4042
}
4143
}

test/AspNet.Security.OAuth.Validation.Tests/OAuthValidationMiddlewareTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ private static TestServer CreateResourceServer(Action<OAuthValidationOptions> co
243243
app.UseOAuthValidation(options => {
244244
options.AutomaticAuthenticate = true;
245245
options.AutomaticChallenge = true;
246-
options.TicketFormat = format.Object;
246+
options.AccessTokenFormat = format.Object;
247247

248248
// Run the configuration delegate
249249
// registered by the unit tests.

test/Owin.Security.OAuth.Validation.Tests/OAuthValidationMiddlewareTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ private static TestServer CreateResourceServer(Action<OAuthValidationOptions> co
223223
return TestServer.Create(app => {
224224
app.UseOAuthValidation(options => {
225225
options.AuthenticationMode = AuthenticationMode.Active;
226-
options.TicketFormat = format.Object;
226+
options.AccessTokenFormat = format.Object;
227227

228228
// Run the configuration delegate
229229
// registered by the unit tests.

0 commit comments

Comments
 (0)