I use spring boot as backend in addition to thymeleaf as the template engine. Atm, I'm trying to implement oauth2 (with keycloak) into my project.
I created a new realm, added a redirect-uri ("http://172.31.52.123:8000/*") + created users and put the id, secret, etc. in my application.properties file.
When I call http://172.31.52.123:8000/ I get the "hi" message from the ViewController below.
When I call http://172.31.52.123:8000/greeting, I will be redirected to http://172.31.52.123:8080/oauth2/authorization/appliance and then to the keycloak login. From there, I get these parameters:
response_type: code
client_id: myClientId
state: hpcfsknjW6QCfMSQWS-k...
redirect_uri: http://172.31.52.123:8080/*
and then these from keycloak again:
state: hpcfsknjW6QCfMSQWS-k...
session_state: f6ca95e5-a117-...
code: 298f32f-f283f ...
After the login, I end up with this:
172.31.52.123 hat Sie zu oft weitergeleitet. -> ERR_TOO_MANY_REDIRECTS
There is nothing in the console. What am I doing wrong? Originally, it should redirect to http://172.31.52.123:8000/*.
application.properties
appliance-base-url: https://authServerBlaBla/auth/realms/myRealmName
spring:
security:
oauth2:
client:
registration:
appliance:
authorizationGrantType: authorization_code
redirectUri: http://172.31.52.123:8080/*
clientId: myClientId
clientSecret: myClientSecret
provider:
appliance:
authorization-uri: ${appliance-base-url}/protocol/openid-connect/auth
token-uri: ${appliance-base-url}/protocol/openid-connect/token
user-info-uri: ${appliance-base-url}/protocol/openid-connect/userinfo
SecurityConfig
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.mvcMatchers("/").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
return http.build();
}
}
ViewController
#Controller
public class ViewController {
#GetMapping(value = {"/"})
#ResponseBody
public String index() {
return "hi";
}
#GetMapping("/greeting")
#ResponseBody
public String greet() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
return "Welcome, " + username;
}
}
You return #ResponseBody => your app should be configured as resource-server (not client).
If your app also serves UI elements (with Thymeleaf, JSF or whatever server-side rendering framework), you'll have to provide two different filter-chains, with securityMatcher patterns to specify where to apply resource-server security and where to apply client one.
I already detailed that in this answer: Use Keycloak Spring Adapter with Spring Boot 3
Details for configuring a Spring backend as both a resource-server (REST API served with #RestController) and a client (server-side rendered HTML with a regular WebMvc #Controller with Thymeleaf, JSF or whatever) in this one of my tutorials.
After having a look in the book:
Keycloak - Identity and Access Management for Modern Applications: Harness the Power of Keycloak, OpenID Connect, and OAuth 2.0 Protocols to Secure Applications
I figured that using Spring Boot + Thymeleaf requires keycloak to identify the application as a client. This might be the case in addition to the authorization code grant type.
All I had to do was the following:
change paths into relative paths (requests url)
remove/add proxies for the communication to work
add the following into your pom:
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-oauth2-client
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>3.0.0</version>
</dependency>
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.0.0</version>
</dependency>
create an application.properties/yaml file with your configuration:
appliance-base-url: https://yourKeycloakInstance/auth/realms/p4udemo
spring:
security:
oauth2:
client:
registration:
democlient:
provider: keycloak
client-id: democlient
client-secret: 73... 324
authorization-grant-type: authorization_code
redirect-uri: "http://yourwebsite:port/login/oauth2/code/"
scope: openid
provider:
keycloak:
authorization-uri: ${appliance-base-url}/protocol/openid-connect/auth
token-uri: ${appliance-base-url}/protocol/openid-connect/token
jwk-set-uri: ${appliance-base-url}/protocol/openid-connect/certs
Add securityConfiguration.java (can be renamed differently):
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.authorizeRequests(authorizeRequests -> authorizeRequests
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
return http.build();
}
}
Related
I've been trying to set up twitter oauth2 PKCE authentification with spring boot. The issue is that the code_verifier parameter is not being picked up by spring security. Do I have to configure a bean so that the code verifier gets picked up? Is there a way to customize a ReactiveOAuth2AccessTokenResponseClient to customize to body send to the token endpoint ?
Here is my spring security config, :
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http) {
return http.authorizeExchange()
.anyExchange().authenticated()
.and().oauth2Login().and().build();
}
security:
oauth2:
client:
registration:
twitter:
client-id: xxx
client-secret: xxx
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/twitter
provider:
twitter:
authorization-uri: https://twitter.com/i/oauth2/authorize?response_type=code&client_id=xxx&redirect_uri=http://localhost:8080/login/oauth2/code/twitter&scope=tweet.read%20users.read%20follows.read%20follows.write&code_challenge=challenge&code_challenge_method=plain
token-uri: https://api.twitter.com/2/oauth2/token
user-info-uri: https://api.twitter.com/2/users/me
user-name-attribute: data
For people still looking for an answer: You can customize the token request body by overriding the WebClientReactiveAuthorizationCodeTokenResponseClient bean and use the setParameter method. Here is an example:
#Bean
public WebClientReactiveAuthorizationCodeTokenResponseClient webClientReactiveAuthorizationCodeTokenResponseClient() {
WebClientReactiveAuthorizationCodeTokenResponseClient webClientReactiveAuthorizationCodeTokenResponseClient =
new WebClientReactiveAuthorizationCodeTokenResponseClient();
webClientReactiveAuthorizationCodeTokenResponseClient.setParametersConverter(source -> {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap();
parameters.add("grant_type", source.getGrantType().getValue());
//...
return parameters;
});
return webClientReactiveAuthorizationCodeTokenResponseClient;
}
I developed backend microservice application using Spring Boot and put API Gateway in front of microservices. To authenticate users I am using Keycloak.
Right now I am developing frontend application using Svelte, I configured my application.yml in gateway application like this:
spring:
cloud:
gateway:
default-filters:
- TokenRelay
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
allowedHeaders: "*"
add-to-simple-url-handler-mapping: true
However, when I am trying to send AJAX request I get the CORS error.
Also I Have spring security (through org.springframework.boot:spring-boot-starter-oauth2-client and org.springframework.boot:spring-boot-starter-oauth2-resource-server dependencies). I defined SecurityWebFilterChain as:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.cors().and()
.authorizeExchange()
.pathMatchers("/actuator/**")
.permitAll()
.and()
.authorizeExchange()
.anyExchange()
.authenticated()
.and()
.oauth2Login(); // to redirect to oauth2 login page.
return http.build();
}
When putting build of frontend in static folder there is no CORS error, but for development I need developer node.js server on localhost on different port.
So, how to fix this cors issue?
You can create a class to define the Cors mapping like this:
#Configuration
#EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}
The example above enables CORS requests from any origin to any endpoint in the application.
To lock this down a bit more, the registry.addMapping method returns a CorsRegistration object, which we can use for additional configuration. There’s also an allowedOrigins method that lets us specify an array of allowed origins. This can be useful if we need to load this array from an external source at runtime.
Additionally, there are also allowedMethods, allowedHeaders, exposedHeaders, maxAge and allowCredentials that we can use to set the response headers and customization options.
CORS With Spring Security:
If you use Spring Security in youproject, you must take an extra step to make sure it plays well with CORS. That's because CORS needs to be processed first. Otherwise, Spring Security will reject the request before it reaches Spring MVC.
Luckily, Spring Security provides an out-of-the-box solution:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()...
}
}
I am using a setup with Keycloak as Identity Provider, Spring Cloud Gateway as API Gateway and multiple Microservices.
I can receive a JWT via my Gateway (redirecting to Keycloak) via http://localhost:8050/auth/realms/dev/protocol/openid-connect/token.
I can use the JWT to access a resource directly located at the Keycloak server (e.g. http://localhost:8080/auth/admin/realms/dev/users).
But when I want to use the Gateway to relay me to the same resource (http://localhost:8050/auth/admin/realms/dev/users) I get the Keycloak Login form as response.
My conclusion is that there must me a misconfiguration in my Spring Cloud Gateway application.
This is the Security Configuration in the Gateway:
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfiguration {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository) {
// Authenticate through configured OpenID Provider
http.oauth2Login();
// Also logout at the OpenID Connect provider
http.logout(logout -> logout.logoutSuccessHandler(
new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
//Exclude /auth from authentication
http.authorizeExchange().pathMatchers("/auth/realms/ahearo/protocol/openid-connect/token").permitAll();
// Require authentication for all requests
http.authorizeExchange().anyExchange().authenticated();
// Allow showing /home within a frame
http.headers().frameOptions().mode(Mode.SAMEORIGIN);
// Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
http.csrf().disable();
return http.build();
}
}
This is my application.yaml in the Gateway:
spring:
application:
name: gw-service
cloud:
gateway:
default-filters:
- TokenRelay
discovery:
locator:
lower-case-service-id: true
enabled: true
routes:
- id: auth
uri: http://localhost:8080
predicates:
- Path=/auth/**
security:
oauth2:
client:
registration:
keycloak:
client-id: 'api-gw'
client-secret: 'not-relevant-but-correct'
authorizationGrantType: authorization_code
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
scope: openid,profile,email,resource.read
provider:
keycloak:
issuerUri: http://localhost:8080/auth/realms/dev
user-name-attribute: preferred_username
server:
port: 8050
eureka:
client:
service-url:
default-zone: http://localhost:8761/eureka
register-with-eureka: true
fetch-registry: true
How can I make the Gateway able to know that the user is authenticated (using the JWT) and not redirect me to the login page?
If you want to make requests to Spring Gateway with access token you need to make it a resource server. Add the following:
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
application.yml
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://.../auth/realms/...
SecurityConfiguration.java
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ReactiveClientRegistrationRepository clientRegistrationRepository) {
// Authenticate through configured OpenID Provider
http.oauth2Login();
// Also logout at the OpenID Connect provider
http.logout(logout -> logout.logoutSuccessHandler(
new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
// Require authentication for all requests
http.authorizeExchange().anyExchange().authenticated();
http.oauth2ResourceServer().jwt();
// Allow showing /home within a frame
http.headers().frameOptions().mode(Mode.SAMEORIGIN);
// Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
http.csrf().disable();
return http.build();
}
I bypassed the problem by communicating directly with Keycloak without relaying requests to it via Spring Cloud Gateway.
That's actually not a workaround but actually best practice/totally ok as far as I understand.
This code is for Client_credentials grant_type. if you use other grant type you need to add client_id and client_secret in request parameters.
public class MyFilter2 extends OncePerRequestFilter {
private final ObjectMapper mapper = new ObjectMapper();
#Value("${auth.server.uri}")
private String authServerUri;
#Value("${client_id}")
private String clientId;
#Value("${client_secret}")
private String clientSecret;
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws IOException {
try {
String token = httpServletRequest.getHeader("Authorization");
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type","application/x-www-form-urlencoded");
headers.set("Authorization",token);
final HttpEntity finalRequest = new HttpEntity("{}", headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.postForEntity(authServerUri,finalRequest,String.class);
if (!HttpStatus.OK.equals(response.getStatusCode())) {
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("status", HttpStatus.UNAUTHORIZED.value());
errorDetails.put("message", "Invalid or empty token");
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
mapper.writeValue(httpServletResponse.getWriter(), errorDetails);
} else {
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}catch(HttpClientErrorException he) {
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("status", HttpStatus.UNAUTHORIZED.value());
errorDetails.put("message", "Invalid or empty token");
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
mapper.writeValue(httpServletResponse.getWriter(), errorDetails);
}catch (Exception exception) {
}
}
I'm trying to retrieve the azure JWT access token from my Spring Boot application from another application by querying a /token endpoint, but the token I receive is seemingly incorrect.
The project has a Spring Boot backend and an Eclipse rcp frontend. I'm attempting to retrieve the access token from the eclipse frontend. For this, I have the controller below:
#Autowired
private OAuth2AuthorizedClientService authorizedClientService;
#GetMapping("/token")
public String user(OAuth2AuthenticationToken authentication) {
OAuth2AuthorizedClient authorizedClient = this.authorizedClientService
.loadAuthorizedClient(authentication.getAuthorizedClientRegistrationId(), authentication.getName());
return authorizedClient.getAccessToken().getTokenValue();
}
Which returns a token with the following format:
PAQABAAAAAABeAFzDwllzTYGDLh_qYbH8hgtbYMB8x7YLamQyQPk_MEXyd9Ckc5epDFQMv3RxjmMie0JDr5uN82U4RFLgU3fnDBxGolo4XVwzLEsTZDmUK_r0YG6ZwLbbQI_ch_Xn8xCxhsFq-AoRbEESDqK3GmK4eXwCYoT0G8_XfZjHTvCNTOMqUb2Q-CD2EalIKf0zSZ5184qrvlXfdNeT_BJdH_tqaodn80Bp2UL2hdnOCDZuWRqKl_2fi4v-eOOKJCcjOqY6SreVEeoKkIvVdayGE8F6qCxFehmlA0sX9sVW34FIVYVo4lDRsTkm-WN2KJwxJmalNcxg0k2ObDnIeC1ulPPpiPq-O_LK9bVA4HEZ63cJi9ZwQHwLPUhOO6TquoCOroHSy5KPoFkX3N796hM1i0NpaaY4MeAx17CSYeZ9P06jvYD7UMTV3OwWt-OVrDm5z_AvbOvyHRf9wjh31H6oLoc-iu_NCspT6NzC2UZQSHBtKdydEcP6sNkRp073jrZEg8UtcVT6HzddIBk2P0tVeIiSyU3SfLETbzJE67xtJVip3ai9aLN28c0qt3rDBaVGDAXjXhqrh5D3NiXdQjS6YTAKy0bVmNk9Yr9o2CGBA2wFjE8OZ6_Hb3k8_13KMJHafx0gAA
Dependencies from pom.xml
Built using spring boot with the following relevant dependencies:
spring-boot-starter-web v2.2.4
azure-active-directory-spring-boot-starter v2.2.1
spring-security-oauth2-client v5.2.1
spring-security-oauth2-jose v5.2.1
spring-security-oauth2-resource-server v5.2.1
Config from application.yml
We support multiple authorization servers, here is the fully configured azure client:
spring:
security:
oauth2:
client:
azure:
client-id: XXX
client-secret: XXX
client-name: Microsoft
scope: openid, https://graph.microsoft.com/user.read, profile
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/azure
client-authentication-method: basic
authentication-method: post
provider:
authorization-uri: https://login.microsoftonline.com/XXX/oauth2/authorize
token-uri: https://login.microsoftonline.com/XXX/oauth2/token
user-info-uri: https://login.microsoftonline.com/XXX/openid/userinfo
jwt-set-uri: https://login.microsoftonline.com/dXXX/discovery/keys
azure:
activedirectory:
tenant-id: XXX
active-directory-groups: XXX
allow-telemetry: false
websecurityconfig.java
#Configuration
#EnableConfigurationProperties
#EnableWebSecurity
#Order(1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
[...]
.anyRequest().authenticated();
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
http.oauth2Login()
.userInfoEndpoint()
.oidcUserService(oidcUserService)
.and()
.authorizationEndpoint();
}
[...]
}
This is how I ended up obtaining the open id token from Azure
#GetMapping("/token")
public String user(OAuth2AuthenticationToken authentication) {
DefaultOidcUser user = (DefaultOidcUser) authentication.getPrincipal();
return user.getIdToken().getTokenValue();
}
I´m trying to upgrade from Spring Boot 2.0 M3 to the current M6 milestone. In Milestone M5 they changed the oAuth2 Client behavior in Spring Security 5.
Now I´m confused how to reconfigure my application to fit the changed oAuth2 implementation using Microsoft Botframework oAuth2 REST API. I´m using the dependency: org.springframework.security:spring-security-oauth2-client in the current version 5.0 RC1.
My current guess is this:
application.yml
spring:
security:
oauth2:
client:
registration:
botframework:
client-id: myClientId
client-secret: myClientSecret
scope: https://api.botframework.com/.default
authorization-grant-type: client_credentials
client-authentication-method: form
provider:
botframework:
token-uri: https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token
Configuration to allow oAuth2 Login
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").permitAll()
.and()
.oauth2Login();
}
Now I need to get the access token to fire a request against the Botframework REST API. Before the version upgrade this was done by using the oAuth2 REST Template. Do I still need this and if yes, how?
Currently the Spring Boot 2.0 M6 auto configuration doesn´t work for me. Any ideas on this topic?
My solution is now:
security:
oauth2:
client:
client-id: ************
client-secret: *************
access-token-uri: https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token
scope: https://api.botframework.com/.default
grant-type: client_credentials
client-authentication-scheme: form
and
#Bean
#Primary
#ConfigurationProperties(prefix = "security.oauth2.client")
public OAuth2ProtectedResourceDetails oauth2RemoteResource() {
return new ClientCredentialsResourceDetails();
}
and everything is allowed for now
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").permitAll();
}
}