Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
70833fb
OIDC resource server: configuration and a protected resource registry…
KrzysztofPajak Oct 8, 2026
0f66cdd
OIDC resource server: canonical resource URIs, audience, authorized p…
KrzysztofPajak Oct 8, 2026
cfd1888
OIDC resource server: RFC 6750 challenges and RFC 9728 protected reso…
KrzysztofPajak Oct 8, 2026
e4fe29d
OIDC resource server: Oidc JwtBearer scheme, startup validation and m…
KrzysztofPajak Oct 8, 2026
ef88aee
OIDC resource server: pipeline tests with an in-memory signing key
KrzysztofPajak Oct 8, 2026
7b1cc26
OIDC resource server: store URL as audience base, fail-closed account…
KrzysztofPajak Oct 8, 2026
ce114a8
OIDC resource server: map the token to a customer by subject link, ve…
KrzysztofPajak Oct 8, 2026
12e5be9
OIDC resource server: admin panel, vendor and store manager condition…
KrzysztofPajak Oct 8, 2026
6768e2b
OIDC resource server: always write the ProblemDetails body of challen…
KrzysztofPajak Oct 9, 2026
f1ed29a
OIDC resource server: the token's customer becomes the customer of th…
KrzysztofPajak Oct 11, 2026
0257877
External authentication: unique index on provider and external identi…
KrzysztofPajak Oct 8, 2026
11eb95f
External authentication: identity link index unique only on MongoDB
KrzysztofPajak Oct 8, 2026
424a487
OIDC resource server: refuse system, non-registered, other-store and …
KrzysztofPajak Oct 8, 2026
98ed651
External authentication: installer creates the identity link index as…
KrzysztofPajak Oct 8, 2026
6752457
OIDC resource server: link name of the login plugin, automatic regist…
KrzysztofPajak Oct 10, 2026
677e160
Web API: accept access tokens of the OpenID Connect provider next to …
KrzysztofPajak Oct 8, 2026
e0ecb69
Web API: never fail on a malformed bearer token while routing to the …
KrzysztofPajak Oct 8, 2026
90ad302
API users with SecretKey tokens marked obsolete in favour of the OIDC…
KrzysztofPajak Oct 8, 2026
e05657f
Web API: BackendAPI switch applies to provider tokens; route asymmetr…
KrzysztofPajak Oct 8, 2026
6d8dc2e
Web API: treat a bearer value that is not a JWT as no token
KrzysztofPajak Oct 9, 2026
db6434f
OIDC resource server: resolve the audience store from the request hos…
KrzysztofPajak Oct 11, 2026
f51fb29
OIDC resource server: RFC 9728 metadata URL with the well-known segme…
KrzysztofPajak Oct 11, 2026
c611acd
OIDC resource server: 404 and a warning instead of a metadata documen…
KrzysztofPajak Oct 11, 2026
7f1a4a8
Skip store and work context setup on the protected resource metadata …
KrzysztofPajak Oct 11, 2026
30741b7
OIDC resource server: answer 503 instead of invalid_token when the au…
KrzysztofPajak Oct 11, 2026
1101972
OIDC resource server: treat a negative ClockSkewSeconds as zero inste…
KrzysztofPajak Oct 11, 2026
c0d3952
OIDC resource server: build the registry's read-only resource list once
KrzysztofPajak Oct 11, 2026
ea2c20e
OIDC resource server: leave enablement of the metadata endpoints to t…
KrzysztofPajak Oct 11, 2026
5af62c3
OIDC resource server tests: dispose request messages and RSA instances
KrzysztofPajak Oct 11, 2026
e084407
OIDC resource server: filter explicitly in the header escaping and re…
KrzysztofPajak Oct 11, 2026
fcd4cba
Merge feature/oidc-resource-server into feature/oidc-customer-mapping…
KrzysztofPajak Oct 11, 2026
c607833
Merge feature/oidc-customer-mapping into feature/oidc-module-api: rev…
KrzysztofPajak Oct 11, 2026
be60cf3
OIDC resource server: build the work context in the store the access …
KrzysztofPajak Oct 11, 2026
964f800
Merge develop into feature/oidc-customer-mapping: #891 squash-merged
KrzysztofPajak Oct 11, 2026
ebb689f
Merge feature/oidc-customer-mapping into feature/oidc-module-api: wor…
KrzysztofPajak Oct 11, 2026
db68499
OIDC Web API tests: a token forwarded from the Bearer scheme pins the…
KrzysztofPajak Oct 11, 2026
3495de8
OIDC: match the request store by host and port before host alone
KrzysztofPajak Oct 11, 2026
50822d7
OIDC: no root protected resource metadata for a store under a path
KrzysztofPajak Oct 11, 2026
f204ad8
OIDC: log an unknown metadata host at debug, a store without URL as w…
KrzysztofPajak Oct 11, 2026
282522d
Context middleware: skip the protected resource metadata route exactly
KrzysztofPajak Oct 11, 2026
2eb1441
OIDC: Retry-After on the 503 for an unreachable identity provider
KrzysztofPajak Oct 11, 2026
f42b5f2
OIDC tests: recovery after an identity provider outage
KrzysztofPajak Oct 11, 2026
f9c0804
OIDC tests: dispose responses and readers
KrzysztofPajak Oct 11, 2026
6333561
Merge feature/oidc-customer-mapping into feature/oidc-module-api: rev…
KrzysztofPajak Oct 11, 2026
ef527ce
OIDC tests: dispose responses in the forwarding pipeline tests
KrzysztofPajak Oct 11, 2026
537a986
External authentication: store of the linked customer
KrzysztofPajak Oct 11, 2026
9b22008
OIDC: one identity link per store when customers are registered per s…
KrzysztofPajak Oct 11, 2026
6a381c3
External authentication index: provider, identifier and store
KrzysztofPajak Oct 11, 2026
9b08fed
Merge feature/oidc-customer-mapping into feature/oidc-module-api: per…
KrzysztofPajak Oct 11, 2026
28609f5
OIDC: a link without a store to another store's customer does not blo…
KrzysztofPajak Oct 11, 2026
f76de76
Merge feature/oidc-customer-mapping into feature/oidc-module-api: per…
KrzysztofPajak Oct 11, 2026
a511da8
External authentication: find a linked identity by its exact identifier
KrzysztofPajak Oct 11, 2026
ac3cb12
Merge feature/oidc-customer-mapping into feature/oidc-module-api: exa…
KrzysztofPajak Oct 11, 2026
41bb378
Enforce one OIDC identity per customer with a customer-derived link id
KrzysztofPajak Oct 11, 2026
10f54e2
Merge feature/oidc-customer-mapping into feature/oidc-module-api: one…
KrzysztofPajak Oct 11, 2026
dc0bcb2
Merge develop into feature/oidc-module-api
KrzysztofPajak Oct 11, 2026
d84f81b
API users: trust only tickets the Bearer scheme issued itself, not re…
KrzysztofPajak Oct 11, 2026
3d544c3
OIDC: forward only the Web API's Bearer scheme to Oidc, not every Jwt…
KrzysztofPajak Oct 11, 2026
6d1b565
OIDC scheme selector: drop the Enabled check it is never registered w…
KrzysztofPajak Oct 11, 2026
f768c12
OIDC scheme selector: route on the signing algorithm alone, without t…
KrzysztofPajak Oct 11, 2026
6ea8c1e
OIDC scheme selector: route by the Oidc scheme's own algorithm list i…
KrzysztofPajak Oct 11, 2026
09824fe
OIDC scheme selector: parse the bearer token once and keep the routin…
KrzysztofPajak Oct 11, 2026
789718c
OIDC request customer resolver: follow the scheme selector's decision…
KrzysztofPajak Oct 11, 2026
4c70e34
OIDC: withdraw the Web API's protected resource while BackendAPI is d…
KrzysztofPajak Oct 11, 2026
abfa8ae
Web API: apply the BackendAPI kill switch before the ignore flag of A…
KrzysztofPajak Oct 11, 2026
13fb897
appsettings: deny vendors and store managers on the Web API's protect…
KrzysztofPajak Oct 11, 2026
535e85c
Web API tests: join the temp settings path instead of combining it
KrzysztofPajak Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ public virtual async Task<Customer> GetAuthenticatedCustomer()
if (!authenticateResult.Succeeded)
return null;

//only a ticket of the Bearer scheme itself carries claims GrandNode signed with its SecretKey; a result the
//scheme forwarded (a provider token handed to the Oidc scheme) names its customer through the OIDC link
if (authenticateResult.Ticket?.AuthenticationScheme != JwtBearerDefaults.AuthenticationScheme)
return null;

//prefer the stable customer id (unambiguous with per-store identity), fall back to e-mail for old tokens
var customerIdClaim = authenticateResult.Principal.Claims.FirstOrDefault(claim => claim.Type == "CustomerId");
if (customerIdClaim != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using Grand.Business.Core.Interfaces.Customers;
using Microsoft.AspNetCore.Authentication.JwtBearer;

#pragma warning disable CS0618 //API users stay supported until removed

namespace Grand.Business.Authentication.Services;

public class JwtBearerAuthenticationService : IJwtBearerAuthenticationService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ private async Task<OidcCustomerResult> Register(string sub, string email, string
LastActivityDateUtc = DateTime.UtcNow
});

//the provider verified the e-mail and access comes from permissions an admin grants, so the store's e-mail
//access comes from permissions an admin grants (and the provider's e-mail check is configurable), so the store's e-mail
//validation / admin approval does not apply; the account gets the Registered group only
await _customerManagerService.RegisterCustomer(new RegistrationRequest(customer, normalizedEmail,
normalizedEmail, CommonHelper.GenerateRandomDigitCode(20), PasswordFormat.Hashed, storeId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

#pragma warning disable CS0618 //API users stay supported until removed

namespace Grand.Business.Authentication.Startup;

public class StartupApplication : IStartupApplication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Grand.Business.Core.Interfaces.Authentication;

[Obsolete("API users with a shared SecretKey are deprecated - configure OidcResourceServer and use access tokens of your identity provider")]
public interface IJwtBearerAuthenticationService
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Grand.Business.Core.Interfaces.Authentication;

/// <summary>
/// Lets a module that can be switched off withdraw the paths it serves from OIDC protection while it is off. A
/// protected resource on, under or over one of these paths is left out of the registry: a token there is never
/// validated by the Oidc scheme, mapped to a customer or used to pin the store, and the resource is not published.
/// Read once, when the registry is built, so the switch must be a startup setting. Register the implementation
/// as a singleton.
/// </summary>
public interface IOidcDisabledPathProvider
{
IEnumerable<string> GetDisabledPaths();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Grand.Business.Core.Interfaces.Customers;

[Obsolete("API users with a shared SecretKey are deprecated - configure OidcResourceServer and use access tokens of your identity provider")]
public interface IUserApiService
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using Grand.Infrastructure.Extensions;
using Grand.Mediator;

#pragma warning disable CS0618 //API users stay supported until removed

namespace Grand.Business.Customers.Services;

public class UserApiService : IUserApiService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

#pragma warning disable CS0618 //API users stay supported until removed

namespace Grand.Business.Customers.Startup;

public class StartupApplication : IStartupApplication
Expand Down
1 change: 1 addition & 0 deletions src/Core/Grand.Domain/Customers/UserApi.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Grand.Domain.Customers;

[Obsolete("API users with a shared SecretKey are deprecated - configure OidcResourceServer and use access tokens of your identity provider")]
public class UserApi : BaseEntity
{
public string Email { get; set; }
Expand Down
2 changes: 2 additions & 0 deletions src/Modules/Grand.Module.Api/Controllers/TokenController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

#pragma warning disable CS0618 //API users stay supported until removed

namespace Grand.Module.Api.Controllers;

[ApiController]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Grand.Domain.Permissions;
using Grand.Data;
using Grand.Domain.Security;
using Grand.Infrastructure.Configuration;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
Expand Down Expand Up @@ -31,7 +32,7 @@ public AuthorizeApiAdminAttribute(bool ignore = false) : base(typeof(AuthorizeAp
/// Represents a filter that confirms access to the admin panel
/// </summary>
private class AuthorizeApiAdminFilter(bool ignoreFilter, IPermissionService permissionService,
SecuritySettings securitySettings) : IAsyncAuthorizationFilter
SecuritySettings securitySettings, BackendAPIConfig backendApiConfig) : IAsyncAuthorizationFilter
{

#region Ctor
Expand All @@ -48,6 +49,14 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext filterContext)
{
ArgumentNullException.ThrowIfNull(filterContext);

//the kill switch also covers tokens forwarded to the OpenID Connect scheme, which skip the Bearer scheme's
//check; it applies to every action, including those that ignore the admin area checks below
if (!backendApiConfig.Enabled)
{
filterContext.Result = new ForbidResult(JwtBearerDefaults.AuthenticationScheme);
return;
}

//check whether this filter has been overridden for the action
var actionFilter = filterContext.ActionDescriptor.FilterDescriptors
.Where(f => f.Scope == FilterScope.Action)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

#pragma warning disable CS0618 //API users stay supported until removed

namespace Grand.Module.Api.Infrastructure;

public class ApiAuthenticationRegistrar : IAuthenticationBuilder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Grand.Business.Core.Interfaces.Authentication;
using Grand.Infrastructure.Configuration;
using Grand.Module.Api.Constants;

namespace Grand.Module.Api.Infrastructure;

/// <summary>
/// While BackendAPI.Enabled is false the Web API's paths are withdrawn from OIDC protection, so an access token
/// there is refused by the Bearer scheme before it is mapped to a customer or pins the store
/// </summary>
public class ApiOidcDisabledPathProvider(BackendAPIConfig config) : IOidcDisabledPathProvider
{
public IEnumerable<string> GetDisabledPaths() =>
config.Enabled ? [] : ["/" + Configurations.RestRoutePrefix];
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Grand.Business.Core.Interfaces.Authentication;
using Grand.Infrastructure;
using Grand.Infrastructure.Configuration;
using Microsoft.AspNetCore.Builder;
Expand Down Expand Up @@ -65,6 +66,8 @@ private static bool IsStrong(string secret)

public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
//a disabled Web API also refuses access tokens of the identity provider, before any customer is resolved
services.AddSingleton<IOidcDisabledPathProvider, ApiOidcDisabledPathProvider>();
}

public int Priority => 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using Grand.Infrastructure.Configuration;
using Grand.Infrastructure.Validators;

#pragma warning disable CS0618 //API users stay supported until removed

namespace Grand.Module.Api.Validators.Common;

public class LoginValidator : BaseGrandValidator<LoginModel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
using Microsoft.Extensions.DependencyInjection;
using System.Security.Cryptography;

#pragma warning disable CS0618 //API users stay supported until removed

namespace Grand.Module.Installer.Services;

public partial class InstallationService : IInstallationService
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Grand.Infrastructure.Migrations;

namespace Grand.Module.Migration.Migrations._2._5;

public class MigrationUpdateResourceString : IMigration
{
public int Priority => 0;
public DbVersion Version => new(2, 5);
public Guid Identity => new("15EDC88E-A865-4BA2-9E43-FB97839F1B92");
public string Name => "Update resource string for english language 2.5";

public bool UpgradeProcess(IServiceProvider serviceProvider)
{
return serviceProvider.ImportLanguageResourcesFromXml("App_Data/Resources/Upgrade/en_250.xml");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
using Moq;
using System.Security.Claims;

#pragma warning disable CS0618 //API users stay supported until removed

namespace Grand.Business.Authentication.Tests.Services;

[TestClass]
Expand Down Expand Up @@ -235,6 +237,25 @@ public async Task GetAuthenticatedCustomer_JwtSchemeRegisteredAndSucceeds_Return
Assert.AreSame(customer, result);
}

[TestMethod]
public async Task GetAuthenticatedCustomer_BearerForwardedToOidc_ReturnsNull()
{
//a provider token the Bearer scheme forwards to the Oidc scheme: its CustomerId claim was not issued by GrandNode
var customer = new Customer { Id = "c1", Active = true };
_customerService.Setup(c => c.GetCustomerById("c1")).ReturnsAsync(customer);
_groupService.Setup(g => g.IsRegistered(customer)).ReturnsAsync(true);
var context = CreateHttpContext(b => b
.AddScheme<AuthenticationSchemeOptions, OkHandler>(JwtBearerDefaults.AuthenticationScheme,
o => o.ForwardDefault = "Oidc")
.AddScheme<AuthenticationSchemeOptions, OkHandler>("Oidc", null));

var result = await CreateService(context).GetAuthenticatedCustomer();

Assert.IsNull(result);
_customerService.Verify(c => c.GetCustomerById(It.IsAny<string>()), Times.Never);
_customerService.Verify(c => c.GetCustomerByEmail(It.IsAny<string>()), Times.Never);
}

private class OkHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public OkHandler(Microsoft.Extensions.Options.IOptionsMonitor<AuthenticationSchemeOptions> options,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

#pragma warning disable CS0618 //API users stay supported until removed

namespace Grand.Business.Customers.Tests.Services;

[TestClass]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using VerifyMSTest;

#pragma warning disable CS0618 //API users stay supported until removed

namespace Grand.Mapping.Tests.AdminShared;

[TestClass]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using Grand.Business.Core.Interfaces.Common.Security;
using Grand.Data;
using Grand.Domain.Permissions;
using Grand.Domain.Security;
using Grand.Infrastructure.Configuration;
using Grand.Module.Api.Filters;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Grand.Module.Api.Tests.Filters;

[TestClass]
public class AuthorizeApiAdminAttributeTests
{
private Mock<IPermissionService> _permissionService;

[TestInitialize]
public void Init()
{
DataSettingsManager.Initialize(Path.Join(Path.GetTempPath(), "grand-api-tests-settings.json"));
DataSettingsManager.Instance.LoadDataSettings(new DataSettings { ConnectionString = "mongodb://localhost" });
_permissionService = new Mock<IPermissionService>();
}

private async Task<AuthorizationFilterContext> Run(bool apiEnabled, bool ignore, bool permitted)
{
_permissionService.Setup(x => x.Authorize(It.IsAny<Permission>())).ReturnsAsync(permitted);
var attribute = new AuthorizeApiAdminAttribute(ignore);
var filter = (IAsyncAuthorizationFilter)ActivatorUtilities.CreateInstance(
new ServiceCollection()
.AddSingleton(_permissionService.Object)
.AddSingleton(new SecuritySettings())
.AddSingleton(new BackendAPIConfig { Enabled = apiEnabled })
.BuildServiceProvider(),
attribute.ImplementationType, ignore);
var context = new AuthorizationFilterContext(
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
[new FilterDescriptor(attribute, FilterScope.Controller).Filter]);
context.Filters.Add((IFilterMetadata)filter);
await filter.OnAuthorizationAsync(context);
return context;
}

[TestMethod]
public async Task ApiDisabled_Forbid()
{
var context = await Run(apiEnabled: false, ignore: false, permitted: true);
Assert.IsInstanceOfType<ForbidResult>(context.Result);
_permissionService.Verify(x => x.Authorize(It.IsAny<Permission>()), Times.Never);
}

[TestMethod]
public async Task ApiDisabled_IgnoredAction_Forbid()
{
//ignore skips the admin panel checks, not the kill switch
var context = await Run(apiEnabled: false, ignore: true, permitted: true);
Assert.IsInstanceOfType<ForbidResult>(context.Result);
}

[TestMethod]
public async Task ApiEnabled_IgnoredAction_NoPermissionCheck()
{
var context = await Run(apiEnabled: true, ignore: true, permitted: false);
Assert.IsNull(context.Result);
_permissionService.Verify(x => x.Authorize(It.IsAny<Permission>()), Times.Never);
}

[TestMethod]
public async Task ApiEnabled_PermissionDenied_Forbid()
{
var context = await Run(apiEnabled: true, ignore: false, permitted: false);
Assert.IsInstanceOfType<ForbidResult>(context.Result);
_permissionService.Verify(x => x.Authorize(It.IsAny<Permission>()), Times.Once);
}

[TestMethod]
public async Task ApiEnabled_PermissionGranted_Allowed()
{
var context = await Run(apiEnabled: true, ignore: false, permitted: true);
Assert.IsNull(context.Result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Grand.Infrastructure.Configuration;
using Grand.Module.Api.Infrastructure;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Grand.Module.Api.Tests.Infrastructure;

[TestClass]
public class ApiOidcDisabledPathProviderTests
{
[TestMethod]
public void ApiDisabled_ApiPathWithdrawn() =>
CollectionAssert.AreEqual(new[] { "/api" },
new ApiOidcDisabledPathProvider(new BackendAPIConfig { Enabled = false }).GetDisabledPaths().ToList());

[TestMethod]
public void ApiEnabled_NothingWithdrawn() =>
Assert.AreEqual(0,
new ApiOidcDisabledPathProvider(new BackendAPIConfig { Enabled = true }).GetDisabledPaths().Count());
}
Loading
Loading