The concise Spring Boot web application protects one resource on path "/resource" with OAuth2/ OIDC using opaque token introspection, while the resource "/helloworld" should be protected with Basic http Authentication. There is an oauth2 credentials server in the demo system, that works and is returning a technical client id that is evaluated after introspection and ends up with disposing the role "oauth". Basic authentication as is spring security standard is mapped to the role "demo".
The "/resource" can then be retrieved by an http request sending the right bearer token, and the oauth2 credentials server probing and returning the matching client id. However, submitting an http request to "/helloworld" with Basic authentication, with credentials username and password "demo", refer application.yaml, the application returns http 401 Unauthorized. The same as, when trying without any authentication, at all.
How to make basic authentication work alongside oauth2 resources in a single Spring Boot application?
DemoApplication.java
@SpringBootApplication
@RestController
public class DemoApplication {
@PreAuthorize("hasRole('demo')")
@GetMapping("/helloworld")
public String hello() {
return "Hello World!";
}
@PreAuthorize("hasRole('oauth2')")
@GetMapping("/resource")
public String resource() {
return "Protected resource";
}
public static void main(String... args) {
SpringApplication.run(DemoApplication.class, args);
}
}
DemoSecurityConfiguration.java
public class DemoSecurityConfiguration {
@Configuration
@Order(10)
@EnableWebSecurity
public static class HelloWorldBasicSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/helloworld")
.httpBasic()
.and()
.authorizeRequests().antMatchers("/helloworld").authenticated()
;
}
}
@Configuration
@Order(0)
@EnableWebSecurity
public static class ResourceSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Autowired
private Environment environment;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/resource")
.oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(
opaqueToken -> opaqueToken.introspector(new DemoAuthoritiesOpaqueTokenIntrospector())))
.authorizeRequests().antMatchers("/resource").authenticated()
;
}
private class DemoAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate;
private final String demoClientId;
public DemoAuthoritiesOpaqueTokenIntrospector() {
String introSpectionUri = environment
.getProperty("spring.security.oauth2.resourceserver.opaque-token.introspection-uri");
String clientId = environment
.getProperty("spring.security.oauth2.resourceserver.opaque-token.client-id");
String clientSecret = environment
.getProperty("spring.security.oauth2.resourceserver.opaque-token.client-secret");
demoClientId = environment.getProperty("demo.security.oauth2.credentials-grant.client-id");
delegate = new NimbusOpaqueTokenIntrospector(introSpectionUri, clientId, clientSecret);
}
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
return new DefaultOAuth2AuthenticatedPrincipal(principal.getName(), principal.getAttributes(),
extractAuthorities(principal));
}
private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
String userId = principal.getAttribute("client_id");
if (demoClientId.equals(userId)) {
return Collections.singleton(new SimpleGrantedAuthority("ROLE_oauth2"));
}
return Collections.emptySet();
}
}
}
}
application.yaml
spring:
security:
basic:
enabled: true
user:
name: demo
password: demo
roles: demo
oauth2:
resourceserver:
opaque-token:
introspection-uri: "https://...oauth2"
client-id: "abba"
client-secret: "secret"
demo:
security:
oauth2:
credentials-grant:
client-id: "rundmc
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.6'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation("org.springframework.security:spring-security-oauth2-resource-server")
implementation("org.springframework.security:spring-security-oauth2-jose")
implementation("org.springframework.security:spring-security-oauth2-client")
}
I think that the properties under spring.security.user
won't work because Spring Boot will back off creating the UserDetailService
bean for you if you have defined some beans, including OAuth2 ones. The relevant part of the code (you can see the full source code here):
@ConditionalOnMissingBean(
value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
AuthenticationManagerResolver.class },
type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository",
"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
public class UserDetailsServiceAutoConfiguration {
That said, you will have to define the UserDetailsService
bean by yourself, one way to do that is:
@Bean
public UserDetailsService inMemoryUserDetailsService() {
UserDetails demo = User.withUsername("demo").password("demo").roles("demo").build();
return new InMemoryUserDetailsManager(demo);
}
I'm not sure right now, but maybe the password should be {noop}demo
to tell the PasswordEncoder
bean that the password is not encoded. Remember that a noop password encoder should only be used for demo purposes.