- Notifications
You must be signed in to change notification settings - Fork70
Spring Boot 2 OAuth2 JWT Authorization server implementation with Database for Users and Clients (JPA, Hibernate, MySQL)
License
dzinot/spring-boot-2-oauth2-authorization-jwt
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Link toSpring Boot 2 OAuth2 JWT Resource Server project
Simple project on how to setupOAuth2 authorization server withJWT tokens usingSpring Boot 2,JPA,Hibernate andMySQL.
AllUsers and Clients are stored in the database.Users can have manyRoles associated with them andRoles can have manyPermissions associated with them which in the end are added as a list ofauthorities in theJWT token.
First we must generate aKeyStore file. To do that execute the following command:
keytool -genkeypair -alias jwt -keyalg RSA -keypass password -keystore jwt.jks -storepass password(if you're under Windows go your Java install dir and there you'll find a jar namedkeytool)
The command will generate a file calledjwt.jks which contains thePublic andPrivate keys.
It is recommended to migrate toPKCS12. To do that execute the following command:
keytool -importkeystore -srckeystore jwt.jks -destkeystore jwt.jks -deststoretype pkcs12Now let's export the public key:
keytool -list -rfc --keystore jwt.jks | openssl x509 -inform pem -pubkeyCopy thejwt.jks file to theResources folder.
Copy from (including)-----BEGIN PUBLIC KEY----- to (including)-----END PUBLIC KEY----- and save it in a file. You'll need this later in your resource servers.
There's a customUser class which implements theUserDetails interface and has all the required methods and an additionalemail field;
There's theUserRepository in which there are 2 methods, one for finding aUser entity byusername and the other byemail. This means we can authenticate aUser based on theusername or theemail.
In order to use our customUser object we must provide with aCustomUserDetailsService which implements theUserDetailsService. TheloadUserByUsername method is overriden and set up to work with our logic.
Databaseoauth2.sql andapplication.yml
The database with all the tables and a test client and users. Check the configuration in theapplication.yml file.
username:admin oruser
password:password
client:adminapp
secret:password
Theadmin is associated with arole_admin and that role is associated with several permissions.Theuser is associated with arole_user and read permissions.
IfcheckUserScopes is set tofalse (defaultSpring Boot 2 functionality), no checks will be done between the clientscope and the userauthorities.
IfcheckUserScopes is set totrue (see documentation below), then when a user tries to authenticate with a client, we check whether at least one of the userauthorities is contained in the clientscope. (I don't know why the default implementation is not done like this)
checkUserScopes is set as a property inside theapplication.yml file.
check-user-scopes: trueAnd we get the value in theOAuth2Configuration class.
@Value("${check-user-scopes}")private Boolean checkUserScopes;ConfigureWebSecurity
InSpring Boot 2 you must use theDelegatingPasswordEncoder.
@Beanpublic PasswordEncoder passwordEncoder() {return PasswordEncoderFactories.createDelegatingPasswordEncoder();}AuthenticationManagerBean
@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}Configure AuthenticationManagerBuilder
@Autowiredprivate UserDetailsService userDetailsService;@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}HTTP Security configuration
@Overridepublic void configure(HttpSecurity http) throws Exception {http.csrf().disable().exceptionHandling().authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)).and().authorizeRequests().antMatchers("/**").authenticated().and().httpBasic();}ConfigureOAuth2
@Configuration@EnableAuthorizationServerpublic class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {...There must be anAuthenticationManager provided
@Autowired@Qualifier("authenticationManagerBean")private AuthenticationManager authenticationManager;Autowire theDataSource and setOAuth2 clients to use the database and thePasswordEncoder.
@Autowiredprivate DataSource dataSource;@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.jdbc(dataSource).passwordEncoder(passwordEncoder);}Configure the endpoints to use the custom beans.
@Autowiredprivate UserDetailsService userDetailsService;@Beanpublic TokenStore tokenStore() {return new JwtTokenStore(jwtAccessTokenConverter());}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtAccessTokenConverter()).authenticationManager(authenticationManager).userDetailsService(userDetailsService);}Configure who has acces to theOAuth2 server
@Overridepublic void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");}In order to add additional data in theJWT token we must implement aCustomTokenEnchancer.
protected static class CustomTokenEnhancer extends JwtAccessTokenConverter {@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {User user = (User) authentication.getPrincipal();Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());info.put("email", user.getEmail());DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);customAccessToken.setAdditionalInformation(info);return super.enhance(customAccessToken, authentication);}}Configure the token converter.
@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter converter = new CustomTokenEnhancer();converter.setKeyPair(new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "password".toCharArray()).getKeyPair("jwt"));return converter;}In order to makecheckUserScopes to work, you must set that field in theRequestFactory and configure Spring to use that factory in theendpoints configuration. This should've worked just like this but for some reason when thecheckUserScopes is enabled the authentication of a user works fine but the refresh token is not working. When you hit the token endpoint with the refresh token, Spring sets theAuthentication in the Security Context to be the one of the client, not the user from the refresh token and it doesn't update it later. This means when checks are done on thescope andauthorities you always get theauthorities from the client, not the user.
I've created aCustomOAuth2RequestFactory that extends theDefaultOAuth2RequestFactory and override thecreateTokenRequest method where I get theAuthentication from the refresh token, autowire theuserDetailsService, get theUser from the database and manually update the Security Context. This means if there are any changes to theUser we always check for details from the database and not the refresh token itself.
class CustomOauth2RequestFactory extends DefaultOAuth2RequestFactory {@Autowiredprivate TokenStore tokenStore;public CustomOauth2RequestFactory(ClientDetailsService clientDetailsService) {super(clientDetailsService);}@Overridepublic TokenRequest createTokenRequest(Map<String, String> requestParameters,ClientDetails authenticatedClient) {if (requestParameters.get("grant_type").equals("refresh_token")) {OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(tokenStore.readRefreshToken(requestParameters.get("refresh_token")));SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(authentication.getName(), null,userDetailsService.loadUserByUsername(authentication.getName()).getAuthorities()));}return super.createTokenRequest(requestParameters, authenticatedClient);}}Define arequestFactory bean. You'll also need theclientDetailsService here.
@Autowiredprivate ClientDetailsService clientDetailsService;@Beanpublic OAuth2RequestFactory requestFactory() {CustomOauth2RequestFactory requestFactory = new CustomOauth2RequestFactory(clientDetailsService);requestFactory.setCheckUserScopes(true);return requestFactory;}Last step is to configure the endpoints to use thisrequestFactory. Because we are doing check on thecheckUserScopes we have this in the endpoints configuration method.
if (checkUserScopes)endpoints.requestFactory(requestFactory());Just clone or download the repo and import it as an existing maven project.
You'll also need to set upProject Lombok or if you don't want to use this library you can remove the associated annotations from the code and write the getters, setters, constructors, etc. by yourself.
To test it I usedHTTPie. It's similar to CURL.
To get aJWT token execute the following command:
http --form POST adminapp:password@localhost:9999/oauth/token grant_type=password username=admin password=passwordACCESS_TOKEN={the access token}REFRESH_TOKEN={the refresh token}To access a resource use (you'll need a different application which has configuredResourceServer):
http localhost:8080/users 'Authorization: Bearer '$ACCESS_TOKENTo use the refresh token functionality:
http --form POST adminapp:password@localhost:9999/oauth/token grant_type=refresh_token refresh_token=$REFRESH_TOKENThis project is licensed under the MIT License - see theLICENSE file for details.
About
Spring Boot 2 OAuth2 JWT Authorization server implementation with Database for Users and Clients (JPA, Hibernate, MySQL)
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.