diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserAuthorities.java b/core/src/main/java/org/springframework/security/core/userdetails/UserAuthorities.java
new file mode 100644
index 00000000000..a470f2d6ba1
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/core/userdetails/UserAuthorities.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.security.core.userdetails;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import org.springframework.security.core.GrantedAuthority;
+
+/**
+ * Represents user authorities. This interface is mostly intended for scenarios where a
+ * password is not need, like X509, CAS, Passkeys, One Time Tokens and others.
+ *
+ * @author Marcus da Coregio
+ * @since 6.4
+ * @see UserAuthoritiesRepository
+ * @see UserDetails
+ */
+public interface UserAuthorities extends Serializable {
+
+ /**
+ * Returns the authorities granted to the user. Cannot return null
.
+ * @return the authorities, sorted by natural key (never null
)
+ */
+ Collection extends GrantedAuthority> getAuthorities();
+
+ /**
+ * Returns the username used to authenticate the user. Cannot return
+ * null
.
+ * @return the username (never null
)
+ */
+ String getUsername();
+
+}
diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserAuthoritiesRepository.java b/core/src/main/java/org/springframework/security/core/userdetails/UserAuthoritiesRepository.java
new file mode 100644
index 00000000000..07d4f836009
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/core/userdetails/UserAuthoritiesRepository.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.security.core.userdetails;
+
+/**
+ * Repository interface for accessing user authorities.
+ *
+ * @author Marcus da Coregio
+ * @since 6.4
+ * @see UserAuthorities
+ */
+public interface UserAuthoritiesRepository {
+
+ /**
+ * Finds the authorities associated with the given username.
+ * @param username the username for which to find authorities
+ * @return the {@link UserAuthorities} object containing authorities associated with
+ * the specified username
+ * @throws UsernameNotFoundException if the user could not be found or the user has no
+ * GrantedAuthority
+ */
+ UserAuthorities findAuthoritiesByUsername(String username) throws UsernameNotFoundException;
+
+}
diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java b/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java
index bab08c5a2ac..485e382c4d1 100644
--- a/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java
+++ b/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java
@@ -16,11 +16,7 @@
package org.springframework.security.core.userdetails;
-import java.io.Serializable;
-import java.util.Collection;
-
import org.springframework.security.core.Authentication;
-import org.springframework.security.core.GrantedAuthority;
/**
* Provides core user information.
@@ -40,13 +36,7 @@
* @see UserDetailsService
* @see UserCache
*/
-public interface UserDetails extends Serializable {
-
- /**
- * Returns the authorities granted to the user. Cannot return null
.
- * @return the authorities, sorted by natural key (never null
)
- */
- Collection extends GrantedAuthority> getAuthorities();
+public interface UserDetails extends UserAuthorities {
/**
* Returns the password used to authenticate the user.
@@ -54,13 +44,6 @@ public interface UserDetails extends Serializable {
*/
String getPassword();
- /**
- * Returns the username used to authenticate the user. Cannot return
- * null
.
- * @return the username (never null
)
- */
- String getUsername();
-
/**
* Indicates whether the user's account has expired. An expired account cannot be
* authenticated.
diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepository.java b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepository.java
new file mode 100644
index 00000000000..8539d031343
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepository.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.security.core.userdetails;
+
+import org.springframework.util.Assert;
+
+/**
+ * An implementation of {@link UserAuthoritiesRepository} that uses a
+ * {@link UserDetailsService} to load the user authorities.
+ *
+ * @author Marcus da Coregio
+ * @since 6.4
+ */
+public class UserDetailsServiceAuthoritiesRepository implements UserAuthoritiesRepository {
+
+ private final UserDetailsService userDetailsService;
+
+ public UserDetailsServiceAuthoritiesRepository(UserDetailsService userDetailsService) {
+ Assert.notNull(userDetailsService, "userDetailsService cannot be null");
+ this.userDetailsService = userDetailsService;
+ }
+
+ @Override
+ public UserAuthorities findAuthoritiesByUsername(String username) {
+ return this.userDetailsService.loadUserByUsername(username);
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepositoryTests.java b/core/src/test/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepositoryTests.java
new file mode 100644
index 00000000000..2ae25a5471a
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepositoryTests.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.security.core.userdetails;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+/**
+ * Tests for {@link UserDetailsServiceAuthoritiesRepository}
+ *
+ * @author Marcus da Coregio
+ */
+class UserDetailsServiceAuthoritiesRepositoryTests {
+
+ UserDetailsService userDetailsService = new InMemoryUserDetailsManager(PasswordEncodedUser.user(),
+ PasswordEncodedUser.admin());
+
+ UserDetailsServiceAuthoritiesRepository userAuthoritiesRepository;
+
+ @BeforeEach
+ void setup() {
+ this.userAuthoritiesRepository = new UserDetailsServiceAuthoritiesRepository(this.userDetailsService);
+ }
+
+ @Test
+ void findUserAuthoritiesWhenUserExistsThenReturn() {
+ UserAuthorities admin = this.userAuthoritiesRepository.findAuthoritiesByUsername("admin");
+ assertThat(admin.getAuthorities()).extracting(GrantedAuthority::getAuthority)
+ .containsExactly("ROLE_ADMIN", "ROLE_USER");
+ }
+
+ @Test
+ void findUserAuthoritiesWhenUserDoesNotExistsThenUsernameNotFoundException() {
+ assertThatExceptionOfType(UsernameNotFoundException.class)
+ .isThrownBy(() -> this.userAuthoritiesRepository.findAuthoritiesByUsername("unknown"));
+ }
+
+}