How to change response_type on Spring OAuth2 - spring

This is my configuration for OAuth2 login with Instagram
instagram:
client:
clientId: clientId
clientSecret: clientSeret
accessTokenUri: https://api.instagram.com/oauth/access_token
userAuthorizationUri: https://api.instagram.com/oauth/authorize
clientAuthenticationScheme: form
scope:
- basic
- public_content
resource:
userInfoUri: https://api.instagram.com/v1/users/self/
delimiter: +
This is the request made by Spring:
https://api.instagram.com/oauth/authorize?client_id=clientId&redirect_uri=http://localhost:8080/login/instagram&response_type=code&state=CG6mMQ
How can I change the response_type to &response_type=token and how can I why isn't Spring adding the scopes?
Here is the App class:
#SpringBootApplication
#EnableOAuth2Client
public class App extends WebSecurityConfigurerAdapter {
#Autowired
OAuth2ClientContext oauth2ClientContext;
public static void main(String[] args) throws Exception {
SpringApplication.run(App.class, args);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**", "/webjars/**")
.permitAll()
.anyRequest()
.authenticated().and().exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/"))
// logout
.and().logout().logoutSuccessUrl("/").permitAll()
// CSRF
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// filters
.and().addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
// facebook ...
// google ...
// instagram
OAuth2ClientAuthenticationProcessingFilter instagramFilter =
new OAuth2ClientAuthenticationProcessingFilter("/login/instagram");
OAuth2RestTemplate instagramTemplate =
new OAuth2RestTemplate(instagram(), oauth2ClientContext);
instagramFilter.setRestTemplate(instagramTemplate);
instagramFilter.setTokenServices(
new UserInfoTokenServices(instagramResource().getUserInfoUri(), instagram().getClientId()));
filters.add(instagramFilter);
filter.setFilters(filters);
return filter;
}
#Bean
#ConfigurationProperties("instagram.client")
public AuthorizationCodeResourceDetails instagram() {
return new AuthorizationCodeResourceDetails();
}
#Bean
#ConfigurationProperties("instagram.resource")
public ResourceServerProperties instagramResource() {
return new ResourceServerProperties();
}
#Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(
OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
}

How can I change the response_type to &response_type=token
As I read the code, AuthorizationCodeAccessTokenProvider‘s response_type is hard code.
private UserRedirectRequiredException getRedirectForAuthorization(AuthorizationCodeResourceDetails resource,
AccessTokenRequest request) {
// we don't have an authorization code yet. So first get that.
TreeMap<String, String> requestParameters = new TreeMap<String, String>();
requestParameters.put("response_type", "code"); // oauth2 spec, section 3
So, If you want to change the response_code, you can extend the AuthorizationCodeAccessTokenProvider, or implements the AccessTokenProvider, and then inject to the OAuth2RestTemplate accessTokenProvider(the default value is a AccessTokenProviderChain that contains AuthorizationCodeAccessTokenProvider, ResourceOwnerPasswordAccessTokenProvider and so on, use your own provider instead of AuthorizationCodeAccessTokenProvider).
Or you can change the redirectStrategy in OAuth2ClientContextFilter, and change the request param when redirect, but I don't recommend this.
How can I why isn't Spring adding the scopes?
AuthorizationCodeAccessTokenProvider will get scopes from AuthorizationCodeResourceDetails, and add them to UserRedirectRequiredException. I think the scope can't be injected to AuthorizationCodeResourceDetails, because the scope is not under the client. Maybe you need to change to this.
instagram:
client:
clientId: clientId
clientSecret: clientSeret
accessTokenUri: https://api.instagram.com/oauth/access_token
userAuthorizationUri: https://api.instagram.com/oauth/authorize
clientAuthenticationScheme: form
scope:
- basic
- public_content

You can access the query parameters from the UserRedirectRequiredException. So in the code which throws the exception and thus causes the redirect to happen, e.g. in your filter, you can do something like:
try {
accessToken = restTemplate.getAccessToken();
}
catch (UserRedirectRequiredException e) {
Map<String, String> requestParams = e.getRequestParams();
requestParams.put("response_type", "token");
throw e; //don't forget to rethrow
}
You can only replace values this way. If you needed to add a parameter value alongside an existing value, you'd need to use a delimiter such as '+'. There is no standard way of adding multiple parameter values and would depend on what the owner of the API accepts. Some APIs may not accept delimeters at all.

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() ?

How can I do to my spring boot resource server oauth 2 get user's extra data from api when user authenticate e keep it into security context?

I have a resource server done with Spring Boot. I'm using Spring Security 5.3 to authenticate and authorize the frontend exchange data. I've configured a authorization server "issuer-uri" in application.yml that provides and validates the access_token (jwt).
Until there ok. The problem that authorization server doesn't provide at once every user's information that I need in access_token or id_token. With the sub claim and access_token I need to do a request to endpoint to get more extra data about user.
I would like to know how can I do a request to get that information just when the user authenticates and put them into security context togheter the information that's already comes. So that way, I could get that information in some service when needed without make a request to endpoint each time:
SecurityContextHolder.getContext().getAuthentication().getDetails()
It's here my WebSecurityConfigurerAdapter
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String CLAIM_ROLES = "role";
private static final String AUTHORITY_PREFIX = "ROLE_";
#Value("${sso.issuers_uri}")
private String issuers;
Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);
#Override
protected void configure(HttpSecurity http) throws Exception {
String[] result = issuers.split(",");
List<String> arrIssuers = Arrays.asList(result);
arrIssuers.stream().forEach(issuer -> addManager(authenticationManagers, issuer));
http
.httpBasic().disable()
.formLogin(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.authorizeRequests(auth -> auth
.antMatchers(
"/*",
"/signin-oidc",
"/uri-login_unico",
"/assets/**","/views/**",
"index.html",
"/api/segmentos/listar_publicados",
"/api/modelos",
"/api/modelos/*"
).permitAll()
.antMatchers(
"/api/admin/**"
).hasRole("role.PGR.Admin")
.antMatchers(
"/api/govbr/**"
).hasAnyAuthority("SCOPE_govbr_empresa")
.anyRequest().authenticated()
).oauth2ResourceServer(oauth2ResourceServer -> {
oauth2ResourceServer.authenticationManagerResolver(this.authenticationManagerResolver);
});
}
public void addManager(Map<String, AuthenticationManager> authenticationManagers, String issuer) {
JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(JwtDecoders.fromOidcIssuerLocation(issuer));
authenticationProvider.setJwtAuthenticationConverter(getJwtAuthenticationConverter());
authenticationManagers.put(issuer, authenticationProvider::authenticate);
}
private Converter<Jwt, AbstractAuthenticationToken> getJwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(getJwtGrantedAuthoritiesConverter());
return jwtAuthenticationConverter;
}
private Converter<Jwt, Collection<GrantedAuthority>> getJwtGrantedAuthoritiesConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthorityPrefix(AUTHORITY_PREFIX);
converter.setAuthoritiesClaimName(CLAIM_ROLES);
return converter;
}
}
I don't know if I need to do a custom AuthenticationManger or if I can do this with a security filter after authenticated. If someone could help me, I really apprecite it. Tks!!!

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();
}
}

Add optional Google Sign In in my Spring Boot + Security + web Application

I am working on a Spring boot web application. I have now working a registration and login system using Spring Security with a custom userDetailService.
Now I want add a register-login system using Google Accounts. I created my Google API keys and added them to the application.properties. I think is not necessary use .yml propertie files here:
# ===============================
# = OAUTH2
# ===============================
security.oauth2.client.client-id=clientId Here
security.oauth2.client.client-secret=clientSecret here
security.oauth2.client.access-token-uri=https://www.googleapis.com/oauth2/v3/token
security.oauth2.client.user-authorization-uri=https://accounts.google.com/o/oauth2/auth
security.oauth2.client.token-name=oauth_token
security.oauth2.client.authentication-scheme=query
security.oauth2.client.client-authentication-scheme=form
security.oauth2.client.scope=profile
security.oauth2.resource.user-info-uri=https://www.googleapis.com/userinfo/v2/me
security.oauth2.resource.prefer-token-info=false
I added OAuth2 support to my Spring Boot application on this way:
#SpringBootApplication
#EnableOAuth2Sso
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
}
Now I want keep the posibility to login using Google or login using a website account, but I only found manuals about unique login or multiple providers login (Facebook, Google, Twitter..)
In my SpringSecurity configuration class I have this. I think that I have to create a authenticationProvider for Google and link it to the google access url in my app, but I am so confused yet about this:
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
/**
* Obtenemos información de persistencia
*/
// #formatter:off
auth
//.authenticationProvider(googleOauth2AuthProvider())
.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder);
// #formatter:on
}
...
#Override
protected void configure(HttpSecurity http) throws Exception {
String[] anonymousRequest = { urls};
http
.authorizeRequests()
//..other rules
You have to use a composite filter in which you configure your desired authentication providers, for example:
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(facebook(), "/login/facebook"));
filters.add(ssoFilter(google(), "/login/google"));
filter.setFilters(filters);
return filter;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter oAuth2ClientAuthenticationFilter = new OAuth2ClientAuthenticationProcessingFilter(
path);
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
oAuth2ClientAuthenticationFilter.setRestTemplate(oAuth2RestTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(client.getResource().getUserInfoUri(),
client.getClient().getClientId());
tokenServices.setRestTemplate(oAuth2RestTemplate);
oAuth2ClientAuthenticationFilter.setTokenServices(tokenServices);
return oAuth2ClientAuthenticationFilter;
}
where:
#Bean
#ConfigurationProperties("google")
public ClientResources google() {
return new ClientResources();
}
#Bean
#ConfigurationProperties("facebook")
public ClientResources facebook() {
return new ClientResources();
}
and:
class ClientResources {
#NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
#NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();
public AuthorizationCodeResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
finally, add the filter before the BasicAuthenticationFilter in your HTTP security config:
#Override
protected void configure(HttpSecurity http) throws Exception {
String[] anonymousRequest = { urls};
http
.authorizeRequests()
//..other rules
addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
Ps: your configuration properties has to start with the value specified in the #ConfigurationProperties("facebook"):
facebook:
client:
clientId: yourCliendId
clientSecret: yourClientSecret
accessTokenUri: https://graph.facebook.com/oauth/access_token
userAuthorizationUri: https://www.facebook.com/dialog/oauth
tokenName: oauth_token
authenticationScheme: query
registeredRedirectUri: http://localhost:8083/app.html
preEstablishedRedirectUri: http://localhost:8083/app.html
clientAuthenticationScheme: form
resource:
userInfoUri: https://graph.facebook.com/me
This is inspired from the example presented here: https://github.com/spring-guides/tut-spring-boot-oauth2/tree/master/github
You can achieve this using Spring Social or OAUTH2
If you want to do using spring social be aware that Google is not supported by default in spring boot social so you have to do a couple of extra steps.
Add Maven Dependencies
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-google</artifactId>
<version>1.0.0.RELEASE</version>
</dependency>
Add a GoogleAutoConfiguration Class
Do Ctrl+Shift+T in your IDE(eclipse) and look for FacebookAutoConfiguration class you should be able to find it either in org.springframework.boot.autoconfigure.social package in spring-autoconfigure.jar. Copy this File and replace Facebook with Google.
3.Add GoogleProperties
In the same package add the below class
#ConfigurationProperties(prefix = "spring.social.google")
public class GoogleProperties extends SocialProperties{
Update the application.properties with your google API key
Follow this link for complete description and step by step instruction
Hope It helps !!
If you want to do using OAUTH2 here is a working example

Resources