spring-authorization-server(Oauth2) Customizing

By | 2021년 11월 8일
Table of Contents

spring-authorization-server(Oauth2) Customizing

참조 를 기반으로 운영서버 적용 가능한지 테스트 해봅니다.

Use MySQL

build.gradle

dependencies {
    // runtimeOnly 'com.h2database:h2'
    runtimeOnly 'mysql:mysql-connector-java:8.0.25'
}

application.yml

server:
  port: 9000

spring:
  datasource:
    url: jdbc:mysql://${MYSQL_HOST:localhost}:3306/db_oauth2_test
    username: root
    password:
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update    # 개발용 only
    show-sql: true

logging:
  level:
    root: INFO
    org.springframework.web: INFO
    org.springframework.security: INFO
    org.springframework.security.oauth2: INFO
#    org.springframework.boot.autoconfigure: DEBUG

스키마 생성

스키마를 생성합니다.

CREATE DATABASE `db_oauth2_test`;
CREATE TABLE oauth2_registered_client (
    id varchar(100) NOT NULL,
    client_id varchar(100) NOT NULL,
    client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
    client_secret varchar(200) DEFAULT NULL,
    client_secret_expires_at TIMESTAMP NULL DEFAULT NULL,
    client_name varchar(200) NOT NULL,
    client_authentication_methods varchar(1000) NOT NULL,
    authorization_grant_types varchar(1000) NOT NULL,
    redirect_uris varchar(1000) DEFAULT NULL,
    scopes varchar(1000) NOT NULL,
    client_settings varchar(2000) NOT NULL,
    token_settings varchar(2000) NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE oauth2_authorization_consent (
    registered_client_id varchar(100) NOT NULL,
    principal_name varchar(200) NOT NULL,
    authorities varchar(1000) NOT NULL,
    PRIMARY KEY (registered_client_id, principal_name)
);

CREATE TABLE oauth2_authorization (
    id varchar(100) NOT NULL,
    registered_client_id varchar(100) NOT NULL,
    principal_name varchar(200) NOT NULL,
    authorization_grant_type varchar(100) NOT NULL,
    attributes varchar(4000) DEFAULT NULL,
    state varchar(500) DEFAULT NULL,
    authorization_code_value blob DEFAULT NULL,
    authorization_code_issued_at timestamp NULL DEFAULT NULL,
    authorization_code_expires_at timestamp NULL DEFAULT NULL,
    authorization_code_metadata varchar(2000) DEFAULT NULL,
    access_token_value blob DEFAULT NULL,
    access_token_issued_at timestamp NULL DEFAULT NULL,
    access_token_expires_at timestamp NULL DEFAULT NULL,
    access_token_metadata varchar(2000) DEFAULT NULL,
    access_token_type varchar(100) DEFAULT NULL,
    access_token_scopes varchar(1000) DEFAULT NULL,
    oidc_id_token_value blob DEFAULT NULL,
    oidc_id_token_issued_at timestamp NULL DEFAULT NULL,
    oidc_id_token_expires_at timestamp NULL DEFAULT NULL,
    oidc_id_token_metadata varchar(2000) DEFAULT NULL,
    refresh_token_value blob DEFAULT NULL,
    refresh_token_issued_at timestamp NULL DEFAULT NULL,
    refresh_token_expires_at timestamp NULL DEFAULT NULL,
    refresh_token_metadata varchar(2000) DEFAULT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE users (
    id INT(10) NOT NULL AUTO_INCREMENT,
    username VARCHAR(100) NOT NULL,
    password VARCHAR(200) DEFAULT NULL,
    enabled BOOLEAN NOT NULL,
    acc_locked BOOLEAN NULL,
    acc_expired BOOLEAN NULL,
    creds_expired BOOLEAN NULL,
    PRIMARY KEY (id)
);

CREATE TABLE authorities (
    id INT(10) NOT NULL AUTO_INCREMENT,
    username VARCHAR(100) NOT NULL,
    authority VARCHAR(200) NOT NULL,
    PRIMARY KEY (id)
);

AuthorizationServerConfig.java

@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        return http.formLogin(Customizer.withDefaults()).build();
    }

    // @formatter:off
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("messaging-client")
                .clientSecret("secret")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
                .redirectUri("http://127.0.0.1:8080/authorized")
                .scope(OidcScopes.OPENID)
                .scope("message.read")
                .scope("message.write")
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();

        // Save registered client in db as if in-memory
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        JdbcRegisteredClientRepository.RegisteredClientParametersMapper registeredClientParametersMapper = new JdbcRegisteredClientRepository.RegisteredClientParametersMapper();
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        registeredClientParametersMapper.setPasswordEncoder(encoder);
        registeredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
        registeredClientRepository.save(registeredClient);

        return registeredClientRepository;
    }
    // @formatter:on

    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = Jwks.generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder().issuer("http://auth-server:9000").build();
    }
}

DefaultSecurityConfig.java

@EnableWebSecurity
public class DefaultSecurityConfig {

    private final DataSource dataSource;

    @Bean
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    public DefaultSecurityConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    // @formatter:off
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorizeRequests ->
                authorizeRequests.anyRequest().authenticated()
            )
            .formLogin(withDefaults());
        return http.build();
    }
    // @formatter:on

    // @formatter:off
    @Bean
    UserDetailsService users() {
        JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);

        try {
            UserDetails user = jdbcUserDetailsManager.loadUserByUsername("user1");
        } catch (UsernameNotFoundException ex) {
            PasswordEncoder encoder = new BCryptPasswordEncoder();
            UserDetails user = User
                    .withUsername("user1")
                    .passwordEncoder(encoder::encode)
                    .password("password")
                    .roles("USER")
                    .build();
            jdbcUserDetailsManager.createUser(user);
        }

        return jdbcUserDetailsManager;
    }
    // @formatter:on
}

프로젝트 재실행

프로젝트 종료 후 다시 실행합니다.

SELECT * FROM oauth2_registered_client
SELECT * FROM oauth2_authorization_consent
SELECT * FROM oauth2_authorization

SELECT * FROM users;
SELECT * FROM authorities;

http://localhost:8080/ 에 접속해서 로그인이 가능한 것을 확인할 수 있습니다.

느낀점

문서가 없다.
귀찮아서 안만들었다기보다는 API 자체가 변할 가능성이 너무 높아 작성하지 못한다는 느낌이다.

소스가 아직 깔끔하지 않다.

운영서버에 적용하기에는 위험부담이 너무 많다.

느낌상 이 프로젝트가 안정화될때까지는 spring boot oauth2 가 날라갈 가능성은 없어 보이니,
spring boot oauth2 가 사라지는 일은 없을 것 같고…

답글 남기기