Spring Boot Feign client - interceptor not working - spring-boot

I have feign client interceptor which adds Auth header (bearer token being fetched by RestTemplate). If the server responds with 401 (expired token) I want to reauthenticate and try the request again but the interceptor is not getting triggered 2nd time.
Interceptor code:
#Override
public void apply(RequestTemplate requestTemplate) {
if (AuthenticationService.bearerToken == null)
authenticationService.authenticate();
requestTemplate.header(AUTHORIZATION, BEARER_TOKEN_PREFIX + AuthenticationService.bearerToken );
}
Error decoder:
#Override
public Exception decode(String s, Response response) {
FeignException exception = feign.FeignException.errorStatus(s, response);
switch (response.status()) {
case 401:
authenticationService.authenticate();
return new RetryableException(response.status(), exception.getMessage(), response.request().httpMethod(), exception, null, response.request());
case 500:
throw new BadActionException(s, response.reason());
default:
break;
}
return exception;
}
Client config class:
#Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor (authenticationService);
}
#Bean
public RestClientDecoder restClientDecoder() {
return new RestClientDecoder(authenticationService);
}
Feign client:
#FeignClient(value = "server", url = "${server.base-url}", configuration = RestClientConfig.class)
public interface RestClient {
#PostMapping("api/test/{id}/confirm")
void test(#PathVariable Long id);
}
Side note: is there built in interceptor for authentication other than oAuth and BasicAuth? The server I am communicating with has simple jwt auth with expiration.

Related

How to cache a JWT in Spring-Security from an OAuth2Client

In my case I have an application for SpringBootAdmin. SpringBootAdmin sends requests to a lot of applications all the time.
For these requests I set an access token (JWT) which I pull from Keycloak via the AuthorizedClientServiceOAuth2AuthorizedClientManager.
Now the problem is that this token is not cached, and Spring-Security sends about 100 requests per minute to Keycloak to get the access token.
So is there a way to cache this JWT ?
Here is my SecurityConfig:
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
#Override
public void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests()
.anyRequest()
.permitAll()
.and()
.oauth2Client()
.and()
.csrf()
.ignoringAntMatchers("/**");
}
}
My ClientRegistration:
#Configuration
public class ClientRegistrationConfiguration
{
private static final String KEYCLOAK = "keycloak";
#Bean
public ClientRegistration clientRegistration(OAuth2ClientProperties properties)
{
return withRegistrationId(KEYCLOAK)
.tokenUri(properties.getProvider().get(KEYCLOAK).getTokenUri())
.clientId(properties.getRegistration().get(KEYCLOAK).getClientId())
.clientSecret(properties.getRegistration().get(KEYCLOAK).getClientSecret())
.authorizationGrantType(CLIENT_CREDENTIALS)
.build();
}
#Bean
public ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration)
{
return new InMemoryClientRegistrationRepository(clientRegistration);
}
#Bean
public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(ClientRegistrationRepository clientRegistrationRepository)
{
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}
#Bean
public AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientServiceOAuth2AuthorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService)
{
var authorizedClientProvider = builder().clientCredentials().build();
var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
And my RequestConfiguration:
#Configuration
public class HttpRequestConfiguration
{
#Bean
public HttpHeadersProvider customHttpHeadersProvider(AuthorizedClientServiceOAuth2AuthorizedClientManager clientManager)
{
return instance ->
{
var httpHeaders = new HttpHeaders();
var token = Objects.requireNonNull(clientManager.authorize(withClientRegistrationId("keycloak").principal("Keycloak").build())).getAccessToken();
httpHeaders.add("Authorization", "Bearer " + token.getTokenValue());
return httpHeaders;
};
}
}
Your headers provider is a bean, so you can simply cache the token there. If you write your provider like that:
#Bean
public HttpHeadersProvider customHttpHeadersProvider(AuthorizedClientServiceOAuth2AuthorizedClientManager clientManager)
{
var token = Objects.requireNonNull(clientManager.authorize(withClientRegistrationId("keycloak").principal("Keycloak").build())).getAccessToken();
return instance ->
{
var httpHeaders = new HttpHeaders();
httpHeaders.add("Authorization", "Bearer " + token.getTokenValue());
return httpHeaders;
};
}
Then the token will be requested once, only when the bean is created. This will fix calling Keycloak on each request but has the usual problem of caching - at some point the access token will expire and you need a way to get a new token. One way to do it would be to catch 401 errors from your client and force recreation of the customHttpHeadersProvider bean when that happens.
Another way would be to create an object which will be a "token provider" and a bean itself. That object would keep the token in memory and have a method to refresh the token. Then you could create the authorization header near the request itself, instead of using a headers provider bean. You will have more control then over when do you want to refresh the access token.

Spring oauth2 authorization server: unable to logout users

I have created an angular app that serves as an oauth2 client. I have created my authorization server with spring oauth2 using the following security configs
#Bean
#Order(1)
public SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(corsFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable()
.headers().frameOptions().disable()
.and()
.antMatcher("/auth/account/**")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.and()
.oauth2ResourceServer().jwt();
return http.build();
}
#Bean
#Order(2)
public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
// #formatter:off
http
.addFilterBefore(corsFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/management/**").permitAll()
.antMatchers("/h2-console/**").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.and()
.formLogin(withDefaults());
return http.build();
}
and here is my authorization server config
#Configuration
public class AuthServerConfig {
private final DataSource dataSource;
private final AuthProperties authProps;
private final PasswordEncoder encoder;
public AuthServerConfig(DataSource dataSource, AuthProperties authProps, PasswordEncoder encoder) {
this.dataSource = dataSource;
this.authProps = authProps;
this.encoder = encoder;
}
#Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource);
}
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}
#Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
JdbcRegisteredClientRepository clientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
RegisteredClient webClient = RegisteredClient.withId("98a9104c-wertyuiop")
.clientId(authProps.getClientId())
.clientName(authProps.getClientName())
.clientSecret(encoder.encode(authProps.getClientSecret()))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:4200/xxxx/yyy")
.redirectUri("http://127.0.0.1:8000/xxxx/yyy")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("farmer:read")
.scope("farmer:write")
.tokenSettings(tokenSettings())
.build();
clientRepository.save(webClient);
return clientRepository;
}
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
#Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
#Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
private static RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
#Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer(authProps.getIssuerUri())
.build();
}
#Bean
public TokenSettings tokenSettings() {
return TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofDays(1))
.refreshTokenTimeToLive(Duration.ofDays(1))
.build();
}
}
Here is my build.gradle file
plugins {
id 'org.springframework.boot' version '2.6.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'org.liquibase.gradle' version '2.1.0'
id 'java'
}
group = 'com.shamba.records'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/release' }
}
ext {
set('springCloudVersion', "2021.0.0")
set('liquibaseVersion', "4.6.1")
}
configurations {
liquibaseRuntime.extendsFrom runtimeClasspath
}
dependencies {
implementation 'tech.jhipster:jhipster-framework:7.4.0'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:0.2.1'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.security:spring-security-cas:5.6.1'
// mapstruct
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
// jackson
implementation 'com.fasterxml.jackson.module:jackson-module-jaxb-annotations'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hppc'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'org.zalando:problem-spring-web:0.26.0'
// configure liquibase
implementation "org.liquibase:liquibase-core:${liquibaseVersion}"
liquibaseRuntime 'org.liquibase:liquibase-groovy-dsl:3.0.0'
liquibaseRuntime 'info.picocli:picocli:4.6.1'
liquibaseRuntime 'org.postgresql:postgresql'
liquibaseRuntime group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
liquibaseRuntime 'org.liquibase.ext:liquibase-hibernate5:3.6'
liquibaseRuntime sourceSets.main.output
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
test {
useJUnitPlatform()
}
and here is part of the properties, I have omitted other things because of brevity
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://${AUTH_SERVICE_HOST:127.0.0.1}:5000
jwk-set-uri: http://${AUTH_SERVICE_HOST:127.0.0.1}:5000/oauth2/jwks
I am able to sign in and sign out users using authorization code flow but the issue comes in after the first successful sign-in, when the users click on the sign in the user is automatically logged in by the auth server even after calling the /oauth2/revoke endpoint and specifying the logout configs below in the auth server
.and()
.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
I also tried to implement a custom endpoint /auth/account/revoke to manually log out users but nothing seems to work. here is the implementation
#RestController
#RequestMapping("auth/account")
public class AccountResource {
#GetMapping("/revoke")
public void revoke(HttpServletRequest request) {
Assert.notNull(request, "HttpServletRequest required");
HttpSession session = request.getSession(false);
if (!Objects.isNull(session)) {
session.removeAttribute("SPRING_SECURITY_CONTEXT");
session.invalidate();
}
SecurityContextHolder.getContext().setAuthentication(null);
SecurityContextHolder.clearContext();
}
}
what could be the issue? any help counts
---------updates-------------
After upgrading spring-security-oauth2-authorization-server version 0.2.2 I updated this method
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}
to this
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer.tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint
.revocationResponseHandler((request, response, authentication) -> {
Assert.notNull(request, "HttpServletRequest required");
HttpSession session = request.getSession(false);
if (!Objects.isNull(session)) {
session.removeAttribute("SPRING_SECURITY_CONTEXT");
session.invalidate();
}
SecurityContextHolder.getContext().setAuthentication(null);
SecurityContextHolder.clearContext();
response.setStatus(HttpStatus.OK.value());
})
);
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
return http.formLogin(Customizer.withDefaults()).build();
}
There are two concepts in play that are somewhat confusingly related.
Secure logout
Token revocation
Regarding logging out of an application, this is necessary when a browser-based session is in use, which would usually be the case with the authorization_code flow. Strictly speaking, terminating the session is all that is required to achieve your goal.
Regarding token revocation, this is more of an OAuth-related security concern and is distinct in that sense from traditional logout functionality. Typically, the most immediate need for token revocation is as a risk mitigation strategy when a refresh_token (or to a lesser extent the associated access_token) is stolen. If a token is not stolen, one could conceivably discard the token from memory on the client-side, and simply let it expire on the server. However, this is unlikely to be a recommendation, as it allows risk to exist that can be closed down proactively by revoking the token on logout.
So the question is, how do we achieve both simultaneously, right? Unfortunately, it's not built-in to either Spring Security or Spring Authorization Server at the moment, though there are specifications that could be used as the basis of feature development.
In general, you will need to solve this on the backend, as there's not much a frontend can do. If you simply POST to a /logout endpoint from your UI, you can't manipulate the cookies stored for that host in the browser (when using CORS). I recommended using the backend-for-frontend pattern for your OAuth client. But even (and especially) if you don't, you will need to ensure you can terminate the session regardless of whether the cookie exists in the browser. This means storing the session in a database, associating it with the refresh token in some way, and making a secure call to the revocation endpoint using the refresh token to delete both from the database simultaneously.
You can achieve this by setting the AuthenticationSuccessHandler on the OAuth2TokenRevocationEndpointFilter, like so:
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer.tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint
.revocationResponseHandler((request, response, authentication) -> {
/* delete session here... */
response.setStatus(HttpStatus.OK.value());
})
);
// ...
Hopefully that's enough to get you started. You may find some benefit in working through the specifics of how this might be achieved yourself (e.g. it's a good learning experience). If you're stuck, it could be a good opportunity to request a How-to guide, as we're fielding ideas for guides right now. See #499 for a list of existing how-to guides, and please feel free to submit your own!
For anyone facing the same issue, I was not able to make it work when using angular directly as a client, but following the advice of #SteveRiesenberg I recommended using the backend-for-frontend pattern for your OAuth client I was able to make it work, therefore I advise anyone to use this pattern as it will help you avoid some of the pitfalls you would otherwise have faced as I did, plus it integrates seamlessly. For a head start you can refer to the sample project below created by the spring security team SpringOne 2021 repository Also here is a link to the presentation Spring Security 5.5 From Taxi to Takeoff
I created success handler for revocation endpoint like Steve pointed out and invalidated sessions manually. Revoke is called from another application as a part of logout success handler.
RevocationResponseHandler:
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, OAuth2AuthorizationService authorizationService, FindByIndexNameSessionRepository<?> sessions) throws Exception {
var authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<HttpSecurity>();
authorizationServerConfigurer.tokenRevocationEndpoint(c -> c.revocationResponseHandler(sessionInvalidatingSuccessHandler(authorizationService, sessions)));
// ...
}
private AuthenticationSuccessHandler sessionInvalidatingSuccessHandler(OAuth2AuthorizationService authorizationService, FindByIndexNameSessionRepository<?> sessions) {
return (request, response, authentication) -> {
String token = request.getParameter(OAuth2ParameterNames.TOKEN);
if (token != null) {
OAuth2Authorization authorization = authorizationService.findByToken(token, null);
if (authorization != null) {
sessions.findByPrincipalName(authorization.getPrincipalName()).forEach((sessionId, session) -> sessions.deleteById(sessionId));
}
}
response.setStatus(HttpStatus.OK.value());
};
}
I faced the same issue. after spending five days of hard work, I found JSESSIONID to be removed from Server Side and Client Side.
I was trying the following things
revoked oauth2 token by calling authorization server's oauth2/revoke endpoint (both refresh and access tokens) but logout did not work.
I tried to call /logout endpoint of Authorization Server that also did not work out
I tried to remove JSESSIONID from client browser which also went in vein.
I tried to remove JSESSIONID from Server and Client side. that worked out. Now I am able to implement logout successfully
I added the following endpoint in Spring Authorization Server
#GetMapping("/do/logout")
#ResponseBody
public void doLogout(HttpSession session,HttpServletRequest request, HttpServletResponse response) {
System.out.println("entered logout point");
session.invalidate();
final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
auth.setAuthenticated(false);
SecurityContextHolder.clearContext();
SecurityContextHolder.getContext().setAuthentication(null);
}
Cookie cookieWithSlash = new Cookie("JSESSIONID", null);
cookieWithSlash.setPath(request.getContextPath() + "/");
cookieWithSlash.setDomain("auth-server");
cookieWithSlash.setMaxAge(0);
response.addCookie(cookieWithSlash); // For Tomcat
}
and the following endpoint in client application
#PostMapping("/complete/logout/process")
public String testLogout(HttpSession session, HttpServletRequest request,
HttpServletResponse response,
#RegisteredOAuth2AuthorizedClient("messaging-client-oidc") OAuth2AuthorizedClient authorizedClient)
throws IOException {
final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
auth.setAuthenticated(false);
SecurityContextHolder.clearContext();
for (Cookie cookie : request.getCookies()) {
String cookieName = cookie.getName();
LOG.info("cookie name={}", cookieName);
Cookie cookieToDelete = new Cookie(cookieName, null);
cookieToDelete.setPath(request.getContextPath() + "/");
cookieToDelete.setMaxAge(0);
response.addCookie(cookieToDelete);
}
SecurityContextHolder.getContext().setAuthentication(null);
}
return "logout";
}
and in html I called two endpoints /server side logout and client side logout
that cleared both the JSESSIONID cookie from the browser
<form method="post" id="clientLogout" th:action="#{/complete/logout/process}">
</form>
button class="btn btn-sm btn-outline-primary" type="button"
onclick="javascript:logout();">Logout</button>
<script>
function logout() {
// calling server side logout
var tmpwin = window.open("http://auth-server:9000/do/logout", "_blank");
// calling client side logout
document.getElementById("clientLogout").submit();
tmpwin.close();
}
</script>
logging out from server side and client side clears both the JSESSIONID cookies finally able to logout
if anything I am missing or doing wrong please educate me. Thanks
Additionally you may invalidate oauth2 access and refresh tokens if you want
#PostMapping("do/oauth2/token/revoke")
#ResponseBody
public void doOauth2TokenRevoke(#RegisteredOAuth2AuthorizedClient("messaging-client-oidc") OAuth2AuthorizedClient authorizedClient) {
String clientId = authorizedClient.getClientRegistration().getClientId();
String clientSecret = "secret";
String accesstoken = authorizedClient.getAccessToken().getTokenValue();
String refreshtoken = authorizedClient.getRefreshToken().getTokenValue();
LinkedMultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
map.add("token", refreshtoken);
map.add("token_type_hint", "refresh_token");
WebClient client3 = WebClient.builder().baseUrl("http://auth-server:9000").build();
client3.post()
.uri("/oauth2/revoke")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.headers(h -> h.setBasicAuth(clientId, clientSecret))
.body(BodyInserters.fromMultipartData(map))
.retrieve()
.bodyToMono(Void.class)
.block();
LinkedMultiValueMap<String, String> map2 = new LinkedMultiValueMap<String, String>();
map2.add("token", accesstoken);
map2.add("token_type_hint", "access_token");
WebClient client2 = WebClient.builder().baseUrl("http://auth-server:9000").build();
client2.post()
.uri("/oauth2/revoke")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.headers(h -> h.setBasicAuth(clientId, clientSecret))
.body(BodyInserters.fromMultipartData(map2))
.retrieve()
.bodyToMono(Void.class)
.block();
}
On defaultSecurityFilterChain set SessionCreationPolicy to NEVER and add a custom logOut Filter that invalidates the session and redirects to your redirect Uri.
And from your UI make a post to /logout?redirect_uri=
#Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, UserService userService) throws Exception {
http.csrf().disable();
http
.formLogin().loginPage("/login")
.and()
.logout(logout->{
logout.logoutUrl("/logout");
})
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.authorizeRequests()
.antMatchers("/login*").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(new CustomLogoutFilter(), SecurityContextPersistenceFilter.class);
return http.build();
}
public class CustomLogoutFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(request.getServletPath().equals("/logout")) {
HttpSession session = request.getSession(false);
if(session != null) {
log.info("Invalidating session");
session.invalidate();
}
SecurityContext context = SecurityContextHolder.getContext();
if(context != null) {
log.info("Clearing security context");
SecurityContextHolder.clearContext();
context.setAuthentication(null);
}
String redirectUri = request.getParameter("redirect_uri");
if(Utils.isEmpty(redirectUri)) {
redirectUri = request.getHeader("referer");
}
if(Utils.notEmpty(redirectUri)) {
response.sendRedirect(redirectUri);
return;
}
}
filterChain.doFilter(request, response);
}
}
I managed to create a filter named TokenValidationFilter at resource server that calls authorization server's introspect endpoint resulting in the following:
/**
* Performs token introspection when
* an authenticated endpoint gets hit.
*/
#Component
public class TokenValidationFilter extends OncePerRequestFilter {
#Value( "${spring.security.oauth2.client.provider.authz-server.introspect}" )
private String introspectEndpoint;
#Value( "${spring.security.oauth2.client.registration.authz-server.client-id}" )
private String clientId;
#Value(
"${spring.security.oauth2.client.registration.authz-server.client-secret}" )
private String clientSecret;
#Override
public void doFilterInternal( HttpServletRequest request,
HttpServletResponse response, FilterChain chain )
throws ServletException {
try {
var token = request.getHeader( "authorization" );
// For endpoints that do not require Authorization header claim
if (token == null) {
chain.doFilter(request, response);
return;
}
var result = performTokenIntrospection( token );
// Parses authz-server http response into json
ObjectMapper mapper = new ObjectMapper();
JsonNode json = mapper.readTree( result.body() );
// Checks token validation
if ( json.get( "active" ).asBoolean( false ) ) {
chain.doFilter(request, response);
} else {
prepareRejectionResponse( response );
}
}
catch ( Exception ex ) {
throw new ServletException( ex );
}
}
private HttpResponse<String> performTokenIntrospection( String token )
throws IOException, InterruptedException,
NullPointerException, URISyntaxException {
var uri = new URI( introspectEndpoint );
// Prepares request to authz server introspection endpoint
var body = String.format( "token=%s&client_id=%s&client_secret=%s",
token.replaceAll( "[Bb][Ee][Aa][Rr][Ee][Rr] ", "" ),
clientId, clientSecret);
var req = HttpRequest
.newBuilder()
.uri(uri)
.headers(
"content-type", MediaType.APPLICATION_FORM_URLENCODED_VALUE,
"authorization", token
)
.POST( HttpRequest.BodyPublishers.ofString( body ) )
.build();
// Performs token introspection and returns its result
return HttpClient.newHttpClient()
.send( req, HttpResponse.BodyHandlers.ofString() );
}
private void prepareRejectionResponse( HttpServletResponse response )
throws IOException {
response.setStatus( HttpStatus.UNAUTHORIZED.value() );
response.setContentType( MediaType.APPLICATION_JSON_VALUE );
var writer = response.getWriter();
writer.print( "{\"message\": \"invalid token\"}" );
writer.close();
}
}
As you can see the filter attributes uses resource server's ( client-id and client-secret ) credentials, and i added token introspection's endpoint on my application.yml file.
Finally, registering the filter in the configuration class my ResourceServerConfig looked like:
#EnableWebSecurity
public class ResourceServerConfig {
#Bean
SecurityFilterChain securityFilterChain( HttpSecurity http )
throws Exception {
//... omitted code above ...
http.addFilterBefore( tokenValidationFilter,
WebAsyncManagerIntegrationFilter.class );
return http.build();
}
// ... omitted code bellow ...
}
I just tested on development environment but yet to find its drawbacks, there might be better solutions but it works without spamming another application.
As far as i know (which is little btw), the solution presented backend-for-frontend looks better in security perspective.
Obs: if you apply this solution remeber always to revoke by refresh_token as oauth2 framework specifies.
.antMatchers("/auth/account/**").authenticated() ?

Can we override CheckTokenEndpoint and provide custom CheckTokenEndpoint in Spring Oauth2?

My application has separate authorization server and resource server. Authorization server provides access token to resource server. Resource server then sends the request for protected resource with access token.
Resource server uses RemoteTokenServices to validate whether the access token is proper or not.
#Bean
public RemoteTokenServices remoteTokenServices(final #Value("${auth.server.url}") String checkTokenUrl,
final #Value("${auth.server.clientId}") String clientId,
final #Value("${auth.server.clientsecret}") String clientSecret)
{
final RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl(checkTokenUrl+"?name=value");
remoteTokenServices.setClientId(clientId);
remoteTokenServices.setClientSecret(clientSecret);
remoteTokenServices.setAccessTokenConverter(accessTokenConverter());
return remoteTokenServices;
}
application.yml
auth:
server:
url: http://localhost:9191/api/oauth/check_token/
clientId: clientid
clientsecret: secret
I want to pass additional parameter like resource id so that I can verify if the token is authorized for that resource or not.
I want to get that parameter in org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint,
and want override below method to add some logic. Is it possible?
#RequestMapping(value = "/oauth/check_token")
#ResponseBody
public Map<String, ?> checkToken(#RequestParam("token") String value) {
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
}
OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
Map<String, ?> response = accessTokenConverter.convertAccessToken(token, authentication);
return response;
}
How to send some parameter to oauth/check_token and override checkToken() method?
Basically what I am doing is when access token is generated, I am saving some record about the resources that token is allowed for.
When I receive the request for the resource on resource server, I want to pass the resource id to auth server and want to verify the token is authorized for that resource or not?
I've just create new CustomCheckTokenEndpoint and copy whole code of CheckTokenEndpoint then override checkToken(...) method
#Configuration
#EnableAuthorizationServer
public class CustomAuthorizationServer extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.pathMapping("/oauth/check_token", "/my/oauth/check_token");
}
}
**CustomCheckTokenEndpoint.java**
public class CustomCheckTokenEndpoint {
// copy whole CheckTokenEndpoint
#RequestMapping(value = "/my/oauth/check_token")
#ResponseBody
public Map<String, ?> checkToken(String value) {
// your code will be here
}
}

spring security + oauth2 + reactjs + restful http client

I am doing spring boot 1.5+ security with auth2 authentication and reactjs. for http calls using restful http client. Authentication is working perfectly and I am successfully accessing data from resource server. The issue is logout code is not working and I am getting this error on console:
POST http://localhost:8080/logout 403 ()
error: "Forbidden"
message: "Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'.
I am sharing my code also.
1) ReactJs code
handleLogout = (e) => {
client({
method: 'POST',
path: '/logout',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}}).then(response => {
console.log(response);
});
}
2) restful http client
'use strict';
// client is custom code that configures rest.js to include support for HAL, URI Templates,
// and other things. It also sets the default Accept request header to application/hal+json.
// get the rest client
var rest = require('rest');
// provides default values for the request object. default values can be provided for the method, path, params, headers, entity
// If the value does not exist in the request already than the default value utilized
var defaultRequest = require('rest/interceptor/defaultRequest');
// Converts request and response entities using MIME converter registry
// Converters are looked up by the Content-Type header value. Content types without a converter default to plain text.
var mime = require('rest/interceptor/mime');
// define the request URI by expanding the path as a URI template
var uriTemplateInterceptor = require('./uriTemplateInterceptor');
// Marks the response as an error based on the status code
// The errorCode interceptor will mark a request in error if the status code is equal or greater than the configured value.
var errorCode = require('rest/interceptor/errorCode');
var csrf = require('rest/interceptor/csrf');
// A registry of converters for MIME types is provided. Each time a request or response entity needs to be encoded or
// decoded, the 'Content-Type' is used to lookup a converter from the registry.
// The converter is then used to serialize/deserialize the entity across the wire.
var baseRegistry = require('rest/mime/registry');
var registry = baseRegistry.child();
registry.register('text/uri-list', require('./uriListConverter'));
registry.register('application/hal+json', require('rest/mime/type/application/hal'));
// wrap all the above interceptors in rest client
// default interceptor provide Accept header value 'application/hal+json' if there is not accept header in request
module.exports = rest
.wrap(mime, { registry: registry })
.wrap(uriTemplateInterceptor)
.wrap(errorCode)
.wrap(csrf)
.wrap(defaultRequest, { headers: { 'Accept': 'application/hal+json' }});
3) application.yml of client application
debug: true
spring:
aop:
proxy-target-class: true
security:
user:
password: none
oauth2:
client:
access-token-uri: http://localhost:9999/uaa/oauth/token
user-authorization-uri: http://localhost:9999/uaa/oauth/authorize
client-id: acme
client-secret: acmesecret
resource:
user-info-uri: http://localhost:9999/uaa/user
jwt:
key-value: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgnBn+WU3i6KarB6gYlg40ckBiWmtVEpYkggvHxow74T19oDyO2VRqyY9oaJ/cvnlsZgTOYAUjTECjL8Ww7F7NJZpxMPFviqbx/ZeIEoOvd7DOqK3P5RBtLsV5A8tjtfqYw/Th4YEmzY/XkxjHH+KMyhmkPO+/tp3eGmcMDJgH+LwA6yhDgCI4ztLqJYY73gX0pEDTPwVmo6g1+MW8x6Ctry3AWBZyULGt+I82xv+snqEriF4uzO6CP2ixPCnMfF1k4dqnRZ/V98hnSLclfMkchEnfKYg1CWgD+oCJo+kBuCiMqmeQBFFw908OyFKxL7Yw0KEkkySxpa4Ndu978yxEwIDAQAB
-----END PUBLIC KEY-----
zuul:
routes:
resource:
path: /resource/**
url: http://localhost:9000/resource
user:
path: /user/**
url: http://localhost:9999/uaa/user
logging:
level:
org.springframework.security: DEBUG
4) CorsFilter configuration in authorization server
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
System.out.println("*********** running doFilter method of CorsFilter of auth-server***********");
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
response.addHeader("Access-Control-Allow-Headers", "x-auth-token, x-requested-with");
response.addHeader("Access-Control-Max-Age", "3600");
if (request.getMethod()!="OPTIONS") {
try {
chain.doFilter(req, res);
} catch (IOException e) {
e.printStackTrace();
} catch (ServletException e) {
e.printStackTrace();
}
} else {
}
}
public void init(FilterConfig filterConfig) {}
public void destroy() {}
}
5) AuthrorizationServerConfigurerAdapter of authentication server
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Bean
public #Autowired JwtAccessTokenConverter jwtAccessTokenConverter() throws Exception {
System.out.println("*********** running jwtAccessTokenConverter ***********");
// Setting up a JWT token using JwtAccessTokenConverter.
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// JWT token signing key
KeyPair keyPair = new KeyStoreKeyFactory(
new ClassPathResource("keystore.jks"), "suleman123".toCharArray())
.getKeyPair("resourcekey");
converter.setKeyPair(keyPair);
return converter;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
System.out.println("*********** running configure(ClientDetailsServiceConfigurer clients) ***********");
clients.inMemory()
.withClient("acme") // registers a client with client Id 'acme'
.secret("acmesecret") // registers a client with password 'acmesecret'
.authorizedGrantTypes("authorization_code", "refresh_token",
"password") // We registered the client and authorized the “password“, “authorization_code” and “refresh_token” grant types
.scopes("openid") // scope to which the client is limited
.autoApprove(true);
}
/**
*
*/
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
System.out.println("*********** running configure(AuthorizationServerEndpointsConfigurer endpoints) ***********");
// we choose to inject an existing authentication manager from the spring container
// With this step we can share the authentication manager with the Basic authentication filter
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter());
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
System.out.println("*********** running configure(AuthorizationServerSecurityConfigurer oauthServer) ***********");
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess(
"isAuthenticated()");
}
}
Finally got this working. What I have done to make it work:
1) I have installed 'react-cookie' library
npm install react-cookie --save
2) In my reactjs code I have imported react-cookie library and in method where I am using restful http client to generate logout request I am fetching Csrf-Token from cookie and sending it as request header.
handleLogout = (e) => {
client({
method: 'POST',
path: 'logout',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf8',
'X-Requested-With': 'XMLHttpRequest',
'X-Csrf-Token': Cookie.load('XSRF-TOKEN')
}
}).then(response => {
this.setState({authenticated: false});
console.log(response);
});
}
3) In authorization server instead of using my custom Cors Filter class which I have mentioned in my question, now I am using Spring Cors Filter code
#Configuration
public class CorsFilterConfig {
#Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}
4) In application.properties file of Authorization Server I have added this property, so CorsFilter will run before SpringSecurityFilterChain
security.filter-order=50

How to bypass UsernamePasswordAuthentication in Spring Security

I'm implementing an API that accepts a JWT as request parameter and on authentication, returns a new JWT.
#RequestMapping(value = "/authenticate/token", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public ResponseEntity authenticate(#RequestParam("login_token") final String token, HttpServletResponse response) {
LOG.debug("Request to login with token : {}", token);
try {
String jwt = authService.loginByToken(token);
response.addHeader(JWTConfigurer.AUTHORIZATION_HEADER, "Bearer " + jwt);
return ResponseEntity.ok(new IdentityToken(jwt));
} catch (AuthenticationException ae) {
LOG.trace("Authentication exception trace: {}", ae);
return new ResponseEntity<>(Collections.singletonMap("AuthenticationException",
ae.getLocalizedMessage()), HttpStatus.UNAUTHORIZED);
}
}
My loginByToken implementation looks as below
#Override public String loginByToken(String token) {
if (!tokenProvider.validateToken(token)) {
throw new BadCredentialsException("Token is invalid.");
}
SecureToken secureToken = tokenProvider.parseJwtToken(token);
User user = userRepository.findByEmail(secureToken.getEmail());
// TODO: Check Account Status is valid, User status is valid
Calendar c = Calendar.getInstance();
c.setTime(new Date());
c.add(Calendar.DATE, Constants.PASSWORD_EXPIRY_DAYS);
if (user.getPasswordExpiryDt() != null
&& user.getPasswordExpiryDt().after(c.getTime())) {
throw new BadCredentialsException("Password changed");
}
// TODO: Find how to create authentication object and return ID token.
// return tokenProvider.createToken(authentication, false);
return token;
}
At this point, I'm not sure how to create an authentication object that contains all user details that I could pass to createToken function that creates an identity token.
Here is my project without the changes mentioned in this post - https://github.com/santoshkt/ngx-pipes-test.
I have read about Anonymous Authentication, PreAuthenticated etc but not sure how to deal with this case. Will appreciate any pointers on how to do this.
If you want to use Spring Security, you should probably not use a Spring MVC endpoint to handle (pre-)authentication.
In your case you probably want to change your Spring security configuration so that it will have a filter that obtains your token from your request parameters and an authentication provider that retrieves the user/authentication object from your token:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/authenticate/token")
.authorizeRequests()
.anyRequest().authenticated()
.and()
// This is a filter bean you'll have to write
.addFilterBefore(filter(), RequestHeaderAuthenticationFilter.class)
// This is your token verifier/decoder
.authenticationProvider(authenticationProvider())
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
For the filter you could extend from AbstractPreAuthenticatedProcessingFilter and make it return the login_token parameter. In here you have to implement two methods being getPreAuthenticatedPrincipal() and getPreAuthenticatedCredentials().
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
// You could already decode your token here to return your username
return request.getParameter("login_token");
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return request.getParameter("login_token");
}
Your authentication provider should be of type PreAuthenticatedAuthenticationProvider and in here you can set an AuthenticationUserDetailsService:
#Bean
public AuthenticationProvider authenticationProvider() {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
// service is a bean of type AuthenticationUserDetailsService
// You could autowire this in your security configuration class
provider.setPreAuthenticatedUserDetailsService(service);
return provider;
}
Now you can create your own AuthenticationUserDetailsService to retrieve a UserDetails object based on your token:
#Service
public class TokenAuthenticationUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
#Override
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken authentication) throws UsernameNotFoundException {
// In this case the authentication.getCredentials() will contain your token and you can return a UserDetails object
return new User(/** ... */);
}
}
Since you want to provide the HTML page for the JWT token request the best approach is that you create you own Spring Security Custom Entry Point
You may give a look here for an example
If it's another system to manage the authentication and you want just manage the authorization you can "trust" the other System and then manage your own authorizations; in this case you can use the PreAuthentication Scenario as described here; you can find a sample here
I hope it's useful

Resources