Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] JWT bearer grant type support #18912

Draft
wants to merge 35 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
defadbe
First pass at refactoring
kirktrue Feb 14, 2025
9f2b079
More refactoring
kirktrue Feb 14, 2025
06119da
Updates
kirktrue Feb 15, 2025
7aceaa5
Update AuthenticateCallbackHandler.java
kirktrue Feb 15, 2025
0d7554b
Update AuthenticateCallbackHandler.java
kirktrue Feb 15, 2025
c410fc3
Update AuthenticateCallbackHandler.java
kirktrue Feb 15, 2025
62d96f7
Update AuthenticateCallbackHandler.java
kirktrue Feb 15, 2025
4102c20
Moving things around more
kirktrue Feb 15, 2025
56ed3a9
Updates
kirktrue Feb 15, 2025
04016ed
More updates
kirktrue Feb 15, 2025
7b04655
More updates
kirktrue Feb 15, 2025
bbebbce
Moved internals back to internals for now
kirktrue Feb 18, 2025
88d187d
Moved more code back to internals
kirktrue Feb 18, 2025
c16eaaf
Fixed refresh tests
kirktrue Feb 19, 2025
3ffbb13
Fixed the remaining broken unit test
kirktrue Feb 19, 2025
8a18ef1
First pass at incorporating Zach's JWT bearer code
kirktrue Feb 19, 2025
9026358
First pass at hooking the JWT bearer retriever into the rest of the code
kirktrue Feb 19, 2025
c58d27e
Reverted FileAccessTokenRetriever name change
kirktrue Feb 19, 2025
4939c8a
Rename to revert to original code
kirktrue Feb 19, 2025
0ff639b
More refactoring
kirktrue Feb 19, 2025
15582d0
Refactoring
kirktrue Feb 19, 2025
58ea79f
Clean up of Javadoc
kirktrue Feb 19, 2025
bb5f1c0
Updated formatting
kirktrue Feb 19, 2025
a88b553
Incorporating jwt-bearer configuration and JAAS options
kirktrue Feb 19, 2025
247a75d
More refactoring
kirktrue Feb 19, 2025
038343a
More refactoring
kirktrue Feb 19, 2025
14c8746
spotlessApply fixups
kirktrue Feb 20, 2025
f0113a1
Fixed out-of-order final static and allowing Jackson annotations
kirktrue Feb 20, 2025
a6db62c
The great refactoring of OAuthCompatibilityTool
kirktrue Feb 20, 2025
a7c31a5
Update AccessTokenRetriever.java
kirktrue Feb 20, 2025
07dfaee
Update ValidatorAccessTokenValidator.java
kirktrue Feb 20, 2025
a65fbc1
Merge branch 'apache:trunk' into KAFKA-18573-add-jwt-bearer-grant-type
kirktrue Feb 21, 2025
df66e1c
Update AccessTokenRetriever.java
kirktrue Feb 21, 2025
cf1abbf
Renamed ValidateException to InvalidJwtException
kirktrue Feb 21, 2025
7f62a08
Minor refactoring of class and method names
kirktrue Feb 21, 2025
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
Prev Previous commit
Next Next commit
Updates
  • Loading branch information
kirktrue committed Feb 15, 2025
commit 56ed3a945f786fcf97e500d66f1b9478b106eeb3
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public class SaslConfigs {
public static final String SASL_OAUTHBEARER_ACCESS_TOKEN_RETRIEVER_CLASS_DOC = "The fully qualified name of a class that implements the AccessTokenRetriever interface.";

public static final String SASL_OAUTHBEARER_ACCESS_TOKEN_VALIDATOR_CLASS = "sasl.oauthbearer.access.token.validator.class";
public static final String DEFAULT_SASL_OAUTHBEARER_ACCESS_TOKEN_VALIDATOR_CLASS = "org.apache.kafka.common.security.oauthbearer.DefaultClientAccessTokenValidator";
public static final String DEFAULT_SASL_OAUTHBEARER_ACCESS_TOKEN_VALIDATOR_CLASS = "org.apache.kafka.common.security.oauthbearer.DefaultAccessTokenValidator";
public static final String SASL_OAUTHBEARER_ACCESS_TOKEN_VALIDATOR_CLASS_DOC = "The fully qualified name of a class that implements the AccessTokenValidator interface.";

public static final String SASL_OAUTHBEARER_SCOPE_CLAIM_NAME = "sasl.oauthbearer.scope.claim.name";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.apache.kafka.common.security.oauthbearer;

import org.apache.kafka.common.security.oauthbearer.internals.secured.ClientAccessTokenValidator;
import org.apache.kafka.common.security.oauthbearer.internals.secured.ValidateException;

/**
Expand All @@ -40,9 +41,9 @@
* <li><a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-access-token-jwt">RFC 6750, Section 2.1</a></li>
* </ul>
*
* @see DefaultClientAccessTokenValidator A basic AccessTokenValidator used by client-side login
* @see ClientAccessTokenValidator A basic AccessTokenValidator used by client-side login
* authentication
* @see DefaultBrokerAccessTokenValidator A more robust AccessTokenValidator that is used on the broker
* @see DefaultAccessTokenValidator A more robust AccessTokenValidator that is used on the broker
* to validate the token's contents and verify the signature
*/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.apache.kafka.common.security.oauthbearer.internals.secured.ClientCredentialsAccessTokenRetriever;
import org.apache.kafka.common.security.oauthbearer.internals.secured.ConfigurationUtils;
import org.apache.kafka.common.security.oauthbearer.internals.secured.FileAccessTokenRetriever;
import org.apache.kafka.common.utils.Utils;

import java.io.IOException;
import java.net.URL;
Expand Down Expand Up @@ -56,8 +57,7 @@ public String retrieve() throws IOException {

@Override
public void close() {
if (delegate != null)
delegate.close();
Utils.closeQuietly(delegate, "delegate");
}

public AccessTokenRetriever delegate() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.kafka.common.security.oauthbearer;

import org.apache.kafka.common.security.oauthbearer.internals.secured.BrokerAccessTokenValidator;
import org.apache.kafka.common.utils.Time;

/**
* Implementation of {@link AccessTokenValidator} that is used
* by the broker to perform more extensive validation of the JWT access token that is received
* from the client, but ultimately from posting the client credentials to the OAuth/OIDC provider's
* token endpoint.
*
* The validation steps performed (primary by the jose4j library) are:
*
* <ol>
* <li>
* Basic structural validation of the <code>b64token</code> value as defined in
* <a href="https://tools.ietf.org/html/rfc6750#section-2.1">RFC 6750 Section 2.1</a>
* </li>
* <li>Basic conversion of the token into an in-memory data structure</li>
* <li>
* Presence of scope, <code>exp</code>, subject, <code>iss</code>, and
* <code>iat</code> claims
* </li>
* <li>
* Signature matching validation against the <code>kid</code> and those provided by
* the OAuth/OIDC provider's JWKS
* </li>
* </ol>
*/

public class DefaultAccessTokenValidator extends BrokerAccessTokenValidator {

public DefaultAccessTokenValidator() {
super(Time.SYSTEM);
}

public DefaultAccessTokenValidator(Time time) {
super(time);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public void configure(Map<String, ?> configs, String saslMechanism, List<AppConf

accessTokenValidator = newInstance(
SaslConfigs.SASL_OAUTHBEARER_ACCESS_TOKEN_VALIDATOR_CLASS,
DefaultBrokerAccessTokenValidator.class,
DefaultAccessTokenValidator.class,
configs,
saslMechanism,
jaasConfigEntries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public void configure(Map<String, ?> configs, String saslMechanism, List<AppConf
try {
accessTokenValidator = newInstance(
SaslConfigs.SASL_OAUTHBEARER_ACCESS_TOKEN_VALIDATOR_CLASS,
DefaultBrokerAccessTokenValidator.class,
DefaultAccessTokenValidator.class,
configs,
saslMechanism,
jaasConfigEntries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,13 @@
* limitations under the License.
*/

package org.apache.kafka.common.security.oauthbearer;
package org.apache.kafka.common.security.oauthbearer.internals.secured;

import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
import org.apache.kafka.common.security.oauthbearer.internals.secured.BasicOAuthBearerToken;
import org.apache.kafka.common.security.oauthbearer.internals.secured.ClaimValidationUtils;
import org.apache.kafka.common.security.oauthbearer.internals.secured.ConfigurationUtils;
import org.apache.kafka.common.security.oauthbearer.internals.secured.DelegatingVerificationKeyResolver;
import org.apache.kafka.common.security.oauthbearer.internals.secured.JaasOptionsUtils;
import org.apache.kafka.common.security.oauthbearer.internals.secured.RefreshingHttpsJwksVerificationKeyResolver;
import org.apache.kafka.common.security.oauthbearer.internals.secured.SerializedJwt;
import org.apache.kafka.common.security.oauthbearer.internals.secured.ValidateException;
import org.apache.kafka.common.security.oauthbearer.AccessTokenValidator;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.common.utils.Utils;

import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.MalformedClaimException;
Expand All @@ -43,6 +36,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.security.auth.login.AppConfigurationEntry;
import java.security.Key;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -53,8 +47,6 @@
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import javax.security.auth.login.AppConfigurationEntry;

import static org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_CLOCK_SKEW_SECONDS;
import static org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_EXPECTED_AUDIENCE;
import static org.apache.kafka.common.config.SaslConfigs.SASL_OAUTHBEARER_EXPECTED_ISSUER;
Expand Down Expand Up @@ -87,9 +79,9 @@
* </ol>
*/

public class DefaultBrokerAccessTokenValidator implements AccessTokenValidator {
public class BrokerAccessTokenValidator implements AccessTokenValidator {

private static final Logger log = LoggerFactory.getLogger(DefaultBrokerAccessTokenValidator.class);
private static final Logger log = LoggerFactory.getLogger(BrokerAccessTokenValidator.class);

/**
* Because a {@link CloseableVerificationKeyResolver} instance can spawn threads and issue
Expand All @@ -110,11 +102,11 @@ public class DefaultBrokerAccessTokenValidator implements AccessTokenValidator {

protected String subClaimName;

public DefaultBrokerAccessTokenValidator() {
public BrokerAccessTokenValidator() {
this(Time.SYSTEM);
}

public DefaultBrokerAccessTokenValidator(Time time) {
public BrokerAccessTokenValidator(Time time) {
this.time = time;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@
* limitations under the License.
*/

package org.apache.kafka.common.security.oauthbearer;
package org.apache.kafka.common.security.oauthbearer.internals.secured;

import org.apache.kafka.common.security.oauthbearer.internals.secured.BasicOAuthBearerToken;
import org.apache.kafka.common.security.oauthbearer.internals.secured.ClaimValidationUtils;
import org.apache.kafka.common.security.oauthbearer.internals.secured.ConfigurationUtils;
import org.apache.kafka.common.security.oauthbearer.internals.secured.SerializedJwt;
import org.apache.kafka.common.security.oauthbearer.internals.secured.ValidateException;
import org.apache.kafka.common.security.oauthbearer.AccessTokenValidator;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken;
import org.apache.kafka.common.security.oauthbearer.internals.unsecured.OAuthBearerIllegalTokenException;
import org.apache.kafka.common.security.oauthbearer.internals.unsecured.OAuthBearerUnsecuredJws;

Expand Down Expand Up @@ -59,9 +56,9 @@
* </ol>
*/

public class DefaultClientAccessTokenValidator implements AccessTokenValidator {
public class ClientAccessTokenValidator implements AccessTokenValidator {

private static final Logger log = LoggerFactory.getLogger(DefaultClientAccessTokenValidator.class);
private static final Logger log = LoggerFactory.getLogger(ClientAccessTokenValidator.class);

public static final String EXPIRATION_CLAIM_NAME = "exp";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
* limitations under the License.
*/

package org.apache.kafka.common.security.oauthbearer;
package org.apache.kafka.common.security.oauthbearer.internals.secured;

import org.apache.kafka.common.security.oauthbearer.OAuthBearerConfigurable;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerValidatorCallbackHandler;
import org.jose4j.keys.resolvers.VerificationKeyResolver;

import java.io.Closeable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

package org.apache.kafka.common.security.oauthbearer.internals.secured;

import org.apache.kafka.common.security.oauthbearer.CloseableVerificationKeyResolver;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.common.utils.Utils;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
package org.apache.kafka.common.security.oauthbearer.internals.secured;

import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.security.oauthbearer.CloseableVerificationKeyResolver;
import org.apache.kafka.common.utils.Utils;

import org.jose4j.jwk.JsonWebKeySet;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
package org.apache.kafka.common.security.oauthbearer.internals.secured;

import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.security.oauthbearer.DefaultBrokerAccessTokenValidator;
import org.apache.kafka.common.security.oauthbearer.DefaultAccessTokenValidator;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerConfigurable;
import org.apache.kafka.common.utils.Time;

Expand Down Expand Up @@ -60,12 +60,12 @@
* This instance is created and provided to the
* {@link org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver} that is used when using
* an HTTP-/HTTPS-based {@link org.jose4j.keys.resolvers.VerificationKeyResolver}, which is then
* provided to the {@link DefaultBrokerAccessTokenValidator} to use in validating the signature of
* provided to the {@link DefaultAccessTokenValidator} to use in validating the signature of
* a JWT.
*
* @see org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver
* @see org.jose4j.keys.resolvers.VerificationKeyResolver
* @see DefaultBrokerAccessTokenValidator
* @see DefaultAccessTokenValidator
*/

public class RefreshingHttpsJwks implements OAuthBearerConfigurable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

package org.apache.kafka.common.security.oauthbearer.internals.secured;

import org.apache.kafka.common.security.oauthbearer.CloseableVerificationKeyResolver;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.common.utils.Utils;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import org.apache.kafka.common.security.oauthbearer.internals.secured.AccessTokenBuilder;

import org.apache.kafka.common.security.oauthbearer.internals.secured.CloseableVerificationKeyResolver;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.lang.InvalidAlgorithmException;
Expand All @@ -35,15 +36,15 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class DefaultBrokerAccessTokenValidatorTest extends AccessTokenValidatorTest {
public class DefaultAccessTokenValidatorTest extends AccessTokenValidatorTest {

@Override
protected AccessTokenValidator createAccessTokenValidator(AccessTokenBuilder builder) throws Exception {
Key key = builder.jwk() != null ? builder.jwk().getKey() : null;
CloseableVerificationKeyResolver keyResolver = mock(CloseableVerificationKeyResolver.class);
when(keyResolver.resolveKey(any(), any())).thenReturn(key);

return new DefaultBrokerAccessTokenValidator() {
return new DefaultAccessTokenValidator() {
@Override
public void configure(Map<String, ?> configs, String saslMechanism, List<AppConfigurationEntry> jaasConfigEntries) {
super.configure(keyResolver, configs, saslMechanism, jaasConfigEntries);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.apache.kafka.common.security.auth.SaslExtensionsCallback;
import org.apache.kafka.common.security.oauthbearer.internals.OAuthBearerClientInitialResponse;
import org.apache.kafka.common.security.oauthbearer.internals.secured.AccessTokenBuilder;
import org.apache.kafka.common.security.oauthbearer.internals.secured.ClientAccessTokenValidator;
import org.apache.kafka.common.security.oauthbearer.internals.secured.FileAccessTokenRetriever;
import org.apache.kafka.common.security.oauthbearer.internals.secured.HttpAccessTokenRetriever;
import org.apache.kafka.common.security.oauthbearer.internals.secured.OAuthBearerTest;
Expand Down Expand Up @@ -137,7 +138,7 @@ public void testInvalidCallbackGeneratesUnsupportedCallbackException() throws IO
OAuthBearerTestableLoginCallbackHandler handler = new OAuthBearerTestableLoginCallbackHandler();
AccessTokenRetriever accessTokenRetriever = mock(AccessTokenRetriever.class);
when(accessTokenRetriever.retrieve()).thenReturn("foo");
AccessTokenValidator accessTokenValidator = new DefaultClientAccessTokenValidator();
AccessTokenValidator accessTokenValidator = new ClientAccessTokenValidator();
handler.init(accessTokenRetriever, accessTokenValidator);

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.kafka.common.security.oauthbearer;

import org.apache.kafka.common.security.oauthbearer.internals.secured.AccessTokenBuilder;
import org.apache.kafka.common.security.oauthbearer.internals.secured.CloseableVerificationKeyResolver;
import org.apache.kafka.common.security.oauthbearer.internals.secured.OAuthBearerTest;

import org.jose4j.jws.AlgorithmIdentifiers;
Expand Down Expand Up @@ -96,7 +97,7 @@ private OAuthBearerValidatorCallbackHandler createHandler(Map<String, ?> configs
CloseableVerificationKeyResolver verificationKeyResolver = mock(CloseableVerificationKeyResolver.class);
Key key = builder.jwk() != null ? builder.jwk().getPublicKey() : null;
when(verificationKeyResolver.resolveKey(any(), any())).thenReturn(key);
DefaultBrokerAccessTokenValidator accessTokenValidator = new DefaultBrokerAccessTokenValidator(time);
DefaultAccessTokenValidator accessTokenValidator = new DefaultAccessTokenValidator(time);
accessTokenValidator.configure(verificationKeyResolver, configs, null, List.of());
handler.configure(accessTokenValidator);
return handler;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
* limitations under the License.
*/

package org.apache.kafka.common.security.oauthbearer;
package org.apache.kafka.common.security.oauthbearer.internals.secured;

import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.security.oauthbearer.internals.secured.AccessTokenBuilder;
import org.apache.kafka.common.security.oauthbearer.AccessTokenRetriever;
import org.apache.kafka.common.security.oauthbearer.AccessTokenValidator;
import org.apache.kafka.common.security.oauthbearer.AccessTokenValidatorTest;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerTestableLoginCallbackHandler;

import org.junit.jupiter.api.Test;

Expand All @@ -28,11 +31,11 @@
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;

public class DefaultClientAccessTokenValidatorTest extends AccessTokenValidatorTest {
public class ClientAccessTokenValidatorTest extends AccessTokenValidatorTest {

@Override
protected AccessTokenValidator createAccessTokenValidator(AccessTokenBuilder builder) {
return new DefaultClientAccessTokenValidator();
return new ClientAccessTokenValidator();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
package org.apache.kafka.common.security.oauthbearer.internals.secured;

import org.apache.kafka.common.config.ConfigException;
import org.apache.kafka.common.security.oauthbearer.CloseableVerificationKeyResolver;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import org.apache.kafka.common.security.authenticator.TestJaasConfig;
import org.apache.kafka.common.security.oauthbearer.AccessTokenRetriever;
import org.apache.kafka.common.security.oauthbearer.AccessTokenValidator;
import org.apache.kafka.common.security.oauthbearer.DefaultClientAccessTokenValidator;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginCallbackHandler;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerTestableLoginCallbackHandler;
Expand Down Expand Up @@ -114,7 +113,7 @@ protected OAuthBearerLoginCallbackHandler createHandler(AccessTokenRetriever acc
Map<String, ?> configs) {
List<AppConfigurationEntry> jaasConfigEntries = List.of();
OAuthBearerTestableLoginCallbackHandler handler = new OAuthBearerTestableLoginCallbackHandler();
AccessTokenValidator accessTokenValidator = new DefaultClientAccessTokenValidator();
AccessTokenValidator accessTokenValidator = new ClientAccessTokenValidator();
accessTokenRetriever.configure(configs, null, jaasConfigEntries);
accessTokenValidator.configure(configs, null, jaasConfigEntries);
handler.init(accessTokenRetriever, accessTokenValidator);
Expand Down
Loading
Loading