Hi I'm trying to add security to a Kotlin Spring Boot project with Java 15 I've started. I want to use Keycloak as Auth Server.
I don't have a frontend yet (REACT app), I'm building the REST API first.
My problem is when I try to hit a protected endpoint instead of having the Keycloak login page I think the Spring Security login page pops up because it says invalid credentials and the looks are a basic form instead of the style Keycloak has for login. I don't know what's missing on my config.
This is the SecurityConfig:
#Configuration
#EnableWebSecurity
#ComponentScan(basePackageClasses = [KeycloakSecurityComponents::class])
internal class SecurityConfig : KeycloakWebSecurityConfigurerAdapter() {
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
val keycloakAuthenticationProvider: KeycloakAuthenticationProvider =
keycloakAuthenticationProvider()
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(
SimpleAuthorityMapper()
)
auth.authenticationProvider(keycloakAuthenticationProvider)
}
#Bean
fun keycloakConfigResolver(): KeycloakSpringBootConfigResolver {
return KeycloakSpringBootConfigResolver()
}
#Bean
override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy {
return RegisterSessionAuthenticationStrategy(
SessionRegistryImpl()
)
}
override fun configure(http: HttpSecurity) {
super.configure(http)
http.authorizeRequests()
.antMatchers("/carts*")
.hasRole("user")
.anyRequest()
.permitAll()
}
}
Controller:
#RestController
#RequestMapping("/carts")
class CartController(private val cartService: CartService) {
#GetMapping("/{id}")
fun getCart(#PathVariable id: String): Cart? {
return cartService.findById(id)
}
}
application-yml:
keycloak:
auth-server-url: http://localhost:8081/auth
realm: TestRealm
resource: login-app
public-client: true
principal-attribute: preferred_username
build.gradle dependencies:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.+")
implementation("org.keycloak:keycloak-spring-boot-starter")
developmentOnly("org.springframework.boot:spring-boot-devtools")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("io.mockk:mockk:1.10.6")
}
dependencyManagement {
imports {
mavenBom("org.keycloak.bom:keycloak-adapter-bom:12.0.4")
}
}
When I do a direct request I get a valid token so I'm assuming Keycloak is working (I'm running it on a docker container at port 8081).
curl --location --request POST 'http://localhost:8081/auth/realms/TestRealm/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=login-app' \
--data-urlencode 'username=user2' \
--data-urlencode 'password=user2' \
--data-urlencode 'grant_type=password'
A request to "localhost:8080/carts/605271fa9f2ad0418ca4858d" redirects to "http://localhost:8080/login" and this shows:
Instead what I'd expect to be similar to this (taken from another example):
Every guide I've seen they are redirected to the Keycloak login out of the box. I'm kind of lost here, any ideas?
Thanks!!
You have to disable the auto configuration for security
use this in your Main class
#EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class})
Lost the afternoon for not adding the package to my SecurityConfig (copied the example from somewhere else and probably overwrote the package definition) class so the component scan was skipping completely this config.
Nevertheless the way I made it work is requesting the token directly to the keycloak url and sending it with the header --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiA...'.
I guess the Keycloak login page is something I have to configure later on from the frontend directly since I'm not using Spring MVC
Related
I'm using Spring Boot 1.5.13 and with that Spring Security 4.2.6 and Spring Security oAuth2 2.0.15.
I want to find a best practice setup for our Spring Boot applications that serve a mixed set of content: A REST API, and some web pages that provide a convenience "landing page" for developers with some links on it, plus Swagger based API documentation, which is also web content.
I have a configuration that allows me to run the app with proper authorization code flow, hence I can access all web content via Browser and get authenticated by the configured IdP (in my case PingFederate), plus I can make API calls from within the Browser, i.e. directly or with a REST Client, e.g. with RESTClient.
This is my security configuration:
#Slf4j
#Configuration
#EnableWebSecurity
#EnableOAuth2Sso // this annotation must stay here!
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login**", "/webjars/**", "/css/**").permitAll()
.antMatchers("/cfhealth").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/protected", "/api/**").authenticated();
}
#Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
}
and the oAuth2 configuration:
#Configuration
#Slf4j
public class OAuth2Config extends ResourceServerConfigurerAdapter {
#Value("${pingfederate.pk-uri}")
String pingFederatePublicKeyUri;
#Autowired
PingFederateKeyUtils pingFederateKeyUtils;
#Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices(tokenServices());
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
String certificate = pingFederateKeyUtils.getKeyFromServer(pingFederatePublicKeyUri);
String publicKey = pingFederateKeyUtils.extractPublicKey(certificate);
converter.setVerifier(pingFederateKeyUtils.createSignatureVerifier(publicKey));
return converter;
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
}
But when I want to call a REST API programmatically/outside the Browser with a bearer token in the header, e.g. with curl, the authorization code flow kicks in and redirects to the local login endpoint. What I want is that API calls accept the bearer token for authentication, without creating a session, and that all web content/mvc calls in the Browser establish a session.
curl -i -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" -X GET http://localhost:8080/authdemo/api/hello
Adding the #EnableResourceServer annotation to the above SecurityConfig class (and adding security.oauth2.resource.filter-order=3 in the application properties file, I can make the curl command work, but then the authorization code flow is broken, I get the following output in the Browser for all URLs in my application:
<oauth>
<error_description>
Full authentication is required to access this resource
</error_description>
<error>unauthorized</error>
</oauth>
Now is there a way to get this szenario working nicely? If yes, how would that look like? Or is it only supported in later versions of Spring Boot+Security+oAuth2?
The question at Spring Boot with Security OAuth2 - how to use resource server with web login form? is quite similar
I found the solution: It takes multiple HttpSecurity configurations. I found out by reading the great article written by Matt Raible at https://developer.okta.com/blog/2018/02/13/secure-spring-microservices-with-oauth where he introduced me to the notion of requestMatchers(.). This is how I finally implemented it:
#Configuration
#EnableResourceServer
#EnableWebSecurity(debug = true)
#EnableOAuth2Sso
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(new RequestHeaderRequestMatcher("Authorization"))
.authorizeRequests().anyRequest().fullyAuthenticated();
}
}
With that I can access the service with a Browser, leading to a authorization code flow. But accessing the API (or actually any part of the service) leads to a validation of the provided Bearer token.
And to illustrate the way how some endpoints can be exluded/made public in such a case, here's how I configure the actuator endpoints and one very simple 'ping' endpoint I've added myself:
#Configuration
#Order(1)
public class ActuatorSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new OrRequestMatcher(EndpointRequest.to("health", "info"),
new AntPathRequestMatcher("/cfhealth"))).authorizeRequests().anyRequest().permitAll();
}
}
And my implementation of the /cfhealth endpoint:
#Controller
#Slf4j
public class MainController {
#GetMapping(value = "/cfhealth")
#ResponseBody
public String cfhealth() {
return "ok";
}
}
I'm happy to learn from others if that's the best practice way of Spring Security configuration or if there are better ways to do it. I've spent quite some time on the topic in the last few weeks on it, and it takes quite some effort to grasp the basic Spring Security concepts.
I have a Spring Boot 2 app with Spring security, as follow:
#SpringBootApplication(exclude = [(SecurityAutoConfiguration::class)])
class UntappdCqrsApplication
fun main(args: Array<String>) {
runApplication<UntappdCqrsApplication>(*args)
}
and the configuration class
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
class TokenConfiguration(
val jwtTokenProvider: JwtTokenProvider
) : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.antMatchers("/users/signup").permitAll()
.anyRequest().authenticated()
http.apply(JwtTokenConfigurer(jwtTokenProvider));
}
}
There are two endpoints: POST users/signup and GET users/test.
According to my configuration, /signup should not require authentication and /test should, but both endpoints are accessible without any authentication.
If I add #EnableWebSecurity in my TokenConfiguration class, Spring now generates a default password and both endpoints are now protected.
I think I'm missing something here, but I have no idea what
You haven't provided the source or imports for your JwtTokenProvider or JwtTokenConfigurer classes, but it seems likely that your JwtTokenProvider is throwing an unchecked exception or even directly sending a response on authentication failure. This will prevent permitAll() from ever being triggered.
See my response to a similar question:
https://stackoverflow.com/a/46086769/873590
I'm trying to enable OAuth2 Password Flow in my Spring Boot application, however, I just can't get it to run, no matter what I try.
There following are my configurations.
For the TokenStore I configured a JdbcTokenStore bean and the UserDetailsService is simply loading the user from my custom user tables.
I also added the the property security.oauth2.resource.filter-order=3.
For Spring Boot I'm using 2.0.0.RELEASE and for Spring Security 2.3.0.RELEASE.
Both the authorization server and the resource server are in a single application.
The following POST (with SoapUI) keeps failing:
http://localhost:8080/oauth/token?grant_type=password&username=hans.wurst&password=bla&client_id=androidAppClient
I get the message
Access is denied (user is anonymous);
Debug tells me that the following filters were applied:
Security filter chain: [
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
LogoutFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
]
Authorization server:
#Configuration
#EnableAuthorizationServer
class AuthorizationServerConfiguration : AuthorizationServerConfigurerAdapter() {
#Autowired
private lateinit var dataSource: DataSource
#Autowired
private lateinit var tokenStore: TokenStore
#Autowired
#Qualifier("authenticationManagerBean")
private lateinit var authenticationManager: AuthenticationManager
override fun configure(clients: ClientDetailsServiceConfigurer) {
clients.jdbc(dataSource)
}
override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore)
}
}
Web security:
#Configuration
class WebSecurityConfiguration : WebSecurityConfigurerAdapter() {
#Autowired
#Qualifier(USER_DETAILS_SERVICE)
private lateinit var userDetailsService: UserDetailsService
#Bean
#Throws(Exception::class)
override fun authenticationManagerBean(): AuthenticationManager = super.authenticationManagerBean()
#Throws(Exception::class)
override fun configure(web: WebSecurity) {
web.debug(true)
}
override fun configure(auth: AuthenticationManagerBuilder) {
auth.userDetailsService(userDetailsService)
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest()
.authenticated()
}
}
Resource server:
#Configuration
#EnableResourceServer
class ResourceServerConfiguration : ResourceServerConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.antMatchers("/registration", "/customer/check")
.anonymous()
.anyRequest()
.authenticated()
}
}
User details service:
const val USER_DETAILS_SERVICE = "userDetailsService";
#Transactional
#Service(USER_DETAILS_SERVICE)
class CustomerUserDetailsService #Autowired constructor(private val customerRepository: CustomerRepository) : UserDetailsService {
#Throws(UsernameNotFoundException::class)
override fun loadUserByUsername(username: String): UserDetails =
customerRepository
.findCustomerCredentialsByUserName(username)
?.let { User(it.userName, it.password, emptyList()) }
?: throw UsernameNotFoundException("Could not find customer with username $username")
}
It appears that client androidAppClient doesn't have password grant type set in authorized_grant_types.
Check client details table in DB (oauth_client_details), make sure password grant type is set for this client. If it is set along with other grant types then make sure there are no spaces in grant types like, ('password,refresh_token').
The /oauth/token endpoint's post request expects client credentials in Base64 "clientId:clientSecret" format.
curl --request POST \
--url http://localhost:8443/auth-service/oauth/token \
--header 'authorization: Basic UkVXQVJEU19QT1QUxfQURNSU46cGFzc3dvcmQ=' \
--header 'content-type: multipart/form-data; boundary=---011000010111000001101001' \
--form username=testuser \
--form password=password \
--form grant_type=password
So I have a Spring Boot application and I am sending a request to it using PostMan. It is using Spring Security along with JWT for authentication. I'm trying to get authorization to work but am running into issues. Spring is able to login the user and return a token fine. But when I put the token in the header it's not working at all. I get no response from the server. When the token is removed, it works fine. Right now all requests should be able to go through regardless of being logged in or not.
My Spring Web Configuration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
The REST path I'm trying to access:
#RestController("threadService")
#RequestMapping("/api/thread")
public class ThreadService {
#RequestMapping(value="/list", method=RequestMethod.GET)
public List<ThreadDetails> getThreadList() {
logger.info("getThreadList");
return threadDao.getThreadList();
}
}
The failed GET request I'm issuing after I have logged in and gotten a token:
GET /api/thread/list HTTP/1.1
Host: localhost:8080
Authorization : Bearer (JWT token here)
Cache-Control: no-cache
Postman-Token: 69565839-4806-b4f6-9a03-11382a80c7da
The above request works fine when there is no Authorization in the header.
Not sure it is exactly the problem I was facing.
When I want to communicate with the restservice exposed by spring boot application, the "Authorization" is not set. I followed the steps which are required to communicate but the value wont be passes through header.
The solution I found, the "common-codec" library was missing. Once I add the dependency in my web application, it start sending the "Authorization" in header to my spring boot application.
Hope this helps to someone.
I am using spring security oauth in my project. I am excluding some urls from authentication by configuring in spring security ResourceServerConfigurerAdapter. I added http.authorizeRequests().antMatchers(url).permitAll().
Now, what I am seeing is that, if I don't pass the Authorization header to these urls, it is not authenticated. And the API is called properly.
If the call is made with an Authorization header, then it validates the token and fails the call if the token is not validated.
My question is what do I need to do so that the token is ignored in the request for which I have permitAll.
Spring OAuth2 will intercept all url with header: Authorization Bearer xxx.
To avoid Spring OAuth2 from intercept the url. I have created a SecurityConfiguration which has higher order than Spring OAuth2 configuration.
#Configuration
#EnableWebSecurity
#Order(1) // this is important to run this before Spring OAuth2
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
List<RequestMatcher> requestMatchers = new ArrayList<RequestMatcher>();
// allow /api/public/product/** and /api/public/content/** not intercepted by Spring OAuth2
requestMatchers.add(new AntPathRequestMatcher("/api/public/product/**"));
requestMatchers.add(new AntPathRequestMatcher("/api/public/content/**"));
http
.requestMatcher(new OrRequestMatcher(requestMatchers))
.authorizeRequests()
.antMatchers("/api/public/product/**", "/api/public/content/**").permitAll()
}
}
The above configuration allows /api/public/product/** and /api/public/content/** to be handled by this configuration, not by Spring OAuth2 because this configuration has higher #Order.
Therefore, even setting invalid token to above api call will not result in invalid access token.
As per spring-oauth2 docs https://projects.spring.io/spring-security-oauth/docs/oauth2.html
Note: if your Authorization Server is also a Resource Server then there is another security filter chain with lower priority controlling the API resources. Fo those requests to be protected by access tokens you need their paths not to be matched by the ones in the main user-facing filter chain, so be sure to include a request matcher that picks out only non-API resources in the WebSecurityConfigurer above.
So define WebSecurityConfigurer implementation with higher order than ResourceServerConfig.
In case you are dealing with Reactive Spring webflux, from SooCheng Koh's answer.
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
#Order(1) // this is important to run this before Spring OAuth2
public class PublicSecurityConfiguration {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.pathMatchers("/api/public/**").permitAll();
return http.build();
}
}
It's not a bug it's a feature :)
As already mentioned by other people, even if you have permitAll, Spring Security will still check the token if there is a header "Authorization".
I don't like the workaround on the backend with Order(1) so I did a change on the frontend simply removing the header "Authorization" for the specific request.
Angular example with interceptor:
#Injectable()
export class PermitAllInterceptor implements HttpInterceptor {
constructor() {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if(req.url.includes('permitAllUrl')){
req = req.clone({ headers: req.headers.delete('Authorization') });
}
return next.handle(req);
}
}
and then just register the interceptor in app.module.ts:
{
provide: HTTP_INTERCEPTORS,
useClass: PermitAllInterceptor ,
multi: true
}