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 Spring Web Starter, Spring Security Starter và OAuth2 Authorization Server Starter:
để làm ví dụ.
Kết quả:
Với cơ chế auto-configuration, hiện tại thì Spring Boot đã hỗ trợ cho Spring Authorization Server nên các bạn chỉ cần cấu hình một số thông tin bắt buộc như thông tin Client Application, thông tin user là có thể up và running một Authorization Server với Spring Authorization Server rồi.
Trong bài viết này, mình sẽ không sử dụng cơ chế auto-configuration này của Spring Boot các bạn nhé!
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 11 12 |
public static void applyDefaultSecurity(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); http .securityMatcher(endpointsMatcher) .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated() ) .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) .apply(authorizationServerConfigurer); } |
Như các bạn thấy, phương thức applyDefaultSecurity() đị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 |
package com.huongdanjava.springauthorizationserver; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.security.oauth2.server.authorization.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.oauth2.server.authorization.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 một bean của class AuthorizationServerSettings để hoàn thành việc cấu hình thông tin cho Authorization Server.
Class AuthorizationServerSettings cho phép chúng ta custom các cấu hình mặc định của Spring Authorization Server liên quan đến Issuer Identifier, JWK Set endpoint, và một số settings khác nữa. Các bạn có thể cấu hình bean cho class AuthorizationServerSettings như sau:
1 2 3 4 5 6 7 |
@Bean public AuthorizationServerSettings authorizationServerSettings() { // @formatter:off return AuthorizationServerSettings.builder() .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 26 |
package com.huongdanjava.springauthorizationserver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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; @Configuration @EnableWebSecurity public class SpringSecurityConfiguration { @Bean SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { // @formatter:off http .authorizeHttpRequests(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("test") .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 authenticate với một số endpoint sử dụng client secret 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:
Nguyễn Khánh Toàn
anh ơi cho em hỏi em ấn send request thì nó thành công luôn không phải login nữa á mà khi lấy được code rồi điền trong postman thì nó báo 401 Unauthorized
Khanh Nguyen
Chắc bạn làm bị sai bước nào đó rồi. Bạn có thể post cụ thể các bước bạn làm lên group này nhé! Mình support cho dễ https://www.facebook.com/groups/721409674679531.
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.