How to persist OAuth2AuthorizedClient in redis-session - spring-boot

My project uses redis session with springboot session and spring security 5.1.10. I just migrated the old oauth2 implementation. Before, when I restarted the app I still had the access_token and refresh_token. With this implementation the user is logged in, but I loose the AuthorizedClients so loadAuthorizedClient function returns null after restarting. Also in production we have many containers with the same app. Is there any springboot stardard way to achieve this? like register some bean or something.
application.yml
...
session:
store-type: redis
redis:
namespace: spring:session:${spring.application.name}
redis:
host: ${redissession.host}
password: ${redissession.password}
port: ${redissession.port}
security:
oauth2:
client:
registration:
biocryptology:
provider: example
client-id: client
client-secret: xxx
client-authentication-method: basic
authorization-grant-type: authorization_code
redirect-uri-template: "{baseUrl}/login"
scope:
- openid
provider:
example:
issuer-uri: https://....
...
Controller.java
#Autowired
private OAuth2AuthorizedClientService clientService;
#GetMapping("/user")
public String getOidcUserPrincipal() throws InvalidSessionException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication.getPrincipal() instanceof OidcUser)) {
throw new InvalidSessionException();
}
OidcUser principal = ((OidcUser) authentication.getPrincipal());
LOG.info("oidc: {}", principal.getName());
OAuth2AuthenticationToken oauth2Token = (OAuth2AuthenticationToken) authentication;
LOG.info("authentication: {}", oauth2Token);
OAuth2AuthorizedClient client = clientService
.loadAuthorizedClient(oauth2Token.getAuthorizedClientRegistrationId(), authentication.getName());
LOG.info("client: {}", client);
return "logged";
}
The goal is getting the access_token and refresh_token across containers, any other way without OAuth2AuthorizedClientService maybe?
EDIT:
<!-- Spring -->
<spring-cloud.version>Greenwich.SR5</spring-cloud.version>

Registering a bean did the trick, it saves it in session, but then OAuth2AuthorizedClientService breaks down for every case and needs a workaround searching in session directly or using OAuth2AuthorizedClientRepository autowired:
#Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository() {
return new HttpSessionOAuth2AuthorizedClientRepository();
}
controller.java
#Autowired
private OAuth2AuthorizedClientRepository clientRepository;
#GetMapping("/user")
public Map<String, Object> getOidcUserPrincipal(HttpServletRequest request) throws InvalidSessionException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication.getPrincipal() instanceof OidcUser)) {
throw new InvalidSessionException();
}
OidcUser principal = ((OidcUser) authentication.getPrincipal());
OAuth2AuthorizedClient client = clientRepository
.loadAuthorizedClient(oauth2Token.getAuthorizedClientRegistrationId(), authentication, request);
LOG.info("client: {}", client);
if (Objects.nonNull(client)) {
String token = client.getAccessToken().getTokenValue();
String refreshtoken = client.getRefreshToken().getTokenValue();
LOG.info("token: {} {}", token, refreshtoken);
}
return principal.getClaims();
}

Related

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

How to configure multiple OAuth2RestTemplates for different services?

How can I configure multiple OAuth2RestTemplates (via OAuth2ProtectedResourceDetails) using Spring Boot so that I can access multiple APIs. They are all configured in same tenant as we see with all configuration being the same except for the scopes.
I believe I did read you cannot have multiple scopes because each JWT token is resource specific but I cannot see examples of having multiple RestTemplates.
Thank you!
security:
oauth2:
client:
client-id: x
client-secret: y
user-authorization-uri: z
access-token-uri: a
scope: B
grant-type: client_credentials
client2:
client-id: x
client-secret: y
user-authorization-uri: z
access-token-uri: a
scope: Q
grant-type: client_credentials
#Bean(name="ngsWbRestTemplate")
public OAuth2RestTemplate buildNgsWbRestTemplate(
OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails
){
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(oAuth2ProtectedResourceDetails);
restTemplate.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
restTemplate.getAccessToken().getValue();
return restTemplate;
}
#Bean(name="OdpRestTemplate")
public OAuth2RestTemplate buildOdpRestTemplate(
OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
#Value("#{propertyService.getValue('ODP_BASE_URI')}") String odpBaseUri
){
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(oAuth2ProtectedResourceDetails);
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(odpBaseUri));
restTemplate.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
// test access token retrieval
restTemplate.getAccessToken().getValue();
return restTemplate;
}
I recently made a client that integrates the information of multiple providers who protect their APIs with the OAUth2 protocol. I used this dependency:
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
In the configuration.yml file you have to set all the properties needed for your client in order to get tokens from Authorization Servers.
example:
oauth2:
okta:
clientId: XXX-XXX-XXX
clientSecret: XXX-XXX-XXX
grantType: client_credentials
accessTokenUri: https://dev-xxxx.okta.com/oauth2/default/v1/token
scope: custom_service #Created in okta
keycloak:
clientId: app-resource-server
clientSecret: 60bc378c-5c95-4dee-b525-e71993d1596d
grantType: password #For password grant_type
username: user
password: user
accessTokenUri: http://localhost:9001/auth/realms/development/protocol/openid-connect/token
scope: openid profile email
In the main class you need to create a different bean for each resource server that you'd like to send requests with its corresponding access_token in the Authorization header.
#SpringBootApplication
public class DemoApplication {
/* Inject your client properties into ClientCredentialsResourceDetails object
if you need to get tokens using client_credentials grant type*/
#Bean
#ConfigurationProperties("example.oauth2.okta")
protected ClientCredentialsResourceDetails oktaOAuth2Details() {
return new ClientCredentialsResourceDetails();
}
/*Inject your client properties into ResourceOwnerPasswordResourceDetails object
if you need to pass an username and a password*/
#Bean
#ConfigurationProperties("example.oauth2.keycloak")
protected ResourceOwnerPasswordResourceDetails keycloakOAuth2Details() {
return new ResourceOwnerPasswordResourceDetails();
}
//Create the OAuth2RestTemplate bean with the corresponding clientOAuth2Details
#Bean("oktaOAuth2RestTemplate")
protected OAuth2RestTemplate oktaOAuth2RestTemplate() {
return new OAuth2RestTemplate(oktaOAuth2Details());
}
#Bean("keycloakOAuth2RestTemplate")
protected OAuth2RestTemplate keycloakOAuth2RestTemplate() {
return new OAuth2RestTemplate(keycloakOAuth2Details());
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Then you can autowire the OAuth2RestTemplate choosing the desired implementation with the #Qualifier annotation as shown below
#Component
public class AsyncService {
//#Value("#{ #environment['example.baseUrl'] }")
private static final String API_URL1 = "http://localhost:8081";
private static final String API_URL2 = "http://localhost:8082";
private final Logger log = LoggerFactory.getLogger(AsyncService.class);
#Autowired
#Qualifier("oktaOAuth2RestTemplate")
OAuth2RestTemplate oktaOAuth2RestTemplate;
#Autowired
#Qualifier("keycloakOAuth2RestTemplate")
OAuth2RestTemplate keycloakOAuth2RestTemplate;
public void oktaRequest() {
log.info("Okta access_token: {}", oktaOAuth2RestTemplate.getAccessToken());
log.info(oktaOAuth2RestTemplate.getForObject(API_URL1 + "/message", String.class));
}
public void keylcloakRequest() {
log.info("Keycloak access_token: {}", keycloakOAuth2RestTemplate.getAccessToken());
log.info(keycloakOAuth2RestTemplate.getForObject(API_URL2 + "/message", String.class));
}
//#Async
public void requestMessage() {
oktaRequest();
keycloakRequest();
}
}
Spring will refresh the tokens automatically when they expire and that's so cool.

Create route in Spring Cloud Gateway with OAuth2 Resource Owner Password grant type

How to configure a route in Spring Cloud Gateway to use an OAuth2 client with authorization-grant-type: password? In other words, how to add the Authorization header with the token in the requests to an API? Because I'm integrating with a legacy application, I must use the grant type password.
I have this application:
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("route_path", r -> r.path("/**")
.filters(f -> f.addRequestHeader("Authorization", "bearer <token>"))
.uri("http://localhost:8092/messages"))
.build();
}
}
Replacing the <token> with an actual token, everything just works fine.
I found this project that does something similar: https://github.com/jgrandja/spring-security-oauth-5-2-migrate. It has a client (messaging-client-password) that is used to configure the WebClient to add OAuth2 support to make requests (i.e. by adding the Authorization header).
We can't use this sample project right away because Spring Cloud Gateway is reactive and the way we configure things changes significantly. I think to solve this problem is mostly about converting the WebClientConfig class.
UPDATE
I kinda make it work, but it is in very bad shape.
First, I found how to convert WebClientConfig to be reactive:
#Configuration
public class WebClientConfig {
#Bean
WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth.setDefaultOAuth2AuthorizedClient(true);
oauth.setDefaultClientRegistrationId("messaging-client-password");
return WebClient.builder()
.filter(oauth)
.build();
}
#Bean
ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.refreshToken()
.password()
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// For the `password` grant, the `username` and `password` are supplied via request parameters,
// so map it to `OAuth2AuthorizationContext.getAttributes()`.
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
ServerWebExchange serverWebExchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName());
String username = serverWebExchange.getRequest().getQueryParams().getFirst(OAuth2ParameterNames.USERNAME);
String password = serverWebExchange.getRequest().getQueryParams().getFirst(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
// `PasswordOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return Mono.just(contextAttributes);
};
}
}
With this configuration, we can use the WebClient to make a request. This somehow initializes the OAuth2 client after calling the endpoint:
#GetMapping("/explicit")
public Mono<String[]> explicit() {
return this.webClient
.get()
.uri("http://localhost:8092/messages")
.attributes(clientRegistrationId("messaging-client-password"))
.retrieve()
.bodyToMono(String[].class);
}
Then, by calling this one we are able to get the reference to the authorized client:
private OAuth2AuthorizedClient authorizedClient;
#GetMapping("/token")
public String token(#RegisteredOAuth2AuthorizedClient("messaging-client-password") OAuth2AuthorizedClient authorizedClient) {
this.authorizedClient = authorizedClient;
return authorizedClient.getAccessToken().getTokenValue();
}
And finally, by configuring a global filter, we can modify the request to include the Authorization header:
#Bean
public GlobalFilter customGlobalFilter() {
return (exchange, chain) -> {
//adds header to proxied request
exchange.getRequest().mutate().header("Authorization", authorizedClient.getAccessToken().getTokenType().getValue() + " " + authorizedClient.getAccessToken().getTokenValue()).build();
return chain.filter(exchange);
};
}
After running this three requests in order, we can use the password grant with Spring Cloud Gateway.
Of course, this process is very messy. What still needs to be done:
Get the reference for the authorized client inside the filter
Initialize the authorized client with the credentials using contextAttributesMapper
Write all of this in a filter, not in a global filter. TokenRelayGatewayFilterFactory implementation can provide a good help to do this.
I implemented authorization-grant-type: password using WebClientHttpRoutingFilter.
By default, spring cloud gateway use Netty Routing Filter but there is an alternative that not requires Netty (https://cloud.spring.io/spring-cloud-gateway/reference/html/#the-netty-routing-filter)
WebClientHttpRoutingFilter uses WebClient for route the requests.
The WebClient can be configured with a ReactiveOAuth2AuthorizedClientManager through of an ExchangeFilterFunction (https://docs.spring.io/spring-security/site/docs/current/reference/html5/#webclient). The ReactiveOAuth2AuthorizedClientManager will be responsible of management the access/refresh tokens and will do all the hard work for you
Here you can review this implementation. In addition, I implemented the client-credentials grant with this approach

Storing JWT tokens on OAuth2 web client using Spring Security

I'm implementing an OAuth2 web application Client using Spring Boot 2.1.3 and Spring Security 5.1.3 that is obtaining JWT tokens from an authorization server through authorization code grant type and calls a protected resource server.
This is how the implementation looks up till now:
Security configuration and a restTemplate bean used to call the protected resource:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.and()
.oauth2Client()
.and().logout().logoutSuccessUrl("/");
}
#Bean
public RestTemplate restTemplate(OAuth2AuthorizedClientService clientService) {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new AuthorizationHeaderInterceptor(clientService));
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
The interceptor that adds the authorization header (from the framework's InMemoryOAuth2AuthorizedClientService) in the restTemplate:
public class AuthorizationHeaderInterceptor implements ClientHttpRequestInterceptor {
private OAuth2AuthorizedClientService clientService;
public AuthorizationHeaderInterceptor(OAuth2AuthorizedClientService clientService) {
this.clientService = clientService;
}
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String accessToken = null;
if (authentication != null && authentication.getClass().isAssignableFrom(OAuth2AuthenticationToken.class)) {
OAuth2AuthenticationToken auth = (OAuth2AuthenticationToken) authentication;
String clientRegistrationId = auth.getAuthorizedClientRegistrationId();
OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(clientRegistrationId, auth.getName());
accessToken = client.getAccessToken().getTokenValue();
request.getHeaders().add("Authorization", "Bearer " + accessToken);
}
return execution.execute(request, bytes);
}
}
And the controller that calls the protected resource server:
#Controller
#RequestMapping("/profile")
public class ProfileController {
#Autowired
private RestTemplate restTemplate;
#Value("${oauth.resourceServerBase}")
private String resourceServerBase;
#GetMapping
public String getProfile(Model model) {
Profile profile = restTemplate.getForEntity(resourceServerBase + "/api/profile/", Profile.class).getBody();
model.addAttribute("profile", profile);
return "profile";
}
}
The OAuth2 client configuration is directly in the application.yml:
spring:
security:
oauth2:
client:
registration:
auth-server:
client-id: webClient
client-secret: clientSecret
scope: read,write
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8081/client/login/oauth2/code/auth-server
provider:
auth-server:
authorization-uri: http://localhost:8080/auth-server/oauth/authorize
token-uri: http://localhost:8080/auth-server/oauth/token
user-info-uri: http://localhost:8082/resource-server/users/info
user-name-attribute: user_name
After doing some debugging I've observed that at the end of a successful authentication flow through OAuth2LoginAuthtenticationFilter the framework is storing the obtained access and refresh JWT tokens under OAuth2AuthorizedClient model in memory through the provided InMemoryOAuth2AuthorizedClientService.
I am trying to find out how to override this behaviour so that the tokens can remain available after a server restart. And also keep the user logged in based on this.
Should I just provide a custom OAuth2AuthorizedClientService implementation? How could I configure Spring Security to use it? And should this custom implementation store the tokens in a cookie?
Should I just provide a custom OAuth2AuthorizedClientService
implementation?
I think yes, for solving your use case
How could I configure Spring Security to use it?
From spring doc:
If you would like to provide a custom implementation of
AuthorizationRequestRepository that stores the attributes of
OAuth2AuthorizationRequest in a Cookie, you may configure it as shown
in the following example:
#EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Client()
.authorizationCodeGrant()
.authorizationRequestRepository(this.cookieAuthorizationRequestRepository())
...
}
private AuthorizationRequestRepository<OAuth2AuthorizationRequest> cookieAuthorizationRequestRepository() {
return new HttpCookieOAuth2AuthorizationRequestRepository();
}
}

Spring secure endpoint with only client credentials (Basic)

I have oauth2 authorization server with one custom endpoint (log out specific user manually as admin)
I want this endpoint to be secured with rest client credentials (client id and secret as Basic encoded header value), similar to /oauth/check_token.
This endpoint can be called only from my resource server with specific scope.
I need to check if the client is authenticated.
I would like to be able to add #PreAuthorize("#oauth2.hasScope('TEST_SCOPE')")on the controller`s method.
I could not find any docs or way to use the Spring`s mechanism for client authentication check.
EDIT 1
I use java config not an xml one
So I ended up with the following solution
Authentication Manager
public class ClientAuthenticationManager implements AuthenticationManager {
private ClientDetailsService clientDetailsService;
private PasswordEncoder passwordEncoder;
public HGClientAuthenticationManager(ClientDetailsService clientDetailsService, PasswordEncoder passwordEncoder) {
Assert.notNull(clientDetailsService, "Given clientDetailsService must not be null!");
Assert.notNull(passwordEncoder, "Given passwordEncoder must not be null!");
this.clientDetailsService = clientDetailsService;
this.passwordEncoder = passwordEncoder;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ClientDetails clientDetails = null;
try {
clientDetails = this.clientDetailsService.loadClientByClientId(authentication.getPrincipal().toString());
} catch (ClientRegistrationException e) {
throw new BadCredentialsException("Invalid client id or password");
}
if (!passwordEncoder.matches(authentication.getCredentials().toString(), clientDetails.getClientSecret())) {
throw new BadCredentialsException("Invalid client id or password");
}
return new OAuth2Authentication(
new OAuth2Request(null, clientDetails.getClientId(), clientDetails.getAuthorities(), true,
clientDetails.getScope(), clientDetails.getResourceIds(), null, null, null),
null);
}
}
Filter declaration
private BasicAuthenticationFilter basicAuthenticationFilter() {
ClientDetailsUserDetailsService clientDetailsUserDetailsService = new ClientDetailsUserDetailsService(
this.clientDetailsService);
clientDetailsUserDetailsService.setPasswordEncoder(this.passwordEncoder);
return new BasicAuthenticationFilter(
new ClientAuthenticationManager(this.clientDetailsService, this.passwordEncoder));
}
Filter registration
httpSecurity.addFilterBefore(this.basicAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
WARNING!!!
This will prevent any other types of authentication (oauth2, etc.).
ONLY Basic authentication is accepted and ONLY for registered clients.
#PreAuthorize("#oauth2.hasScope('TEST_SCOPE')") On the controller method should be sufficiƫnt. If the client is not authenticated, no scope is available and the scope check will fail.
If you want, you can use the Spring Security expression #PreAuthorize("isAuthenticated()") to check if a client is authenticated: https://docs.spring.io/spring-security/site/docs/5.0.0.RELEASE/reference/htmlsingle/#el-common-built-in
You could also configure the HttpSecurity instead of working with #PreAuthorize

Resources