├── .travis.yml ├── .gitignore ├── src └── main │ ├── resources │ └── application.properties │ └── java │ └── com │ └── nbondarchuk │ └── oauth2 │ ├── model │ └── User.java │ ├── service │ ├── UserService.java │ └── UserServiceImpl.java │ ├── OAuth2Example.java │ ├── endpoint │ ├── exception │ │ ├── WebApplicationExceptionMapper.java │ │ ├── AccessDeniedExceptionMapper.java │ │ └── UncaughtExceptionMapper.java │ └── WelcomeEndpoint.java │ ├── conf │ ├── ServiceLayerConfig.java │ ├── WebConfig.java │ └── SecurityConfig.java │ └── security │ ├── MyOAuth2User.java │ └── MyOAuth2UserService.java └── pom.xml /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | script: 3 | - mvn clean package 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !/.idea/ 2 | /.idea/* 3 | !/.idea/runConfigurations/ 4 | 5 | /**/*.iml 6 | /**/target/ 7 | /.DS_Store -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | client_id=ZJsdANfWkJ7jcktw2x 2 | client_secret=28uUrJ9m43svbkcnXVNj8qeBjFtd8jaD -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/model/User.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author Nikolay Bondarchuk 7 | * @since 2020-04-05 8 | */ 9 | @Data 10 | public class User { 11 | 12 | private Long id; 13 | 14 | private String login; 15 | 16 | private String name; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.service; 2 | 3 | import com.nbondarchuk.oauth2.model.User; 4 | 5 | import java.util.Optional; 6 | 7 | /** 8 | * @author Nikolay Bondarchuk 9 | * @since 2020-04-05 10 | */ 11 | public interface UserService { 12 | 13 | User create(User user); 14 | 15 | Optional findByLogin(String login); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/OAuth2Example.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author Nikolay Bondarchuk 8 | * @since 2020-04-04 9 | */ 10 | @SpringBootApplication 11 | public class OAuth2Example { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(OAuth2Example.class, args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/endpoint/exception/WebApplicationExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.endpoint.exception; 2 | 3 | import javax.ws.rs.WebApplicationException; 4 | import javax.ws.rs.core.Response; 5 | import javax.ws.rs.ext.ExceptionMapper; 6 | 7 | /** 8 | * @author Nikolay Bondarchuk 9 | * @since 2020-04-05 10 | */ 11 | public class WebApplicationExceptionMapper implements ExceptionMapper { 12 | 13 | @Override 14 | public Response toResponse(WebApplicationException e) { 15 | return e.getResponse(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/conf/ServiceLayerConfig.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.conf; 2 | 3 | import com.nbondarchuk.oauth2.service.UserService; 4 | import com.nbondarchuk.oauth2.service.UserServiceImpl; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * @author Nikolay Bondarchuk 10 | * @since 2020-04-05 11 | */ 12 | @Configuration 13 | public class ServiceLayerConfig { 14 | 15 | @Bean 16 | public UserService userService() { 17 | return new UserServiceImpl(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/endpoint/exception/AccessDeniedExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.endpoint.exception; 2 | 3 | import org.springframework.security.access.AccessDeniedException; 4 | 5 | import javax.ws.rs.core.Response; 6 | import javax.ws.rs.ext.ExceptionMapper; 7 | 8 | import static javax.ws.rs.core.Response.Status.FORBIDDEN; 9 | 10 | /** 11 | * @author Nikolay Bondarchuk 12 | * @since 2020-04-05 13 | */ 14 | public class AccessDeniedExceptionMapper implements ExceptionMapper { 15 | 16 | @Override 17 | public Response toResponse(AccessDeniedException e) { 18 | return Response.status(FORBIDDEN).entity(e.getMessage()).build(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/service/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.service; 2 | 3 | import com.nbondarchuk.oauth2.model.User; 4 | 5 | import java.util.Map; 6 | import java.util.Optional; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.atomic.AtomicLong; 9 | 10 | /** 11 | * @author Nikolay Bondarchuk 12 | * @since 2020-04-05 13 | */ 14 | public class UserServiceImpl implements UserService { 15 | 16 | private static final AtomicLong userIdCounter = new AtomicLong(); 17 | 18 | private final Map userStore = new ConcurrentHashMap<>(); 19 | 20 | @Override 21 | public User create(User user) { 22 | user.setId(userIdCounter.incrementAndGet()); 23 | userStore.put(user.getLogin(), user); 24 | return user; 25 | } 26 | 27 | @Override 28 | public Optional findByLogin(String login) { 29 | return Optional.ofNullable(userStore.get(login)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/endpoint/exception/UncaughtExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.endpoint.exception; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.apache.commons.lang3.exception.ExceptionUtils; 7 | 8 | import javax.ws.rs.Produces; 9 | import javax.ws.rs.core.MediaType; 10 | import javax.ws.rs.core.Response; 11 | import javax.ws.rs.ext.ExceptionMapper; 12 | 13 | /** 14 | * @author Nikolay Bondarchuk 15 | * @since 2020-04-05 16 | */ 17 | public class UncaughtExceptionMapper implements ExceptionMapper { 18 | 19 | @Override 20 | public Response toResponse(Throwable t) { 21 | return Response.serverError().entity(Error.of(t)).type(MediaType.APPLICATION_JSON_TYPE).build(); 22 | } 23 | 24 | public static class Error { 25 | 26 | @Getter 27 | @Setter 28 | @JsonProperty 29 | private String message; 30 | 31 | @Getter 32 | @Setter 33 | @JsonProperty 34 | private String stacktrace; 35 | 36 | public static Error of(Throwable t) { 37 | Error error = new Error(); 38 | error.setMessage(t.getMessage()); 39 | error.setStacktrace(ExceptionUtils.getStackTrace(t)); 40 | return error; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/conf/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.conf; 2 | 3 | import com.nbondarchuk.oauth2.endpoint.WelcomeEndpoint; 4 | import com.nbondarchuk.oauth2.endpoint.exception.AccessDeniedExceptionMapper; 5 | import com.nbondarchuk.oauth2.endpoint.exception.UncaughtExceptionMapper; 6 | import com.nbondarchuk.oauth2.endpoint.exception.WebApplicationExceptionMapper; 7 | import org.glassfish.jersey.server.ResourceConfig; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | import javax.annotation.PostConstruct; 12 | 13 | /** 14 | * @author Nikolay Bondarchuk 15 | * @since 2020-04-04 16 | */ 17 | @Configuration 18 | public class WebConfig extends ResourceConfig { 19 | 20 | @PostConstruct 21 | public void init() { 22 | registerEndpoints(); 23 | registerExceptionMappers(); 24 | } 25 | 26 | @Bean 27 | public WelcomeEndpoint welcomeEndpoint() { 28 | return new WelcomeEndpoint(); 29 | } 30 | 31 | private void registerEndpoints() { 32 | register(WelcomeEndpoint.class); 33 | } 34 | 35 | private void registerExceptionMappers() { 36 | register(UncaughtExceptionMapper.class); 37 | register(AccessDeniedExceptionMapper.class); 38 | register(WebApplicationExceptionMapper.class); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/endpoint/WelcomeEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.endpoint; 2 | 3 | import com.nbondarchuk.oauth2.model.User; 4 | import com.nbondarchuk.oauth2.security.MyOAuth2User; 5 | import com.nbondarchuk.oauth2.service.UserService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | 10 | import javax.ws.rs.GET; 11 | import javax.ws.rs.Path; 12 | 13 | /** 14 | * @author Nikolay Bondarchuk 15 | * @since 2020-04-05 16 | */ 17 | @Path("/") 18 | public class WelcomeEndpoint { 19 | 20 | @Autowired 21 | private UserService userService; 22 | 23 | @GET 24 | public String welcome() { 25 | User currentUser = getCurrentUser(); 26 | return String.format("Welcome, %s! (user id: %s, user login: %s)", 27 | currentUser.getName(), currentUser.getId(), currentUser.getLogin()); 28 | } 29 | 30 | public User getCurrentUser() { 31 | return userService.findByLogin(getAuthenticatedUser().getName()).orElseThrow(() -> new RuntimeException("No user logged in.")); 32 | } 33 | 34 | private Authentication getAuthentication() { 35 | return SecurityContextHolder.getContext().getAuthentication(); 36 | } 37 | 38 | private MyOAuth2User getAuthenticatedUser() { 39 | return (MyOAuth2User) getAuthentication().getPrincipal(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/conf/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.conf; 2 | 3 | import com.nbondarchuk.oauth2.security.MyOAuth2UserService; 4 | import com.nbondarchuk.oauth2.service.UserService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 12 | import org.springframework.security.oauth2.client.registration.ClientRegistration; 13 | import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; 14 | import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; 15 | 16 | import java.util.List; 17 | 18 | import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE; 19 | import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.BASIC; 20 | 21 | /** 22 | * @author Nikolay Bondarchuk 23 | * @since 2020-04-05 24 | */ 25 | @Configuration 26 | public class SecurityConfig { 27 | 28 | @Value("${client_id}") 29 | private String clientId; 30 | 31 | @Value("${client_secret}") 32 | private String clientSecret; 33 | 34 | @Bean 35 | public ClientRegistration clientRegistration() { 36 | return ClientRegistration 37 | .withRegistrationId("bitbucket") 38 | .clientId(clientId) 39 | .clientSecret(clientSecret) 40 | .userNameAttributeName("username") 41 | .clientAuthenticationMethod(BASIC) 42 | .authorizationGrantType(AUTHORIZATION_CODE) 43 | .userInfoUri("https://api.bitbucket.org/2.0/user") 44 | .tokenUri("https://bitbucket.org/site/oauth2/access_token") 45 | .authorizationUri("https://bitbucket.org/site/oauth2/authorize") 46 | .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") 47 | .build(); 48 | } 49 | 50 | @Bean 51 | @Autowired 52 | public MyOAuth2UserService oAuth2userService(UserService userService) { 53 | return new MyOAuth2UserService(userService); 54 | } 55 | 56 | @Bean 57 | @Autowired 58 | public ClientRegistrationRepository clientRegistrationRepository(List registrations) { 59 | return new InMemoryClientRegistrationRepository(registrations); 60 | } 61 | 62 | @Configuration 63 | @EnableWebSecurity 64 | public static class AuthConfig extends WebSecurityConfigurerAdapter { 65 | 66 | private final MyOAuth2UserService userService; 67 | 68 | @Autowired 69 | public AuthConfig(MyOAuth2UserService userService) { 70 | this.userService = userService; 71 | } 72 | 73 | @Override 74 | protected void configure(HttpSecurity http) throws Exception { 75 | http 76 | .authorizeRequests(authorizeRequests -> authorizeRequests 77 | .anyRequest().authenticated() 78 | ) 79 | .oauth2Login(oauth2Login -> oauth2Login 80 | .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(userService)) 81 | ); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/security/MyOAuth2User.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.security; 2 | 3 | import org.apache.commons.collections4.CollectionUtils; 4 | import org.apache.commons.collections4.MapUtils; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.security.oauth2.core.user.OAuth2User; 9 | 10 | import java.util.*; 11 | 12 | import static com.google.common.base.Preconditions.checkArgument; 13 | import static java.util.Collections.unmodifiableMap; 14 | import static java.util.Collections.unmodifiableSet; 15 | 16 | /** 17 | * @author Nikolay Bondarchuk 18 | * @since 2020-04-05 19 | */ 20 | public class MyOAuth2User implements OAuth2User, UserDetails { 21 | 22 | public static final String ID_ATTR = "id"; 23 | 24 | private final String nameAttributeKey; 25 | 26 | private final Map attributes; 27 | 28 | private final Set authorities; 29 | 30 | public MyOAuth2User(String nameAttributeKey, Map attributes, Collection authorities) { 31 | checkArgument(MapUtils.isNotEmpty(attributes), "attributes cannot be empty"); 32 | checkArgument(CollectionUtils.isNotEmpty(authorities), "authorities cannot be empty"); 33 | checkArgument(StringUtils.isNotBlank(nameAttributeKey), "nameAttributeKey cannot be empty"); 34 | 35 | validateAttributes(nameAttributeKey, attributes); 36 | 37 | this.nameAttributeKey = nameAttributeKey; 38 | this.attributes = unmodifiableMap(new LinkedHashMap<>(attributes)); 39 | this.authorities = unmodifiableSet(new LinkedHashSet<>(sortAuthorities(authorities))); 40 | } 41 | 42 | public Long getId() { 43 | return getAttribute(ID_ATTR); 44 | } 45 | 46 | @Override 47 | public String getName() { 48 | return getAttribute(nameAttributeKey); 49 | } 50 | 51 | @Override 52 | public String getUsername() { 53 | return getName(); 54 | } 55 | 56 | @Override 57 | public String getPassword() { 58 | return null; 59 | } 60 | 61 | @Override 62 | public boolean isEnabled() { 63 | return true; 64 | } 65 | 66 | @Override 67 | public boolean isAccountNonLocked() { 68 | return true; 69 | } 70 | 71 | @Override 72 | public boolean isAccountNonExpired() { 73 | return true; 74 | } 75 | 76 | @Override 77 | public boolean isCredentialsNonExpired() { 78 | return true; 79 | } 80 | 81 | @Override 82 | public Map getAttributes() { 83 | return attributes; 84 | } 85 | 86 | @Override 87 | public Collection getAuthorities() { 88 | return authorities; 89 | } 90 | 91 | @Override 92 | public boolean equals(Object obj) { 93 | if (this == obj) { 94 | return true; 95 | } 96 | if (obj == null || getClass() != obj.getClass()) { 97 | return false; 98 | } 99 | 100 | MyOAuth2User that = (MyOAuth2User) obj; 101 | 102 | if (!getId().equals(that.getId())) { 103 | return false; 104 | } 105 | if (!getName().equals(that.getName())) { 106 | return false; 107 | } 108 | if (!getAuthorities().equals(that.getAuthorities())) { 109 | return false; 110 | } 111 | return this.getAttributes().equals(that.getAttributes()); 112 | } 113 | 114 | @Override 115 | public int hashCode() { 116 | int result = getId().hashCode(); 117 | result = 31 * result + getName().hashCode(); 118 | result = 31 * result + getAuthorities().hashCode(); 119 | result = 31 * result + getAttributes().hashCode(); 120 | return result; 121 | } 122 | 123 | @Override 124 | @SuppressWarnings("StringBufferReplaceableByString") 125 | public String toString() { 126 | StringBuilder sb = new StringBuilder(); 127 | sb.append("Id: ["); 128 | sb.append(getId()); 129 | sb.append("], Name: ["); 130 | sb.append(getName()); 131 | sb.append("], Granted Authorities: ["); 132 | sb.append(getAuthorities()); 133 | sb.append("], User Attributes: ["); 134 | sb.append(getAttributes()); 135 | sb.append("]"); 136 | return sb.toString(); 137 | } 138 | 139 | private void validateAttribute(String attrName, Map attributes) { 140 | if (!attributes.containsKey(attrName)) { 141 | throw new IllegalArgumentException("Missing " + attrName + " attribute in attributes"); 142 | } 143 | } 144 | 145 | private void validateAttributes(String nameAttributeKey, Map attributes) { 146 | validateAttribute(ID_ATTR, attributes); 147 | validateAttribute(nameAttributeKey, attributes); 148 | } 149 | 150 | private Set sortAuthorities(Collection authorities) { 151 | SortedSet sortedAuthorities = new TreeSet<>(Comparator.comparing(GrantedAuthority::getAuthority)); 152 | sortedAuthorities.addAll(authorities); 153 | return sortedAuthorities; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | org.example 7 | oauth2-bitbucket-example 8 | 1.0-SNAPSHOT 9 | 4.0.0 10 | 11 | 12 | 1.8 13 | 28.1-jre 14 | 1.18.10 15 | 3.9 16 | 4.4 17 | 2.2.2.RELEASE 18 | 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-dependencies 25 | ${spring.boot.version} 26 | pom 27 | import 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-logging 44 | 45 | 46 | 47 | 48 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-log4j2 54 | 55 | 56 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-jersey 62 | 63 | 64 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-security 70 | 71 | 72 | 75 | 76 | org.springframework.security 77 | spring-security-oauth2-jose 78 | 79 | 80 | 83 | 84 | org.springframework.security 85 | spring-security-oauth2-client 86 | 87 | 88 | 89 | org.projectlombok 90 | lombok 91 | ${lombok.version} 92 | provided 93 | 94 | 95 | 96 | com.google.guava 97 | guava 98 | ${guava.version} 99 | 100 | 101 | 102 | org.apache.commons 103 | commons-lang3 104 | ${commons.lang.version} 105 | 106 | 107 | 108 | org.apache.commons 109 | commons-collections4 110 | ${commons.collections.version} 111 | 112 | 113 | 114 | 115 | com.fasterxml.jackson.dataformat 116 | jackson-dataformat-yaml 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | org.apache.maven.plugins 125 | maven-compiler-plugin 126 | 3.8.1 127 | 128 | true 129 | ${jdk.version} 130 | ${jdk.version} 131 | 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-resources-plugin 137 | 3.0.0 138 | 139 | UTF-8 140 | 141 | 142 | 143 | 144 | org.springframework.boot 145 | spring-boot-maven-plugin 146 | 2.2.2.RELEASE 147 | 148 | 149 | 150 | repackage 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /src/main/java/com/nbondarchuk/oauth2/security/MyOAuth2UserService.java: -------------------------------------------------------------------------------- 1 | package com.nbondarchuk.oauth2.security; 2 | 3 | import com.nbondarchuk.oauth2.model.User; 4 | import com.nbondarchuk.oauth2.service.UserService; 5 | import org.springframework.core.ParameterizedTypeReference; 6 | import org.springframework.core.convert.converter.Converter; 7 | import org.springframework.http.RequestEntity; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.security.core.GrantedAuthority; 10 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 11 | import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; 12 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 13 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequestEntityConverter; 14 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; 15 | import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 16 | import org.springframework.security.oauth2.core.OAuth2AuthorizationException; 17 | import org.springframework.security.oauth2.core.OAuth2Error; 18 | import org.springframework.security.oauth2.core.user.OAuth2User; 19 | import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; 20 | import org.springframework.util.StringUtils; 21 | import org.springframework.web.client.RestClientException; 22 | import org.springframework.web.client.RestOperations; 23 | import org.springframework.web.client.RestTemplate; 24 | 25 | import java.util.LinkedHashSet; 26 | import java.util.Map; 27 | import java.util.Optional; 28 | import java.util.Set; 29 | 30 | import static com.google.common.base.Preconditions.checkNotNull; 31 | import static java.util.Objects.requireNonNull; 32 | import static org.apache.commons.collections4.MapUtils.emptyIfNull; 33 | 34 | /** 35 | * @author Nikolay Bondarchuk 36 | * @since 2020-04-05 37 | */ 38 | public class MyOAuth2UserService implements OAuth2UserService { 39 | 40 | private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri"; 41 | 42 | private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response"; 43 | 44 | private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute"; 45 | 46 | private static final ParameterizedTypeReference> PARAMETERIZED_RESPONSE_TYPE = new ParameterizedTypeReference>() { 47 | }; 48 | 49 | private final UserService userService; 50 | 51 | private final RestOperations restOperations; 52 | 53 | private final Converter> requestEntityConverter = new OAuth2UserRequestEntityConverter(); 54 | 55 | public MyOAuth2UserService(UserService userService) { 56 | this.userService = requireNonNull(userService); 57 | this.restOperations = createRestTemplate(); 58 | } 59 | 60 | @Override 61 | public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { 62 | checkNotNull(userRequest, "userRequest cannot be null"); 63 | if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) { 64 | OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE, "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: " + userRequest.getClientRegistration().getRegistrationId(), null); 65 | throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); 66 | } 67 | 68 | String registrationId = userRequest.getClientRegistration().getRegistrationId(); 69 | String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); 70 | if (!StringUtils.hasText(userNameAttributeName)) { 71 | OAuth2Error oauth2Error = new OAuth2Error( 72 | MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE, 73 | "Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: " + registrationId, null); 74 | throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); 75 | } 76 | 77 | ResponseEntity> response; 78 | try { 79 | // OAuth2UserRequestEntityConverter cannot return null values. 80 | //noinspection ConstantConditions 81 | response = this.restOperations.exchange(requestEntityConverter.convert(userRequest), PARAMETERIZED_RESPONSE_TYPE); 82 | } catch (OAuth2AuthorizationException ex) { 83 | OAuth2Error oauth2Error = ex.getError(); 84 | StringBuilder errorDetails = new StringBuilder(); 85 | errorDetails.append("Error details: ["); 86 | errorDetails.append("UserInfo Uri: ").append( 87 | userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri()); 88 | errorDetails.append(", Error Code: ").append(oauth2Error.getErrorCode()); 89 | if (oauth2Error.getDescription() != null) { 90 | errorDetails.append(", Error Description: ").append(oauth2Error.getDescription()); 91 | } 92 | errorDetails.append("]"); 93 | oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, 94 | "An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(), null); 95 | throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); 96 | } catch (RestClientException ex) { 97 | OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, 98 | "An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null); 99 | throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); 100 | } 101 | 102 | Map userAttributes = emptyIfNull(response.getBody()); 103 | 104 | Set authorities = new LinkedHashSet<>(); 105 | authorities.add(new OAuth2UserAuthority(userAttributes)); 106 | 107 | for (String authority : userRequest.getAccessToken().getScopes()) { 108 | authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority)); 109 | } 110 | 111 | // ищем пользователя в нашей БД, либо создаем нового 112 | // если пользователь не найден и система не подразумевает автоматической регистрации, 113 | // необходимо сгенерировать тут исключение 114 | User user = findOrCreate(userAttributes); 115 | userAttributes.put(MyOAuth2User.ID_ATTR, user.getId()); 116 | return new MyOAuth2User(userNameAttributeName, userAttributes, authorities); 117 | } 118 | 119 | private User findOrCreate(Map userAttributes) { 120 | String login = (String) userAttributes.get("username"); 121 | String username = (String) userAttributes.get("display_name"); 122 | 123 | Optional userOpt = userService.findByLogin(login); 124 | if (!userOpt.isPresent()) { 125 | User user = new User(); 126 | user.setLogin(login); 127 | user.setName(username); 128 | return userService.create(user); 129 | } 130 | return userOpt.get(); 131 | } 132 | 133 | private RestTemplate createRestTemplate() { 134 | RestTemplate restTemplate = new RestTemplate(); 135 | restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); 136 | return restTemplate; 137 | } 138 | } 139 | --------------------------------------------------------------------------------