javaspring-securitykeycloakswagger-ui

How to implement Spring Boot Security OAuth2 with Keyclaok and Swagger


I want to be able to log in via Swagger UI using Keycloak and Spring Boot Security. So far, i can log in with bearer generated token via postman, but how to configure user credentials login?

Security config:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private static final String[] AUTH_WHITELIST = {"/swagger-resources", "/swagger-resources/**", "/configuration/ui",
            "/configuration/security", "/swagger-ui.html", "/webjars/**", "/v3/api-docs/**", "v3/api-docs",
            "/api/public/**", "/api/public/authenticate", "/actuator/*", "/swagger-ui/**", "/api-docs/**"};

    private final JwtAuthConverter jwtAuthConverter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable());
        http.authorizeHttpRequests(auth -> auth.requestMatchers(AUTH_WHITELIST).permitAll().anyRequest().authenticated());
        http.oauth2ResourceServer(o2 -> o2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)));
        http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}

Converterer:

@Component
public class JwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {

    private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();

    @Value("${jwt.auth.converter.principle_attribute}")
    private String principleAttribute;
    @Value("${jwt.auth.converter.resource-id}")
    private String resourceId;

    @Override
    public AbstractAuthenticationToken convert(@NonNull Jwt jwt) {
        Collection<GrantedAuthority> authorities = Stream.concat(jwtGrantedAuthoritiesConverter.convert(jwt).stream(), extractResourceRoles(jwt).stream()).collect(Collectors.toSet());
        return new JwtAuthenticationToken(jwt, authorities, getPrincipleClaimName(jwt));
    }

    private String getPrincipleClaimName(Jwt jwt) {
        String claimName = JwtClaimNames.SUB;
        if (principleAttribute != null) {
            claimName = principleAttribute;
        }
        return jwt.getClaim(claimName);
    }

    private Collection<? extends GrantedAuthority> extractResourceRoles(Jwt jwt) {
        Map<String, Object> resourceAccess;
        Map<String, Object> resource;
        Collection<String> resourceRoles;
        if (jwt.getClaim("resource_access") == null) {
            return Set.of();
        }
        resourceAccess = jwt.getClaim("resource_access");

        if (resourceAccess.get(resourceId) == null) {
            return Set.of();
        }
        resource = (Map<String, Object>) resourceAccess.get(resourceId);

        resourceRoles = (Collection<String>) resource.get("roles");
        return resourceRoles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toSet());
    }
}

Swagger config:

@Configuration
public class SwaggerConfig {
    private static final String OAUTH_SCHEME_BEARER = "bearerAuth";

    @Bean
    public OpenAPI customizeOpenAPI() {
        return new OpenAPI()
                .addSecurityItem(new SecurityRequirement()
                        .addList(OAUTH_SCHEME_BEARER))
                .components(new Components()
                        .addSecuritySchemes(OAUTH_SCHEME_BEARER, new SecurityScheme()
                                .name(OAUTH_SCHEME_BEARER)
                                .type(SecurityScheme.Type.HTTP)
                                .scheme("bearer")
                                .bearerFormat("JWT")));
    }

}

Any suggestions or tutorial links, would be welcoming, I tried what I could find, but with no luck.


Solution

  • I had to re-write my swagger config, and it looks like this:

    private static final String OAUTH_SCHEME = "auth";
    
    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    String authURL;
    
    @Bean
    public OpenAPI customizeOpenAPI() {
        return new OpenAPI()
                .addSecurityItem(new SecurityRequirement()
                        .addList(OAUTH_SCHEME))
                .components(new Components()
                        .addSecuritySchemes(OAUTH_SCHEME, createOAuthScheme()))
                .addSecurityItem(new SecurityRequirement().addList(OAUTH_SCHEME));
    }
    
    private SecurityScheme createOAuthScheme() {
        return new SecurityScheme().type(SecurityScheme.Type.OAUTH2).flows(createOAuthFlows());
    }
    
    private OAuthFlows createOAuthFlows() {
        final var oauthFlow = new OAuthFlow()
                .authorizationUrl(authURL + "/protocol/openid-connect" + "/auth")
                .refreshUrl(authURL + "/protocol/openid-connect" + "/token")
                .tokenUrl(authURL + "/protocol/openid-connect" + "/token")
                .scopes(new Scopes());
        return new OAuthFlows().authorizationCode(oauthFlow);
    }
    

    I also had to add some configurations in my application.yml

    server:
      port: 8081
    
    keycloak:
      username: test-client
      password: *****
      realm: test-realm
      uri: http://localhost:8080
      auth-server-url: ${keycloak.uri}/realms/${keycloak.realm}/protocol/openid-connect
    
    springdoc:
      swagger-ui:
        oauth:
          clientId: ${keycloak.username}
          clientSecret: ${keycloak.password}
    
    spring:
      security:
        oauth2:
          resourceserver:
            jwt:
              issuer-uri: ${keycloak.uri}/realms/${keycloak.realm}
              jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs
          client:
            registration:
              keycloak:
                authorization-grant-type: client_credentials
                scope: openid
            provider:
              keycloak:
                issuer-uri: ${keycloak.uri}/realms/${keycloak.realm}
    
    jwt:
      auth:
        converter:
          resource-id: ${keycloak.username}
          principle_attribute: preferred_username
    

    It works now, by redirecting to keycloak logging page, i'm also pre-filling client id and secret in properties to make it easier.