Skip to content

Commit 7213e94

Browse files
committed
Merge branch 'v14/dev' into release/14.0
2 parents 396ee78 + 4e0df98 commit 7213e94

File tree

380 files changed

+2204
-13086
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

380 files changed

+2204
-13086
lines changed

.github/BUILD.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ If the answer is yes, please read on. Otherwise, make sure to head on over [to t
1313

1414
↖️ You can jump to any section by using the "table of contents" button ( ![Table of contents icon](img/tableofcontentsicon.svg) ) above.
1515

16+
## Getting Started:
17+
To run umbraco, we first need to initialize the client git submodule:
18+
* Execute `git submodule init` and then `git submodule update` to get the files into Umbraco.Web.UI.Client project
19+
* If you are going to work on the Backoffice, you can either go to the Umbraco.Web.UI.Client folder and check out a new branch or set it up in your IDE, which will allow you to commit to each repository simultaneously:
20+
* **Rider**: Preferences -> Version Control -> Directory Mappings -> Click the '+' sign
21+
* If you get a white page delete Umbraco.Cms.StaticAssets\wwwroot\umbraco folder and run `npm ci && npm run build:for:cms` inside Umbraco.Web.UI.Client folder to clear out any leftover files from older versions.
22+
23+
### Latest version
24+
* If you want to get the latest changes from the client repository, run `git submodule update` again which will pull the latest main branch.
25+
1626

1727
## Debugging source locally
1828

.github/New BackOffice - README.md

Lines changed: 0 additions & 38 deletions
This file was deleted.

.gitmodules

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
[submodule "src/Umbraco.Web.UI.New.Client"]
2-
path = src/Umbraco.Web.UI.New.Client
1+
[submodule "src/Umbraco.Web.UI.Client"]
2+
path = src/Umbraco.Web.UI.Client
33
url = https://github.com/umbraco/Umbraco.CMS.Backoffice.git

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
7373
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
7474
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
75-
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.2" />
75+
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.3" />
7676
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.0" />
7777
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
7878
</ItemGroup>

build/azure-pipelines.yml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,17 @@ stages:
9393
workingDirectory: src/Umbraco.Web.UI.Login
9494
- script: npm ci --no-fund --no-audit --prefer-offline
9595
displayName: Run npm ci (Bellissima)
96-
workingDirectory: src/Umbraco.Web.UI.New.Client
96+
workingDirectory: src/Umbraco.Web.UI.Client
9797
- script: npm run generate:api-local
9898
displayName: Generate API models (Bellissima)
99-
workingDirectory: src/Umbraco.Web.UI.New.Client
99+
workingDirectory: src/Umbraco.Web.UI.Client
100100
enabled: false
101101
- script: npm run build:for:cms
102102
displayName: Run build (Bellissima)
103-
workingDirectory: src/Umbraco.Web.UI.New.Client
103+
workingDirectory: src/Umbraco.Web.UI.Client
104104
- script: npm run build
105105
displayName: Run Login Build (Bellissima)
106-
workingDirectory: src/Umbraco.Web.UI.New.Client/apps/auth
106+
workingDirectory: src/Umbraco.Web.UI.Client/apps/auth
107107
- task: UseDotNet@2
108108
displayName: Use .NET SDK from global.json
109109
inputs:
@@ -142,7 +142,7 @@ stages:
142142
displayName: Prepare Bellissima npm package
143143
env:
144144
PACKAGE_VERSION: $(build.NBGV_NpmPackageVersion)
145-
workingDirectory: src/Umbraco.Web.UI.New.Client
145+
workingDirectory: src/Umbraco.Web.UI.Client
146146
- task: PublishPipelineArtifact@1
147147
displayName: Publish Bellissima npm artifact
148148
inputs:
@@ -225,26 +225,26 @@ stages:
225225
- task: Cache@2
226226
displayName: Cache node_modules
227227
inputs:
228-
key: '"npm_client" | "$(Agent.OS)"| $(Build.SourcesDirectory)/src/Umbraco.Web.UI.New.Client/package-lock.json'
228+
key: '"npm_client" | "$(Agent.OS)"| $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/package-lock.json'
229229
restoreKeys: |
230230
"npm_client" | "$(Agent.OS)"
231231
"npm_client"
232232
path: $(npm_config_cache)
233233
- script: npm ci --no-fund --no-audit --prefer-offline
234-
workingDirectory: src/Umbraco.Web.UI.New.Client
234+
workingDirectory: src/Umbraco.Web.UI.Client
235235
displayName: Run npm ci
236236
- script: npm run storybook:build
237237
displayName: Build Storybook
238238
env:
239239
VITE_BASE_PATH: $(BASE_PATH)/
240-
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.New.Client
240+
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client
241241
- script: sed -i "s|/umbraco/backoffice|$(BASE_PATH)/umbraco/backoffice|" assets/*.js
242242
displayName: Replace BASE_PATH on assets
243-
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.New.Client/storybook-static
243+
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/storybook-static
244244
- task: ArchiveFiles@2
245245
displayName: Archive js Docs
246246
inputs:
247-
rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.New.Client/storybook-static
247+
rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/storybook-static
248248
includeRootFolder: false
249249
archiveFile: $(Build.ArtifactStagingDirectory)/ui-docs.zip
250250
- task: PublishPipelineArtifact@1
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
using System.Security.Claims;
2+
using Microsoft.AspNetCore.Authentication;
3+
using Microsoft.AspNetCore.Authentication.Cookies;
4+
using Microsoft.AspNetCore.DataProtection;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Options;
8+
using Umbraco.Cms.Api.Management.Security;
9+
using Umbraco.Cms.Core;
10+
using Umbraco.Cms.Core.Configuration.Models;
11+
using Umbraco.Cms.Core.Net;
12+
using Umbraco.Cms.Core.Services;
13+
using Umbraco.Cms.Web.Common.Security;
14+
using Umbraco.Extensions;
15+
16+
namespace Umbraco.Cms.Api.Management.Configuration;
17+
18+
/// <summary>
19+
/// Used to configure <see cref="CookieAuthenticationOptions" /> for the back office authentication type
20+
/// </summary>
21+
public class ConfigureBackOfficeCookieOptions : IConfigureNamedOptions<CookieAuthenticationOptions>
22+
{
23+
private readonly IDataProtectionProvider _dataProtection;
24+
private readonly GlobalSettings _globalSettings;
25+
private readonly IIpResolver _ipResolver;
26+
private readonly IRuntimeState _runtimeState;
27+
private readonly SecuritySettings _securitySettings;
28+
private readonly IUserService _userService;
29+
private readonly TimeProvider _timeProvider;
30+
31+
/// <summary>
32+
/// Initializes a new instance of the <see cref="ConfigureBackOfficeCookieOptions" /> class.
33+
/// </summary>
34+
/// <param name="securitySettings">The <see cref="SecuritySettings" /> options</param>
35+
/// <param name="globalSettings">The <see cref="GlobalSettings" /> options</param>
36+
/// <param name="runtimeState">The <see cref="IRuntimeState" /></param>
37+
/// <param name="dataProtection">The <see cref="IDataProtectionProvider" /></param>
38+
/// <param name="userService">The <see cref="IUserService" /></param>
39+
/// <param name="ipResolver">The <see cref="IIpResolver" /></param>
40+
/// <param name="timeProvider">The <see cref="TimeProvider" /></param>
41+
public ConfigureBackOfficeCookieOptions(
42+
IOptions<SecuritySettings> securitySettings,
43+
IOptions<GlobalSettings> globalSettings,
44+
IRuntimeState runtimeState,
45+
IDataProtectionProvider dataProtection,
46+
IUserService userService,
47+
IIpResolver ipResolver,
48+
TimeProvider timeProvider)
49+
{
50+
_securitySettings = securitySettings.Value;
51+
_globalSettings = globalSettings.Value;
52+
_runtimeState = runtimeState;
53+
_dataProtection = dataProtection;
54+
_userService = userService;
55+
_ipResolver = ipResolver;
56+
_timeProvider = timeProvider;
57+
}
58+
59+
/// <inheritdoc />
60+
public void Configure(string? name, CookieAuthenticationOptions options)
61+
{
62+
if (name != Constants.Security.BackOfficeAuthenticationType)
63+
{
64+
return;
65+
}
66+
67+
Configure(options);
68+
}
69+
70+
/// <inheritdoc />
71+
public void Configure(CookieAuthenticationOptions options)
72+
{
73+
options.SlidingExpiration = false;
74+
options.ExpireTimeSpan = _globalSettings.TimeOut;
75+
options.Cookie.Domain = _securitySettings.AuthCookieDomain;
76+
options.Cookie.Name = _securitySettings.AuthCookieName;
77+
options.Cookie.HttpOnly = true;
78+
options.Cookie.SecurePolicy =
79+
_globalSettings.UseHttps ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
80+
options.Cookie.Path = "/";
81+
82+
// NOTE: matches route in BackOfficeLoginController
83+
const string backOfficeLoginPath = "/umbraco/login";
84+
options.LoginPath = backOfficeLoginPath;
85+
options.LogoutPath = backOfficeLoginPath;
86+
options.AccessDeniedPath = backOfficeLoginPath;
87+
88+
options.DataProtectionProvider = _dataProtection;
89+
90+
// NOTE: This is borrowed directly from aspnetcore source
91+
// Note: the purpose for the data protector must remain fixed for interop to work.
92+
IDataProtector dataProtector = options.DataProtectionProvider.CreateProtector(
93+
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
94+
Constants.Security.BackOfficeAuthenticationType,
95+
"v2");
96+
var ticketDataFormat = new TicketDataFormat(dataProtector);
97+
98+
options.TicketDataFormat = new BackOfficeSecureDataFormat(_globalSettings.TimeOut, ticketDataFormat);
99+
100+
options.Events = new CookieAuthenticationEvents
101+
{
102+
// IMPORTANT! If you set any of OnRedirectToLogin, OnRedirectToAccessDenied, OnRedirectToLogout, OnRedirectToReturnUrl
103+
// you need to be aware that this will bypass the default behavior of returning the correct status codes for ajax requests and
104+
// not redirecting for non-ajax requests. This is because the default behavior is baked into this class here:
105+
// https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs#L58
106+
// It would be possible to re-use the default behavior if any of these need to be set but that must be taken into account else
107+
// our back office requests will not function correctly. For now we don't need to set/configure any of these callbacks because
108+
// the defaults work fine with our setup.
109+
OnValidatePrincipal = async ctx =>
110+
{
111+
// We need to resolve the BackOfficeSecurityStampValidator per request as a requirement (even in aspnetcore they do this)
112+
BackOfficeSecurityStampValidator securityStampValidator =
113+
ctx.HttpContext.RequestServices.GetRequiredService<BackOfficeSecurityStampValidator>();
114+
115+
// Same goes for the signinmanager
116+
IBackOfficeSignInManager signInManager =
117+
ctx.HttpContext.RequestServices.GetRequiredService<IBackOfficeSignInManager>();
118+
119+
ClaimsIdentity? backOfficeIdentity = ctx.Principal?.GetUmbracoIdentity();
120+
if (backOfficeIdentity == null)
121+
{
122+
ctx.RejectPrincipal();
123+
await signInManager.SignOutAsync();
124+
}
125+
126+
// ensure the thread culture is set
127+
backOfficeIdentity?.EnsureCulture();
128+
129+
EnsureTicketRenewalIfKeepUserLoggedIn(ctx);
130+
131+
// add or update a claim to track when the cookie expires, we use this to track time remaining
132+
backOfficeIdentity?.AddOrUpdateClaim(new Claim(
133+
Constants.Security.TicketExpiresClaimType,
134+
ctx.Properties.ExpiresUtc!.Value.ToString("o"),
135+
ClaimValueTypes.DateTime,
136+
Constants.Security.BackOfficeAuthenticationType,
137+
Constants.Security.BackOfficeAuthenticationType,
138+
backOfficeIdentity));
139+
140+
await securityStampValidator.ValidateAsync(ctx);
141+
142+
// We have to manually specify Issued and Expires,
143+
// because the SecurityStampValidator refreshes the principal every 30 minutes,
144+
// When the principal is refreshed the Issued is update to time of refresh, however, the Expires remains unchanged
145+
// When we then try and renew, the difference of issued and expires effectively becomes the new ExpireTimeSpan
146+
// meaning we effectively lose 30 minutes of our ExpireTimeSpan for EVERY principal refresh if we don't
147+
// https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs#L115
148+
ctx.Properties.IssuedUtc = _timeProvider.GetUtcNow();
149+
ctx.Properties.ExpiresUtc = _timeProvider.GetUtcNow().Add(_globalSettings.TimeOut);
150+
ctx.ShouldRenew = true;
151+
},
152+
OnSigningIn = ctx =>
153+
{
154+
// occurs when sign in is successful but before the ticket is written to the outbound cookie
155+
ClaimsIdentity? backOfficeIdentity = ctx.Principal?.GetUmbracoIdentity();
156+
if (backOfficeIdentity != null)
157+
{
158+
// generate a session id and assign it
159+
// create a session token - if we are configured and not in an upgrade state then use the db, otherwise just generate one
160+
Guid session = _runtimeState.Level == RuntimeLevel.Run
161+
? _userService.CreateLoginSession(
162+
backOfficeIdentity.GetId()!.Value,
163+
_ipResolver.GetCurrentRequestIpAddress())
164+
: Guid.NewGuid();
165+
166+
// add our session claim
167+
backOfficeIdentity.AddClaim(new Claim(
168+
Constants.Security.SessionIdClaimType,
169+
session.ToString(),
170+
ClaimValueTypes.String,
171+
Constants.Security.BackOfficeAuthenticationType,
172+
Constants.Security.BackOfficeAuthenticationType,
173+
backOfficeIdentity));
174+
175+
// since it is a cookie-based authentication add that claim
176+
backOfficeIdentity.AddClaim(new Claim(
177+
ClaimTypes.CookiePath,
178+
"/",
179+
ClaimValueTypes.String,
180+
Constants.Security.BackOfficeAuthenticationType,
181+
Constants.Security.BackOfficeAuthenticationType,
182+
backOfficeIdentity));
183+
}
184+
185+
return Task.CompletedTask;
186+
},
187+
OnSignedIn = ctx =>
188+
{
189+
// occurs when sign in is successful and after the ticket is written to the outbound cookie
190+
191+
// When we are signed in with the cookie, assign the principal to the current HttpContext
192+
ctx.HttpContext.SetPrincipalForRequest(ctx.Principal);
193+
194+
return Task.CompletedTask;
195+
},
196+
OnSigningOut = ctx =>
197+
{
198+
// Clear the user's session on sign out
199+
if (ctx.HttpContext?.User?.Identity != null)
200+
{
201+
var claimsIdentity = ctx.HttpContext.User.Identity as ClaimsIdentity;
202+
var sessionId = claimsIdentity?.FindFirstValue(Constants.Security.SessionIdClaimType);
203+
if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out Guid guidSession))
204+
{
205+
_userService.ClearLoginSession(guidSession);
206+
}
207+
}
208+
209+
// Remove all of our cookies
210+
var cookies = new[]
211+
{
212+
_securitySettings.AuthCookieName,
213+
Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalCookieName,
214+
Constants.Web.AngularCookieName, Constants.Web.CsrfValidationCookieName
215+
};
216+
foreach (var cookie in cookies)
217+
{
218+
ctx.Options.CookieManager.DeleteCookie(ctx.HttpContext!, cookie, new CookieOptions { Path = "/" });
219+
}
220+
221+
return Task.CompletedTask;
222+
}
223+
};
224+
}
225+
226+
/// <summary>
227+
/// Ensures the ticket is renewed if the <see cref="SecuritySettings.KeepUserLoggedIn" /> is set to true
228+
/// and the current request is for the get user seconds endpoint
229+
/// </summary>
230+
/// <param name="context">The <see cref="CookieValidatePrincipalContext" /></param>
231+
private void EnsureTicketRenewalIfKeepUserLoggedIn(CookieValidatePrincipalContext context)
232+
{
233+
if (!_securitySettings.KeepUserLoggedIn)
234+
{
235+
return;
236+
}
237+
238+
DateTimeOffset currentUtc = _timeProvider.GetUtcNow();
239+
DateTimeOffset? issuedUtc = context.Properties.IssuedUtc;
240+
DateTimeOffset? expiresUtc = context.Properties.ExpiresUtc;
241+
242+
if (expiresUtc.HasValue && issuedUtc.HasValue)
243+
{
244+
TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value);
245+
TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc);
246+
247+
// if it's time to renew, then do it
248+
if (timeRemaining < timeElapsed)
249+
{
250+
context.ShouldRenew = true;
251+
}
252+
}
253+
}
254+
}

0 commit comments

Comments
 (0)