├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── kristijangeorgiev │ │ └── resource │ │ ├── SpringBoot2Oauth2ResourceJwtApplication.java │ │ ├── configuration │ │ ├── ResourceServerConfiguration.java │ │ └── WebMvcConfiguration.java │ │ ├── controller │ │ └── ResourceController.java │ │ ├── model │ │ └── CustomPrincipal.java │ │ └── util │ │ ├── CustomAccessTokenConverter.java │ │ └── CustomUserAuthenticationConverter.java └── resources │ └── application.yml └── test └── java └── com └── kristijangeorgiev └── resource └── SpringBoot2Oauth2ResourceJwtApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .project 3 | .metadata 4 | .gradle 5 | bin/ 6 | tmp/ 7 | *.tmp 8 | *.bak 9 | *.swp 10 | *~.nib 11 | local.properties 12 | .settings/ 13 | .loadpath 14 | 15 | # Java 16 | *.class 17 | 18 | # Package Files 19 | *.jar 20 | *.war 21 | *.ear 22 | 23 | # Maven 24 | target/ 25 | pom.xml.tag 26 | pom.xml.releaseBackup 27 | pom.xml.versionsBackup 28 | pom.xml.next 29 | release.properties 30 | dependency-reduced-pom.xml 31 | buildNumber.properties 32 | .mvn/ 33 | mvnw 34 | mvnw.cmd 35 | !.mvn/wrapper/maven-wrapper.jar 36 | 37 | # STS (Spring Tool Suite) 38 | .springBeans 39 | .apt_generated 40 | .classpath 41 | .factorypath 42 | .project 43 | .settings 44 | .sts4-cache 45 | 46 | # Locally stored "Eclipse launch configurations" 47 | *.launch 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kristijan Georgiev 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 2 OAuth2 JWT Resource Server 2 | 3 | ### Link to [Spring Boot 2 OAuth2 JWT Authorization Server](https://github.com/dzinot/spring-boot-2-oauth2-authorization-jwt) project 4 | --- 5 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.kristijangeorgiev 7 | spring-boot-2-oauth2-resource-jwt 8 | 1.0.0 9 | jar 10 | 11 | spring-boot-2-oauth2-resource-jwt 12 | Spring Boot 2 OAuth2 Resource JWT 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.1.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | Finchley.RC1 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-actuator 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 37 | 38 | org.springframework.cloud 39 | spring-cloud-starter-oauth2 40 | 41 | 42 | org.springframework.cloud 43 | spring-cloud-starter-security 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-devtools 49 | runtime 50 | 51 | 52 | mysql 53 | mysql-connector-java 54 | runtime 55 | 56 | 57 | org.projectlombok 58 | lombok 59 | true 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-test 64 | test 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.springframework.cloud 72 | spring-cloud-dependencies 73 | ${spring-cloud.version} 74 | pom 75 | import 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-maven-plugin 85 | 86 | 87 | 88 | 89 | 90 | 91 | spring-milestones 92 | Spring Milestones 93 | https://repo.spring.io/milestone 94 | 95 | false 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/main/java/com/kristijangeorgiev/resource/SpringBoot2Oauth2ResourceJwtApplication.java: -------------------------------------------------------------------------------- 1 | package com.kristijangeorgiev.resource; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * 8 | * @author Kristijan Georgiev 9 | * 10 | */ 11 | @SpringBootApplication 12 | public class SpringBoot2Oauth2ResourceJwtApplication { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(SpringBoot2Oauth2ResourceJwtApplication.class, args); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/kristijangeorgiev/resource/configuration/ResourceServerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.kristijangeorgiev.resource.configuration; 2 | 3 | import javax.servlet.http.HttpServletResponse; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 10 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 11 | import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; 12 | import org.springframework.security.oauth2.provider.token.TokenStore; 13 | 14 | /** 15 | * 16 | * @author Kristijan Georgiev 17 | * 18 | */ 19 | @Configuration 20 | @EnableResourceServer 21 | @EnableGlobalMethodSecurity(prePostEnabled = true) 22 | public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { 23 | 24 | @Autowired 25 | public TokenStore tokenStore; 26 | 27 | @Override 28 | public void configure(HttpSecurity http) throws Exception { 29 | http.authorizeRequests().anyRequest().permitAll().and().cors().disable().csrf().disable().httpBasic().disable() 30 | .exceptionHandling() 31 | .authenticationEntryPoint( 32 | (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) 33 | .accessDeniedHandler( 34 | (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)); 35 | } 36 | 37 | @Override 38 | public void configure(ResourceServerSecurityConfigurer resources) throws Exception { 39 | resources.resourceId("mw/adminapp").tokenStore(tokenStore); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/kristijangeorgiev/resource/configuration/WebMvcConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.kristijangeorgiev.resource.configuration; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.MethodParameter; 8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | import org.springframework.web.bind.support.WebDataBinderFactory; 11 | import org.springframework.web.context.request.NativeWebRequest; 12 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 13 | import org.springframework.web.method.support.ModelAndViewContainer; 14 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 15 | 16 | import com.kristijangeorgiev.resource.model.CustomPrincipal; 17 | 18 | /** 19 | * 20 | * @author Kristijan Georgiev 21 | * 22 | */ 23 | @Configuration 24 | @EnableWebSecurity 25 | public class WebMvcConfiguration implements WebMvcConfigurer { 26 | 27 | @Override 28 | public void addArgumentResolvers(List argumentResolvers) { 29 | argumentResolvers.add(currentUserHandlerMethodArgumentResolver()); 30 | } 31 | 32 | @Bean 33 | public HandlerMethodArgumentResolver currentUserHandlerMethodArgumentResolver() { 34 | return new HandlerMethodArgumentResolver() { 35 | @Override 36 | public boolean supportsParameter(MethodParameter parameter) { 37 | return parameter.getParameterType().equals(CustomPrincipal.class); 38 | } 39 | 40 | @Override 41 | public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, 42 | NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 43 | try { 44 | return (CustomPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 45 | } catch (Exception e) { 46 | return null; 47 | } 48 | } 49 | }; 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/com/kristijangeorgiev/resource/controller/ResourceController.java: -------------------------------------------------------------------------------- 1 | package com.kristijangeorgiev.resource.controller; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.security.access.prepost.PreAuthorize; 6 | import org.springframework.security.core.context.SecurityContextHolder; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import com.kristijangeorgiev.resource.model.CustomPrincipal; 11 | 12 | /** 13 | * 14 | * @author Kristijan Georgiev 15 | * 16 | */ 17 | @RestController 18 | public class ResourceController { 19 | 20 | @GetMapping("/context") 21 | @PreAuthorize("hasAuthority('role_admin')") 22 | public String context() { 23 | CustomPrincipal principal = (CustomPrincipal) SecurityContextHolder.getContext().getAuthentication() 24 | .getPrincipal(); 25 | return principal.getUsername() + " " + principal.getEmail(); 26 | } 27 | 28 | @GetMapping("/secured") 29 | @PreAuthorize("hasAuthority('role_admin')") 30 | public String secured(CustomPrincipal principal) { 31 | return principal.getUsername() + " " + principal.getEmail(); 32 | } 33 | 34 | @GetMapping("/resource") 35 | public String resource() { 36 | return UUID.randomUUID().toString(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/kristijangeorgiev/resource/model/CustomPrincipal.java: -------------------------------------------------------------------------------- 1 | package com.kristijangeorgiev.resource.model; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.springframework.stereotype.Component; 6 | 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | @Data 12 | @Component 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class CustomPrincipal implements Serializable { 16 | 17 | private static final long serialVersionUID = 1L; 18 | 19 | private String username; 20 | private String email; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/kristijangeorgiev/resource/util/CustomAccessTokenConverter.java: -------------------------------------------------------------------------------- 1 | package com.kristijangeorgiev.resource.util; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.Collections; 6 | import java.util.Date; 7 | import java.util.HashMap; 8 | import java.util.LinkedHashSet; 9 | import java.util.Map; 10 | import java.util.Set; 11 | 12 | import org.springframework.boot.autoconfigure.security.oauth2.resource.JwtAccessTokenConverterConfigurer; 13 | import org.springframework.security.core.Authentication; 14 | import org.springframework.security.core.GrantedAuthority; 15 | import org.springframework.security.core.authority.AuthorityUtils; 16 | import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; 17 | import org.springframework.security.oauth2.common.OAuth2AccessToken; 18 | import org.springframework.security.oauth2.provider.OAuth2Authentication; 19 | import org.springframework.security.oauth2.provider.OAuth2Request; 20 | import org.springframework.security.oauth2.provider.token.AccessTokenConverter; 21 | import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter; 22 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; 23 | import org.springframework.stereotype.Component; 24 | 25 | /** 26 | * 27 | * @author Kristijan Georgiev 28 | * 29 | */ 30 | @Component 31 | public class CustomAccessTokenConverter implements AccessTokenConverter, JwtAccessTokenConverterConfigurer { 32 | 33 | private boolean includeGrantType; 34 | 35 | private UserAuthenticationConverter userTokenConverter = new CustomUserAuthenticationConverter(); 36 | 37 | @Override 38 | public void configure(JwtAccessTokenConverter converter) { 39 | converter.setAccessTokenConverter(this); 40 | } 41 | 42 | public OAuth2AccessToken extractAccessToken(String value, Map map) { 43 | DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(value); 44 | Map info = new HashMap(map); 45 | 46 | info.remove(EXP); 47 | info.remove(AUD); 48 | info.remove(CLIENT_ID); 49 | info.remove(SCOPE); 50 | 51 | if (map.containsKey(EXP)) 52 | token.setExpiration(new Date((Long) map.get(EXP) * 1000L)); 53 | 54 | if (map.containsKey(JTI)) 55 | info.put(JTI, map.get(JTI)); 56 | 57 | token.setScope(extractScope(map)); 58 | token.setAdditionalInformation(info); 59 | return token; 60 | } 61 | 62 | @Override 63 | public OAuth2Authentication extractAuthentication(Map map) { 64 | Set scope = extractScope(map); 65 | Map parameters = new HashMap(); 66 | Authentication user = userTokenConverter.extractAuthentication(map); 67 | 68 | String clientId = (String) map.get(CLIENT_ID); 69 | parameters.put(CLIENT_ID, clientId); 70 | 71 | if (includeGrantType && map.containsKey(GRANT_TYPE)) 72 | parameters.put(GRANT_TYPE, (String) map.get(GRANT_TYPE)); 73 | 74 | Set resourceIds = new LinkedHashSet( 75 | map.containsKey(AUD) ? getAudience(map) : Collections.emptySet()); 76 | 77 | Collection authorities = null; 78 | 79 | if (user == null && map.containsKey(AUTHORITIES)) { 80 | @SuppressWarnings("unchecked") 81 | String[] roles = ((Collection) map.get(AUTHORITIES)).toArray(new String[0]); 82 | authorities = AuthorityUtils.createAuthorityList(roles); 83 | } 84 | 85 | OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, 86 | null, null); 87 | 88 | return new OAuth2Authentication(request, user); 89 | } 90 | 91 | public Map convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { 92 | Map response = new HashMap(); 93 | OAuth2Request clientToken = authentication.getOAuth2Request(); 94 | 95 | if (!authentication.isClientOnly()) 96 | response.putAll(userTokenConverter.convertUserAuthentication(authentication.getUserAuthentication())); 97 | else if (clientToken.getAuthorities() != null && !clientToken.getAuthorities().isEmpty()) 98 | response.put(UserAuthenticationConverter.AUTHORITIES, 99 | AuthorityUtils.authorityListToSet(clientToken.getAuthorities())); 100 | 101 | if (token.getScope() != null) 102 | response.put(SCOPE, token.getScope()); 103 | 104 | if (token.getAdditionalInformation().containsKey(JTI)) 105 | response.put(JTI, token.getAdditionalInformation().get(JTI)); 106 | 107 | if (token.getExpiration() != null) 108 | response.put(EXP, token.getExpiration().getTime() / 1000); 109 | 110 | if (includeGrantType && authentication.getOAuth2Request().getGrantType() != null) 111 | response.put(GRANT_TYPE, authentication.getOAuth2Request().getGrantType()); 112 | 113 | response.putAll(token.getAdditionalInformation()); 114 | 115 | response.put(CLIENT_ID, clientToken.getClientId()); 116 | if (clientToken.getResourceIds() != null && !clientToken.getResourceIds().isEmpty()) 117 | response.put(AUD, clientToken.getResourceIds()); 118 | 119 | return response; 120 | } 121 | 122 | private Collection getAudience(Map map) { 123 | Object auds = map.get(AUD); 124 | 125 | if (auds instanceof Collection) { 126 | @SuppressWarnings("unchecked") 127 | Collection result = (Collection) auds; 128 | return result; 129 | } 130 | 131 | return Collections.singleton((String) auds); 132 | } 133 | 134 | private Set extractScope(Map map) { 135 | Set scope = Collections.emptySet(); 136 | 137 | if (map.containsKey(SCOPE)) { 138 | Object scopeObj = map.get(SCOPE); 139 | 140 | if (String.class.isInstance(scopeObj)) 141 | scope = new LinkedHashSet(Arrays.asList(String.class.cast(scopeObj).split(" "))); 142 | else if (Collection.class.isAssignableFrom(scopeObj.getClass())) { 143 | @SuppressWarnings("unchecked") 144 | Collection scopeColl = (Collection) scopeObj; 145 | scope = new LinkedHashSet(scopeColl); 146 | } 147 | } 148 | return scope; 149 | } 150 | 151 | public void setUserTokenConverter(UserAuthenticationConverter userTokenConverter) { 152 | this.userTokenConverter = userTokenConverter; 153 | } 154 | 155 | public void setIncludeGrantType(boolean includeGrantType) { 156 | this.includeGrantType = includeGrantType; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/com/kristijangeorgiev/resource/util/CustomUserAuthenticationConverter.java: -------------------------------------------------------------------------------- 1 | package com.kristijangeorgiev.resource.util; 2 | 3 | import java.util.Collection; 4 | import java.util.LinkedHashMap; 5 | import java.util.Map; 6 | 7 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.GrantedAuthority; 10 | import org.springframework.security.core.authority.AuthorityUtils; 11 | import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter; 12 | import org.springframework.util.StringUtils; 13 | 14 | import com.kristijangeorgiev.resource.model.CustomPrincipal; 15 | 16 | public class CustomUserAuthenticationConverter implements UserAuthenticationConverter { 17 | 18 | private final String EMAIL = "email"; 19 | 20 | private Collection defaultAuthorities; 21 | 22 | public void setDefaultAuthorities(String[] defaultAuthorities) { 23 | this.defaultAuthorities = AuthorityUtils 24 | .commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(defaultAuthorities)); 25 | } 26 | 27 | @Override 28 | public Map convertUserAuthentication(Authentication userAuthentication) { 29 | Map response = new LinkedHashMap(); 30 | response.put(USERNAME, userAuthentication.getName()); 31 | 32 | if (userAuthentication.getAuthorities() != null && !userAuthentication.getAuthorities().isEmpty()) 33 | response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(userAuthentication.getAuthorities())); 34 | 35 | return response; 36 | } 37 | 38 | @Override 39 | public Authentication extractAuthentication(Map map) { 40 | if (map.containsKey(USERNAME)) 41 | return new UsernamePasswordAuthenticationToken( 42 | new CustomPrincipal(map.get(USERNAME).toString(), map.get(EMAIL).toString()), "N/A", 43 | getAuthorities(map)); 44 | return null; 45 | } 46 | 47 | private Collection getAuthorities(Map map) { 48 | if (!map.containsKey(AUTHORITIES)) 49 | return defaultAuthorities; 50 | 51 | Object authorities = map.get(AUTHORITIES); 52 | 53 | if (authorities instanceof String) 54 | return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities); 55 | 56 | if (authorities instanceof Collection) 57 | return AuthorityUtils.commaSeparatedStringToAuthorityList( 58 | StringUtils.collectionToCommaDelimitedString((Collection) authorities)); 59 | 60 | throw new IllegalArgumentException("Authorities must be either a String or a Collection"); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | security: 4 | oauth2: 5 | resource: 6 | jwt: 7 | key-value: | 8 | -----BEGIN PUBLIC KEY----- 9 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1t9wCc2TG91cvSOUCJAz 10 | 5xWJxYaxgpQfz+H5GWqUWIrU2SDpwrLd9ewIKjdxcaMSDeLb3ydP0a8WyWvUbBna 11 | A2vG3QdL+2D+9qKOnbT5FIttHwKWjElEl3zAHtyDi2J+bRbX3sUJPTmkPv5Yu9ir 12 | hB9riy5U3GygaAy4nkvPYuO5XW9izGR5pdsfJmabNEgUScxKp3ns3f0DOHFkZCoo 13 | yuSDFDQMYNSMcPHRZjU8BpSXqOYfO/y3QFIagnaMFlIyWcyRXVN1o25z9sVZuJn+ 14 | k+gTskfgBW/ttR553VaxfP/r5qd7zeRF2BO6mTLcIqyNeadwxou1JfC1GEJDeKW9 15 | qQIDAQAB 16 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /src/test/java/com/kristijangeorgiev/resource/SpringBoot2Oauth2ResourceJwtApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.kristijangeorgiev.resource; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class SpringBoot2Oauth2ResourceJwtApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | --------------------------------------------------------------------------------