SpringDoc OIDC: how to show only Implicit Flow among the available authorizations? - swagger-ui

I'm trying to configure SpringDoc/Swagger-UI in order to show only the Implicit Flow
when clicking on the Authorize button.
However, it shows all the possible authorization methods supported by the IDAM,
as show at /.well-known/openid-configuration:
"grant_types_supported":["authorization_code","implicit","refresh_token","password","client_credentials","urn:ietf:params:oauth:grant-type:device_code","urn:openid:params:grant-type:ciba"]
authorization_code
implicit
refresh_token
password
client_credentials
urn:ietf:params:oauth:grant-type:device_code
urn:openid:params:grant-type:ciba
This is my current configuration:
#Configuration
#RequiredArgsConstructor
public class OpenAPIConfiguration {
private final OAuth2Configuration oAuth2Configuration;
#Bean
public SecurityScheme securityScheme() {
String tokenIssuer = this.oAuth2Configuration.getIssuers().get(0);
String openIdConnectUrl = tokenIssuer + "/.well-known/openid-configuration";
OAuthFlow implicitOAuthFlow = new OAuthFlow();
return new SecurityScheme()
.name("OIDC-Auth")
.type(SecurityScheme.Type.OPENIDCONNECT)
.scheme("bearer")
.bearerFormat("jwt")
.in(SecurityScheme.In.HEADER)
.openIdConnectUrl(openIdConnectUrl)
.flows(new OAuthFlows().implicit(implicitOAuthFlow));
}
#Bean
public SecurityRequirement securityRequirement() {
return new SecurityRequirement().addList("OIDC-Auth");
}
#Bean
public OpenAPI openAPI(SecurityScheme securityScheme, SecurityRequirement securityRequirement) {
return new OpenAPI()
.info(new Info()
.title("MY API")
.version("1"))
.components(new Components()
.addSecuritySchemes(securityScheme.getName(), securityScheme))
.addSecurityItem(securityRequirement);
}
}
How can I limit the flows to be displayed on the UI?

The example below works for me:
...
private static final String PROTOCOL_OPENID_CONNECT = "%s/realms/%s/protocol/openid-connect";
#Bean
OpenAPI customOpenApi() {
return new OpenAPI()
.addServersItem(new Server().url(API_SERVER_URL))
.components(createOauth2SecurityScheme())
.security(createOauth2SecurityRequirement())
.info(createInfo());
}
private Components createOauth2SecurityScheme() {
return new Components().addSecuritySchemes("oAuth2", createOauth2Scheme());
}
private List<SecurityRequirement> createOauth2SecurityRequirement() {
return List.of(new SecurityRequirement().addList("oAuth2"));
}
private SecurityScheme createOauth2Scheme() {
String authUrl = String.format(PROTOCOL_OPENID_CONNECT, AUTH_SERVER_URL, REALM);
String tokenUrl = String.format(PROTOCOL_OPENID_CONNECT, AUTH_SERVER_URL, REALM);
return new SecurityScheme()
.type(SecurityScheme.Type.OAUTH2)
.description("OAuth2 Flow")
.flows(new OAuthFlows()
.authorizationCode(
new OAuthFlow()
.authorizationUrl(authUrl + "/auth")
.tokenUrl(tokenUrl + "/token")
.scopes(new Scopes())
));
}
...

Related

Swagger endless loading when i click in Execute

I am using Swagger with keycloak. I am getting the below error when I click on Execute button.
when I click on Execute on swagger I see the loading image, and there is no any request on the network tap because I have an error on the console.
I will add my config code and YML file to allow you to see what I do.
anyone can help me, please?
Here is the output in the console:
Here is the error code in the console tap (Its generated code):
here is the swagger UI page:
here is the swagger config:
#Slf4j
#Configuration
#TbCoreComponent
public class SwaggerConfiguration {
#Value("${swagger.api_path_regex}")
private String apiPathRegex;
#Value("${swagger.security_path_regex}")
private String securityPathRegex;
#Value("${swagger.non_security_path_regex}")
private String nonSecurityPathRegex;
#Value("${swagger.title}")
private String title;
#Value("${swagger.description}")
private String description;
#Value("${swagger.contact.name}")
private String contactName;
#Value("${swagger.contact.url}")
private String contactUrl;
#Value("${swagger.contact.email}")
private String contactEmail;
#Value("${swagger.version}")
private String version;
#Value("${app.version:unknown}")
private String appVersion;
// Used to get token from Keyclaok
#Value("${swagger.auth.token_url}")
private String keycloakAuthTokenUrl;
#Value("${security.keycloak.realm}")
private String keycloakRealm;
#Value("${security.keycloak.clientId}")
private String keyclaokAuthCliendId;
#Bean
public Docket yousefApi() {
TypeResolver typeResolver = new TypeResolver();
return new Docket(DocumentationType.SWAGGER_2)
.groupName("Hammad")
.apiInfo(apiInfo())
.additionalModels(
typeResolver.resolve(ThingsboardErrorResponse.class),
typeResolver.resolve(ThingsboardCredentialsExpiredResponse.class)
)
.select()
.paths(apiPaths())
.paths(any())
.build()
.globalResponses(HttpMethod.GET, defaultErrorResponses(false))
.globalResponses(HttpMethod.POST, defaultErrorResponses(true))
.globalResponses(HttpMethod.DELETE, defaultErrorResponses(false))
.securitySchemes(newArrayList(apiKey()))
.securityContexts(newArrayList(securityContext()))
.enableUrlTemplating(true);
}
#Bean
#Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
ApiListingBuilderPlugin loginEndpointListingBuilder() {
return new ApiListingBuilderPlugin() {
#Override
public void apply(ApiListingContext apiListingContext) {
if (apiListingContext.getResourceGroup().getGroupName().equals("Hammad")) {
ApiListing apiListing = apiListingContext.apiListingBuilder().build();
if (apiListing.getResourcePath().equals(keycloakAuthTokenUrl)) {
apiListingContext.apiListingBuilder().tags(Set.of(new Tag("login-endpoint", "Login Endpoint")));
apiListingContext.apiListingBuilder().description("Login Endpoint");
}
}
}
#Override
public boolean supports(#NotNull DocumentationType delimiter) {
return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
}
};
}
#Bean
UiConfiguration uiConfig() {
return UiConfigurationBuilder.builder()
.deepLinking(true)
.displayOperationId(false)
.defaultModelsExpandDepth(1)
.defaultModelExpandDepth(1)
.defaultModelRendering(ModelRendering.EXAMPLE)
.displayRequestDuration(false)
.docExpansion(DocExpansion.NONE)
.filter(false)
.maxDisplayedTags(null)
.operationsSorter(OperationsSorter.ALPHA)
.showExtensions(false)
.showCommonExtensions(false)
.supportedSubmitMethods(UiConfiguration.Constants.DEFAULT_SUBMIT_METHODS)
.validatorUrl(null)
.persistAuthorization(true)
.syntaxHighlightActivate(true)
.syntaxHighlightTheme("agate")
.build();
}
private ApiKey apiKey() {
return new ApiKey("Bearer", "X-Authorization", "header");
}
private OAuth securityScheme() {
List<GrantType> grantTypes = newArrayList(new ResourceOwnerPasswordCredentialsGrant(keycloakAuthTokenUrl));
return new OAuth("KeycloakAuth", new ArrayList<>(), grantTypes);
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.operationSelector(securityPathOperationSelector())
.build();
}
private Predicate<String> apiPaths() {
return regex(apiPathRegex);
}
private Predicate<OperationContext> securityPathOperationSelector() {
return new SecurityPathOperationSelector(securityPathRegex, nonSecurityPathRegex);
}
private AuthorizationScope[] scopes() {
AuthorizationScope[] authorizationScopes = new AuthorizationScope[3];
authorizationScopes[0] = new AuthorizationScope(Authority.SYS_ADMIN.name(), "System administrator");
authorizationScopes[1] = new AuthorizationScope(Authority.TENANT_ADMIN.name(), "Tenant administrator");
authorizationScopes[2] = new AuthorizationScope(Authority.CUSTOMER_USER.name(), "Customer");
return authorizationScopes;
}
List<SecurityReference> defaultAuth() {
return newArrayList(new SecurityReference("KeycloakAuth", scopes()), new SecurityReference("Bearer", scopes()));
}
private ApiInfo apiInfo() {
String apiVersion = version;
if (StringUtils.isEmpty(apiVersion)) {
apiVersion = appVersion;
}
return new ApiInfoBuilder()
.title(title)
.description(description)
.contact(new Contact(contactName, contactUrl, contactEmail))
.version(apiVersion)
.build();
}
/** Helper methods **/
private List<Response> defaultErrorResponses(boolean isPost) {
return List.of(
errorResponse("400", "Bad Request",
ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST)),
errorResponse("401", "Unauthorized",
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
errorResponse("403", "Forbidden",
ThingsboardErrorResponse.of("You don't have permission to perform this operation!",
ThingsboardErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN)),
errorResponse("404", "Not Found",
ThingsboardErrorResponse.of("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND, HttpStatus.NOT_FOUND)),
errorResponse("429", "Too Many Requests",
ThingsboardErrorResponse.of("Too many requests for current tenant!",
ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS))
);
}
private Response errorResponse(String code, String description, ThingsboardErrorResponse example) {
return errorResponse(code, description, List.of(errorExample("error-code-" + code, description, example)));
}
private Response errorResponse(String code, String description, List<Example> examples) {
return errorResponse(code, description, examples, ThingsboardErrorResponse.class);
}
private Response errorResponse(String code, String description, List<Example> examples,
Class<? extends ThingsboardErrorResponse> errorResponseClass) {
return new ResponseBuilder()
.code(code)
.description(description)
.examples(examples)
.representation(MediaType.APPLICATION_JSON)
.apply(classRepresentation(errorResponseClass, true))
.build();
}
private Example errorExample(String id, String summary, ThingsboardErrorResponse example) {
return new ExampleBuilder()
.mediaType(MediaType.APPLICATION_JSON_VALUE)
.summary(summary)
.id(id)
.value(example).build();
}
private Consumer<RepresentationBuilder> classRepresentation(Class<?> clazz, boolean isResponse) {
return r -> r.model(
m ->
m.referenceModel(ref ->
ref.key(k ->
k.qualifiedModelName(q ->
q.namespace(clazz.getPackageName())
.name(clazz.getSimpleName())).isResponse(isResponse)))
);
}
private static class SecurityPathOperationSelector implements Predicate<OperationContext> {
private final Predicate<String> securityPathSelector;
SecurityPathOperationSelector(String securityPathRegex, String nonSecurityPathRegex) {
this.securityPathSelector = (not(regex(nonSecurityPathRegex)));
}
#Override
public boolean test(OperationContext operationContext) {
return this.securityPathSelector.test(operationContext.requestMappingPattern());
}
}
}
here is the YML file:
swagger:
auth:
token_url: ${security.keycloak.serverUrl}/realms/${security.keycloak.realm}/protocol/openid-connect/token/
auth_url: ${security.keycloak.serverUrl}/realms/${security.keycloak.realm}/protocol/openid-connect/auth/
api_path_regex: "${SWAGGER_API_PATH_REGEX:/api/(customer|device|user|tenant).*}"
security_path_regex: "${SWAGGER_SECURITY_PATH_REGEX:/api/(customer|device|user|tenant).*}"
non_security_path_regex: "${SWAGGER_NON_SECURITY_PATH_REGEX:/api/(?:noauth|v1)/.*}"
title: "${SWAGGER_TITLE:yousefCo REST API}"
description: "${SWAGGER_DESCRIPTION: yousefCo open-source IoT platform REST API documentation.}"
contact:
name: "${SWAGGER_CONTACT_NAME:yousefCo Team}"
url: "${SWAGGER_CONTACT_URL:http://iot.test.net}"
email: "${SWAGGER_CONTACT_EMAIL:info#gmail.com}"
license:
title: "${SWAGGER_LICENSE_TITLE:Apache License Version 2.0}"
url: "${SWAGGER_LICENSE_URL:https://github.com/yousef/yousef/blob/master/LICENSE}"
version: "${SWAGGER_VERSION:}"
I think you have mixed the configuration between swagger version 2 and 3, your overall setting is configured to be used in Swagger 3.0, so
First, You have to change the DocumentationType to OAS_30.
Second, When using the ApiKey security scheme, note that the field name will be used as the header when calling an API, so you have to change it to:
private ApiKey apiKey() {
return new ApiKey("X-Authorization", "AnyNameYouWant", "header");
}
Note that you have to add the Bearer word before the token, also you have to change the name SecurityReference to X-Authorization.
Third, If you need to config the OAuth2 security scheme, instead of using the OAuth class, use the OAuth2Scheme builder class with password flow, like this:
return OAuth2Scheme.OAUTH2_PASSWORD_FLOW_BUILDER
.name("KeycloakAuth")
.scopes(newArrayList(scopes()))
.tokenUrl(keycloakAuthTokenUrl)
.refreshUrl(keycloakAuthTokenUrl)
.build();
Don't forget to add the same scopes in SecurityReference into the OAuth2Scheme scopes.

RSocket Java Spring #AuthenticationPrincipal with JWT

How can we use #AuthenticationPrincipal with a RSocket Method #AuthenticationPrincipal Mono token
public Mono<String> uppercase(String s, #AuthenticationPrincipal Mono<JwtAuthenticationToken> token) {
//Token is always null
return Mono.just(s.toUpperCase());
}
I created a RSocketSecurityConfiguration class:
#Configuration
#EnableRSocketSecurity
#EnableReactiveMethodSecurity
#Slf4j
public class RSocketSecurityConfiguration {
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUri;
#Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
rsocket
.authorizePayload(authorize ->
authorize
.anyRequest().authenticated()
.anyExchange().permitAll()
)
.jwt(jwtSpec -> {
jwtSpec.authenticationManager(jwtReactiveAuthenticationManager(reactiveJwtDecoder()));
});
return rsocket.build();
}
#Bean
ReactiveJwtDecoder reactiveJwtDecoder() {
NimbusReactiveJwtDecoder decoder = (NimbusReactiveJwtDecoder)
ReactiveJwtDecoders.fromOidcIssuerLocation(issuerUri);
return decoder;
}
#Bean
public JwtReactiveAuthenticationManager jwtReactiveAuthenticationManager(ReactiveJwtDecoder reactiveJwtDecoder) {
JwtReactiveAuthenticationManager jwtReactiveAuthenticationManager = new JwtReactiveAuthenticationManager(reactiveJwtDecoder);
JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
authenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
jwtReactiveAuthenticationManager.setJwtAuthenticationConverter( new ReactiveJwtAuthenticationConverterAdapter(authenticationConverter));
return jwtReactiveAuthenticationManager;
}
#Bean
RSocketMessageHandler messageHandler(RSocketStrategies strategies) {
RSocketMessageHandler mh = new RSocketMessageHandler();
mh.getArgumentResolverConfigurer().addCustomResolver(new AuthenticationPrincipalArgumentResolver());
mh.setRSocketStrategies(strategies);
return mh;
}
Full UpperCaseController:
#Slf4j
#Controller
public class UpperCaseController {
#MessageMapping("uppercase")
public Mono<String> uppercase(String s, #AuthenticationPrincipal Mono<JwtAuthenticationToken> token) {
JwtAuthenticationToken currentToken = token.block();
if ( currentToken == null ) {
log.info("token is null");
}
return Mono.just(s.toUpperCase());
}
}
Full ConnectController:
#Slf4j
#Controller
public class ConnectController {
#ConnectMapping("connect")
void connectShellClientAndAskForTelemetry(RSocketRequester requester,
#Payload String client) {
requester.rsocket()
.onClose()
.doFirst(() -> {
// Add all new clients to a client list
log.info("Client: {} CONNECTED.", client);
})
.doOnError(error -> {
// Warn when channels are closed by clients
log.warn("Channel to client {} CLOSED", client);
})
.doFinally(consumer -> {
// Remove disconnected clients from the client list
log.info("Client {} DISCONNECTED", client);
})
.subscribe();
}
}
RSocket Client:
#Component
#Slf4j
public class RSocketClient {
private static final MimeType SIMPLE_AUTH = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
MimeType BEARER_AUTH =
MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
private static final String BEARER_TOKEN = "....";
private final RSocketRequester requester;
private RSocketStrategies rsocketStrategies;
public RSocketClient(RSocketRequester.Builder requesterBuilder,
#Qualifier("rSocketStrategies") RSocketStrategies strategies) {
this.rsocketStrategies = strategies;
SocketAcceptor responder = RSocketMessageHandler.responder(rsocketStrategies, new RSocketClientHandler());
requester = requesterBuilder
.setupRoute("connect")
.setupData("MyTestClient")
.setupMetadata(new BearerTokenMetadata(BEARER_TOKEN), BEARER_AUTH)
.rsocketStrategies(builder ->
builder.encoder(new BearerTokenAuthenticationEncoder()))
.rsocketConnector(connector -> connector.acceptor(responder))
.connectTcp("localhost", 7000)
.block();
requester.rsocket()
.onClose()
.doOnError(error -> log.warn("Connection CLOSED"))
.doFinally(consumer -> log.info("Client DISCONNECTED"))
.subscribe();
}
public void uppercase() {
String response = requester
.route("uppercase")
.metadata(BEARER_TOKEN, BEARER_AUTH)
.data("Hello")
.retrieveMono(String.class).block();
log.info(response);
}
}
I have done something very similar for Spring REST and it works fine but for RSocket the token is always null.
I assume you have started with https://spring.io/blog/2020/06/17/getting-started-with-rsocket-spring-security
I was able to get this working for my codebase using a different type than #Payload
#ConnectMapping
fun handle(requester: RSocketRequester, #AuthenticationPrincipal jwt: String) {
logger.debug("connected $jwt")
}
#MessageMapping("runCommand")
suspend fun runCommand(request: CommandRequest, rSocketRequester: RSocketRequester, #AuthenticationPrincipal jwt: String): Flow<CommandResponse> {
...
}
I am using Keycloak as jwt Issuer. Just follow up this git repo. Only thing that didn't work for me is
#CurrentUserProfile Mono<UserProfile> currentUserProfile
Solution for that w.r.t above git repo will be using either of the below
#CurrentUserProfile Mono<Jwt> currentUserProfile
or directly use
#AuthenticationPrincipal Jwt currentUserProfile
Hope this will work for you. Enjoy!

How to set user authorities from user claims return by an oauth server in spring security

I recently wrote a spring boot project that uses spring security oauth2, the auth server is IdentityServer4 for some reason, I can successfully login and get username in my project but I cannot find any way to set user's authority/role.
request.isUserInRole always return false.
#PreAuthorize("hasRole('rolename')") always lead me to 403.
Where can I place some code to set the authorities?
The server has returned some user claims through userinfo endpoint, and my project received them, and I can even see it in the principle param of my controller.
This method always return 403
#ResponseBody
#RequestMapping("admin")
#PreAuthorize("hasRole('admin')")
public String admin(HttpServletRequest request){
return "welcome, you are admin!" + request.isUserInRole("ROLE_admin");
}
application.properties
spring.security.oauth2.client.provider.test.issuer-uri = http://localhost:5000
spring.security.oauth2.client.provider.test.user-name-attribute = name
spring.security.oauth2.client.registration.test.client-id = java
spring.security.oauth2.client.registration.test.client-secret = secret
spring.security.oauth2.client.registration.test.authorization-grant-type = authorization_code
spring.security.oauth2.client.registration.test.scope = openid profile
I print the claims
#ResponseBody
#RequestMapping()
public Object index(Principal user){
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken)user;
return token.getPrincipal().getAttributes();
}
and get the result show there is a claim named 'role'
{"key":"value","role":"admin","preferred_username":"bob"}
Anybody can help me and give me a solution please?
EDIT 1:
The reason is oauth2 client has removed the extracter, and I have to implement the userAuthoritiesMapper.
Finally I got this work by adding the following class:
#Configuration
public class AppConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login().userInfoEndpoint().userAuthoritiesMapper(this.userAuthoritiesMapper());
//.oidcUserService(this.oidcUserService());
super.configure(http);
}
private GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (OidcUserAuthority.class.isInstance(authority)) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
if (userInfo.containsClaim("role")){
String roleName = "ROLE_" + userInfo.getClaimAsString("role");
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey("role")){
String roleName = "ROLE_" + (String)userAttributes.get("role");
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
});
return mappedAuthorities;
};
}
}
The framework changes so fast and the demos on the web is too old!
I spent a few hours and I find the solution. The problem is with spring oauth security, by default it obtain the user roles from the token using the key 'authorities'. So, I implemented a custom token converter.
The first you need is the custom user token converter, here is the class:
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
public class CustomUserTokenConverter implements UserAuthenticationConverter {
private Collection<? extends GrantedAuthority> defaultAuthorities;
private UserDetailsService userDetailsService;
private final String AUTHORITIES = "role";
private final String USERNAME = "preferred_username";
private final String USER_IDENTIFIER = "sub";
public CustomUserTokenConverter() {
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public void setDefaultAuthorities(String[] defaultAuthorities) {
this.defaultAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(defaultAuthorities));
}
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
Map<String, Object> response = new LinkedHashMap();
response.put(USERNAME, authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USER_IDENTIFIER)) {
Object principal = map.get(USER_IDENTIFIER);
Collection<? extends GrantedAuthority> authorities = this.getAuthorities(map);
if (this.userDetailsService != null) {
UserDetails user = this.userDetailsService.loadUserByUsername((String)map.get(USER_IDENTIFIER));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
} else {
return null;
}
}
private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
if (!map.containsKey(AUTHORITIES)) {
return this.defaultAuthorities;
} else {
Object authorities = map.get(AUTHORITIES);
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String)authorities);
} else if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString((Collection)authorities));
} else {
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}
}
}
}
The you need a custom token converter, here is:
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.stereotype.Component;
import java.util.Map;
#Component
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
OAuth2Authentication authentication = super.extractAuthentication(claims);
authentication.setDetails(claims);
return authentication;
}
}
And finally you ResourceServerConfiguration looks like this:
import hello.helper.CustomAccessTokenConverter;
import hello.helper.CustomUserTokenConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(final HttpSecurity http) throws Exception {
// #formatter:off
http.authorizeRequests()
.anyRequest().access("hasAnyAuthority('Admin')");
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("arawaks");
}
#Bean
#Primary
public RemoteTokenServices tokenServices() {
final RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setClientId("resourceId");
tokenServices.setClientSecret("resource.secret");
tokenServices.setCheckTokenEndpointUrl("http://localhost:5001/connect/introspect");
tokenServices.setAccessTokenConverter(accessTokenConverter());
return tokenServices;
}
#Bean
public CustomAccessTokenConverter accessTokenConverter() {
final CustomAccessTokenConverter converter = new CustomAccessTokenConverter();
converter.setUserTokenConverter(new CustomUserTokenConverter());
return converter;
}
}
Apparently #wjsgzcn answer (EDIT 1) DOES NOT WORK for reasons below
If you print the attributes returned by the Oauth2UserAuthirty class you will soon notice the contents of the JSON data does not have the role key instead has an authorities key hence you need to use that key to iterate over the list of authorities (roles) to get the actual role name.
Hence the following lines of code will not work as there is no role key in the JSON data returned by the oauth2UserAuthority.getAttributes();
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey("role")){
String roleName = "ROLE_" + (String)userAttributes.get("role");
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
So instead use the following to get the actual role from the getAttributes
if (userAttributes.containsKey("authorities")){
ObjectMapper objectMapper = new ObjectMapper();
ArrayList<Role> authorityList =
objectMapper.convertValue(userAttributes.get("authorities"), new
TypeReference<ArrayList<Role>>() {});
log.info("authList: {}", authorityList);
for(Role role: authorityList){
String roleName = "ROLE_" + role.getAuthority();
log.info("role: {}", roleName);
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
Where the Role is a pojo class like so
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Role {
#JsonProperty
private String authority;
}
That way you will be able to get the ROLE_ post prefix which is the actual role granted to the user after successfully authenticated to the Authorization server and the client is returned the LIST of granted authorities (roles).
Now the complete GrantedAuthoritesMapper look like the following:
private GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (OidcUserAuthority.class.isInstance(authority)) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
OidcIdToken idToken = oidcUserAuthority.getIdToken();
OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
// Map the claims found in idToken and/or userInfo
// to one or more GrantedAuthority's and add it to mappedAuthorities
if (userInfo.containsClaim("authorities")){
ObjectMapper objectMapper = new ObjectMapper();
ArrayList<Role> authorityList = objectMapper.convertValue(userInfo.getClaimAsMap("authorities"), new TypeReference<ArrayList<Role>>() {});
log.info("authList: {}", authorityList);
for(Role role: authorityList){
String roleName = "ROLE_" + role.getAuthority();
log.info("role: {}", roleName);
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
log.info("userAttributes: {}", userAttributes);
// Map the attributes found in userAttributes
// to one or more GrantedAuthority's and add it to mappedAuthorities
if (userAttributes.containsKey("authorities")){
ObjectMapper objectMapper = new ObjectMapper();
ArrayList<Role> authorityList = objectMapper.convertValue(userAttributes.get("authorities"), new TypeReference<ArrayList<Role>>() {});
log.info("authList: {}", authorityList);
for(Role role: authorityList){
String roleName = "ROLE_" + role.getAuthority();
log.info("role: {}", roleName);
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
}
});
log.info("The user authorities: {}", mappedAuthorities);
return mappedAuthorities;
};
}
Now you are able to use the userAuthorityMapper in your oauth2Login as follows
#Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.antMatchers("/", "/login**").permitAll()
.antMatchers("/clientPage/**").hasRole("CLIENT")
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.userAuthoritiesMapper(userAuthoritiesMapper());
}

Add a custom header to OAuth2 authentication in Springfox Swagger

I'm trying to add a custom header to the OAuth Security Scheme (Springfox Swagger 2.8.0).
Any ideas how this could be achieved?
My current configuration (using OAuth with ImplicitGrant, server side is keycloak) looks like:
#Bean
public SecurityContext securityContext() {
return SecurityContext.builder().securityReferences(defaultAuth()).build();
}
private List<SecurityReference> defaultAuth() {
return Arrays.asList(new SecurityReference(SECURITY_SCHEME_OAUTH2, defaultScope().toArray(new AuthorizationScope[] {})));
}
private Predicate<String> postPaths() {
return regex("/.*");
}
private Predicate<String> springBootActuatorJmxPaths() {
return regex("^/(?!env|restart|pause|resume|refresh).*$");
}
private List<AuthorizationScope> defaultScope() {
AuthorizationScope authorizationScope = new AuthorizationScope("openid", "Basic Open ID Connect Scope");
List<AuthorizationScope> authorizationScopes = new ArrayList<>();
authorizationScopes.add(authorizationScope);
return authorizationScopes;
}
#Bean
public Docket postsApi(List<SecurityContext> securityContexts) {
return new Docket(DocumentationType.SWAGGER_2).groupName("public-api")
.apiInfo(apiInfo()).select().paths(postPaths())
.apis(RequestHandlerSelectors.basePackage("com.example"))
.paths(springBootActuatorJmxPaths())
.build()
.securitySchemes(Collections.singletonList(oauth()))
.securityContexts(securityContexts)
;
}
#Bean
List<GrantType> grantTypes() {
List<GrantType> grantTypes = new ArrayList<>();
grantTypes.add(
new ImplicitGrant(
new LoginEndpoint(oAuthServerUri + "/realms/" + REALM_NAME + "/protocol/openid-connect/auth"),
"access_token"
)
);
return grantTypes;
}
#Bean
SecurityScheme oauth() {
return new OAuthBuilder()
.name(SECURITY_SCHEME_OAUTH2)
.scopes(defaultScope())
.grantTypes(grantTypes())
.build();
}
#Bean
public SecurityConfiguration securityInfo() {
return SecurityConfigurationBuilder.builder()
.clientId(clientId)
.realm(REALM_NAME)
.appName(serviceName)
.scopeSeparator(" ")
.build();
}
It's currently not possible in Springfox Swagger, more details here: https://github.com/springfox/springfox/issues/2266

Social login, spring-security-oauth2 and spring-security-jwt?

I'm developing a rest service which is going to be available in browser via
browser single page app and a mobile app. At the moment my service is working
without spring at all. The oauth2 client is implemented inside filters so to say "by hand".
I'm trying to migrate it to spring boot.
Much manuals read and much info googled and I'm trying to understand if the
following is actually possible for a customer:
Authorize with facebook oauth2 service (and get an access_token) with all the help
from spring-security-oauth2.
Create a JWT and pass it to the client so that all further requests are
backed with the JWT.
Since in my opinion spring boot is all about the configuration and declarations
I want to understand if this is possible with spring-security-oauth2 and
spring-security-jwt?
I'm not askng for a solution but just a yes/no from knowledge bearers since I'm deep in
the spring manuals and the answer becomes further...
short answer: Yes you can do it!
You have to add security dependencies to your build.gradle or pom.xml file:
compile "org.springframework.boot:spring-boot-starter-security"
compile "org.springframework.security:spring-security-config"
compile "org.springframework.security:spring-security-data"
compile "org.springframework.security:spring-security-web"
compile "org.springframework.social:spring-social-security"
compile "org.springframework.social:spring-social-google"
compile "org.springframework.social:spring-social-facebook"
compile "org.springframework.social:spring-social-twitter"
then you have to add social config to your project alongside with your security config:
#Configuration
#EnableSocial
public class SocialConfiguration implements SocialConfigurer {
private final Logger log = LoggerFactory.getLogger(SocialConfiguration.class);
private final SocialUserConnectionRepository socialUserConnectionRepository;
private final Environment environment;
public SocialConfiguration(SocialUserConnectionRepository socialUserConnectionRepository,
Environment environment) {
this.socialUserConnectionRepository = socialUserConnectionRepository;
this.environment = environment;
}
#Bean
public ConnectController connectController(ConnectionFactoryLocator connectionFactoryLocator,
ConnectionRepository connectionRepository) {
ConnectController controller = new ConnectController(connectionFactoryLocator, connectionRepository);
controller.setApplicationUrl(environment.getProperty("spring.application.url"));
return controller;
}
#Override
public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
// Google configuration
String googleClientId = environment.getProperty("spring.social.google.client-id");
String googleClientSecret = environment.getProperty("spring.social.google.client-secret");
if (googleClientId != null && googleClientSecret != null) {
log.debug("Configuring GoogleConnectionFactory");
connectionFactoryConfigurer.addConnectionFactory(
new GoogleConnectionFactory(
googleClientId,
googleClientSecret
)
);
} else {
log.error("Cannot configure GoogleConnectionFactory id or secret null");
}
// Facebook configuration
String facebookClientId = environment.getProperty("spring.social.facebook.client-id");
String facebookClientSecret = environment.getProperty("spring.social.facebook.client-secret");
if (facebookClientId != null && facebookClientSecret != null) {
log.debug("Configuring FacebookConnectionFactory");
connectionFactoryConfigurer.addConnectionFactory(
new FacebookConnectionFactory(
facebookClientId,
facebookClientSecret
)
);
} else {
log.error("Cannot configure FacebookConnectionFactory id or secret null");
}
// Twitter configuration
String twitterClientId = environment.getProperty("spring.social.twitter.client-id");
String twitterClientSecret = environment.getProperty("spring.social.twitter.client-secret");
if (twitterClientId != null && twitterClientSecret != null) {
log.debug("Configuring TwitterConnectionFactory");
connectionFactoryConfigurer.addConnectionFactory(
new TwitterConnectionFactory(
twitterClientId,
twitterClientSecret
)
);
} else {
log.error("Cannot configure TwitterConnectionFactory id or secret null");
}
// jhipster-needle-add-social-connection-factory
}
#Override
public UserIdSource getUserIdSource() {
return new AuthenticationNameUserIdSource();
}
#Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
return new CustomSocialUsersConnectionRepository(socialUserConnectionRepository, connectionFactoryLocator);
}
#Bean
public SignInAdapter signInAdapter(UserDetailsService userDetailsService, JHipsterProperties jHipsterProperties,
TokenProvider tokenProvider) {
return new CustomSignInAdapter(userDetailsService, jHipsterProperties,
tokenProvider);
}
#Bean
public ProviderSignInController providerSignInController(ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository usersConnectionRepository, SignInAdapter signInAdapter) {
ProviderSignInController providerSignInController = new ProviderSignInController(connectionFactoryLocator, usersConnectionRepository, signInAdapter);
providerSignInController.setSignUpUrl("/social/signup");
providerSignInController.setApplicationUrl(environment.getProperty("spring.application.url"));
return providerSignInController;
}
#Bean
public ProviderSignInUtils getProviderSignInUtils(ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository usersConnectionRepository) {
return new ProviderSignInUtils(connectionFactoryLocator, usersConnectionRepository);
}
}
then you have to write adapter for your social login:
public class CustomSignInAdapter implements SignInAdapter {
#SuppressWarnings("unused")
private final Logger log = LoggerFactory.getLogger(CustomSignInAdapter.class);
private final UserDetailsService userDetailsService;
private final JHipsterProperties jHipsterProperties;
private final TokenProvider tokenProvider;
public CustomSignInAdapter(UserDetailsService userDetailsService, JHipsterProperties jHipsterProperties,
TokenProvider tokenProvider) {
this.userDetailsService = userDetailsService;
this.jHipsterProperties = jHipsterProperties;
this.tokenProvider = tokenProvider;
}
#Override
public String signIn(String userId, Connection<?> connection, NativeWebRequest request){
try {
UserDetails user = userDetailsService.loadUserByUsername(userId);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
user,
null,
user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
String jwt = tokenProvider.createToken(authenticationToken, false);
ServletWebRequest servletWebRequest = (ServletWebRequest) request;
servletWebRequest.getResponse().addCookie(getSocialAuthenticationCookie(jwt));
} catch (AuthenticationException ae) {
log.error("Social authentication error");
log.trace("Authentication exception trace: {}", ae);
}
return jHipsterProperties.getSocial().getRedirectAfterSignIn();
}
private Cookie getSocialAuthenticationCookie(String token) {
Cookie socialAuthCookie = new Cookie("social-authentication", token);
socialAuthCookie.setPath("/");
socialAuthCookie.setMaxAge(10);
return socialAuthCookie;
}
}
you can find sample project in my github:
https://github.com/ksadjad/oauth-test

Resources