We already discussed how to configure an OAuth 2.0 authentication and how to build a custom token store. In the last article of this series, you will learn how to implement a custom dynamic client registration using spring-security-oauth2. I recommend you reading Part 1 and Part 2 first, as we are going to continue from where we have left off.
Let’s start by creating the entity responsible for storing the client data:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import com.couchbase.client.java.repository.annotation.Id; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.data.couchbase.core.mapping.Document; import javax.validation.constraints.NotNull; import java.util.*; @NoArgsConstructor @Data @Document public class CustomClientDetails extends BasicEntity { @Id @NotNull private String id; @NotNull private String clientId; private String clientSecret; private Set<String> resourceIds = new HashSet<>(); private boolean secretRequired; private boolean scoped; private Set<String> scope = new HashSet<>(); private Set<String> authorizedGrantTypes = new HashSet<>(); private Set<String> registeredRedirectUri = new HashSet<>(); private Collection<String> authorities = new HashSet<>(); private Integer accessTokenValiditySeconds; private Integer refreshTokenValiditySeconds; private boolean autoApprove; private Map<String, Object> additionalInformation = new HashMap<>(); } |
Here is the respective repository:
1 2 3 4 5 6 7 8 9 10 11 |
import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed; import org.springframework.data.couchbase.core.query.ViewIndexed; import org.springframework.data.couchbase.repository.CouchbasePagingAndSortingRepository; @N1qlPrimaryIndexed @ViewIndexed(designDoc = "customClientDetails") public interface CustomClientDetailsRepository extends CouchbasePagingAndSortingRepository<CustomClientDetails, String> { CustomClientDetails findByClientId(String clientId); } |
Now, we can implement the ClientDetailsService interface from the spring security class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.ClientRegistrationException; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.stereotype.Service; import java.util.stream.Collectors; @Service public class CouchbaseClientDetailsService implements ClientDetailsService { @Autowired private CustomClientDetailsRepository customClientDetailsRepository; @Override public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException { CustomClientDetails client = customClientDetailsRepository.findByClientId(clientId); String resourceIds = client.getResourceIds().stream().collect(Collectors.joining(",")); String scopes = client.getScope().stream().collect(Collectors.joining(",")); String grantTypes = client.getAuthorizedGrantTypes().stream().collect(Collectors.joining(",")); String authorities = client.getAuthorities().stream().collect(Collectors.joining(",")); BaseClientDetails base = new BaseClientDetails(client.getClientId(), resourceIds, scopes, grantTypes, authorities); base.setClientSecret(client.getClientSecret()); base.setAccessTokenValiditySeconds(client.getAccessTokenValiditySeconds()); base.setRefreshTokenValiditySeconds(client.getRefreshTokenValiditySeconds()); base.setAdditionalInformation(client.getAdditionalInformation()); base.setAutoApproveScopes(client.getScope()); return base; } } |
Note that I’m using the BaseClientDetails class rather than implementing the ClientDetails interface. It seems to be the best option, as even the standard JDBC implementation uses it.
Finally, we need to change our AuthorizationServerConfig to use our CouchbaseClientDetailsService:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private CouchbaseClientDetailsService couchbaseClientDetailsService; @Override public void configure(ClientDetailsServiceConfigurer configurer) throws Exception { configurer.withClientDetails(couchbaseClientDetailsService); } } |
Here is how the whole class looks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private TokenStore tokenStore; @Autowired private AuthenticationManager authenticationManager; @Autowired private CouchbaseClientDetailsService couchbaseClientDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Override public void configure(ClientDetailsServiceConfigurer configurer) throws Exception { configurer.withClientDetails(couchbaseClientDetailsService); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore) .authenticationManager(authenticationManager); } @Override public void configure( AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()"); } } |
Now you can simply insert a new client into your database and use these credentials to authenticate via OAuth:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
static final String GRANT_TYPE_PASSWORD = "password"; static final String AUTHORIZATION_CODE = "authorization_code"; static final String REFRESH_TOKEN = "refresh_token"; static final String IMPLICIT = "implicit"; static final String SCOPE_READ = "read"; static final String SCOPE_WRITE = "write"; static final String TRUST = "trust"; private void createOauthClients() { CustomClientDetails client = new CustomClientDetails(); client.setId("someId"); client.setResourceIds(new HashSet<>(Arrays.asList("resource_id")) ); client.setClientId("android-client"); client.setClientSecret("android-secret"); client.setAuthorizedGrantTypes(new HashSet<>(Arrays.asList(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT))); client.setScope(new HashSet<>(Arrays.asList(SCOPE_READ, SCOPE_WRITE, TRUST))); client.setSecretRequired(true); client.setAccessTokenValiditySeconds(50000); client.setRefreshTokenValiditySeconds(50000); client.setScoped(false); customClientDetailsRepository.save(client); } |
TL;DR – The trick is to implement the org.springframework.security.oauth2.provider.ClientDetailsService interface and pass it as a parameter to your ClientDetailsServiceConfigurer:
1 2 3 4 |
@Override public void configure(ClientDetailsServiceConfigurer configurer) throws Exception { configurer.withClientDetails(couchbaseClientDetailsService); } |
If you have any questions, tweet me at @deniswsrosa