Fine-grained authorization with public client using keycloak - oauth-2.0

From my understanding, a client (in our case SPA) will exchange an authorization_code for an access_token with the authorization server (keycloak).
This access token can then be sent to the Resource Server (in our case, Spring server), which then controls and grant access to the resources available.
A confidential client can customize these authorization to a deep level (using permissions, scope and policies). Whereas a public client only could use client scope. (Is there any difference between scope and client scope ?)
The difference between a public and a confidential client is that the public client can't be trusted to hold credentials. (Such as a client secret).
It still doesn't stop you from using any role mapping (afaik coarse-grained authz) whatsoever, therefore :
How come only coarse-grained authorization is available for a public client ?
How should I workaround that ? Configure my resource reserver as a keycloak client & somehow share the JWT securely ? While mapping the public's app roles to the resource server ?
Thanks !

Just define how Spring authorities should be mapped from JWT claims or introspected attributes.
In the following, I map authorities from Keycloak "roles" from "realm" and two different clients (one confidential and one public, but this makes absolutely no difference). Just open one of your JWT access-token in a tool like https://jwt.io or call introspection endpoint with a valid access-token to see attributes (and figure out where are roles, permissions, groups or whatever should be mapped to Spring authorities).
JWT decoder
You can configure a Converter<Jwt, AbstractAuthenticationToken>:
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) {
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);
}
public interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
}
public interface Jwt2AuthenticationConverter extends Converter<Jwt, AbstractAuthenticationToken> {
}
#SuppressWarnings("unchecked")
#Bean
public Jwt2AuthoritiesConverter authoritiesConverter() {
return jwt -> {
final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());
final var resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access", Map.of());
// We assume here you have "spring-addons-confidential" and
// "spring-addons-public" clients configured with "client roles" mapper in
// Keycloak
final var confidentialClientAccess = (Map<String, Object>) resourceAccess
.getOrDefault("spring-addons-confidential", Map.of());
final var confidentialClientRoles = (Collection<String>) confidentialClientAccess.getOrDefault("roles",
List.of());
final var publicClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-public",
Map.of());
final var publicClientRoles = (Collection<String>) publicClientAccess.getOrDefault("roles", List.of());
return Stream
.concat(realmRoles.stream(),
Stream.concat(confidentialClientRoles.stream(), publicClientRoles.stream()))
.map(SimpleGrantedAuthority::new).toList();
};
}
#Bean
public Jwt2AuthenticationConverter authenticationConverter(Converter<Jwt, Collection<? extends GrantedAuthority>> authoritiesConverter) {
return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
}
Token introspection
Since spring-security 5.8, you can define an authentication converter for access-token introspection too:
#Bean
SecurityFilterChain filterChain(HttpSecurity http, OpaqueTokenAuthenticationConverter authenticationConverter) {
http.oauth2ResourceServer().opaqueToken().authenticationConverter(authenticationConverter);
}
#Bean
OpaqueTokenAuthenticationConverter authenticationConverter(
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) {
return (String introspectedToken,
OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> new BearerTokenAuthentication(
authenticatedPrincipal,
new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken,
authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT),
authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP)),
authoritiesConverter.convert(authenticatedPrincipal.getAttributes()));
}
public interface Attributes2AuthoritiesConverter extends Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> { }
#SuppressWarnings("unchecked")
#Bean
public Attributes2AuthoritiesConverter authoritiesConverter() {
return attributes -> {
final var realmAccess = (Map<String, Object>) attributes.getOrDefault("realm_access", Map.of());
final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());
final var resourceAccess = (Map<String, Object>) attributes.getOrDefault("resource_access", Map.of());
// We assume here you have "spring-addons-confidential" and
// "spring-addons-public" clients configured with "client roles" mapper in
// Keycloak
final var confidentialClientAccess = (Map<String, Object>) resourceAccess
.getOrDefault("spring-addons-confidential", Map.of());
final var confidentialClientRoles = (Collection<String>) confidentialClientAccess.getOrDefault("roles",
List.of());
final var publicClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-public",
Map.of());
final var publicClientRoles = (Collection<String>) publicClientAccess.getOrDefault("roles", List.of());
return Stream
.concat(realmRoles.stream(),
Stream.concat(confidentialClientRoles.stream(), publicClientRoles.stream()))
.map(SimpleGrantedAuthority::new).toList();
};
}
Make things simple
I maintain spring-boot starters which are thin wrappers arround spring-boot-starter-oauth2-resource-server. It allows to configure most of resource-servers security from properties file: authorities mapping (with hand on prefix and case), off course, but also other usefull stuff like CORS, CSRF, public routes and session-management:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.4</version>
</dependency>
#EnableMethodSecurity
public static class WebSecurityConfig { }
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,ressource_access.some-client.roles
com.c4-soft.springaddons.security.cors[0].path=/some-api
And as a bonus, it comes with tools to ease your access-control unit & integration tests:
#Test
#WithMockJwtAuth(authorities = "ROLE_AUTHORIZED_PERSONNEL", claims = #OpenIdClaims(sub = "Ch4mpy"))
void greetMockCh4mpy() throws Exception {
mockMvc.perform(get("/greet")).andExpect(content().string("Hello Ch4mpy! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
}
Refer to tutorials and samples for more details and declinations for
servlet or reactive apps
JWT decoding or token introspection
Spring default or custom Authentication implementations

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

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

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

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/

Facebook Login with Spring Social using Existing User Access Token

Here's what I currently have:
Spring REST service where many of the APIs require the user to be authenticated
A 'registration' API (/api/v1/register)
A 'login' API that takes username/password (/api/v1/login)
'Facebook Login' API that relies on Spring Social and Spring Security to create a User Connection and log my user in (/auth/facebook)
My problem is that I want these APIs to be used by multiple clients, but the way Facebook Login is right now, it doesn't work well on mobile (works great on a website).
Here's the mobile scenario:
I use Facebook's iOS SDK to request permission from the user
Facebook returns a user access token
I want to send my backend service this token and have Spring Social accept it, create the User Connection, etc.
Can this be done? Or am I going to have to write my own API to persist the User Connection?
Appreciate any help!
I had the exact same issue and here's how I made it work. You probably have a SocialConfigurer somewhere with the following:
#Configuration
#EnableSocial
public class SocialConfig implements SocialConfigurer {
#Autowired
private DataSource dataSource;
#Bean
public FacebookConnectionFactory facebookConnectionFactory() {
FacebookConnectionFactory facebookConnectionFactory = new FacebookConnectionFactory("AppID", "AppSecret");
facebookConnectionFactory.setScope("email");
return facebookConnectionFactory;
}
#Override
public void addConnectionFactories(ConnectionFactoryConfigurer cfConfig, Environment env) {
cfConfig.addConnectionFactory(facebookConnectionFactory());
}
#Override
public UserIdSource getUserIdSource() {
return new AuthenticationNameUserIdSource();
}
#Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
}
// Other #Bean maybe ...
}
From here, what you can do is, in a Controller/RestController, add a mapping with a RequestParam for your token that you will send to your server:
#Autowired
private FacebookConnectionFactory facebookConnectionFactory;
#Autowired
private UsersConnectionRepository usersConnectionRepository;
#RequestMapping(value = "/my-facebook-url", method = RequestMethod.POST)
public String fb(#RequestParam String token) {
AccessGrant accessGrant = new AccessGrant(token);
Connection<Facebook> connection = facebookConnectionFactory.createConnection(accessGrant);
UserProfile userProfile = connection.fetchUserProfile();
usersConnectionRepository.createConnectionRepository(userProfile.getEmail()).addConnection(connection);
// ...
return "Done";
}
Useful references
UsersConnectionRepository
ConnectionRepository

Resources