OAuth2 with Google and Spring Boot - I can't log out - spring

I've been trying to get a successful Oauth2 login with Google and Spring Boot for a while now. This only works partially. Why partly - because I can't manage the logout or when I pressed the logout button I see an empty, white browser page with my URL (http://localhost:8181/ben/"). After a refresh of the page I get error from google, but if I open a new tab, enter my url, I'm still logged in to google, because I can see my user, which I'm outputting to my react application.
#SpringBootApplication
#EnableOAuth2Sso
#RestController
#CrossOrigin
public class SocialApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(SocialApplication.class, args);
}
#RequestMapping("/user")
public Principal user(Principal principal) {
return principal;
}
#RequestMapping("/logout")
public String fetchSignoutSite(HttpServletRequest request, HttpServletResponse response) {
Cookie rememberMeCookie = new Cookie("JSESSIONID", "");
rememberMeCookie.setMaxAge(0);
response.addCookie(rememberMeCookie);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
}
auth.getPrincipal();
return "redirect:/ben/login";
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests().antMatchers("/ben/*").permitAll().anyRequest().authenticated().and()
.logout().logoutSuccessUrl("http://localhost:8181/ben/login").invalidateHttpSession(true)
.clearAuthentication(true).deleteCookies("JSESSIONID");
}
My application.yml file looks like this:
# Spring Boot configuration
spring:
profiles:
active: google
# Spring Security configuration
security:
oauth2:
client:
clientId: 415772070383-3sapp4flauo6iqsq8eag7knpcii50v9k.apps.googleusercontent.com
clientSecret: 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
accessTokenUri: https://www.googleapis.com/oauth2/v4/token
userAuthorizationUri: https://accounts.google.com/o/oauth2/v2/auth
clientAuthenticationScheme: form
scope:
- email
- profile
resource:
userInfoUri: https://www.googleapis.com/oauth2/v3/userinfo
preferTokenInfo: true
# Server configuration
server:
port: 8181
servlet:
context-path: /ben

That fetchSignoutSite only emptying the JsessionId and logging out from Spring Security context. So you would still need to add part where you go to google and sign out from there which I have no experience on implementation.

Related

External keycloak server and Spring boot app behind reverse proxy - redirect to root context after success login

My spring boot service is working behind reverse proxy and secured by external keycloak server.
After successful login at Keycloak server it redirects me to my service and then I get redirect to root of context path instead of initial url.
So request chain is looks like:
initial url: http://~HOSTNAME~/~SERVICE-NAME~/rest/info/654321
and redirects:
http://~HOSTNAME~/~SERVICE-NAME~/rest/sso/login
https://ext-keycloak.server/auth/realms/test/protocol/openid-connect/auth?response_type=code&client_id=dev&redirect_uri=http%3A%2F%2F~HOSTNAME~%2F~SERVICE-NAME~%2Frest%2Fsso%2Flogin&state=60ebad0d-8c68-43cd-9461&login=true&scope=openid
http://~HOSTNAME~/~SERVICE-NAME~/rest/sso/login?state=60ebad0d-8c68-43cd-9461&session_state=074aaa0d-4f72-440e&code=a8c92c50-70f8-438c-4fe311f0b3b6.074aaa0d-440e-8726.8166b689-bbdd-493a-8b8f
http://~HOSTNAME~/~SERVICE-NAME~/rest/ - I have no handlers here and getting error.
First problem was that application generated wrong redirect uri for keycloak. All services are in kubernetes cluster and have urls like: http://~HOSTNAME~/~SERVICE-NAME~/rest (where '/rest' is context path).
~SERVICE-NAME~ part is used to locate service in cluster and application gets request without this prefix. But proxy adds header X-Original-Request with original url and I decided to use it (unfortunately I can't change configuration of proxy and keycloak servers). I made filter to use header value to generate correct redirect uri by copy-pasting from Spring's org.springframework.web.filter.ForwardedHeaderFilter. Now it generates correct redirect_uri but I'm getting wrong redirect at the end as described above.
How can I get redirect to initial page in this case?
Spring security config:
#EnableWebSecurity
#ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
private final PermissionConfig permissionConfig;
#Autowired
public SecurityConfig(PermissionConfig permissionConfig) {
this.permissionConfig = permissionConfig;
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new NullAuthoritiesMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
var urlRegistry = http.authorizeRequests()
.antMatchers("/actuator/**")
.permitAll()
.antMatchers("/info/**")
.hasAnyAuthority(permissionConfig.getRoles().toArray(new String[0]));
}
#Bean
public FilterRegistrationBean<OriginalUriHeaderFilter> originalUriHeaderFilter() {
OriginalUriHeaderFilter filter = new OriginalUriHeaderFilter();
FilterRegistrationBean<OriginalUriHeaderFilter> registration = new FilterRegistrationBean<>(filter);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR);
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}
spring keycloak config (yaml)
keycloak:
auth-server-url: 'https://ext-keycloak.server/auth/'
realm: test
ssl-required: NONE
resource: dev
credentials:
secret: 'hex-value'
confidential-port: 0
disable-trust-manager: true
For future searchs, an another approach is to use the KeycloakCookieBasedRedirect.createCookieFromRedirectUrl method, that includes a cookie for keycloak redirect.
Uh, the problem was with service prefix not with Keycloak.
When I tryed to get page Spring set JSESSIONID cookie with path=/rest, stored request to session and redirected me to Keycloak. After login Spring couldn't find session and redirected me to root context because browser didn't provide JSESSIONID cookie for path /~SERVICE-NAME~/rest !!
By default Spring sets cookie path=server.servlet.contextPath. All I've done just added cookie path to application.yaml:
server:
port: 8080
servlet:
contextPath: /rest
session:
cookie:
path: /
Spring Server Properties
Spring DebugFilter is quite useful thing

Spring Cloud Gateway redirects to Keycloak login page although Bearer token is set

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) {
}
}

Spring security oauth 2 client app can't check token validity

here is what I'm trying to achieve :
I have an enterprise oauth 2 provider, I want to use their login form and get code, access token and so on from this provider
here is my conf
security:
oauth2:
client:
clientId: MY_CLIENT_ID
clientSecret: asecret
accessTokenUri: https://blabla/oauth-server/oauth/token
userAuthorizationUri: https://blabla/oauth-server/oauth/authorize
tokenName: access_token
scope : read
userInfoUri: https://localhost/user
I can get my code which is changed for an access token, everything is fine, it's calling my local endpoint to get user information (roles for instance)
but when I debug the code I can't see any expires_in value anywhere and my token doesn't expire at all.
here's my resource server conf
#EnableResourceServer
#EnableOAuth2Sso
#RestController
public class SecurityController extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "my_rest_api";
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID).stateless(true);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers().anyRequest().and().authorizeRequests();
http.
anonymous().disable()
.requestMatchers().antMatchers("/**/*")
.and().authorizeRequests()
.antMatchers("/**/*").access("#oauth2.hasScope('read')")
.and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
}
}
I can't fidn out how to revoke the token
any idea is welcome, i've lost a bunch of hour reading tutorials ...
Add access-token-validity-seconds to your yaml
security:
oauth2:
client:
clientId: MY_CLIENT_ID
clientSecret: asecret
accessTokenUri: https://blabla/oauth-server/oauth/token
userAuthorizationUri: https://blabla/oauth-server/oauth/authorize
tokenName: access_token
scope : read
userInfoUri: https://localhost/user
access-token-validity-seconds: 30 //Adds 30 seconds of token validity

Spring Zuul Eureka Security Authentication get user info from Zuul

I'm using #EnableOAuth2Sso to authenticate a user with a third party authentication server on the Zuul server. I need to pass user info from Zuul to the routed servers. I've set up the request endpoint /userinfo to return a jsonified representation of a flattened version of the userinfo from the third party. How do I get this userinfo to one of the resource servers?
What I've tried so far:
I've tried making a request using the #LoadBalanced #Bean RestTemplate been. However, I get redirected to the third party for authorization. The sensitive-headers is set to none. I've checked which headers were going through:
["upgrade-insecure-requests","user-agent","accept","accept-language","cookie",
"authorization","x-forwarded-host","x-forwarded-proto",
"x-forwarded-prefix","x-forwarded-port","x-forwarded-for","accept-encoding",
"content-length", "host","connection"]
So, then I tried using #LoadBalanced #Bean OAuth2RestTemplate. I had to set the config security.basic.enabled=false to prevent the Authentication User Login Prompt from appearing. This produces UserRedirectRequiredException
Resource Server
#RequestMapping(value = "/test", method = RequestMethod.GET)
public ResponseEntity<String> test3() {
return restTemplate.getForEntity("http://zuul-server/userinfo", String.class);
}
Zuul Server
#RequestMapping(value = "/userinfo", produces = MediaType.APPLICATION_JSON_VALUE)
public User getInfo(OAuth2Authentication auth) {
return service.getUser(auth); // Returns User Object
}
Additional Notes
The Resource Server has not been annotated with #EnableResourceServer. If it was, a forwarded request will result in Invalid access token error message.
This is what I have working on our system for passing Oauth2 JWT tokens.
#Configuration
#EnableResourceServer
public class JwtSecurityConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.antMatchers("/**").hasAuthority("ROLE_API")
.and()
.csrf().disable();
}
}
And the config portion you might need.
services:
path: /services/**
serviceId: services
stripPrefix: false
sensitiveHeaders: true
auth:
path: /oauth/**
serviceId: saapi-auth-server
stripPrefix: false
sensitiveHeaders: true
There was very little documentation on this. So it was really just hacking away at it until I could get it to pass tokens on.

Spring OAuth2 ResourceServer external AuthorizationServer

How do you setup a separate Spring OAuth2 ResourceServer only, that uses and 3rd party AuthorizationServer
All examples I see always implement the ResourceServer and AuthorizationServer in same application.
I don't want to implement the AuthorizationServer as someone else is going to provide this.
Have tried with no luck
#Configuration
#EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter
And application.yml includes
security:
oauth2:
resource:
userInfoUri: https://...../userinfo
Adding to my question some further details::
In my understanding - with OAuth there are 4 players:
resource owner: a person
resource server: server exposing a protected API (protected by the authentication server)
authentication server: the server that handles issuing access tokens to clients
client: an application (say website) accessing the resource server API's after resource owner have given consent
I have tried various tutorials, but all seem to implement their own Authorisation server
http://www.swisspush.org/security/2016/10/17/oauth2-in-depth-introduction-for-enterprises
https://gigsterous.github.io/engineering/2017/03/01/spring-boot-4.html
or are examples of implementing the client player
http://www.baeldung.com/spring-security-openid-connect
https://spring.io/guides/tutorials/spring-boot-oauth2/
My Question is:
How do I implement just the Resource Server which secures my REST API, via a 3rd party authentication server, nothing more.
I have work this out - all you need is:
#SpringBootApplication
#EnableResourceServer
public class ResourceServer {
public static void main(String[] args) {
SpringApplication.run(ResourceServer.class, args);
}
}
With the application.yml as posted in the original question of:
security:
oauth2:
resource:
userInfoUri: https://........userinfo
I've created two sample separate applications, one of them acting as oauth client, and another one acting as a resource server, and both of them are using an external authentication server (which is facebook in this example).
The scenario in the example is as follows, the user opens app1 (oauth client) and gets redirected to first page, and once he clicks login, he'll be redirected to facebook login, and after a successful login, he will get back to the first page. If he clicked on the first button, a call to an api within the same application will be made, and will appear beside message 1 label, and if he clicked on the second button, a call to an api within app2 (resource server) will be made, and the message will be displayed beside message 2 label.
If you checked the logs, you will find the api call going from app1 to app2 containing the access token in the request parameters.
Logs for app1 calling app2
Please find the source code on the git repository here
This is the configuration for app1 (oauth client)
app1 web security config
#Configuration
#EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests().antMatchers("/", "/login**", "/webjars/**", "/error**").permitAll()
.anyRequest().authenticated().and().logout().logoutSuccessUrl("/").permitAll().and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
config.addAllowedMethod("PUT");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
app1 application properties
security:
oauth2:
client:
clientId: <your client id>
clientSecret: <your client secret>
accessTokenUri: https://graph.facebook.com/oauth/access_token
userAuthorizationUri: https://www.facebook.com/dialog/oauth?redirect_url=https://localhost:8443/
tokenName: access_token
authenticationScheme: query
clientAuthenticationScheme: form
registered-redirect-uri: https://localhost:8443/
pre-established-redirect-uri: https://localhost:8443/
resource:
userInfoUri: https://graph.facebook.com/me
logging:
level:
org.springframework.security: DEBUG
This is the configuration for app2 (resource server)
app2 resource server config
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
String[] ignoredPaths = new String[] { "/error", "/login", "/doLogut", "/home", "/pageNotFound", "/css/**",
"/js/**", "/fonts/**", "/img/**" };
#Value("${security.oauth2.resource.user-info-uri}")
private String userInfoUri;
#Value("${security.oauth2.client.client-id}")
private String clientId;
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(ignoredPaths).permitAll().anyRequest().authenticated();
}
#Primary
#Bean
public UserInfoTokenServices tokenService() {
final UserInfoTokenServices tokenService = new UserInfoTokenServices(userInfoUri, clientId);
return tokenService;
}
}
app2 application properties
security:
oauth2:
resource:
userInfoUri: https://graph.facebook.com/me
client:
client-id: <your client id>
logging:
level:
org.springframework.security: DEBUG
This is where app1 controller calls an api on app2 (hi2 api)
#RestController
#CrossOrigin(origins = "*", allowedHeaders = "*")
public class UserController {
#Autowired
OAuth2RestTemplate restTemplate;
#RequestMapping("/user")
public Principal user(Principal principal) {
return principal;
}
#RequestMapping("/hi")
public String hi(Principal principal) {
return "Hi, " + principal.getName();
}
#RequestMapping("/hi2")
public String hi2(Principal principal) {
final String greeting = restTemplate.getForObject("http://127.0.0.1:8082/api/hello", String.class);
System.out.println(greeting);
return greeting;
}
}

Resources