├── .gitignore ├── LICENSE ├── README.md └── identity-server ├── pom.xml └── src └── main ├── java └── com │ └── hascode │ └── tutorial │ ├── DatabaseConfig.java │ ├── Oauth2AuthorizationServerApplication.java │ ├── OpenApiTokenEnhancer.java │ ├── SecurityConfig.java │ ├── auth │ ├── JwtAuthFilter.java │ ├── JwtAuthToken.java │ ├── JwtAuthenticatedProfile.java │ ├── JwtAuthenticationEntryPoint.java │ ├── JwtAuthenticationProvider.java │ ├── LoginCredentials.java │ └── SecretKeyProvider.java │ ├── domain │ ├── DetailedProfile.java │ ├── Login.java │ ├── MinimalProfile.java │ ├── Name.java │ ├── Picture.java │ └── Profile.java │ ├── exceptions │ ├── FailedToLoginException.java │ ├── JwtAuthenticationException.java │ └── ProfileNotFoundException.java │ ├── service │ ├── JwtService.java │ ├── LoginService.java │ └── ProfileService.java │ └── web │ ├── LoginController.java │ ├── ProfileController.java │ └── advices │ └── GlobalExceptionHandler.java └── resources ├── application.properties ├── jwt.key ├── profiles.json └── schema.sql /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | **/.idea 3 | build_reports 4 | .project 5 | .classpath 6 | .settings/ 7 | target/ 8 | logs/ 9 | bin/ 10 | build/ 11 | out/ 12 | */*.class 13 | */**/*.class 14 | .metadata/ 15 | classes/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Nilath Jayamaha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot OAuth 2 with JWT authentication and JDBC client store 2 | 3 | This is based on the original project from: 4 | http://www.hascode.com/2016/03/setting-up-an-oauth2-authorization-server-and-resource-provider-with-spring-boot/ 5 | Please refer to: **2016 Micha Kops / hasCode.com** 6 | 7 | 8 | ## Enhanced functionality 9 | Added JWT based authorization, OpenAPI access token enrichment, JDBC based client store. 10 | 11 | Pre-requisits: 12 | Createa Mysql database jdbc:mysql://localhost:3306/identity with user name root and blank password. 13 | This is configured in DatabaseConfig. 14 | 15 | 1. Start up com.hascode.tutorial.Oauth2AuthorizationServerApplication as a spring boot application. 16 | 17 | 2. POST to http://localhost:9000/login with Content-Type application/json and body {"username":"greenrabbit948", "password":"celeste"} 18 | A JWT will be returned in the header, with a success 200. 19 | 20 | 3. GET to http://localhost:9000/oauth/authorize?redirect_uri=http://localhost:8080/&client_id=ING_BANK&response_type=code 21 | with a header a Authorization Bearer jwt-token 22 | You will be redirected to localhost:8080 with an authorization code. 23 | 24 | 4. Use the authorization code 25 | POST http://localhost:9000/oauth/token?redirect_uri=http://localhost:8080/&grant_type=authorization_code&code=YOUR_AUTH_CODE_FROM_ABOVE 26 | 5. You will get an access token and an id_token in a json response 27 | ``` 28 | { 29 | "access_token": "950d44c9-54b5-445f-8e82-02995d2468a0", 30 | "token_type": "bearer", 31 | "expires_in": 43199, 32 | "scope": "openid", 33 | "id_token": "ad9f151c-1296-4dc4-976b-3eaed9eb08dd" 34 | } 35 | ``` 36 | 37 | 38 | ## Original Doco: 39 | 40 | Examples how to set up an OAuth2 identity server and resource provider within a few minutes using [Spring Boot] and Maven. 41 | 42 | Please feel free to take a look at [my blog] for the full tutorial. 43 | 44 | ## Running the Identity Server 45 | 46 | Using Maven 47 | 48 | ``` 49 | cd identity-server && mvn spring-boot:run 50 | ``` 51 | 52 | ## Running the Resource Provider 53 | 54 | Using Maven 55 | 56 | ``` 57 | cd resource-provider && mvn spring-boot:run 58 | ``` 59 | 60 | ## Requesting a Token 61 | 62 | Using Curl 63 | 64 | ``` 65 | curl -XPOST -k -vi foo:foosecret@localhost:9000/hascode/oauth/token \ 66 | -d grant_type=password -d client_id=foo -d client_secret=abc123 \ 67 | -d redirect_uri=http://www.hascode.com -d username=bar -d password=barsecret 68 | ``` 69 | 70 | ## Accessing the secured Resource 71 | 72 | ``` 73 | TOKEN = 'xxxxxxx' 74 | curl -vi -H "Authorization: Bearer $TOKEN" http://localhost:9001/resource/ 75 | ``` 76 | 77 | --- 78 | -------------------------------------------------------------------------------- /identity-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.hascode.tutorial 7 | spring-oath2-identity-server 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | spring-oath2-sample 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.3.1.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.security.oauth 29 | spring-security-oauth2 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-security 34 | 35 | 36 | org.springframework.security 37 | spring-security-jwt 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-web 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-data-jpa 46 | 47 | 48 | mysql 49 | mysql-connector-java 50 | 51 | 52 | org.projectlombok 53 | lombok 54 | 1.16.8 55 | provided 56 | 57 | 58 | io.jsonwebtoken 59 | jjwt 60 | 0.6.0 61 | 62 | 63 | 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-maven-plugin 69 | 70 | 71 | 72 | 73 | 74 | 75 | spring-snapshots 76 | Spring Snapshots 77 | https://repo.spring.io/snapshot 78 | 79 | true 80 | 81 | 82 | 83 | spring-milestones 84 | Spring Milestones 85 | https://repo.spring.io/milestone 86 | 87 | false 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/DatabaseConfig.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.jdbc.datasource.DriverManagerDataSource; 6 | 7 | @Configuration 8 | public class DatabaseConfig { 9 | 10 | @Bean(name = "dataSource") 11 | public DriverManagerDataSource dataSource() { 12 | DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource(); 13 | driverManagerDataSource.setDriverClassName("com.mysql.jdbc.Driver"); 14 | driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/identity"); 15 | driverManagerDataSource.setUsername("root"); 16 | driverManagerDataSource.setPassword(null); 17 | return driverManagerDataSource; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/Oauth2AuthorizationServerApplication.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.jdbc.datasource.DriverManagerDataSource; 9 | import org.springframework.security.authentication.AuthenticationManager; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 12 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 13 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 14 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 15 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 16 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 17 | import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; 18 | import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices; 19 | import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices; 20 | import org.springframework.security.oauth2.provider.token.TokenEnhancer; 21 | import org.springframework.security.oauth2.provider.token.TokenStore; 22 | import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; 23 | import org.springframework.web.bind.annotation.RequestMapping; 24 | import org.springframework.web.bind.annotation.RestController; 25 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 26 | 27 | import java.security.Principal; 28 | 29 | @SpringBootApplication 30 | @RestController 31 | public class Oauth2AuthorizationServerApplication extends WebMvcConfigurerAdapter { 32 | 33 | public static void main(String[] args) { 34 | SpringApplication.run(Oauth2AuthorizationServerApplication.class, args); 35 | } 36 | 37 | @Configuration 38 | @EnableResourceServer 39 | protected static class ResourceServer extends ResourceServerConfigurerAdapter { 40 | 41 | @Autowired 42 | private TokenStore tokenStore; 43 | 44 | @Override 45 | public void configure(ResourceServerSecurityConfigurer resources) 46 | throws Exception { 47 | resources.tokenStore(tokenStore); 48 | } 49 | 50 | @Override 51 | public void configure(HttpSecurity http) throws Exception { 52 | http.authorizeRequests() 53 | .antMatchers("/login") 54 | .permitAll(); 55 | //http.authorizeRequests().anyRequest().authenticated(); 56 | } 57 | 58 | } 59 | 60 | @Configuration 61 | @EnableAuthorizationServer 62 | protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter { 63 | @Autowired 64 | private AuthenticationManager authenticationManager; 65 | 66 | @Autowired 67 | private DriverManagerDataSource dataSource; 68 | 69 | @Override 70 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 71 | endpoints.authorizationCodeServices(authorizationCodeServices()) 72 | .authenticationManager(authenticationManager) 73 | .tokenStore(tokenStore()) 74 | .tokenEnhancer(tokenEnhancer()); 75 | } 76 | 77 | /** 78 | * Enhances the json response for the /token endpoint with the openid id_token param. 79 | * 80 | */ 81 | @Bean 82 | public TokenEnhancer tokenEnhancer() { 83 | return new OpenApiTokenEnhancer(); 84 | } 85 | 86 | /** 87 | * Stores the client oauth authorization codes and client access tokens in a jdbc datastore. This store is managed by oauth2. 88 | * @return 89 | */ 90 | @Bean 91 | public JdbcTokenStore tokenStore() { 92 | return new JdbcTokenStore(dataSource); 93 | } 94 | @Bean 95 | protected AuthorizationCodeServices authorizationCodeServices() { 96 | return new JdbcAuthorizationCodeServices(dataSource); 97 | } 98 | 99 | 100 | @Override 101 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 102 | clients.jdbc(dataSource) 103 | .withClient("ING_BANK") 104 | .secret("ingbanksecret") 105 | .authorizedGrantTypes("authorization_code", "password") 106 | .scopes("openid"); 107 | } 108 | } 109 | 110 | @RequestMapping("/user") 111 | public Principal user(Principal user) { 112 | return user; 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/OpenApiTokenEnhancer.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial; 2 | 3 | import com.hascode.tutorial.auth.JwtAuthenticatedProfile; 4 | import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; 5 | import org.springframework.security.oauth2.common.OAuth2AccessToken; 6 | import org.springframework.security.oauth2.provider.OAuth2Authentication; 7 | import org.springframework.security.oauth2.provider.token.TokenEnhancer; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.UUID; 12 | 13 | public class OpenApiTokenEnhancer implements TokenEnhancer { 14 | 15 | @Override 16 | public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { 17 | JwtAuthenticatedProfile user = (JwtAuthenticatedProfile) authentication.getPrincipal(); 18 | final Map additionalInfo = new HashMap<>(); 19 | 20 | additionalInfo.put("id_token", UUID.randomUUID().toString()); 21 | 22 | ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); 23 | 24 | return accessToken; 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial; 2 | 3 | import com.hascode.tutorial.auth.JwtAuthFilter; 4 | import com.hascode.tutorial.auth.JwtAuthenticationEntryPoint; 5 | import com.hascode.tutorial.auth.JwtAuthenticationProvider; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.http.HttpMethod; 9 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 13 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 14 | 15 | @Configuration 16 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 17 | 18 | @Autowired 19 | private JwtAuthFilter jwtAuthFilter; 20 | 21 | @Autowired 22 | private JwtAuthenticationProvider jwtAuthenticationProvider; 23 | 24 | @Autowired 25 | private JwtAuthenticationEntryPoint jwtAuthEndPoint; 26 | 27 | @Override 28 | public void configure(AuthenticationManagerBuilder auth) throws Exception { 29 | auth.authenticationProvider(jwtAuthenticationProvider); 30 | } 31 | 32 | @Override 33 | protected void configure(HttpSecurity http) throws Exception { 34 | http.csrf().ignoringAntMatchers("/login"); 35 | 36 | http.authorizeRequests() 37 | .antMatchers("/login") 38 | .permitAll() 39 | .antMatchers("/oauth/token") 40 | .authenticated() 41 | .antMatchers("/oauth/authorize") 42 | .hasAuthority("ROLE_USER") 43 | .and() 44 | .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) 45 | .exceptionHandling() 46 | .authenticationEntryPoint(jwtAuthEndPoint); 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/auth/JwtAuthFilter.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.auth; 2 | 3 | import org.springframework.security.core.context.SecurityContextHolder; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.web.filter.OncePerRequestFilter; 6 | 7 | import javax.servlet.FilterChain; 8 | import javax.servlet.ServletException; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | import java.io.IOException; 12 | 13 | @Component 14 | public class JwtAuthFilter extends OncePerRequestFilter { 15 | 16 | @Override 17 | protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain chain) throws ServletException, IOException { 18 | String authorization = httpServletRequest.getHeader("Authorization"); 19 | if (authorization != null && !authorization.startsWith("Basic")) { 20 | JwtAuthToken token = new JwtAuthToken(authorization.replaceAll("Bearer ", "")); 21 | SecurityContextHolder.getContext().setAuthentication(token); 22 | } 23 | chain.doFilter(httpServletRequest, httpServletResponse); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/auth/JwtAuthToken.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.auth; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.GrantedAuthority; 5 | 6 | import java.util.Collection; 7 | 8 | public class JwtAuthToken implements Authentication { 9 | private final String token; 10 | 11 | public JwtAuthToken(String token) { 12 | this.token = token; 13 | } 14 | 15 | @Override 16 | public Collection getAuthorities() { 17 | return null; 18 | } 19 | 20 | @Override 21 | public Object getCredentials() { 22 | return token; 23 | } 24 | 25 | @Override 26 | public Object getDetails() { 27 | return null; 28 | } 29 | 30 | @Override 31 | public Object getPrincipal() { 32 | return null; 33 | } 34 | 35 | @Override 36 | public boolean isAuthenticated() { 37 | return false; 38 | } 39 | 40 | @Override 41 | public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { 42 | 43 | } 44 | 45 | @Override 46 | public String getName() { 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/auth/JwtAuthenticatedProfile.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.auth; 2 | 3 | import com.hascode.tutorial.domain.MinimalProfile; 4 | import org.springframework.security.core.Authentication; 5 | import org.springframework.security.core.GrantedAuthority; 6 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 7 | 8 | import java.util.Collection; 9 | import java.util.Collections; 10 | 11 | public class JwtAuthenticatedProfile implements Authentication { 12 | 13 | private final MinimalProfile minimalProfile; 14 | 15 | public JwtAuthenticatedProfile(MinimalProfile minimalProfile) { 16 | this.minimalProfile = minimalProfile; 17 | } 18 | 19 | @Override 20 | public Collection getAuthorities() { 21 | return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); 22 | } 23 | 24 | @Override 25 | public Object getCredentials() { 26 | return null; 27 | } 28 | 29 | @Override 30 | public Object getDetails() { 31 | return null; 32 | } 33 | 34 | @Override 35 | public Object getPrincipal() { 36 | return this; 37 | } 38 | 39 | @Override 40 | public boolean isAuthenticated() { 41 | return true; 42 | } 43 | 44 | @Override 45 | public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { 46 | 47 | } 48 | 49 | @Override 50 | public String getName() { 51 | return minimalProfile.getUsername(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/auth/JwtAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.auth; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.springframework.http.MediaType; 5 | import org.springframework.security.core.AuthenticationException; 6 | import org.springframework.security.web.AuthenticationEntryPoint; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.servlet.ServletException; 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import java.io.IOException; 13 | import java.util.Collections; 14 | 15 | import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; 16 | 17 | @Component 18 | public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { 19 | @Override 20 | public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) 21 | throws IOException, ServletException { 22 | httpServletResponse.setStatus(SC_FORBIDDEN); 23 | httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); 24 | 25 | String message; 26 | if(e.getCause() != null) { 27 | message = e.getCause().getMessage(); 28 | } else { 29 | message = e.getMessage(); 30 | } 31 | byte[] body = new ObjectMapper() 32 | .writeValueAsBytes(Collections.singletonMap("error", message)); 33 | httpServletResponse.getOutputStream().write(body); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/auth/JwtAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.auth; 2 | 3 | import com.hascode.tutorial.domain.MinimalProfile; 4 | import com.hascode.tutorial.exceptions.JwtAuthenticationException; 5 | import com.hascode.tutorial.service.JwtService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.authentication.AuthenticationProvider; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.AuthenticationException; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.Optional; 13 | 14 | @Component 15 | public class JwtAuthenticationProvider implements AuthenticationProvider { 16 | private final JwtService jwtService; 17 | 18 | @SuppressWarnings("unused") 19 | public JwtAuthenticationProvider() { 20 | this(null); 21 | } 22 | 23 | @Autowired 24 | public JwtAuthenticationProvider(JwtService jwtService) { 25 | this.jwtService = jwtService; 26 | } 27 | 28 | @Override 29 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 30 | try { 31 | Optional possibleProfile = jwtService.verify((String) authentication.getCredentials()); 32 | return new JwtAuthenticatedProfile(possibleProfile.get()); 33 | } catch (Exception e) { 34 | throw new JwtAuthenticationException("Failed to verify token", e); 35 | } 36 | } 37 | 38 | @Override 39 | public boolean supports(Class authentication) { 40 | return JwtAuthToken.class.equals(authentication); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/auth/LoginCredentials.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.auth; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class LoginCredentials { 7 | private String username; 8 | private String password; 9 | } 10 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/auth/SecretKeyProvider.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.auth; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.io.IOException; 6 | import java.net.URISyntaxException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Paths; 9 | 10 | @Component 11 | public class SecretKeyProvider { 12 | public byte[] getKey() throws URISyntaxException, IOException { 13 | return Files.readAllBytes(Paths.get(this.getClass().getResource("/jwt.key").toURI())); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/domain/DetailedProfile.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.domain; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | @Data 8 | public class DetailedProfile implements Serializable { 9 | 10 | private final Picture picture; 11 | private final Name name; 12 | private final String email; 13 | private final String username; 14 | 15 | public DetailedProfile(Profile profile) { 16 | name = profile.getName(); 17 | email = profile.getEmail(); 18 | picture = profile.getPicture(); 19 | username = profile.getLogin().getUsername(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/domain/Login.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.domain; 2 | 3 | import lombok.*; 4 | 5 | import java.io.Serializable; 6 | 7 | @Data 8 | public class Login implements Serializable { 9 | String username; 10 | String password; 11 | String salt; 12 | String md5; 13 | String sha1; 14 | String sha256; 15 | } 16 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/domain/MinimalProfile.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.domain; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | import java.net.URL; 7 | 8 | @Data 9 | public class MinimalProfile implements Serializable { 10 | private final String username; 11 | private final Name name; 12 | private final URL thumbnail; 13 | 14 | public MinimalProfile(Profile profile) { 15 | name = profile.getName(); 16 | username = profile.getLogin().getUsername(); 17 | thumbnail = profile.getPicture().getThumbnail(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/domain/Name.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.domain; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | @Data 8 | public class Name implements Serializable { 9 | private String title; 10 | private String first; 11 | private String last; 12 | } 13 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/domain/Picture.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.domain; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | import java.net.URL; 7 | 8 | @Data 9 | public class Picture implements Serializable { 10 | private URL large; 11 | private URL medium; 12 | private URL thumbnail; 13 | } 14 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/domain/Profile.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.domain; 2 | 3 | 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | 8 | @Data 9 | public class Profile implements Serializable { 10 | private Name name; 11 | private String email; 12 | private Login login; 13 | private Picture picture; 14 | } 15 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/exceptions/FailedToLoginException.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.exceptions; 2 | 3 | import static java.lang.String.format; 4 | 5 | public class FailedToLoginException extends RuntimeException { 6 | public FailedToLoginException(String username) { 7 | super(format("Failed to login with username %s", username)); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/exceptions/JwtAuthenticationException.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.exceptions; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | 5 | public class JwtAuthenticationException extends AuthenticationException { 6 | public JwtAuthenticationException(String msg, Throwable t) { 7 | super(msg, t); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/exceptions/ProfileNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.exceptions; 2 | 3 | import static java.lang.String.format; 4 | 5 | public class ProfileNotFoundException extends RuntimeException { 6 | public ProfileNotFoundException(String username) { 7 | super(format("Profile with username %s does not exist", username)); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/service/JwtService.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.service; 2 | 3 | import com.hascode.tutorial.auth.SecretKeyProvider; 4 | import com.hascode.tutorial.domain.MinimalProfile; 5 | import io.jsonwebtoken.Claims; 6 | import io.jsonwebtoken.Jws; 7 | import io.jsonwebtoken.Jwts; 8 | import io.jsonwebtoken.SignatureAlgorithm; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.io.IOException; 13 | import java.net.URISyntaxException; 14 | import java.time.LocalDateTime; 15 | import java.util.Date; 16 | import java.util.Optional; 17 | 18 | import static java.time.ZoneOffset.UTC; 19 | 20 | 21 | @Component 22 | public class JwtService { 23 | private static final String ISSUER = "in.sdqali.jwt"; 24 | public static final String USERNAME = "username"; 25 | private final SecretKeyProvider secretKeyProvider; 26 | private final ProfileService profileService; 27 | 28 | @SuppressWarnings("unused") 29 | public JwtService() { 30 | this(null, null); 31 | } 32 | 33 | @Autowired 34 | public JwtService(SecretKeyProvider secretKeyProvider, ProfileService profileService) { 35 | this.secretKeyProvider = secretKeyProvider; 36 | this.profileService = profileService; 37 | } 38 | 39 | public String tokenFor(MinimalProfile minimalProfile) throws IOException, URISyntaxException { 40 | byte[] secretKey = secretKeyProvider.getKey(); 41 | Date expiration = Date.from(LocalDateTime.now(UTC).plusHours(2).toInstant(UTC)); 42 | return Jwts.builder() 43 | .setSubject(minimalProfile.getUsername()) 44 | .setExpiration(expiration) 45 | .setIssuer(ISSUER) 46 | .signWith(SignatureAlgorithm.HS512, secretKey) 47 | .compact(); 48 | } 49 | 50 | public Optional verify(String token) throws IOException, URISyntaxException { 51 | byte[] secretKey = secretKeyProvider.getKey(); 52 | Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); 53 | return profileService.minimal(claims.getBody().getSubject().toString()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/service/LoginService.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.service; 2 | 3 | import com.hascode.tutorial.auth.LoginCredentials; 4 | import com.hascode.tutorial.domain.MinimalProfile; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Optional; 9 | 10 | @Component 11 | public class LoginService { 12 | 13 | private ProfileService profileService; 14 | 15 | @SuppressWarnings("unused") 16 | public LoginService() { 17 | this(null); 18 | } 19 | 20 | @Autowired 21 | public LoginService(ProfileService profileService) { 22 | this.profileService = profileService; 23 | } 24 | 25 | public Optional login(LoginCredentials credentials) { 26 | return profileService.get(credentials.getUsername()) 27 | .filter(profile -> profile.getLogin().getPassword().equals(credentials.getPassword())) 28 | .map(profile -> new MinimalProfile(profile)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/service/ProfileService.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.service; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.hascode.tutorial.domain.DetailedProfile; 6 | import com.hascode.tutorial.domain.MinimalProfile; 7 | import com.hascode.tutorial.domain.Profile; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.io.IOException; 11 | import java.net.URISyntaxException; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.util.List; 15 | import java.util.Optional; 16 | 17 | @Component 18 | public class ProfileService { 19 | private final List profiles; 20 | 21 | private final Path PROFILES_FILE = Paths.get(this.getClass().getResource("/profiles.json").toURI()); 22 | 23 | public ProfileService() throws IOException, URISyntaxException { 24 | ObjectMapper objectMapper = new ObjectMapper(); 25 | profiles = objectMapper.readValue(PROFILES_FILE.toFile(), new TypeReference>() { 26 | }); 27 | } 28 | 29 | protected Optional get(String username) { 30 | return profiles.stream() 31 | .filter(profile -> profile.getLogin().getUsername().equals(username)) 32 | .findFirst(); 33 | } 34 | 35 | public Optional minimal(String username) { 36 | return get(username).map(profile -> new MinimalProfile(profile)); 37 | } 38 | 39 | public Optional detailed(String username) { 40 | return get(username).map(profile -> new DetailedProfile(profile)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/web/LoginController.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.web; 2 | 3 | import com.hascode.tutorial.auth.LoginCredentials; 4 | import com.hascode.tutorial.domain.MinimalProfile; 5 | import com.hascode.tutorial.exceptions.FailedToLoginException; 6 | import com.hascode.tutorial.service.JwtService; 7 | import com.hascode.tutorial.service.LoginService; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import javax.servlet.http.HttpServletResponse; 14 | 15 | import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; 16 | import static org.springframework.web.bind.annotation.RequestMethod.POST; 17 | 18 | @RestController 19 | @RequestMapping(path = "/login") 20 | public class LoginController { 21 | 22 | private final LoginService loginService; 23 | private final JwtService jwtService; 24 | 25 | @SuppressWarnings("unused") 26 | public LoginController() { 27 | this(null, null); 28 | } 29 | 30 | @Autowired 31 | public LoginController(LoginService loginService, JwtService jwtService) { 32 | this.loginService = loginService; 33 | this.jwtService = jwtService; 34 | } 35 | 36 | @RequestMapping(path = "", 37 | method = POST, 38 | produces = APPLICATION_JSON_VALUE) 39 | public MinimalProfile login(@RequestBody LoginCredentials credentials, 40 | HttpServletResponse response) { 41 | return loginService.login(credentials) 42 | .map(minimalProfile -> { 43 | try { 44 | response.setHeader("Token", jwtService.tokenFor(minimalProfile)); 45 | } catch (Exception e) { 46 | throw new RuntimeException(e); 47 | } 48 | return minimalProfile; 49 | }) 50 | .orElseThrow(() -> new FailedToLoginException(credentials.getUsername())); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/web/ProfileController.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.web; 2 | 3 | import com.hascode.tutorial.domain.DetailedProfile; 4 | import com.hascode.tutorial.domain.MinimalProfile; 5 | import com.hascode.tutorial.exceptions.ProfileNotFoundException; 6 | import com.hascode.tutorial.service.ProfileService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; 13 | import static org.springframework.web.bind.annotation.RequestMethod.GET; 14 | 15 | @RestController 16 | @RequestMapping(path = "/profile") 17 | public class ProfileController { 18 | 19 | private final ProfileService profileService; 20 | 21 | @SuppressWarnings("unused") 22 | public ProfileController() { 23 | this(null); 24 | } 25 | 26 | @Autowired 27 | public ProfileController(ProfileService profileService) { 28 | this.profileService = profileService; 29 | } 30 | 31 | @RequestMapping(path = "/{username}", 32 | method = GET, 33 | produces = APPLICATION_JSON_VALUE) 34 | public MinimalProfile minimal(@PathVariable String username) { 35 | return profileService.minimal(username) 36 | .orElseThrow(() -> new ProfileNotFoundException(username)); 37 | } 38 | 39 | @RequestMapping(path = "/details/{username}", 40 | method = GET, 41 | produces = APPLICATION_JSON_VALUE) 42 | public DetailedProfile details(@PathVariable String username) { 43 | return profileService.detailed(username) 44 | .orElseThrow(() -> new ProfileNotFoundException(username)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /identity-server/src/main/java/com/hascode/tutorial/web/advices/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.hascode.tutorial.web.advices; 2 | 3 | import com.hascode.tutorial.exceptions.FailedToLoginException; 4 | import com.hascode.tutorial.exceptions.ProfileNotFoundException; 5 | import org.springframework.web.bind.annotation.ControllerAdvice; 6 | import org.springframework.web.bind.annotation.ExceptionHandler; 7 | import org.springframework.web.bind.annotation.ResponseStatus; 8 | 9 | import java.security.SignatureException; 10 | 11 | import static org.springframework.http.HttpStatus.*; 12 | 13 | @ControllerAdvice 14 | public class GlobalExceptionHandler { 15 | @ResponseStatus(NOT_FOUND) 16 | @ExceptionHandler(ProfileNotFoundException.class) 17 | public void profileNotFound() { 18 | } 19 | 20 | @ResponseStatus(UNAUTHORIZED) 21 | @ExceptionHandler(FailedToLoginException.class) 22 | public void failedToLogin() { 23 | } 24 | 25 | @ResponseStatus(FORBIDDEN) 26 | @ExceptionHandler(SignatureException.class) 27 | public void failedToVerify() { 28 | System.out.println(""); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /identity-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | security.basic.enabled=false 2 | server.port=9000 3 | server.contextPath=/ 4 | log4j.category.org.springframework=ALL 5 | logging.level.org.springframework.web=DEBUG -------------------------------------------------------------------------------- /identity-server/src/main/resources/jwt.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilathj/spring-oauth2-jwt-jdbc/60653cb3689b6f44d78e7c11e4de7bce85a363e9/identity-server/src/main/resources/jwt.key -------------------------------------------------------------------------------- /identity-server/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | drop table if exists oauth_client_details; 2 | create table oauth_client_details ( 3 | client_id VARCHAR(255) PRIMARY KEY, 4 | resource_ids VARCHAR(255), 5 | client_secret VARCHAR(255), 6 | scope VARCHAR(255), 7 | authorized_grant_types VARCHAR(255), 8 | web_server_redirect_uri VARCHAR(255), 9 | authorities VARCHAR(255), 10 | access_token_validity INTEGER, 11 | refresh_token_validity INTEGER, 12 | additional_information VARCHAR(4096), 13 | autoapprove VARCHAR(255) 14 | ); 15 | drop table if exists oauth_client_token; 16 | create table oauth_client_token ( 17 | token_id VARCHAR(255), 18 | token LONG VARBINARY, 19 | authentication_id VARCHAR(255) PRIMARY KEY, 20 | user_name VARCHAR(255), 21 | client_id VARCHAR(255) 22 | ); 23 | drop table if exists oauth_access_token; 24 | create table oauth_access_token ( 25 | token_id VARCHAR(255), 26 | token LONG VARBINARY, 27 | authentication_id VARCHAR(255) PRIMARY KEY, 28 | user_name VARCHAR(255), 29 | client_id VARCHAR(255), 30 | authentication LONG VARBINARY, 31 | refresh_token VARCHAR(255) 32 | ); 33 | drop table if exists oauth_refresh_token; 34 | create table oauth_refresh_token ( 35 | token_id VARCHAR(255), 36 | token LONG VARBINARY, 37 | authentication LONG VARBINARY 38 | ); 39 | drop table if exists oauth_code; 40 | create table oauth_code ( 41 | code VARCHAR(255), authentication LONG VARBINARY 42 | ); 43 | drop table if exists oauth_approvals; 44 | create table oauth_approvals ( 45 | userId VARCHAR(255), 46 | clientId VARCHAR(255), 47 | scope VARCHAR(255), 48 | status VARCHAR(10), 49 | expiresAt TIMESTAMP, 50 | lastModifiedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP 51 | ); 52 | drop table if exists ClientDetails; 53 | create table ClientDetails ( 54 | appId VARCHAR(255) PRIMARY KEY, 55 | resourceIds VARCHAR(255), 56 | appSecret VARCHAR(255), 57 | scope VARCHAR(255), 58 | grantTypes VARCHAR(255), 59 | redirectUrl VARCHAR(255), 60 | authorities VARCHAR(255), 61 | access_token_validity INTEGER, 62 | refresh_token_validity INTEGER, 63 | additionalInformation VARCHAR(4096), 64 | autoApproveScopes VARCHAR(255) 65 | ); --------------------------------------------------------------------------------