Keycloak integration in Swagger

前端 未结 3 1921
旧时难觅i
旧时难觅i 2020-12-23 19:40

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 wor

相关标签:
3条回答
  • 2020-12-23 20:11

    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/

    0 讨论(0)
  • 2020-12-23 20:18

    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, ...

    0 讨论(0)
  • 2020-12-23 20:33

    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.

    0 讨论(0)
提交回复
热议问题