Oauth2 Authorization Server
Oauth2 Server
The Oauth2 server will be responsible to issue access and refresh tokens based on the following parameters:
- Client Id
- Client Secret
- Username
- Password
The client id- client secret is a unique combination assigned for each resource server that needs to be registered with the oauth2 server.
Based on the username and password, the users will be authenticated and a token will be assigned to the user which will also contain the list of roles for a given user.
Generate a spring boot project using spring initializer:
with the following dependencies:
Spring Data JPA
spring-boot-starter-security
Spring Web
Add Additional Dependencies:
spring-security-oauth2
spring-security-jwt
Final dependency list:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>${oauth.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.9.RELEASE</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
The following properties need to be defined in the application.properties
security.jwt.key-store=classpath:microservices.jks
security.jwt.key-store-password=
security.jwt.key-pair-alias=microservice
security.jwt.key-pair-password=
Project Structure
The notable classes / configuration are as follows:
OAuth2AuthorizationServerConfigJwt
Here we define all the configurations regarding the keystore, the client details, registering the token service, registering authentication manager and registering user details service with the Auth server endpoints.
@Configuration @EnableAuthorizationServer @EnableResourceServer @EnableConfigurationProperties(SecurityProperties.class) public class OAuth2AuthorizationServerConfigJwt extends AuthorizationServerConfigurerAdapter { @Autowired @Qualifier("authenticationManagerBean") private AuthenticationManager authenticationManager; @Autowired @Qualifier("userDetailsService") UserDetailsService userDetailsService; @Autowired DataSource dataSource; @Autowired SecurityProperties securityProperties; @Override public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()"); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } @Bean @Primary public DefaultTokenServices tokenServices() { final DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(tokenStore()); defaultTokenServices.setSupportRefreshToken(true); return defaultTokenServices; } /** * Register token store authentication manager and user details service */ @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception { final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter())); endpoints.tokenStore(tokenStore()).tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager); endpoints.userDetailsService(userDetailsService); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { final JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); SecurityProperties.JwtProperties jwtProperties = securityProperties.getJwt(); KeyPair keyPair = keyPair(jwtProperties, keyStoreKeyFactory(jwtProperties)); converter.setKeyPair(keyPair); return converter; } @Bean public TokenEnhancer tokenEnhancer() { return new CustomTokenEnhancer(); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } private KeyPair keyPair(SecurityProperties.JwtProperties jwtProperties, KeyStoreKeyFactory keyStoreKeyFactory) { return keyStoreKeyFactory.getKeyPair(jwtProperties.getKeyPairAlias(), jwtProperties.getKeyPairPassword().toCharArray()); } private KeyStoreKeyFactory keyStoreKeyFactory(SecurityProperties.JwtProperties jwtProperties) { return new KeyStoreKeyFactory(jwtProperties.getKeyStore(), jwtProperties.getKeyStorePassword().toCharArray()); } }
WebSecurityConfig
Here we create a bean for the authentication manager and also setting up the user details service .
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private BCryptPasswordEncoder passwordEncoder; @Autowired @Qualifier("userDetailsService") UserDetailsService userdetailsService; @Value("${ignore.security.endpoints}") String ignoredEndpoints; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userdetailsService).passwordEncoder(passwordEncoder); } @Override public void configure(WebSecurity web) { web.ignoring().mvcMatchers(ignoredEndpoints.split(";")); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
UserDetailsServiceImpl
This class should implement the USerDetailsService from Spring security and should contain logic to fetch user name and authorities from our underlying tables.
@Service("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepo userRepo; @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepo.findByUsername(username); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.isEnabled(), true, true, true, getAuthorities(user.getRoles())); } private Collection<? extends GrantedAuthority> getAuthorities(Collection<Role> roles) { return getGrantedAuthorities(getPrivileges(roles)); } private List<String> getPrivileges(Collection<Role> roles) { List<String> privileges = new ArrayList<>(); List<Authority> collection = new ArrayList<>(); for (Role role : roles) { collection.addAll(role.getAuthorities()); } for (Authority item : collection) { privileges.add(item.getName()); } return privileges; } private List<GrantedAuthority> getGrantedAuthorities(List<String> privileges) { List<GrantedAuthority> authorities = new ArrayList<>(); for (String privilege : privileges) { authorities.add(new SimpleGrantedAuthority(privilege)); } return authorities; } }
Table Setup:
Following tables should be created : (refer schema.sql )
OAUTH_CLIENT_DETAILS– Table to store the client details
AUTHORITY – Table to store the authorities
USER_– Table to store user information
ROLE_ - Table to store Roles
USERS_ROLES - Table to store USER_ROLES mapping.
ROLES_AUTHORITIES– Table to store the role-authorities mapping .
The sample data can be found in the oauth_client_details.sql and user_authority_roles_details.sql
Example for getting Access tokens using password grant_type
Once the above setup is done, we can test the Oauth Server by sending a request via Postman.
Request Body:
This contains the user name and password of the user who wants to login and grant_type as password.
Authorization
This is basic authorization with user name a client id and password as the client secret. The details are configured in the database from the oauth_client_details.sql
file.
In our case, client id is employee and client secret is "secret".
(Note : the client secret in the sql file is encoded using spring boots BCryptPasswordEncoder)
Response
Example for getting Access tokens from refresh token
This scenario is generally used when the access token is about to expire, then you can fetch a new access token using the refresh token.
The general idea is that the access token have a relatively smaller expiry period, whereas the refresh token has a longer expiry period.
The request can be simulated by changing the post man request to the following:
As seen above, the endpoint remains the same.
The only difference is that, in the body, instead of sending the grant_type as "password", we send the grant_type as "refresh_token" and instead of sending the users credentials, we send the "refresh token" received with the original request.
Source Code :
The entire source code can be found in Git Hub
-----------------------------------------------------------------------------------------------------------------------
Optional Oauth2 Client
The problem with the above oauth server approach are the following:
To fetch the tokens, we require the client
- To send client id and client secret. - This means that the client (eg ui application needs to store the secrets in the browser cache)
As a potential solution, we can create another microservice like Oauth client which will be responsible to fetch the access and refresh tokens from the Oauth server.
The Oauth client will store the client secrets on the server side and the web applications can send just the client id and the oauth client does the rest.
The notable classes / configuration are as follows:
Oauth2ClientRestController.java
The rest controller exposes two endpoints:
- /accesstoken : to get the access token using password type grant.This requires a TokenRequest object to be populated with the "client id" and the user credentials as a Basic Authentication Header.
- /refreshtoken : to get the access token using refresh token type grantThis requires the TokenRequest object to be populated with "client id" and "existing refresh token".
@RestController @RequestMapping("/oauthclient") @Api(value = "Oauth Client Microservice", description = "Operations pertaining to fetching and refresing tokens from OauthServer") public class Oauth2ClientRestController { @Value("${oauth2.server.uri}") String oauthServerBaseUrl; @Autowired RestTemplate restTemplate; @Autowired OauthClientDetailsConfig oauthClientDetailsConfig; @PostMapping("/accesstoken") public Object getAccessToken(@RequestBody @Valid TokenRequest tokenRequest, @RequestHeader("Authorization") String authHeader) { restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(tokenRequest.getClientId(), oauthClientDetailsConfig.getClientDetailsMap().get(tokenRequest.getClientId()).getClientSecret())); ResponseEntity<?> response = post(OauthClientUtils.preparePostParametersForPasswordTypeGrant(authHeader)); return response.getBody(); } @PostMapping("/refreshtoken") public Object getRefreshToken(@RequestBody @Valid TokenRequest tokenRequest) { restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(tokenRequest.getClientId(), oauthClientDetailsConfig.getClientDetailsMap().get(tokenRequest.getClientId()).getClientSecret())); ResponseEntity<?> response = post(OauthClientUtils.preparePostParametersForRefreshTokenTypeGrant(tokenRequest.getRefreshToken())); return response.getBody(); } private ResponseEntity<?> post(MultiValueMap<String, String> postParameters) { final HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); final HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(postParameters, headers); ResponseEntity<?> response; response = this.restTemplate.postForEntity(oauthServerBaseUrl + "/token", request, Object.class); return response; } }
Example for getting Access tokens using password grant_type
Important parameters:
Method : POST
Authorization: Basic (with user credentials eg usernam:john, password:secret)
Body : (as Json )
{
"clientId" :"employee"
}
Additional Headers: Content-Type : application/json
Endpoint : http://localhost:7004/oauthclient/accesstoken
Example for getting Access tokens using refresh token grant type
Important parameters:
Method : POST
Body : (as Json )
{
"clientId" :"employee",
"refreshToken":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1...."
}
Additional Headers: Content-Type : application/json
Endpoint : http://localhost:7004/oauthclient/accesstoken
Comments
Post a Comment