In the previous blog post, we discussed how to configure a simple OAuth2 authentication. However, our implementation has a major flaw in it: we are using an in-memory token store.
In-Memory token stores should be used only during development or whether your application has a single server, as you can’t easily share them between nodes and, in case of a server restart, you will lose all access tokens in it.
Spring-security-oauth2 already has built-in support for JDBC and JWT. However, if you need to save your tokens somewhere else, you have to create your own spring security token store. Unfortunately, implementing such a thing is not a trivial task, and I hope the following recipe will save you a couple hours of work.
Let’s start by creating the two entities responsible for storing your access and refresh token, and their respective repositories:
|
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 |
importar lombok.Data; importar org.springframework.dados.annotation.Id; importar org.springframework.dados.Couchbase.core.mapping.Documento; importar org.springframework.security.oauth2.common.OAuth2AccessToken; importar org.springframework.security.oauth2.provider.OAuth2Authentication; @Document @Data público class CouchbaseAccessToken { @Id private String identidade; private String tokenId; private OAuth2AccessToken token; private String authenticationId; private String nome de usuário; private String clientId; private String authentication; private String refreshToken; público OAuth2Authentication getAuthentication() { retornar SerializableObjectConverter.deserialize(authentication); } público vazio setAuthentication(OAuth2Authentication authentication) { this.authentication = SerializableObjectConverter.serialize(authentication); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@N1qlPrimaryIndexed @ViewIndexed(designDoc = “couchbaseAccessToken”) público interface CouchbaseAccessTokenRepository estende CouchbasePagingAndSortingRepository<CouchbaseAccessToken, String> { List<CouchbaseAccessToken> findByClientId(String clientId); List<CouchbaseAccessToken> findByClientIdAndUsername(String clientId, String nome de usuário); Optional<CouchbaseAccessToken> findByTokenId(String tokenId); Optional<CouchbaseAccessToken> findByRefreshToken(String refreshToken); Optional<CouchbaseAccessToken> findByAuthenticationId(String authenticationId); } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
importar lombok.Data; importar org.springframework.dados.annotation.Id; importar org.springframework.dados.Couchbase.core.mapping.Documento; importar org.springframework.security.oauth2.common.OAuth2RefreshToken; importar org.springframework.security.oauth2.provider.OAuth2Authentication; @Document @Data público class CouchbaseRefreshToken { @Id private String identidade; private String tokenId; private OAuth2RefreshToken token; private String authentication; público OAuth2Authentication getAuthentication() { retornar SerializableObjectConverter.deserialize(authentication); } público vazio setAuthentication(OAuth2Authentication authentication) { this.authentication = SerializableObjectConverter.serialize(authentication); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
importar org.springframework.dados.Couchbase.core.consulta.N1qlPrimaryIndexed; importar org.springframework.dados.Couchbase.core.consulta.ViewIndexed; importar org.springframework.dados.Couchbase.repository.CouchbasePagingAndSortingRepository; importar java.util.List; importar java.util.Optional; @N1qlPrimaryIndexed @ViewIndexed(designDoc = “couchbaseAccessToken”) público interface CouchbaseAccessTokenRepository estende CouchbasePagingAndSortingRepository<CouchbaseAccessToken, String> { List<CouchbaseAccessToken> findByClientId(String clientId); List<CouchbaseAccessToken> findByClientIdAndUsername(String clientId, String nome de usuário); Optional<CouchbaseAccessToken> findByTokenId(String tokenId); Optional<CouchbaseAccessToken> findByRefreshToken(String refreshToken); Optional<CouchbaseAccessToken> findByAuthenticationId(String authenticationId); } |
Note that OAuth2Authentication is an interface, so I have no option other than serializing the object to store it in the database. Here is the class responsible for serializing/deserializing it:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
público class SerializableObjectConverter { público static String serialize(OAuth2Authentication object) { try { byte[] bytes = SerializationUtils.serialize(object); retornar Base64.encodeBase64String(bytes); } catch(Exception e) { e.printStackTrace(); throw e; } } público static OAuth2Authentication deserialize(String encodedObject) { try { byte[] bytes = Base64.decodeBase64(encodedObject); retornar (OAuth2Authentication) SerializationUtils.deserialize(bytes); } catch(Exception e) { e.printStackTrace(); throw e; } } |
Now, we can finally create our custom spring oauth2 token store. To do that, all we need is to implement the long list of methods of the org.springframework.security.oauth2.provider.token.TokenStore:
|
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
importar org.springframework.security.oauth2.common.OAuth2AccessToken; importar org.springframework.security.oauth2.common.OAuth2RefreshToken; importar org.springframework.security.oauth2.provider.OAuth2Authentication; importar org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator; importar org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator; importar org.springframework.security.oauth2.provider.token.TokenStore; importar java.io.UnsupportedEncodingException; importar java.math.BigInteger; importar java.security.MessageDigest; importar java.security.NoSuchAlgorithmException; importar java.util.*; público class CouchbaseTokenStore implements TokenStore { private CouchbaseAccessTokenRepository cbAccessTokenRepository; private CouchbaseRefreshTokenRepository cbRefreshTokenRepository; público CouchbaseTokenStore(CouchbaseAccessTokenRepository cbAccessTokenRepository, CouchbaseRefreshTokenRepository cbRefreshTokenRepository){ this.cbAccessTokenRepository = cbAccessTokenRepository; this.cbRefreshTokenRepository = cbRefreshTokenRepository; } private AuthenticationKeyGenerator authenticationKeyGenerator = novo DefaultAuthenticationKeyGenerator(); @Override público OAuth2Authentication readAuthentication(OAuth2AccessToken accessToken) { retornar readAuthentication(accessToken.getValue()); } @Override público OAuth2Authentication readAuthentication(String token) { Optional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(token)); if (accessToken.isPresent()) { retornar accessToken.obter().getAuthentication(); } retornar null; } @Override público vazio storeAccessToken(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { String refreshToken = null; if (accessToken.getRefreshToken() != null) { refreshToken = accessToken.getRefreshToken().getValue(); } if (readAccessToken(accessToken.getValue()) != null) { this.removeAccessToken(accessToken); } CouchbaseAccessToken cat = novo CouchbaseAccessToken(); cat.setId(UUID.randomUUID().toString()+UUID.randomUUID().toString()); cat.setTokenId(extractTokenKey(accessToken.getValue())); cat.setToken(accessToken); cat.setAuthenticationId(authenticationKeyGenerator.extractKey(authentication)); cat.setUsername(authentication.isClientOnly() ? null : authentication.getName()); cat.setClientId(authentication.getOAuth2Request().getClientId()); cat.setAuthentication(authentication); cat.setRefreshToken(extractTokenKey(refreshToken)); cbAccessTokenRepository.save(cat); } @Override público OAuth2AccessToken readAccessToken(String tokenValue) { Optional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(tokenValue)); if (accessToken.isPresent()) { retornar accessToken.obter().getToken(); } retornar null; } @Override público vazio removeAccessToken(OAuth2AccessToken oAuth2AccessToken) { Optional<CouchbaseAccessToken> accessToken = cbAccessTokenRepository.findByTokenId(extractTokenKey(oAuth2AccessToken.getValue())); if (accessToken.isPresent()) { cbAccessTokenRepository.delete(accessToken.obter()); } } @Override público vazio storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) { CouchbaseRefreshToken crt = novo CouchbaseRefreshToken(); crt.setId(UUID.randomUUID().toString()+UUID.randomUUID().toString()); crt.setTokenId(extractTokenKey(refreshToken.getValue())); crt.setToken(refreshToken); crt.setAuthentication(authentication); cbRefreshTokenRepository.save(crt); } @Override público OAuth2RefreshToken readRefreshToken(String tokenValue) { Optional<CouchbaseRefreshToken> refreshToken = cbRefreshTokenRepository.findByTokenId(extractTokenKey(tokenValue)); retornar refreshToken.isPresent()? refreshToken.obter().getToken() :null; } @Override público OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken refreshToken) { Optional<CouchbaseRefreshToken> rtk = cbRefreshTokenRepository.findByTokenId(extractTokenKey(refreshToken.getValue())); retornar rtk.isPresent()? rtk.obter().getAuthentication() :null; } @Override público vazio removeRefreshToken(OAuth2RefreshToken refreshToken) { Optional<CouchbaseRefreshToken> rtk = cbRefreshTokenRepository.findByTokenId(extractTokenKey(refreshToken.getValue())); if (rtk.isPresent()) { cbRefreshTokenRepository.delete(rtk.obter()); } } @Override público vazio removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { Optional<CouchbaseAccessToken> token = cbAccessTokenRepository.findByRefreshToken(extractTokenKey(refreshToken.getValue())); if(token.isPresent()){ cbAccessTokenRepository.delete(token.obter()); } } @Override público OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { OAuth2AccessToken accessToken = null; String authenticationId = authenticationKeyGenerator.extractKey(authentication); Optional<CouchbaseAccessToken> token = cbAccessTokenRepository.findByAuthenticationId(authenticationId); if(token.isPresent()) { accessToken = token.obter().getToken(); if(accessToken != null && !authenticationId.equals(this.authenticationKeyGenerator.extractKey(this.readAuthentication(accessToken)))) { this.removeAccessToken(accessToken); this.storeAccessToken(accessToken, authentication); } } retornar accessToken; } @Override público Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) { Collection<OAuth2AccessToken> tokens = novo ArrayList<OAuth2AccessToken>(); List<CouchbaseAccessToken> result = cbAccessTokenRepository.findByClientIdAndUsername(clientId, userName); result.forEach(e-> tokens.add(e.getToken())); retornar tokens; } @Override público Collection<OAuth2AccessToken> findTokensByClientId(String clientId) { Collection<OAuth2AccessToken> tokens = novo ArrayList<OAuth2AccessToken>(); List<CouchbaseAccessToken> result = cbAccessTokenRepository.findByClientId(clientId); result.forEach(e-> tokens.add(e.getToken())); retornar tokens; } private String extractTokenKey(String value) { if(value == null) { retornar null; } else { MessageDigest digest; try { digest = MessageDigest.getInstance(“MD5”); } catch (NoSuchAlgorithmException var5) { throw novo IllegalStateException(“MD5 algorithm not available. Fatal (should be in the JDK).”); } try { byte[] e = digest.digest(value.getBytes(“UTF-8”)); retornar String.format(“%032x”, novo Object[]{novo BigInteger(1, e)}); } catch (UnsupportedEncodingException var4) { throw novo IllegalStateException(“UTF-8 encoding not available. Fatal (should be in the JDK).”); } } } } |
Finally, we can slightly change our SecurityConfig class, which we have created in the previous article. It will return now an instance of CouchbaseTokenStore instead of InMemoryTokenStore:
|
1 2 3 4 5 6 7 8 9 10 11 |
@Autowired private CouchbaseAccessTokenRepository couchbaseAccessTokenRepository; @Autowired private CouchbaseRefreshTokenRepository couchbaseRefreshTokenRepository; @Bean público TokenStore tokenStore() { retornar novo CouchbaseTokenStore(couchbaseAccessTokenRepository, couchbaseRefreshTokenRepository); } |
Here is the complete version of the SecurityConfig 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
@Configuration @EnableWebMvc público class SecurityConfig estende WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService customUserDetailsService; @Autowired private CouchbaseAccessTokenRepository couchbaseAccessTokenRepository; @Autowired private CouchbaseRefreshTokenRepository couchbaseRefreshTokenRepository; @Autowired público vazio globalUserDetails(AuthenticationManagerBuilder autenticação) throws Exception { autenticação.userDetailsService(customUserDetailsService) .passwordEncoder(encoder()); } @Override público vazio configure( WebSecurity web ) throws Exception { web.ignoring().antMatchers( HttpMethod.OPTIONS, “/**” ); } @Override protected vazio configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers(“/oauth/token”).permitAll() .antMatchers(“/api-docs/**”).permitAll() .anyRequest().authenticated() .e().anonymous().disable(); } @Bean público TokenStore tokenStore() { retornar novo CouchbaseTokenStore(couchbaseAccessTokenRepository, couchbaseRefreshTokenRepository); } @Bean público PasswordEncoder encoder(){ retornar NoOpPasswordEncoder.getInstance(); } @Bean público FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = novo UrlBasedCorsConfigurationSource(); CorsConfiguration config = novo CorsConfiguration(); config.setAllowCredentials(verdadeiro); config.addAllowedOrigin(“*”); config.addAllowedHeader(“*”); config.addAllowedMethod(“*”); source.registerCorsConfiguration(“/**”, config); FilterRegistrationBean bean = novo FilterRegistrationBean(novo CorsFilter(source)); bean.setOrder(0); retornar bean; } @Bean @Override público AuthenticationManager authenticationManagerBean() throws Exception { retornar super.authenticationManagerBean(); } } |
Well Done! That is all we had to do.
Your access token will look like the following in your database:
|
1 2 |
SELECIONAR * de test where _class = ‘com.bc.quicktask.standalone.model.CouchbaseAccessToken’ |
|
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 |
[ { “YOUR_BUCKET_NAME”: { “_class”: “com.bc.quicktask.standalone.model.CouchbaseAccessToken”, “authentication”: “rO0ABXNyAEFvcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5Lm9hdXRoMi5wcm92aWRlci5PQXV0aDJBdXRoZW50aWNhdGlvbr1ACwIWYlITAgACTAANc3RvcmVkUmVxdWVzdHQAPExvcmcvc3ByaW5nZnJhbWV3b3JrL3NlY3VyaXR5L29hdXRoMi9wcm92aWRlci9PQXV0aDJSZXF1ZXN0O0wAEnVzZXJBdXRoZW50aWNhdGlvbnQAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL3NlY3VyaXR5L2NvcmUvQXV0aGVudGljYXRpb247eHIAR29yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkuYXV0aGVudGljYXRpb24uQWJzdHJhY3RBdXRoZW50aWNhdGlvblRva2Vu06oofm5HZA4CAANaAA1hdXRoZW50aWNhdGVkTAALYXV0aG9yaXRpZXN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247TAAHZGV0YWlsc3QAEkxqYXZhL2xhbmcvT2JqZWN0O3hwAHNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHQAEExqYXZhL3V0aWwvTGlzdDt4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWNxAH4ABHhwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB+AAxwc3IAOm9yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkub2F1dGgyLnByb3ZpZGVyLk9BdXRoMlJlcXVlc3QAAAAAAAAAAQIAB1oACGFwcHJvdmVkTAALYXV0aG9yaXRpZXNxAH4ABEwACmV4dGVuc2lvbnN0AA9MamF2YS91dGlsL01hcDtMAAtyZWRpcmVjdFVyaXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAB3JlZnJlc2h0ADtMb3JnL3NwcmluZ2ZyYW1ld29yay9zZWN1cml0eS9vYXV0aDIvcHJvdmlkZXIvVG9rZW5SZXF1ZXN0O0wAC3Jlc291cmNlSWRzdAAPTGphdmEvdXRpbC9TZXQ7TAANcmVzcG9uc2VUeXBlc3EAfgAReHIAOG9yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkub2F1dGgyLnByb3ZpZGVyLkJhc2VSZXF1ZXN0Nih6PqNxab0CAANMAAhjbGllbnRJZHEAfgAPTAARcmVxdWVzdFBhcmFtZXRlcnNxAH4ADkwABXNjb3BlcQB+ABF4cHQACG15Y2xpZW50c3IAJWphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVNYXDxpaj+dPUHQgIAAUwAAW1xAH4ADnhwc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAABncIAAAACAAAAAN0AApncmFudF90eXBldAAIcGFzc3dvcmR0AAljbGllbnRfaWR0AAhteWNsaWVudHQACHVzZXJuYW1ldAAGbXl1c2VyeHNyACVqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlU2V0gB2S0Y+bgFUCAAB4cQB+AAlzcgAXamF2YS51dGlsLkxpbmtlZEhhc2hTZXTYbNdald0qHgIAAHhyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAABA/QAAAAAAAA3QABXRydXN0dAAEcmVhZHQABXdyaXRleAFzcQB+ACJ3DAAAABA/QAAAAAAAAHhzcQB+ABc/QAAAAAAAAHcIAAAAEAAAAAB4cHBzcQB+ACJ3DAAAABA/QAAAAAAAAHhzcQB+ACJ3DAAAABA/QAAAAAAAAHhzcgBPb3JnLnNwcmluZ2ZyYW1ld29yay5zZWN1cml0eS5hdXRoZW50aWNhdGlvbi5Vc2VybmFtZVBhc3N3b3JkQXV0aGVudGljYXRpb25Ub2tlbgAAAAAAAAH0AgACTAALY3JlZGVudGlhbHNxAH4ABUwACXByaW5jaXBhbHEAfgAFeHEAfgADAXNyAB9qYXZhLnV0aWwuQ29sbGVjdGlvbnMkRW1wdHlMaXN0ergXtDynnt4CAAB4cHNyABdqYXZhLnV0aWwuTGlua2VkSGFzaE1hcDTATlwQbMD7AgABWgALYWNjZXNzT3JkZXJ4cQB+ABc/QAAAAAAABncIAAAACAAAAAR0AA1jbGllbnRfc2VjcmV0dAAIbXlzZWNyZXRxAH4AGXEAfgAacQB+ABtxAH4AHHEAfgAdcQB+AB54AHBzcgAyY29tLmJjLnF1aWNrdGFzay5zdGFuZGFsb25lLm1vZGVsLkN1c3RvbVVzZXJEZXRhaWz9dbY7wdosOwIAAkwABmdyb3Vwc3EAfgAITAAEdXNlcnQAKExjb20vYmMvcXVpY2t0YXNrL3N0YW5kYWxvbmUvbW9kZWwvVXNlcjt4cHNxAH4ACwAAAAB3BAAAAAB4c3IAJmNvbS5iYy5xdWlja3Rhc2suc3RhbmRhbG9uZS5tb2RlbC5Vc2VyWvIkR494dqQCAAdMAAljb21wYW55SWRxAH4AD0wADWV4dGVybmFsTG9naW5xAH4AD0wAAmlkcQB+AA9MAAlpc0VuYWJsZWR0ABNMamF2YS9sYW5nL0Jvb2xlYW47TAAJaXNWaXNpYmxlcQB+ADhMAAhwYXNzd29yZHEAfgAPTAAIdXNlcm5hbWVxAH4AD3hwdAAKY29tcGFueS0tMXQABm15dXNlcnQACXVzZXJJZC0tMXNyABFqYXZhLmxhbmcuQm9vbGVhbs0gcoDVnPruAgABWgAFdmFsdWV4cAFxAH4APnQACHBhc3N3b3JkdAAGbXl1c2Vy”, “authenticationId”: “202d6940ebd428bbe2098530c8de3958”, “clientId”: “myclient”, “refreshToken”: “7613ffc6480ae83beb8f0988ef9ecfcf”, “token”: { “_class”: “org.springframework.security.oauth2.common.DefaultOAuth2AccessToken”, “additionalInformation”: {}, “expiration”: 1537420023432, “refreshToken”: { “_class”: “org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken”, “expiration”: 1537420023421, “value”: “c44db735-403e-49d6-8a22-cfe0e29f21d3” }, “scope”: [ “trust”, “read”, “write” ], “tokenType”: “bearer”, “value”: “7759804c-e1c6-4d63-9520-8737f5b46dbf” }, “tokenId”: “12b6bd9de380e1dfab348f4c15abb805”, “username”: “myuser” } } ] |
I have used caelwinner’s project as a reference, here is my special thanks to him.
If you have any questions, feel free to tweet me at @deniswsrosa
Autor
Uma resposta
-
Hi Denis,
First of all, thank you for the very brief but concise tutorial. Although it is over a year, it is well explained, and I must acknowledge that you are an excellent teacher.
Please, I have one question and a request to make. Is it possible to add JWT to this implementation OAuth2, and if yes,can you please provide a guide?
Waiting for a reply.
Thank you so much.

Deixe um comentário
Você precisa fazer o login para publicar um comentário.