How to customize the Authorization header of the OAuth2 token request using spring-security-oauth2 with a WebClient? - spring-security

I am trying to upgrade to spring security 5.5.1 on a WebClient call.
I found out that the oauth2 clientId and secret are now URL encoded in AbstractWebClientReactiveOAuth2AccessTokenResponseClient, but my token provider does not support this (for example if the secret contains a + character it works only when it is sent as a + not as %2B).
I understand this is seen as a bug fix from spring-security side ), but I cannot make the token provider change its behavior easily.
So I tried to find a way to work around this.
The [documentation] (https://docs.spring.io/spring-security/site/docs/current/reference/html5/#customizing-the-access-token-request) on how to customize the access token request does not seem to apply when you use a WebClient configuration (which is my case).
In order to remove the clientid/secret encoding I had to extend and copy most of the existing code from AbstractWebClientReactiveOAuth2AccessTokenResponseClient to customize the WebClientReactiveClientCredentialsTokenResponseClient because most of it has private/default visibility.
I traced this in an enhancement issue in the spring-security project.
Is there an easier way to customize the Authorization header of the token request, in order to skip the url encoding ?

There is definitely room for improvement in some of the APIs around customization, and for sure these types of questions/requests/issues from the community will continue to help highlight those areas.
Regarding the AbstractWebClientReactiveOAuth2AccessTokenResponseClient in particular, there is currently no way to override the internal method to populate basic auth credentials in the Authorization header. However, you can customize the WebClient that is used to make the API call. If it's acceptable in your use case (temporarily, while the behavior change is being addressed and/or a customization option is added) you should be able to intercept the request in the WebClient.
Here's a configuration that will create a WebClient capable of using an OAuth2AuthorizedClient:
#Configuration
public class WebClientConfiguration {
#Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
// #formatter:off
ServerOAuth2AuthorizedClientExchangeFilterFunction exchangeFilterFunction =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
exchangeFilterFunction.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.filter(exchangeFilterFunction)
.build();
// #formatter:on
}
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
// #formatter:off
WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
new WebClientReactiveClientCredentialsTokenResponseClient();
accessTokenResponseClient.setWebClient(createAccessTokenResponseWebClient());
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(consumer ->
consumer.accessTokenResponseClient(accessTokenResponseClient)
.build())
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// #formatter:on
return authorizedClientManager;
}
protected WebClient createAccessTokenResponseWebClient() {
// #formatter:off
return WebClient.builder()
.filter((clientRequest, exchangeFunction) -> {
HttpHeaders headers = clientRequest.headers();
String authorizationHeader = headers.getFirst("Authorization");
Assert.notNull(authorizationHeader, "Authorization header cannot be null");
Assert.isTrue(authorizationHeader.startsWith("Basic "),
"Authorization header should start with Basic");
String encodedCredentials = authorizationHeader.substring("Basic ".length());
byte[] decodedBytes = Base64.getDecoder().decode(encodedCredentials);
String credentialsString = new String(decodedBytes, StandardCharsets.UTF_8);
Assert.isTrue(credentialsString.contains(":"), "Decoded credentials should contain a \":\"");
String[] credentials = credentialsString.split(":");
String clientId = URLDecoder.decode(credentials[0], StandardCharsets.UTF_8);
String clientSecret = URLDecoder.decode(credentials[1], StandardCharsets.UTF_8);
ClientRequest newClientRequest = ClientRequest.from(clientRequest)
.headers(httpHeaders -> httpHeaders.setBasicAuth(clientId, clientSecret))
.build();
return exchangeFunction.exchange(newClientRequest);
})
.build();
// #formatter:on
}
}
This test demonstrates that the credentials are decoded for the internal access token response WebClient:
#ExtendWith(MockitoExtension.class)
public class WebClientConfigurationTests {
private WebClientConfiguration webClientConfiguration;
#Mock
private ExchangeFunction exchangeFunction;
#Captor
private ArgumentCaptor<ClientRequest> clientRequestCaptor;
#BeforeEach
public void setUp() {
webClientConfiguration = new WebClientConfiguration();
}
#Test
public void exchangeWhenBasicAuthThenDecoded() {
WebClient webClient = webClientConfiguration.createAccessTokenResponseWebClient()
.mutate()
.exchangeFunction(exchangeFunction)
.build();
when(exchangeFunction.exchange(any(ClientRequest.class)))
.thenReturn(Mono.just(ClientResponse.create(HttpStatus.OK).build()));
webClient.post()
.uri("/oauth/token")
.headers(httpHeaders -> httpHeaders.setBasicAuth("aladdin", URLEncoder.encode("open sesame", StandardCharsets.UTF_8)))
.retrieve()
.bodyToMono(Void.class)
.block();
verify(exchangeFunction).exchange(clientRequestCaptor.capture());
ClientRequest clientRequest = clientRequestCaptor.getValue();
String authorizationHeader = clientRequest.headers().getFirst("Authorization");
assertThat(authorizationHeader).isNotNull();
String encodedCredentials = authorizationHeader.substring("Basic ".length());
byte[] decodedBytes = Base64.getDecoder().decode(encodedCredentials);
String credentialsString = new String(decodedBytes, StandardCharsets.UTF_8);
String[] credentials = credentialsString.split(":");
assertThat(credentials[0]).isEqualTo("aladdin");
assertThat(credentials[1]).isEqualTo("open sesame");
}
}

Related

How to set modify the access token request entity for Client Credentials grant when using Spring Security OAuth2 framrwork

I'm writing client for a 3rd party service that doesn't have the standard request format for getting an access token. The access token request body is a JSON with two attributes and the client_id and client_secret needs to be sent as a basic auth header. How do I build the custom request entity and headers converter to appropriately set these values in the access token request?
I have the client configuration with the client manager and responseclient.
public class RestClientConfig {
private final ClientRegistrationRepository clientRegistrationRepository;
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient){
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(configurer -> configurer.accessTokenResponseClient(accessTokenResponseClient))
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
#Bean
public OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient(){
OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter =
new OAuth2ClientCredentialsGrantRequestEntityConverter();
requestEntityConverter.setParametersConverter(null); --> this is where I'm stuck. Need to build a request entity converter bean to pass to this method
DefaultClientCredentialsTokenResponseClient accessTokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);
return accessTokenResponseClient;
}
}

Spring Oauth2 Client, automatically refresh expired access_token

Let me explain my use case.
I need to have a spring boot oauth2 client application (not a resource server As we already have a separate resource server). Also I have following requirements:
For each out going request to resource server, we need to send id_token. (Done by customizing resttemplate).
For any request, no matter if it invokes resource server or not, If access token is expired my application must refresh it automatically (without any user intervention like any popup or redirection.).
If refresh_token is also expired, user must be logged out.
Questions:
For point 2 and 3, I have spent many hours reading documents and code and Stack Overflow but was not able to find the solution (or did not understand). So I decided to put all pieces together which I found on many blogs and documents, and come up with my solution. Below is my solution for point 2.
Can we please have a look to below code and suggest if there could be any problem with this approach?
How to solve point 3 I am thinking of extending solution for point 2 but not sure what code I need to write, can anyone guide me?
/**
*
* #author agam
*
*/
#Component
public class ExpiredTokenFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(ExpiredTokenFilter.class);
private Duration accessTokenExpiresSkew = Duration.ofMillis(1000);
private Clock clock = Clock.systemUTC();
#Autowired
private OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
#Autowired
CustomOidcUserService userService;
private DefaultRefreshTokenTokenResponseClient accessTokenResponseClient;
private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory;
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
public ExpiredTokenFilter() {
super();
this.accessTokenResponseClient = new DefaultRefreshTokenTokenResponseClient();
this.jwtDecoderFactory = new OidcIdTokenDecoderFactory();
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
log.debug("my custom filter called ");
/**
* check if authentication is done.
*/
if (null != SecurityContextHolder.getContext().getAuthentication()) {
OAuth2AuthenticationToken currentUser = (OAuth2AuthenticationToken) SecurityContextHolder.getContext()
.getAuthentication();
OAuth2AuthorizedClient authorizedClient = this.oAuth2AuthorizedClientService
.loadAuthorizedClient(currentUser.getAuthorizedClientRegistrationId(), currentUser.getName());
/**
* Check if token existing token is expired.
*/
if (isExpired(authorizedClient.getAccessToken())) {
/*
* do something to get new access token
*/
log.debug(
"=========================== Token Expired !! going to refresh ================================================");
ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
/*
* Call Auth server token endpoint to refresh token.
*/
OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(
clientRegistration, authorizedClient.getAccessToken(), authorizedClient.getRefreshToken());
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient
.getTokenResponse(refreshTokenGrantRequest);
/*
* Convert id_token to OidcToken.
*/
OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
/*
* Since I have already implemented a custom OidcUserService, reuse existing
* code to get new user.
*/
OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration,
accessTokenResponse.getAccessToken(), idToken, accessTokenResponse.getAdditionalParameters()));
log.debug(
"=========================== Token Refresh Done !! ================================================");
/*
* Print old and new id_token, just in case.
*/
DefaultOidcUser user = (DefaultOidcUser) currentUser.getPrincipal();
log.debug("new id token is " + oidcUser.getIdToken().getTokenValue());
log.debug("old id token was " + user.getIdToken().getTokenValue());
/*
* Create new authentication(OAuth2AuthenticationToken).
*/
OAuth2AuthenticationToken updatedUser = new OAuth2AuthenticationToken(oidcUser,
oidcUser.getAuthorities(), currentUser.getAuthorizedClientRegistrationId());
/*
* Update access_token and refresh_token by saving new authorized client.
*/
OAuth2AuthorizedClient updatedAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration,
currentUser.getName(), accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken());
this.oAuth2AuthorizedClientService.saveAuthorizedClient(updatedAuthorizedClient, updatedUser);
/*
* Set new authentication in SecurityContextHolder.
*/
SecurityContextHolder.getContext().setAuthentication(updatedUser);
}
}
filterChain.doFilter(request, response);
}
private Boolean isExpired(OAuth2AccessToken oAuth2AccessToken) {
Instant now = this.clock.instant();
Instant expiresAt = oAuth2AccessToken.getExpiresAt();
return now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew));
}
private OidcIdToken createOidcToken(ClientRegistration clientRegistration,
OAuth2AccessTokenResponse accessTokenResponse) {
JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
Jwt jwt;
try {
jwt = jwtDecoder
.decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN));
} catch (JwtException ex) {
OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(), null);
throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), ex);
}
OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(),
jwt.getClaims());
return idToken;
}
}
I am open for any suggestion to improve my code. Thanks.
There are not enough details to understand your use-case fully. It would be great to understand:
Spring security is rapidly evolving around OAuth2, consider mentioning the version you are using. My answer assumes 5.2+
Are you in servlet (user logged in somehow) or non-servlet (like #Scheduled method) environment
From the limited information and my limited knowledge I have following hints:
Consider using WebClient instead of RestTemplate, this is they way to go for the future. It is reactive but don't be scared. It can be used in "blocking" environment as well, you will not use it's full potential but you can still benefit from its better support for OAuth2
WebClient itself has a ServletOAuth2AuthorizedClientExchangeFilterFunction which does pretty much what you are trying to achieve
When creating ServletOAuth2AuthorizedClientExchangeFilterFunction you pass in AuthorizedClientServiceOAuth2AuthorizedClientManager which is a strategy on how to (re)authenticate client.
Sample configuration may look as follows:
#Bean
public WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService) {
AuthorizedClientServiceOAuth2AuthorizedClientManager manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
manager.setAuthorizedClientProvider(new DelegatingOAuth2AuthorizedClientProvider(
new RefreshTokenOAuth2AuthorizedClientProvider(),
new ClientCredentialsOAuth2AuthorizedClientProvider()));
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(manager);
oauth2.setDefaultClientRegistrationId("your-client-registratioin-id");
return WebClient.builder()
.filter(oauth2)
.apply(oauth2.oauth2Configuration())
.build();
}
And use it as:
#Autowire
private final WebClient webClient;
...
webClient.get()
.uri("http://localhost:8081/api/message")
.retrieve()
.bodyToMono(String.class)
.map(string -> "Retrieved using password grant: " + string)
.subscribe(log::info);
Hope this helps to move in the right direction! Have fun

Custom JWT respone in Spring sercurity

I use API oauth/token to get JWT token in spring sercurity oauth2. I try to add some additional information in the response by using ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo) of enhance method of TokenEnhancer interface. But these additionalInfo added to JWT too, so It is too big. Is there any way to add additionalInfo to the body of oauth/token request, but not in JWT.
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
final Map<String, Object> additionalInfo = new HashMap<>();
WebUser webUser = (WebUser) authentication.getUserAuthentication().getPrincipal();
additionalInfo.put("user_name", authentication.getName());
additionalInfo.put("roles", authentication.getAuthorities());
if(webUser.getFunctions() != null) {
additionalInfo.put("functions", webUser.getFunctions().toString());
}else {
additionalInfo.put("functions", null);
}
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
When you modify a token, it is said to be "enhancing the token" in Spring context. Logically, you should first convert the token into JWT then add the other properties so that they do not contribute to you payload of JWT.
Here is a snippet from my project built using spring boot
#Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(
Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter()));
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.tokenEnhancer(tokenEnhancerChain)
.accessTokenConverter(jwtAccessTokenConverter())
.tokenStore(tokenStore());
}
In here, I'm adding few properties using the tokenEnhancer() to my token and then enhancing that token to become a JWT using jwtAccessTokenEnhancer(). If I reverse the order here, I get what you are desire.

Keycloak integration in Swagger

I have a Keycloak protected backend that I would like to access via swagger-ui. Keycloak provides the oauth2 implicit and access code flow, but I was not able to make it work. Currently, Keycloak's documentation is lacking regarding which url should be used for authorizationUrl and tokenUrl within swagger.json.
Each realm within Keycloak offers a huge list of configuration urls by accessing http://keycloak.local/auth/realms/REALM/.well-known/openid-configuration
Furthermore I've tried to directly integrate the keycloak js-client within swagger-ui index.html by adding the following lines:
<script src="keycloak/keycloak.js"></script>
<script>
var keycloak = Keycloak('keycloak.json');
keycloak.init({ onLoad: 'login-required' })
.success(function (authenticated) {
console.log('Login Successful');
window.authorizations.add("oauth2", new ApiKeyAuthorization("Authorization", "Bearer " + keycloak.token, "header"));
}).error(function () {
console.error('Login Failed');
window.location.reload();
}
);
</script>
I also tried something like this after 'Login Successful'
swaggerUi.api.clientAuthorizations.add("key", new SwaggerClient.ApiKeyAuthorization("Authorization", "Bearer " + keycloak.token, "header"));
But it also doesn't work.
Any suggestions how I can integrate keycloak auth within swagger?
Swagger-ui can integrate with keycloak using the implicit authentication mode.
You can setup oauth2 on swagger-ui so that it will ask you to authenticate instead of giving swagger-ui the access token directly.
1st thing, your swagger need to reference a Security definition like:
"securityDefinitions": {
"oauth2": {
"type":"oauth2",
"authorizationUrl":"http://172.17.0.2:8080/auth/realms/master/protocol/openid-connect/auth",
"flow":"implicit",
"scopes": {
"openid":"openid",
"profile":"profile"
}
}
}
Then, you swagger-ui need to reference some other parameter: With the pure js, you can use in the index.html
const ui = SwaggerUIBundle({ ...} );
ui.initOAuth({
clientId: "test-uid",
realm: "Master",
appName: "swagger-ui",
scopeSeparator: " ",
additionalQueryStringParams: {"nonce": "132456"}
})
In this code,
authorizationUrl is the authorization endpoint on your keycloak realm
Scopes are something you can set to your needs
clientId is a client parametrized with implicit mode on keycloak realm
the additional parameter nonce should be random, but swagger-ui don't use it yet.
I add here an example if you want to do all this on Spring-boot:
On this framework, you will mainly use swagger and swagger-ui web-jar from Springfox. This is done by adding the dependencies:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>
Swagger is enable by adding the annotation swagger2 on your main class:
#SpringBootApplication
#EnableSwagger2
public class TestSpringApplication {
...
then you can setup a Configuration class like this:
#Configuration
public class SwaggerConfigurer {
#Bean
public SecurityConfiguration securityConfiguration() {
Map<String, Object> additionalQueryStringParams=new HashMap<>();
additionalQueryStringParams.put("nonce","123456");
return SecurityConfigurationBuilder.builder()
.clientId("test-uid").realm("Master").appName("swagger-ui")
.additionalQueryStringParams(additionalQueryStringParams)
.build();
}
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.testspring"))
.paths(PathSelectors.any())
.build().securitySchemes(buildSecurityScheme()).securityContexts(buildSecurityContext());
}
private List<SecurityContext> buildSecurityContext() {
List<SecurityReference> securityReferences = new ArrayList<>();
securityReferences.add(SecurityReference.builder().reference("oauth2").scopes(scopes().toArray(new AuthorizationScope[]{})).build());
SecurityContext context = SecurityContext.builder().forPaths(Predicates.alwaysTrue()).securityReferences(securityReferences).build();
List<SecurityContext> ret = new ArrayList<>();
ret.add(context);
return ret;
}
private List<? extends SecurityScheme> buildSecurityScheme() {
List<SecurityScheme> lst = new ArrayList<>();
// lst.add(new ApiKey("api_key", "X-API-KEY", "header"));
LoginEndpoint login = new LoginEndpointBuilder().url("http://172.17.0.2:8080/auth/realms/master/protocol/openid-connect/auth").build();
List<GrantType> gTypes = new ArrayList<>();
gTypes.add(new ImplicitGrant(login, "acces_token"));
lst.add(new OAuth("oauth2", scopes(), gTypes));
return lst;
}
private List<AuthorizationScope> scopes() {
List<AuthorizationScope> scopes = new ArrayList<>();
for (String scopeItem : new String[]{"openid=openid", "profile=profile"}) {
String scope[] = scopeItem.split("=");
if (scope.length == 2) {
scopes.add(new AuthorizationScopeBuilder().scope(scope[0]).description(scope[1]).build());
} else {
log.warn("Scope '{}' is not valid (format is scope=description)", scopeItem);
}
}
return scopes;
}
}
There is a lot of thing you can update in this code. This is mainly the same as before:
nonce which should be a random thing (swagger-ui don't use it yet)
clientId which you need to setup accordingly to the client you setup in keycloak
basePackage: You need to set the package in which all your controller are
If you need an api-key, you can enable it and add it on the security scheme list
LoginEndpoint: that need to be the authorization endpoint of you keycloak realm
scopeItems: the scopes you want for this authentication.
It will generate the same thing as before: Updating the swagger to add the securityDefinition and make swagger-UI take the parameter for clientId, nonce, ...
Was struggling with this setup for the past 2 days. Finally got a working solution for those who cannot resolve.
pom.xml
...
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
...
Enable Swagger on main class
...
import springfox.documentation.swagger2.annotations.EnableSwagger2;
#SpringBootApplication
#EnableSwagger2
#EnableAsync
#EnableCaching
public class MainApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(MainApplication.class);
app.run(args);
}
}
SwaggerConfig.java
package com.XXX.XXXXXXXX.app.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.AuthorizationCodeGrantBuilder;
import springfox.documentation.builders.OAuthBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.SecurityConfiguration;
import springfox.documentation.swagger.web.SecurityConfigurationBuilder;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.Arrays;
import static springfox.documentation.builders.PathSelectors.regex;
/*
* Setting up Swagger for spring boot
* https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api
*/
#Configuration
#EnableSwagger2
public class SwaggerConfig {
#Value("${keycloak.auth-server-url}")
private String AUTH_SERVER;
#Value("${keycloak.credentials.secret}")
private String CLIENT_SECRET;
#Value("${keycloak.resource}")
private String CLIENT_ID;
#Value("${keycloak.realm}")
private String REALM;
private static final String OAUTH_NAME = "spring_oauth";
private static final String ALLOWED_PATHS = "/directory_to_controllers/.*";
private static final String GROUP_NAME = "XXXXXXX-api";
private static final String TITLE = "API Documentation for XXXXXXX Application";
private static final String DESCRIPTION = "Description here";
private static final String VERSION = "1.0";
#Bean
public Docket taskApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName(GROUP_NAME)
.useDefaultResponseMessages(true)
.apiInfo(apiInfo())
.select()
.paths(regex(ALLOWED_PATHS))
.build()
.securitySchemes(Arrays.asList(securityScheme()))
.securityContexts(Arrays.asList(securityContext()));
}
private ApiInfo apiInfo() {
return new
ApiInfoBuilder().title(TITLE).description(DESCRIPTION).version(VERSION).build();
}
#Bean
public SecurityConfiguration security() {
return SecurityConfigurationBuilder.builder()
.realm(REALM)
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.appName(GROUP_NAME)
.scopeSeparator(" ")
.build();
}
private SecurityScheme securityScheme() {
GrantType grantType =
new AuthorizationCodeGrantBuilder()
.tokenEndpoint(new TokenEndpoint(AUTH_SERVER + "/realms/" + REALM + "/protocol/openid-connect/token", GROUP_NAME))
.tokenRequestEndpoint(
new TokenRequestEndpoint(AUTH_SERVER + "/realms/" + REALM + "/protocol/openid-connect/auth", CLIENT_ID, CLIENT_SECRET))
.build();
SecurityScheme oauth =
new OAuthBuilder()
.name(OAUTH_NAME)
.grantTypes(Arrays.asList(grantType))
.scopes(Arrays.asList(scopes()))
.build();
return oauth;
}
private AuthorizationScope[] scopes() {
AuthorizationScope[] scopes = {
new AuthorizationScope("user", "for CRUD operations"),
new AuthorizationScope("read", "for read operations"),
new AuthorizationScope("write", "for write operations")
};
return scopes;
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(Arrays.asList(new SecurityReference(OAUTH_NAME, scopes())))
.forPaths(PathSelectors.regex(ALLOWED_PATHS))
.build();
}
}
From terminal, run "mvnw spring-boot:run"
Open browser and hit http://localhost:[port]/[app_name]/swagger-ui.html.
Click the Authorize button:
Swagger Authorize Button
This should present a modal to confirm your keycloak settings.
Click Authorize button once again. You should be redirected to a login screen.
Once credentials are entered and confirmed, you will be redirected back to Swagger-UI fully authenticated.
Swagger-ui + Keycloak (or any other OAuth2 provider) using implicit flow, OpenAPI 3.0 template:
components:
...
securitySchemes:
my_auth_whatever:
type: oauth2
flows:
implicit:
authorizationUrl: https://MY-KEYCLOAK-HOST/auth/realms/MY-REALM-ID/protocol/openid-connect/auth
scopes: {}
...
security:
- my_auth_whatever: []
Make sure the implicit flow is enabled in Keycloak settings for the client that you use.
One downside is that the user is still asked for client_id in the modal when clicks on "Authorize" button in Swagger UI.
The value that user enters may be overwritten by adding query param ?client_id=YOUR-CLIENT-ID to the authorizationUrl but it's kinda the dirty hack and the modal is still showed to the user.
When running swagger-ui in docker - the OAUTH_CLIENT_ID env var may be provided to container to set the default client_id value for the modal.
For non-docker deployment refer to #wargre's approach with changing the index.html (not sure if there's a better way).
For SwaggerAPI (OpenAPI 2.0) example refer to first code snippet in #wargre's answer and this doc: https://swagger.io/docs/specification/2-0/authentication/

Spring Security OAuth2 AngularJS | Logout Flow

Referring to the logout flow in oauth2 spring-guides project, once the the user has authenticated using user/password for the first time, the credentials are not asked next time after logout.
How can I ensure that username/password are asked every time after a logout.
This is what I am trying to implement:-
OAuth2 server issuing JWT token using "authorization_code" grant type
with auto approval. This has html/angularjs form to collect
username/password.
UI/Webfront - Uses #EnableSSO. ALL its endpoints are authenticated
i.e it does not have any unauthorized landing page/ui/link that user
clicks to go to /uaa server. So hitting http://localhost:8080
instantly redirects you to http://localhost:9999/uaa and presents
custom form to collect username/password.
Resource server - Uses #EnableResourceServer. Plain & simple REST api.
With the above approach I am not able to workout the logout flow. HTTP POST /logout to the UI application clears the session/auth in UI application but the users gets logged in again automatically ( as I have opted for auto approval for all scopes) without being asked for username password again.
Looking at logs and networks calls, it looks like that all the "oauth dance" happens all over again successfully without user being asked for username/password again and seems like the auth server remembers last auth token issued for a client ( using org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices? ).
How can I tell auth server to ask for username/password every time it is requested for code/token - stateless.
Or what is the best way to implement logout in my given scenario.
( To recreate somewhat near to my requirements, remove permitAll() part from the UiApplication and configure autoApproval in auth server of the mentioned boot project.)
github issue
I also faced the error as you described and I saw a solution from question
Spring Boot OAuth2 Single Sign Off. I don't mean this is the only and global truth solution.
But in the scenario,
authentication server has login form and you'd authenticated from it
browser still maintain the session with authentication server
after you have finished logout process (revoke tokens,remove cookies...)
and try to re-login again
authentication server do not send login form and automatically sign in
You need to remove authentication informations from authentication server's session as this answer described.
Below snippets are how did I configure for solution
Client (UI Application in your case) application's WebSecurityConfig
...
#Value("${auth-server}/ssoLogout")
private String logoutUrl;
#Autowired
private CustomLogoutHandler logoutHandler;
...
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl(logoutUrl)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.addLogoutHandler(logoutHandler)
.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
// #formatter:on
}
Custom logout handler for client application
#Component
public class CustomLogoutHandler implements LogoutHandler {
private static Logger logger = Logger.getLogger(CustomLogoutHandler.class);
#Value("${auth-server}/invalidateTokens")
private String logoutUrl;
#Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
logger.debug("Excution CustomLogoutHandler for " + authentication.getName());
Object details = authentication.getDetails();
if (details.getClass().isAssignableFrom(OAuth2AuthenticationDetails.class)) {
String accessToken = ((OAuth2AuthenticationDetails) details).getTokenValue();
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("access_token", accessToken);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "bearer " + accessToken);
HttpEntity<Object> entity = new HttpEntity<>(params, headers);
HttpMessageConverter<?> formHttpMessageConverter = new FormHttpMessageConverter();
HttpMessageConverter<?> stringHttpMessageConverternew = new StringHttpMessageConverter();
restTemplate.setMessageConverters(Arrays.asList(new HttpMessageConverter[] { formHttpMessageConverter, stringHttpMessageConverternew }));
try {
ResponseEntity<String> serverResponse = restTemplate.exchange(logoutUrl, HttpMethod.POST, entity, String.class);
logger.debug("Server Response : ==> " + serverResponse);
} catch (HttpClientErrorException e) {
logger.error("HttpClientErrorException invalidating token with SSO authorization server. response.status code: " + e.getStatusCode() + ", server URL: " + logoutUrl);
}
}
authentication.setAuthenticated(false);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
new SecurityContextLogoutHandler().logout(request, response, auth);
}
}
I used JDBC tokenStore, so I need to revoke tokens.At the authentication server side, I added a controller to handle logout processes
#Controller
public class AuthenticationController {
private static Logger logger = Logger.getLogger(AuthenticationController.class);
#Resource(name = "tokenStore")
private TokenStore tokenStore;
#Resource(name = "approvalStore")
private ApprovalStore approvalStore;
#RequestMapping(value = "/invalidateTokens", method = RequestMethod.POST)
public #ResponseBody Map<String, String> revokeAccessToken(HttpServletRequest request, HttpServletResponse response, #RequestParam(name = "access_token") String accessToken, Authentication authentication) {
if (authentication instanceof OAuth2Authentication) {
logger.info("Revoking Approvals ==> " + accessToken);
OAuth2Authentication auth = (OAuth2Authentication) authentication;
String clientId = auth.getOAuth2Request().getClientId();
Authentication user = auth.getUserAuthentication();
if (user != null) {
Collection<Approval> approvals = new ArrayList<Approval>();
for (String scope : auth.getOAuth2Request().getScope()) {
approvals.add(new Approval(user.getName(), clientId, scope, new Date(), ApprovalStatus.APPROVED));
}
approvalStore.revokeApprovals(approvals);
}
}
logger.info("Invalidating access token :- " + accessToken);
OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);
if (oAuth2AccessToken != null) {
if (tokenStore instanceof JdbcTokenStore) {
logger.info("Invalidating Refresh Token :- " + oAuth2AccessToken.getRefreshToken().getValue());
((JdbcTokenStore) tokenStore).removeRefreshToken(oAuth2AccessToken.getRefreshToken());
tokenStore.removeAccessToken(oAuth2AccessToken);
}
}
Map<String, String> ret = new HashMap<>();
ret.put("removed_access_token", accessToken);
return ret;
}
#GetMapping("/ssoLogout")
public void exit(HttpServletRequest request, HttpServletResponse response) throws IOException {
new SecurityContextLogoutHandler().logout(request, null, null);
// my authorization server's login form can save with remember-me cookie
Cookie cookie = new Cookie("my_rememberme_cookie", null);
cookie.setMaxAge(0);
cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
response.addCookie(cookie);
response.sendRedirect(request.getHeader("referer"));
}
}
At authorization server's SecurityConfig, you may need to allow this url as
http
.requestMatchers()
.antMatchers(
"/login"
,"/ssoLogout"
,"/oauth/authorize"
,"/oauth/confirm_access");
I hope this may help a little for you.
As you are using JWT tokens, you can not really revoke them.
As a workaround, you can have a logout rest endpoint that would store the timestamp and userid for logout call.
Later, you can compare the logout time with JWT token issue time, and decide wether to allow an api call or not.
I have realized that redirecting to a controller when you logout from your client app and then programmatically logout on your authserver does the trick. This is my configuration on the client app:
#Configuration
#EnableOAuth2Sso
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Value("${auth-server}/exit")
private String logoutUrl;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutSuccessUrl(logoutUrl)
.and().authorizeRequests().anyRequest().authenticated();
}
}
and this is my configuration on my authserver (is just a controller handling the /exit endpoint):
#Controller
public class LogoutController {
public LogoutController() {
}
#RequestMapping({"/exit"})
public void exit(HttpServletRequest request, HttpServletResponse response) {
(new SecurityContextLogoutHandler()).logout(request, null, null);
try {
response.sendRedirect(request.getHeader("referer"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
Here is a sample app that shows the full implementation using JWT. Check it out and let us know if it helps you.

Resources