Skip to content

Commit 9d420f5

Browse files
authored
Merge pull request #15 from umbraco/bugfix/ensure-secret-encryptor-is-optional
Ensure that when optional secret key for token encryption is not provided token is stored without encryption.
2 parents fbaa205 + c2c74ab commit 9d420f5

File tree

11 files changed

+110
-31
lines changed

11 files changed

+110
-31
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ Not all values are required for all services. Those that are required are marke
9797

9898
Provides an optional key used to encrypt and decrypt tokens when they are saved and retrieved from storage respectively.
9999

100+
If not provided, the value stored in configuration at `Umbraco:CMS:Global:Id` will be used.
101+
100102
##### Services
101103

102104
The collection of services available for authorization and usage.
@@ -297,15 +299,15 @@ Responsible for creating a dictionary of parameters provided in the request to r
297299

298300
Responsible for encrypting and decrypting stored tokens (or other values).
299301

300-
It has two implementations:
302+
It has three implementations:
301303

302304
- `AesSecretEncryptor` - default implementation that is using a standard `AES` cryptographic algorithm for encrypting/decrypting values based on the provided `TokenEncryptionKey`.
305+
- `NoopSecretEncryptor` - provides no encryption saving the provided token as is.
303306
- `DataProtectionSecretEncryptor` - additional implementation that uses the `IDataProtectionProvider` interface for providing data protection services.
304307

305-
Switching the encryption engine to `DataProtectionSecretEncryptor` can be done in code, adding these two lines:
308+
Switching the encryption engine to for example `DataProtectionSecretEncryptor` can be done in code, adding these two lines:
306309

307310
```
308-
builder.Services.AddDataProtection();
309311
builder.Services.AddUnique<ISecretEncryptor, DataProtectionSecretEncrytor>();
310312
```
311313

src/Umbraco.AuthorizedServices/AuthorizedServicesComposer.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
using Microsoft.Extensions.Configuration;
22
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Options;
35
using Umbraco.AuthorizedServices.Configuration;
46
using Umbraco.AuthorizedServices.Manifests;
57
using Umbraco.AuthorizedServices.Migrations;
68
using Umbraco.AuthorizedServices.Services;
79
using Umbraco.AuthorizedServices.Services.Implement;
810
using Umbraco.Cms.Core.Composing;
11+
using Umbraco.Cms.Core.Configuration.Models;
912
using Umbraco.Cms.Core.DependencyInjection;
1013
using Umbraco.Cms.Core.Notifications;
1114
using Umbraco.Extensions;
@@ -16,7 +19,8 @@ internal class AuthorizedServicesComposer : IComposer
1619
{
1720
public void Compose(IUmbracoBuilder builder)
1821
{
19-
IConfigurationSection configSection = builder.Config.GetSection("Umbraco:AuthorizedServices");
22+
const string ConfigurationRoot = "Umbraco:AuthorizedServices";
23+
IConfigurationSection configSection = builder.Config.GetSection(ConfigurationRoot);
2024
builder.Services.Configure<AuthorizedServiceSettings>(configSection);
2125

2226
// manifest filter
@@ -38,9 +42,24 @@ public void Compose(IUmbracoBuilder builder)
3842

3943
builder.Services.AddUnique<ISecretEncryptor>(factory =>
4044
{
45+
ILogger<AuthorizedServicesComposer> logger = factory.GetRequiredService<ILogger<AuthorizedServicesComposer>>();
4146
var tokenEncryptionKey = configSection.GetValue<string>(nameof(AuthorizedServiceSettings.TokenEncryptionKey));
4247

43-
return new AesSecretEncryptor(tokenEncryptionKey ?? string.Empty);
48+
if (string.IsNullOrWhiteSpace(tokenEncryptionKey))
49+
{
50+
logger.LogWarning($"No encryption key was found in configuration at {ConfigurationRoot}:{nameof(AuthorizedServiceSettings.TokenEncryptionKey)}. Falling back to using the Umbraco:CMS:{nameof(GlobalSettings)}:{nameof(GlobalSettings.Id)} value.");
51+
52+
IOptions<GlobalSettings> globalSettings = factory.GetRequiredService<IOptions<GlobalSettings>>();
53+
tokenEncryptionKey = globalSettings.Value.Id;
54+
}
55+
56+
if (string.IsNullOrWhiteSpace(tokenEncryptionKey))
57+
{
58+
logger.LogWarning($"Could not fallback back to using the Umbraco:CMS:{nameof(GlobalSettings)}:{nameof(GlobalSettings.Id)} value as it has not been completed. Access tokens will not be encrypted when stored in the local database.");
59+
return new NoopSecretEncryptor();
60+
}
61+
62+
return new AesSecretEncryptor(tokenEncryptionKey);
4463
});
4564

4665
builder.Services.AddUnique<ITokenFactory, TokenFactory>();

src/Umbraco.AuthorizedServices/ClientApp/src/backoffice/AuthorizedServices/edit.controller.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ function AuthorizedServiceEditController(this: any, $routeParams, $location, aut
3434
authorizedServiceResource.sendSampleRequest(serviceAlias, vm.sampleRequest)
3535
.then(function (response) {
3636
vm.sampleRequestResponse = "Request: " + vm.sampleRequest + "\r\nResponse: " + JSON.stringify(response.data, null, 2);
37+
})
38+
.catch(function (e) {
39+
notificationsService.error("Authorized Services", "The sample request did not complete: " + e.data.ExceptionMessage);
3740
});
3841
};
3942

src/Umbraco.AuthorizedServices/Services/Implement/AesSecretEncryptor.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,17 @@ public string Decrypt(string value)
6262
throw new ArgumentException("The value to be decrypted cannot be empty.", nameof(value));
6363
}
6464

65-
var combined = Convert.FromBase64String(value);
65+
byte[] combined;
66+
try
67+
{
68+
combined = Convert.FromBase64String(value);
69+
}
70+
catch (FormatException)
71+
{
72+
// Can't convert from Base64 string. Probably means we've previously stored the token unencrypted, so we should return that.
73+
return value;
74+
}
75+
6676
var buffer = new byte[combined.Length];
6777
var hash = SHA512.Create();
6878
var aesKey = new byte[24];

src/Umbraco.AuthorizedServices/Services/Implement/DataProtectionSecretEncrytor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
namespace Umbraco.AuthorizedServices.Services.Implement;
44

5-
internal sealed class DataProtectionSecretEncrytor : ISecretEncryptor
5+
internal sealed class DataProtectionSecretEncryptor : ISecretEncryptor
66
{
77
private readonly IDataProtector _protector;
88

99
private const string Purpose = "UmbracoAuthorizedServiceTokens";
1010

11-
public DataProtectionSecretEncrytor(IDataProtectionProvider dataProtectionProvider)
11+
public DataProtectionSecretEncryptor(IDataProtectionProvider dataProtectionProvider)
1212
{
1313
_protector = dataProtectionProvider.CreateProtector(Purpose);
1414
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Umbraco.AuthorizedServices.Services.Implement;
2+
3+
internal sealed class NoopSecretEncryptor : ISecretEncryptor
4+
{
5+
public string Encrypt(string value) => value;
6+
7+
public string Decrypt(string value) => value;
8+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Umbraco.AuthorizedServices.Services;
2+
using Umbraco.AuthorizedServices.Services.Implement;
3+
4+
namespace Umbraco.AuthorizedServices.Tests.Services;
5+
6+
internal class AesSecretEncryptorTests : EncryptorTestsBase
7+
{
8+
protected override ISecretEncryptor CreateEncrytor()
9+
{
10+
const string SecretKey = "secret";
11+
return new AesSecretEncryptor(SecretKey);
12+
}
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.AspNetCore.DataProtection;
2+
using Umbraco.AuthorizedServices.Services;
3+
using Umbraco.AuthorizedServices.Services.Implement;
4+
5+
namespace Umbraco.AuthorizedServices.Tests.Services;
6+
7+
internal class DataProtectionSecretEncrytorTests : EncryptorTestsBase
8+
{
9+
protected override ISecretEncryptor CreateEncrytor()
10+
{
11+
var dataProtectionProvider = new EphemeralDataProtectionProvider();
12+
return new DataProtectionSecretEncryptor(dataProtectionProvider);
13+
}
14+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Umbraco.AuthorizedServices.Services;
2+
3+
namespace Umbraco.AuthorizedServices.Tests.Services;
4+
5+
internal abstract class EncryptorTestsBase
6+
{
7+
protected const string Message = "When seagulls follow the trawler, it is because they think sardines will be thrown into the sea.";
8+
9+
[Test]
10+
public void Encrypt_And_Decrypt()
11+
{
12+
// Arrange
13+
ISecretEncryptor sut = CreateEncrytor();
14+
15+
// Act
16+
var encryptedMessage = sut.Encrypt(Message);
17+
var decryptedMessage = sut.Decrypt(encryptedMessage);
18+
19+
// Assert
20+
decryptedMessage.Should().Be(Message);
21+
}
22+
23+
protected abstract ISecretEncryptor CreateEncrytor();
24+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using Umbraco.AuthorizedServices.Services;
2+
using Umbraco.AuthorizedServices.Services.Implement;
3+
4+
namespace Umbraco.AuthorizedServices.Tests.Services;
5+
6+
internal class NoopSecretEncrytorTests : EncryptorTestsBase
7+
{
8+
protected override ISecretEncryptor CreateEncrytor() => new NoopSecretEncryptor();
9+
}

0 commit comments

Comments
 (0)