Authorization Server trong OAuth có nhiệm vụ issue access token cho phép Client Application có thể sử dụng access token này để request tới resource mà nó cần sử dụng. Resource server sẽ validate access token này với Authorization Server mỗi khi Client Application request tới resource để quyết định có cho phép Client Application access tới resource mà nó muốn này không? Các bạn có thể sử dụng nhiều open source khác như Keycloak, Spring Security OAuth (đã deprecated) hay một project mới của Spring là Spring Authorization Server để hiện thực Authorization Server này. Trong bài viết này, mình sẽ hướng dẫn các bạn cách sử dụng Spring Authorization Server để hiện thực OAuth Authorization Server các bạn nhé!
Đầu tiên, mình sẽ tạo mới project Spring Boot project với Web Starter, Security Starter:
và Spring Authorization Server:
1 2 3 4 5 |
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> <version>0.3.0</version> </dependency> |
để làm ví dụ.
Kết quả:
Cấu hình Authorization Server
Đầu tiên, mình sẽ tạo mới một class AuthorizationServerConfiguration để cấu hình cho Authorization Server.
Mặc định thì Spring Authorization Server hỗ trợ class OAuth2AuthorizationServerConfiguration với các cấu hình mặc định cho một Authorization Server. Nếu các bạn take a look vào code của class này, các bạn sẽ thấy nó có định nghĩa một phương thức applyDefaultSecurity() khởi tạo đối tượng OAuth2AuthorizationServerConfigurer với mục đích apply những cấu hình mặc định mà class OAuth2AuthorizationServerConfigurer này định nghĩa:
1 2 3 4 5 6 7 8 9 10 |
public static void applyDefaultSecurity(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>(); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); http.requestMatcher(endpointsMatcher) .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated()) .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) .apply(authorizationServerConfigurer); } |
Như các bạn thấy, phương thức applyDefaultSecurity() còn định nghĩa security cho những endpoint mặc định của một Authorization Server.
Class OAuth2AuthorizationServerConfiguration còn định nghĩa một bean cho class SecurityFilterChain gọi tới phương thức applyDefaultSecurity() để đăng ký những cấu hình mặc định này với Spring Security của Authorization Server.
Các bạn có thể import class OAuth2AuthorizationServerConfiguration này sử dụng annotation @Import của Spring để sử dụng những cấu hình mặc định này:
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.huongdanjava.springauthorizationserver; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; @Configuration @Import(OAuth2AuthorizationServerConfiguration.class) public class AuthorizationServerConfiguration { } |
hoặc nếu muốn add thêm custom code gì đó thì hãy khai báo một bean cho class SecurityFilterChain và gọi tới phương thức applyDefaultSecurity() như mình như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package com.huongdanjava.springauthorizationserver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.web.SecurityFilterChain; @Configuration public class AuthorizationServerConfiguration { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); return http.formLogin(Customizer.withDefaults()).build(); } } |
Ở đây, mình add thêm code để nếu user không có permission request tới các endpoint mặc định của Authorization Server, Authorization Server sẽ redirect tới trang login.
Với một Authorization Server, một điều quan trọng mà chúng ta cần phải làm là định nghĩa JSON Web Key để verify thông tin trong access token mà user request tới Resource Server có phải do Authorization Server issue không? Bean JwtDecoder với đối tượng của class JWKSource được yêu cầu để hoàn thành phần cấu hình của Authorization Server này. Chúng ta có thể định nghĩa bean cho những đối tượng này như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
@Bean public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } @Bean public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException { RSAKey rsaKey = generateRsa(); JWKSet jwkSet = new JWKSet(rsaKey); return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); } private static RSAKey generateRsa() throws NoSuchAlgorithmException { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); return new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); } private static KeyPair generateRsaKey() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); return keyPairGenerator.generateKeyPair(); } |
Các bạn cũng cần khai báo thêm thông tin issuer. Khi build access token, Spring Authorization Server sẽ sử dụng thông tin issuer được cấu hình trong class ProviderSettings để gán thông tin cho claim “iss”. Chúng ta có thể cấu hình bean cho class ProviderSettings như sau:
1 2 3 4 5 6 7 8 |
@Bean public ProviderSettings providerSettings() { // @formatter:off return ProviderSettings.builder() .issuer("http://localhost:8080") .build(); // @formatter:on } |
Cấu hình Spring Security
Khi Authorization Server redirect tới trang login do user chưa authenticated, chúng ta cần định nghĩa một SecurityFilterChain khác để handle request này và tất cả những request khác nữa của Authorization Server. Bởi vì class OAuth2AuthorizationServerConfiguration chỉ định nghĩa security cho những endpoint mặc định của Authorization Server.
Chúng ta có thể định nghĩa SecurityFilterChain này như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package com.huongdanjava.springauthorizationserver; import org.springframework.context.annotation.Bean; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @EnableWebSecurity public class SpringSecurityConfiguration { @Bean SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { // @formatter:off http .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); // @formatter:on return http.build(); } } |
Lúc này, trang login sẽ hiển thị nếu user chưa đăng nhập.
Đăng ký client với Authorization Server
Spring Authorization Server sử dụng class RegisteredClient để khai báo thông tin của một client đăng ký với Authorization Server và sử dụng implementation của interface RegisteredClientRepository để lưu thông tin của tất cả các client này.
Chúng ta có thể khai báo thông tin client sử dụng memory hoặc một database nào đó:
Để đơn giản, mình sẽ sử dụng memory như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Bean public RegisteredClientRepository registeredClientRepository() { // @formatter:off RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("huongdanjava") .clientSecret("{noop}123456") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri("https://oidcdebugger.com/debug") .scope(OidcScopes.OPENID) .build(); // @formatter:on return new InMemoryRegisteredClientRepository(registeredClient); } |
Có mấy thuộc tính quan trọng của một client cần phải có là: client Id và authorization grant type được enable cho client Id này.
Client Id thì mình không cần phải giải thích nhỉ! Còn authorization grant type thì Spring Authorization Server support tất cả grant types của OAuth 2.
Client secret thì tuỳ theo client type mà chúng ta muốn định nghĩa, nếu client của chúng ta là confidential, xem thêm Các loại client types trong OAuth 2.0, thì client secret là bắt buộc các bạn nhé. Ở đây, các bạn cần khai báo cách mã hoá client secret bằng PasswordEncoder, nếu không muốn mã hoá với mục đích testing, chúng ta có thể sử dụng NoOpPasswordEncoder bằng cách khai báo “{noop}” ở đầu client secret như mình làm ở trên. Nhớ là chỉ cho mục đích testing thôi nha các bạn!
Client Authentication method cũng được yêu cầu nếu client của chúng ta là confidential, được khai báo để định nghĩa cách chúng ta lấy access token như thế nào?
Tuỳ theo grant type của client các bạn đang định nghĩa, một số thông tin required khác mà chúng ta cần phải khai báo. Ví dụ như trong trường hợp của mình thì mình đang định nghĩa một client với grant type authorization_code nên mình phải định nghĩa thêm redirect_uri. Ở đây, mình sẽ sử dụng công cụ https://oidcdebugger.com/ để lấy authorization code nên mình định nghĩa redirect_uri với giá trị https://oidcdebugger.com/debug như các bạn thấy.
Tuỳ theo nhu cầu, các bạn hãy định nghĩa thông tin client cho phù hợp nhé.
Đăng ký user với Authorization Server
Thông tin user đăng nhập vào Authorization Server, mình sử dụng memory với khai báo như sau:
1 2 3 4 5 6 7 8 9 10 |
@Bean public UserDetailsService users() { UserDetails user = User.withDefaultPasswordEncoder() .username("admin") .password("password") .roles("ADMIN") .build(); return new InMemoryUserDetailsManager(user); } |
OK, đến đây thì chúng ta đã hoàn thành những cấu hình cơ bản cho Authorization Server rồi.
Để kiểm tra kết quả, mình sẽ sử dụng công cụ https://oidcdebugger.com/ như mình có nói ở trên, với khai báo như sau:
Nhấn Send request trong trang này, các bạn sẽ thấy trang login của Authorization Server hiển thị như sau:
Đăng nhập bằng thông tin mà chúng ta đã khai báo ở trên, các bạn sẽ thấy kết quả như sau:
Sử dụng authorization code này cùng với client secret mà chúng ta đã khai báo, các bạn có thể lấy được access token cho client này như sau:
mayo
dạ em có build 1 cái client để soo qua server mà khi đăng nhập nó báo về lỗi là :
This application has no explicit mapping for /error, so you are seeing this as a fallback.
dạ cái này có phương pháp giải quyết nào ko ạ, em cảm ơn ạ
Khanh Nguyen
Bạn cần phải xem error là gì bằng cách: cấu hình property logging.level.root=trace trong tập tin application.properties, xong chạy lại để xem lỗi nhé! Message như trên chung chung lắm bạn.
kuzin
a cho e xin link source git với ạ.
Thanks a
Khanh Nguyen
Này nha em https://github.com/khanhnguyenj/huongdanjava.com/tree/master/spring-authorization-server-example
NXD
Cho em hỏi là với OAuth2
grant_type = password
thì có phải là mình gửi usernam/password cho server và server sẽ trả về access token phải ko ạ ?Spring Authorization Server có làm được tính năng này không ạ?
Theo em hiểu thì
grant_type = authorization_code
thì sẽ ko phù hợp với đồ án kiểu sinh viên sử dụng mô hình microservices nho nhỏ của em. vàgrant_type = password
thì sẽ phù hợp hơn đúng ko anh.Em search thì thấy code>grant_type = password có vẻ không được hỗ trợ Spring Authorization Server link=> https://stackoverflow.com/questions/71225713/spring-authorization-server-with-authorizationgranttype-password.
Cảm ơn anh về bài viết này !
Khanh Nguyen
Em nên dùng authorization_code nhé, đừng phân biệt đồ án nhỏ hay lớn. Grant type password ko được recommend trong OAuth 2.1 nữa đâu vì nó ko bảo mật.