├── .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 extends GrantedAuthority> 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 extends GrantedAuthority> 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 extends GrantedAuthority> 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 |
--------------------------------------------------------------------------------