I have a spring boot app that uses JWT for all endpoints. Now I want to add an /actuator endpoint that uses basic auth to enable prometheus scraping metrics.
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
class SecurityConfig(
val userService: UserService
) {
#Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
return http {
csrf { disable() }
formLogin { disable() }
httpBasic { disable() }
authorizeExchange {
authorize(ServerWebExchangeMatchers.pathMatchers(HttpMethod.OPTIONS, "/**"), permitAll)
// the following should not use JWT but basic auth
authorize(ServerWebExchangeMatchers.pathMatchers("/actuator"), authenticated)
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = customConverter()
}
}
}
}
}
In the MVC Stack I would use something like this:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Configuration
#Order(1)
public static class ActuatorWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${management.endpoints.web.base-path}")
private String managementPath;
#Value("${config.actuator.user.name}")
private String actuatorUser;
#Value("${config.actuator.user.password}")
private String actuatorPassword;
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(actuatorUser)
.password(passwordEncoder().encode(actuatorPassword))
.authorities("ROLE_ACTUATOR");
}
#Bean
public PasswordEncoder passwordEncoder() {
return new Argon2PasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher(managementPath + "/**")
.cors().and()
.csrf().disable()
.authorizeRequests()
.anyRequest()
.hasRole("ACTUATOR")
.and()
.httpBasic();
}
}
#Configuration
#Order(2)
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()
.authenticationProvider(...)
.authorizeRequests()
// ...
}
}
}
How does this translate to webflux?
Adding multiple beans is possible but requires using securityMatcher:
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
class SecurityConfig(
val userService: UserService
) {
#Value("\${config.actuator.user.name}")
private val actuatorUser: String? = null
#Value("\${config.actuator.user.password}")
private val actuatorPassword: String? = null
#Bean
#Order(1)
fun springSecurityFilterChainActuator(http: ServerHttpSecurity): SecurityWebFilterChain? {
val encoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
return http {
securityMatcher(ServerWebExchangeMatchers.pathMatchers("/actuator/**"))
csrf { disable() }
formLogin { disable() }
httpBasic {
authenticationManager = UserDetailsRepositoryReactiveAuthenticationManager(
MapReactiveUserDetailsService(
User
.withUsername(actuatorUser)
.password(encoder.encode(actuatorPassword))
.roles("ACTUATOR")
.build()
)
)
}
authorizeExchange {
authorize(anyExchange, hasRole("ACTUATOR"))
}
}
}
#Bean
#Order(2)
fun springSecurityFilterChainApi(http: ServerHttpSecurity): SecurityWebFilterChain? {
return http {
securityMatcher(ServerWebExchangeMatchers.pathMatchers("/**"))
csrf { disable() }
formLogin { disable() }
httpBasic { disable() }
authorizeExchange {
authorize(ServerWebExchangeMatchers.pathMatchers(HttpMethod.OPTIONS, "/**"), permitAll)
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = customConverter()
}
}
}
}
}
Related
I use spring boot with thymeleaf, spring security and spring cloud gateway.
User enter login/password and get a token. I search a way to get this tokin and put it in a cookie or in a hidden field in fragment. Need to to do some ajax call from thymeleaf page.
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class WebFluxSecurityConfig {
#Autowired
private WebFluxAuthManager authManager;
#Autowired
private WebFluxSecurityContextRepository webFluxSecurityContextRepository;
#Bean
protected SecurityWebFilterChain securityFilterChange(ServerHttpSecurity http) throws Exception {
http.csrf().disable()
.securityContextRepository(webFluxSecurityContextRepository)
.authorizeExchange()
// URL that starts with / or /login/
.pathMatchers("/", "/login", "/js/**", "/img/**", "/css/**").permitAll()
.anyExchange().authenticated()
.and().formLogin().loginPage("/login")
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/two")
).and().csrf().disable();
http.authenticationManager(authManager);
return http.build();
}
}
#Component
public class WebFluxSecurityContextRepository implements ServerSecurityContextRepository {
private final WebFluxAuthManager authManager;
public WebFluxSecurityContextRepository(WebFluxAuthManager authManager) {
this.authManager = authManager;
}
#Override
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
return Mono.empty();
}
#Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String authToken = authHeader.substring(7);
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return this.authManager.authenticate(auth).map((authentication) -> {
return new SecurityContextImpl(authentication);
});
} else {
return Mono.empty();
}
}
}
#Component
public class WebFluxAuthManager implements ReactiveAuthenticationManager {
#Value("${gateway.url}")
private String gatewayUrl;
#Autowired
private WebClient webClient;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
Mono<ResponseEntity<String>> mResponse = webClient.post()
.uri("/auth/login")
.acceptCharset(Charset.forName("UTF-8"))
.body(Mono.just(loginRequest), LoginDto.class)
.retrieve()
.toEntity(String.class);
...
...
return Mono.just(new UsernamePasswordAuthenticationToken(username, password, authorities));
}
I have implemented Webflux security by implementing:
ReactiveUserDetailsService
ReactiveAuthenticationManager
ServerSecurityContextRepository
Now, I am trying to introduce RoleHierarchy following the docs here: Role Hierarchy Docs
I have a user with role USER but he is getting 403 Denied on hitting a controller annotated with GUEST role. Role hierarchy is: "ROLE_ADMIN > ROLE_USER ROLE_USER > ROLE_GUEST"
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
private final DaoAuthenticationManager reactiveAuthenticationManager;
private final SecurityContextRepository securityContextRepository;
private static final String ROLE_HIERARCHIES = "ROLE_ADMIN > ROLE_USER ROLE_USER > ROLE_GUEST";
#Autowired
public SecurityConfig(DaoAuthenticationManager reactiveAuthenticationManager,
SecurityContextRepository securityContextRepository) {
this.reactiveAuthenticationManager = reactiveAuthenticationManager;
this.securityContextRepository = securityContextRepository;
}
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(reactiveAuthenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.anyExchange().permitAll()
.and()
.logout().disable()
.build();
}
#Bean(name = "roleHierarchy")
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(ROLE_HIERARCHIES);
return roleHierarchy;
}
#Bean(name = "roleVoter")
public RoleVoter roleVoter() {
return new RoleHierarchyVoter(roleHierarchy());
}
}
#Component
public class DaoAuthenticationManager implements ReactiveAuthenticationManager {
private final DaoUserDetailsService userDetailsService;
private final Scheduler scheduler;
#Autowired
public DaoAuthenticationManager(DaoUserDetailsService userDetailsService,
Scheduler scheduler) {
Assert.notNull(userDetailsService, "userDetailsService cannot be null");
this.userDetailsService = userDetailsService;
this.scheduler = scheduler;
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
final String username = authentication.getName();
return this.userDetailsService.findByUsername(username)
.publishOn(this.scheduler)
.switchIfEmpty(
Mono.defer(() -> Mono.error(new UsernameNotFoundException("Invalid Username"))))
.map(u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(),
u.getAuthorities()));
}
}
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
private final DaoAuthenticationManager authenticationManager;
#Autowired
public SecurityContextRepository(DaoAuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
throw new UnsupportedOperationException("Not supported yet.");
}
#Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
if (request.getHeaders().containsKey("userName") &&
!Objects.requireNonNull(request.getHeaders().get("userName")).isEmpty()) {
String userName = Objects.requireNonNull(swe
.getRequest()
.getHeaders()
.get("userName")).get(0);
Authentication auth = new UsernamePasswordAuthenticationToken(userName,
Security.PASSWORD);
return this.authenticationManager.authenticate(auth).map(SecurityContextImpl::new);
} else {
return Mono.empty();
}
}
}
Anyway to get the role hierarchy thing working in Webflux security.
EDIT
Controller:
#GetMapping
#PreAuthorize("hasRole('USER')")
public Mono<Device> getDevice(#RequestParam String uuid) {
return deviceService.getDevice(uuid);
}
Normal role authorization is working for me, whats not working is the hierarchy part.
Here a very naive solution by overriding DefaultMethodSecurityExpressionHandler.
I supposed you annotated your controller with this king of expression : #PreAuthorize("hasRole('ROLE_USER')")
securityConfig.java
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
private final DaoAuthenticationManager reactiveAuthenticationManager;
private final SecurityContextRepository securityContextRepository;
private static final String ROLE_HIERARCHY = "ROLE_ADMIN > ROLE_USER ROLE_USER > ROLE_GUEST";
#Autowired
public SecurityConfig(DaoAuthenticationManager reactiveAuthenticationManager,
SecurityContextRepository securityContextRepository) {
this.reactiveAuthenticationManager = reactiveAuthenticationManager;
this.securityContextRepository = securityContextRepository;
}
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(reactiveAuthenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.anyExchange().permitAll()
.and()
.logout().disable()
.build();
}
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(ROLE_HIERARCHY);
return roleHierarchy;
}
// Overriding spring default bean
#Bean
public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
}
Then you have to authorize spring bean overriding by modifying your application property file:
application.properties
spring.main.allow-bean-definition-overriding=true
Sources : issue 1 issue role hierarchy doc
Going a little bit further... This part can be optimized and cleaner.
Using url patterns setup from ServerHttpSecurity object.
Note that the following setup won't use role hierarchy :
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(reactiveAuthenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.pathMatchers("/user/**").hasRole("ROLE_USER") // This won't use role hierarchy because it will use implemention of hasRole defined in your 'reactiveAuthenticationManager'
.anyExchange().permitAll()
.and()
.logout().disable()
.build();
}
A solution could be to create your own implementation of ReactiveAuthorizationManager and overriding check method in order to call access(...) from your http object (ServerHttpSecurity). Ie :
public class CustomReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
private final static Logger logger = LoggerFactory.getLogger(CustomReactiveAuthorizationManager.class);
private final RoleHierarchyVoter roleHierarchyVoter;
private final String authority;
CustomReactiveAuthorizationManager(String role, RoleHierarchy roleHierarchy) {
this.authority = ROLE_PREFIX + role;
this.roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy);
}
#Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
return authentication
.map(a -> {
ConfigAttribute ca = (ConfigAttribute) () -> authority;
int voteResult = roleHierarchyVoter.vote(a, object, Collections.singletonList(ca));
boolean isAuthorized = voteResult == AccessDecisionVoter.ACCESS_GRANTED;
return new AuthorizationDecision(isAuthorized);
})
.defaultIfEmpty(new AuthorizationDecision(false))
.doOnError(error -> logger.error("An error occured voting decision", error));
}
}
and then calling access method :
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, RoleHierarchy roleHierarchy() {
return http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(reactiveAuthenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.pathMatchers("/user/**").access(new CustomReactiveAuthorizationManager<>("USER", roleHierarchy))
.anyExchange().permitAll()
.and()
.logout().disable()
.build();
}
One way I was able to achieve role hierarchy in Webflux was by creating custom annotations.
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#PreAuthorize("hasRole('ADMIN')")
public #interface IsAdmin {
}
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#PreAuthorize("hasAnyRole('ADMIN', 'USER')")
public #interface IsUser {
}
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#PreAuthorize("hasAnyRole('ADMIN', 'USER', 'GUEST')")
public #interface IsGuest {
}
–––––––––––––––––
And annotating the controllers like this:
#GetMapping
#IsUser
public Mono<Device> getDevice(#RequestParam String uuid) {
return deviceService.getDevice(uuid);
}
#PostMapping
#IsAdmin
#ResponseStatus(HttpStatus.CREATED)
public Mono<Device> createDevice(#Valid #RequestBody Device device) {
return deviceService.createDevice(device);
}
I'm wondering where the issue is with my code, every time I run a post test (irrespective of what controller it targets, or method), I return a 403 error, when in some cases I expect a 401, and in others a 200 response (with auth).
This is a snippet from my controller:
#RestController
#CrossOrigin("*")
#RequestMapping("/user")
class UserController #Autowired constructor(val userRepository: UserRepository) {
#PostMapping("/create")
fun addUser(#RequestBody user: User): ResponseEntity<User> {
return ResponseEntity.ok(userRepository.save(user))
}
}
And my unit test targeting this controller
#RunWith(SpringRunner::class)
#WebMvcTest(UserController::class)
class UserControllerTests {
#Autowired
val mvc: MockMvc? = null
#MockBean
val repository: UserRepository? = null
val userCollection = mutableListOf<BioRiskUser>()
#Test
fun testAddUserNoAuth() {
val user = BioRiskUser(
0L,
"user",
"password",
mutableListOf(Role(
0L,
"administrator"
)))
repository!!
`when`(repository.save(user)).thenReturn(createUser(user))
mvc!!
mvc.perform(post("/create"))
.andExpect(status().isUnauthorized)
}
private fun createUser(user: BioRiskUser): BioRiskUser? {
user.id=userCollection.count().toLong()
userCollection.add(user)
return user
}
}
What am I missing?
As requested, my security config...
#Configuration
#EnableWebSecurity
class SecurityConfig(private val userRepository: UserRepository, private val userDetailsService: UserDetailsService) : WebSecurityConfigurerAdapter() {
#Bean
override fun authenticationManagerBean(): AuthenticationManager {
return super.authenticationManagerBean()
}
override fun configure(auth: AuthenticationManagerBuilder) {
auth.authenticationProvider(authProvider())
}
override fun configure(http: HttpSecurity) {
http
.csrf().disable()
.cors()
.and()
.httpBasic()
.realmName("App Realm")
.and()
.authorizeRequests()
.antMatchers("/img/*", "/error", "/favicon.ico", "/doc")
.anonymous()
.anyRequest().authenticated()
.and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/user")
.permitAll()
}
#Bean
fun authProvider(): DaoAuthenticationProvider {
val authProvider = CustomAuthProvider(userRepository)
authProvider.setUserDetailsService(userDetailsService)
authProvider.setPasswordEncoder(encoder())
return authProvider
}
}
and the auth provider
class CustomAuthProvider constructor(val userRepository: UserRepository) : DaoAuthenticationProvider() {
override fun authenticate(authentication: Authentication?): Authentication {
authentication!!
val user = userRepository.findByUsername(authentication.name)
if (!user.isPresent) {
throw BadCredentialsException("Invalid username or password")
}
val result = super.authenticate(authentication)
return UsernamePasswordAuthenticationToken(user, result.credentials, result.authorities)
}
override fun supports(authentication: Class<*>?): Boolean {
return authentication?.equals(UsernamePasswordAuthenticationToken::class.java) ?: false
}
}
In my case, the csrf-Protection seems to be still active in my WebMvcTest (even if disabled in your configuration).
So to workaround this, I simply changed my WebMvcTest to something like:
#Test
public void testFoo() throws Exception {
MvcResult result = mvc.perform(
post("/foo").with(csrf()))
.andExpect(status().isOk())
.andReturn();
// ...
}
So the missing .with(csrf()) was the problem in my case.
You need to add #ContextConfiguration(classes=SecurityConfig.class) to the top of your UserControllerTests class after the #WebMvcTest(UserController::class) annotation.
Your problem comes from the CSRF, if you enable debug logging the problem will become obvious, and it comes from the fact that #WebMvcTest load only the web layer and not the whole context, your KeycloakWebSecurityConfigurerAdapter is not loaded.
The loaded config comes from org.springframework.boot.autoconfigure.security.servlet.DefaultConfigurerAdapter (= to org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter contains crsf().
As of today you have 3 options to resolve this:
Options 1
Create a WebSecurityConfigurerAdapter inside your test class.
The solution suits you if you have only few #WebMvcTest annotated class in your project.
#ExtendWith(SpringExtension.class)
#WebMvcTest(controllers = {MyController.class})
public class MyControllerTest {
#TestConfiguration
static class DefaultConfigWithoutCsrf extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable();
}
}
...
}
Options 2
Create a WebSecurityConfigurerAdapter inside a superclass and make your test extend from it.
The solution suits you if you have multiple #WebMvcTest annotated class in your project.
#Import(WebMvcTestWithoutCsrf.DefaultConfigWithoutCsrf.class)
public interface WebMvcCsrfDisabler {
static class DefaultConfigWithoutCsrf extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable();
}
}
}
#ExtendWith(SpringExtension.class)
#WebMvcTest(controllers = {MyControllerTest .class})
public class MyControllerTest implements WebMvcCsrfDisabler {
...
}
Options 3
Use the spring-security csrf SecurityMockMvcRequestPostProcessors.
This solution is bulky and prone to error, checking for permission denial and forgeting the with(csrf()) will result in false positive test.
#ExtendWith(SpringExtension.class)
#WebMvcTest(controllers = {MyController.class})
public class MyControllerTest {
...
#Test
public void myTest() {
mvc.perform(post("/path")
.with(csrf()) // <=== THIS IS THE PART THAT FIX CSRF ISSUE
.content(...)
)
.andExpect(...);
}
}
Here's an issue:
override fun configure(http: HttpSecurity) {
http
.csrf().disable()
.cors()
.and()
.httpBasic()
.realmName("App Realm")
.and()
.authorizeRequests()
.antMatchers("/img/*", "/error", "/favicon.ico", "/doc")
.anonymous()
.anyRequest().authenticated()
.and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/user")
.permitAll()
}
More particularly here:
.anyRequest().authenticated()
You're requiring for each request to be authenticated, therefore you get 403.
This tutorial explains well how to perform testing with mock user.
The easy way is to have something like this:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class SecuredControllerRestTemplateIntegrationTest {
#Autowired
private val template: TestRestTemplate
#Test
fun createUser(): Unit {
val result = template.withBasicAuth("username", "password")
.postForObject("/user/create", HttpEntity(User(...)), User.class)
assertEquals(HttpStatus.OK, result.getStatusCode())
}
}
couldn't find a doc which illustrates how to use access_token to get user info.
How can I make a such endpoint?
I see when I access the /user endpoint OAuth2AuthenticationProcessingFilter is not included.
I think I messed up something below
#Configuration
#EnableWebSecurity
#Order(2)
// after ResourceServerConfigurerAdapter
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// https://stackoverflow.com/questions/26387003/how-to-make-spring-boot-never-issue-session-cookie
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/", "index", "/login**", "/webjars/**")
.permitAll()
.and().exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(oAuth2FailedHandler)
.and().csrf().disable().authorizeRequests()
.and().addFilterBefore(new CORSFilter(), BasicAuthenticationFilter.class); }
}
I also have..
#Order(3)
#Configuration
#EnableResourceServer
#Slf4j
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(final ResourceServerSecurityConfigurer config) {
config.tokenServices(tokenServices());
}
#Override
public void configure(HttpSecurity http) throws Exception {
// super.configure(http);
log.info("hello OAuth2ResourceServerConfig:configure");
http
.authorizeRequests().anyRequest().authenticated();
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
// JDBC token store configuration
#Bean
public DataSource dataSource() {
final DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("spring.datasource.driver-class-name"));
dataSource.setUrl(env.getProperty("spring.datasource.url"));
dataSource.setUsername(env.getProperty("spring.datasource.username"));
dataSource.setPassword(env.getProperty("spring.datasource.password"));
return dataSource;
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource());
}
}
#Configuration
//#PropertySource({ "classpath:persistence.properties" }) #EnableAuthorizationServer
#Slf4j
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// ....
}
Add a REST controller with something like this:
#RequestMapping("/user")
public #ResponseBody Principal user(Principal user) {
return user;
}
Then you can call it, using your access token in the Authorization header.
I'm trying to follow this tutorial in order to get oauth working.
But when I try to make the below request
curl -X POST -d "client_id=client-id&client_secret=secret&grant_type=password&username=demo&password=1234" http://localhost:8080/oauth/token
I get the following error message
{"timestamp":"2018-01-25T14:47:42.286+0000","status":401,"error":"Unauthorized","message":"Unauthorized","path":"/oauth/token"}
My AuthorizationServerConfiguration looks like the following
#Configuration
#EnableAuthorizationServer
class AuthorizationServerConfiguration : AuthorizationServerConfigurerAdapter() {
#Autowired
private val tokenStore: TokenStore? = null
#Autowired
private val userApprovalHandler: UserApprovalHandler? = null
#Autowired
#Qualifier("authenticationManagerBean")
private val authenticationManager: AuthenticationManager? = null
#Throws(Exception::class)
override fun configure(clients: ClientDetailsServiceConfigurer?) {
clients!!.inMemory()
.withClient("client-id")
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read", "write", "trust")
.secret("secret")
.accessTokenValiditySeconds(120)//Access token is only valid for 2 minutes.
.refreshTokenValiditySeconds(600)//Refresh token is only valid for 10 minutes.
}
#Throws(Exception::class)
override fun configure(endpoints: AuthorizationServerEndpointsConfigurer?) {
endpoints!!.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager)
}
#Throws(Exception::class)
override fun configure(oauthServer: AuthorizationServerSecurityConfigurer?) {
oauthServer!!.realm(REALM + "/client")
}
companion object {
private val REALM = "MY_OAUTH_REALM"
}
}
My ResourceServerConfiguration looks like the following
#Configuration
#EnableResourceServer
class ResourceServerConfiguration : ResourceServerConfigurerAdapter() {
override fun configure(resources: ResourceServerSecurityConfigurer?) {
resources!!.resourceId(RESOURCE_ID).stateless(false)
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.anonymous().disable()
.requestMatchers().antMatchers("/users/**")
.and().authorizeRequests()
.antMatchers("/users/**").access("hasRole('ADMIN')")
.and().exceptionHandling().accessDeniedHandler(OAuth2AccessDeniedHandler())
}
companion object {
private val RESOURCE_ID = "my_rest_api"
}
}
My OAuth2SecurityConfiguration looks like the following
#Configuration
#EnableWebSecurity
class OAuth2SecurityConfiguration : WebSecurityConfigurerAdapter() {
#Autowired
private val clientDetailsService: ClientDetailsService? = null
#Autowired
#Throws(Exception::class)
fun globalUserDetails(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
.withUser("bill").password("abc123").roles("ADMIN").and()
.withUser("demo").password("1234").roles("USER")
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers("/oauth/token").permitAll()
}
#Bean
#Throws(Exception::class)
override fun authenticationManagerBean(): AuthenticationManager {
return super.authenticationManagerBean()
}
#Bean
fun tokenStore(): TokenStore {
return InMemoryTokenStore()
}
#Bean
#Autowired
fun userApprovalHandler(tokenStore: TokenStore): TokenStoreUserApprovalHandler {
val handler = TokenStoreUserApprovalHandler()
handler.setTokenStore(tokenStore)
handler.setRequestFactory(DefaultOAuth2RequestFactory(clientDetailsService))
handler.setClientDetailsService(clientDetailsService)
return handler
}
#Bean
#Autowired
#Throws(Exception::class)
fun approvalStore(tokenStore: TokenStore): ApprovalStore {
val store = TokenApprovalStore()
store.setTokenStore(tokenStore)
return store
}
}
Any clue about what I'm doing wrong?
The request of token requires Basic Authentication.
Try to add the following option in your request:
-u "client-id:secret"
The request:
curl -X POST -u "client-id:secret" -d "client_id=client-id&client_secret=secret&grant_type=password&username=demo&password=1234" http://localhost:8080/oauth/token