How to encode password with HMAC-SHA512 in Spring Boot Security - java

I have an old application running in PHP that uses the function base64_encode(hash_hmac(“sha512”, $p_password, $p_salt, true)) to encode passwords in our database.
I am migrating this application to Java Spring Boot and want to encode the passwords during authentification in the exact same way.
I have found how to make the same hashing method with Java in this post Compute HMAC-SHA512 with secret key in java and I also learnt that we can have several password encoders for old and new users with https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#constructing-delegatingpasswordencoder
But I still cannot find an example of how I can integrate this hasing method in Spring authentication process. I have to create a PasswordEncoder bean and I don't know what to put inside.
I tried Pbkdf2PasswordEncoder because it can make some SHA-512 hash like in my app but I get the error Detected a Non-hex character at 1 or 2 position.
It is probably due to the fact that the passwords are not prefixed by {pbkdf2} in the database. The following code is what I am currently using as PasswordEncoder
#Bean
public PasswordEncoder passwordEncoder() {
Pbkdf2PasswordEncoder passwordEncoder = new Pbkdf2PasswordEncoder("salt");
passwordEncoder.setAlgorithm(Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA512);
return passwordEncoder;
}
I need help to set up the right password encoder to use HMAC-SHA512 in my authentification process with Java Spring and in a second time, combine it with BCrytPasswordEncoder (for new users) with DelegatingPasswordEncoder.
Maybe it requires to update the passwords in DB to prefix them with the right encoder ?
If my question is not accurate enough or missing information, please ask me for more details :)

You need to add a DelegatingPasswordEncoder to your project configuration file.
The DelegatingPasswordEncoder acts as a PasswordEncoder, and we use it when we have to choose from a collection of implementations.:
#Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
#Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
Pbkdf2PasswordEncoder bcryprPe = new Pbkdf2PasswordEncoder("salt");
bcryprPe.setAlgorithm(
Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA512);
encoders.put("pbkdf2", pbkdf2Pe);
// add other PasswordEncoder here:
encoders.put("scrypt", new SCryptPasswordEncoder());
return new DelegatingPasswordEncoder("pbkdf2", encoders);
}
}
With
return new DelegatingPasswordEncoder("pbkdf2", encoders);
we are saying to Spring Security: "Use the 'pbkdf2' as default password encoder".
If the provided hash is {scrypt}12345, the DelegatingPasswordEncoder delegates to the SCryptPasswordEncoder, if there is no prefix, the application will use default one.

I finally got what I wanted. I created an implementation of PasswordEncoder inspired by https://github.com/lathspell/java_test/blob/master/java_test_openldap/src/main/java/LdapSha512PasswordEncoder.java
in WebSecurityConfig.java
#Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("SSHA-512", new Hmac512PasswordEncoder("salt"));
encoders.put("bcrypt", new BCryptPasswordEncoder());
return new DelegatingPasswordEncoder("SSHA-512", encoders);
}
in Hmac512PasswordEncoder.java
public class Hmac512PasswordEncoder implements PasswordEncoder {
private static final String SSHA512_PREFIX = "{SSHA-512}";
private static final String HMAC_SHA512 = "HmacSHA512";
private final String salt;
public Hmac512PasswordEncoder(String salt) {
if (salt == null) {
throw new IllegalArgumentException("salt cannot be null");
}
this.salt = salt;
}
public String encode(CharSequence rawPassword) {
String result = null;
try {
Mac sha512Hmac = Mac.getInstance(HMAC_SHA512);
final byte[] byteKey = Utf8.encode(salt);
SecretKeySpec keySpec = new SecretKeySpec(byteKey, HMAC_SHA512);
sha512Hmac.init(keySpec);
byte[] macData = sha512Hmac.doFinal(Utf8.encode(rawPassword.toString()));
result = SSHA512_PREFIX + Base64.getEncoder().encodeToString(macData);
//result = bytesToHex(macData);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
return result;
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null || encodedPassword == null) {
return false;
}
String encodedRawPass = encode(rawPassword);
return MessageDigest.isEqual(Utf8.encode(encodedRawPass), Utf8.encode(encodedPassword));
}
}

Related

How to use Spring OAuth2 Client in SPA and multi-node application?

I want to implement a feature that user connects his account with external applications (similar feature is in Facebook). User has to log in to external application and grant permission to access data by my application.
Once user connected an external app, data will be exchanged in background using access and refresh tokens.
Application architecture is:
SPA front-end (Angular)
REST API (Spring), multiple nodes
ScyllaDB
Envoy proxy (with JWT verification)
The first idea is to use Spring OAuth2 Client. However, some changes need to be made:
there is no Principal because JWT is verified by Envoy proxy and X-USER-ID header is added
REST API is stateless and we shouldn't store authorization code in session
even with sessions, there are multiple nodes and we need to share authorization code between nodes
custom URL, e.g. /app_name/connect instead of /oauth2/authorization/app_name
redirect URL may be invalid (but it's verified by Spring's filter)
How this could work:
user click "Connect with app" in SPA
SPA redirects user to /oauth2/authorization/app_name (or custom URL)
Spring redirects user to external app's authentication server
user authenticates and grants permissions
external app redirects user back to Spring (or straight to SPA?)
Spring redirects user back to SPA (or SPA sends access token to REST API?)
Despite Spring Security components can be replaced, many of them are coupled and you need to rewrite OAuth2 Client flow almost from scratch. Maybe I'm doing something wrong and it can be achieved easier.
What I already did:
http
.cors().and()
.csrf().disable()
.authorizeRequests().anyRequest().permitAll().and()
.oauth2Client(); // get rid of these two filters?
#Configuration
#RequiredArgsConstructor
public class OAuth2ClientConfig {
private final CassandraTemplate cassandraTemplate;
// overriding original client service - we need to store tokens in database
#Bean
public OAuth2AuthorizedClientService authorizedClientService(
CassandraTemplate cassandraTemplate,
ClientRegistrationRepository clientRegistrationRepository) {
return new ScyllaOAuth2AuthorizedClientService(cassandraTemplate, clientRegistrationRepository);
}
// configure client provider to use authorization code with refresh token
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
var authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.build();
var authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
// the specs recommend to use WebClient for exchanging data instead of RestTemplate
#Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
// override request repository - and I'm stuck there
#Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
return new ScyllaOAuth2AuthorizationRequestRepository(cassandraTemplate);
}
}
Because there are multiple nodes of REST API, we can't use sessions. We need to store request somewhere, e.g. ScyllaDB, Redis, Hazelcast, etc. I decided to store it as JSON in ScyllaDB but I ran into trouble.
#Slf4j
#RequiredArgsConstructor
public final class ScyllaOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
private final CassandraTemplate cassandraTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
#Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
var stateParameter = this.getStateParameter(request);
if (stateParameter == null) {
return null;
}
return this.getAuthorizationRequest(request, stateParameter);
}
#Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request,
HttpServletResponse response) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(response, "response cannot be null");
if (authorizationRequest == null) {
this.removeAuthorizationRequest(request, response);
return;
}
var state = authorizationRequest.getState();
var userId = UUID.fromString(request.getHeader(Constants.USER_ID));
Assert.hasText(state, "authorizationRequest.state cannot be empty");
try {
// serialization of Auth2AuthorizationRequest to JSON works
cassandraTemplate.getCqlOperations().execute("insert into oauth2_requests (user_id,state,data) values (?,?,?)",
userId, state, objectMapper.writeValueAsString(authorizationRequest));
} catch (JsonProcessingException e) {
log.warn("Unable to save authorization request", e);
}
}
#Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
var stateParameter = this.getStateParameter(request);
if (stateParameter == null) {
return null;
}
var userId = UUID.fromString(request.getHeader(Constants.USER_ID));
var originalRequest = this.getAuthorizationRequest(request, stateParameter);
cassandraTemplate.getCqlOperations().execute("delete from oauth2_requests where user_id=? and state=?",
userId, stateParameter);
return originalRequest;
}
private String getStateParameter(HttpServletRequest request) {
return request.getParameter(OAuth2ParameterNames.STATE);
}
private UUID getUserId(HttpServletRequest request) {
return UUID.fromString(request.getHeader(Constants.USER_ID));
}
private OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest request, String state) {
var userId = getUserId(request);
var jsonRequest = cassandraTemplate.getCqlOperations().queryForObject(
"select data from oauth2_requests where user_id=? and state=?", String.class, userId, state);
if (StringUtils.isNotBlank(jsonRequest)) {
try {
// trying to mess with OAuth2ClientJackson2Module
var objectMapper = new Jackson2ObjectMapperBuilder().autoDetectFields(true)
.autoDetectGettersSetters(true)
.modules(new OAuth2ClientJackson2Module())
.visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
.build();
return objectMapper.readValue(jsonRequest, OAuth2AuthorizationRequest.class);
} catch (JsonProcessingException e) {
log.warn("Error decoding authentication request", e);
}
}
return null;
}
}
I get error when trying to deserialize JSON to OAuth2AuthorizationRequest:
Missing type id when trying to resolve subtype of [simple type, class org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest]: missing type id property '#class'
Without adding OAuth2ClientJackson2Module there is another error:
Cannot construct instance of `org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
By the way, OAuth2ClientJackson2Module seems never used in original filters.
Maybe it's better to serialize this object Java way and store it as BLOB or do not store request in database but somewhere other.
Another part is the controller action:
// it had to be /apps/app_name/connect but in Spring OAuth2 Client it's hardcoded to append provider name at the end
#GetMapping("/apps/connect/app_name")
public void connect(HttpServletRequest request, HttpServletResponse response) throws IOException {
userAppService.authorize(request, response, "app_name");
}
To get rid of filters which verify redirect URL and have many things hardcoded:
#Service
#RequiredArgsConstructor
public class UserAppService {
private final HttpSecurity httpSecurity;
private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private final AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;
private final ClientRegistrationRepository clientRegistrationRepository;
private final OAuth2AuthorizedClientManager authorizedClientManager;
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public void authorize(HttpServletRequest request, HttpServletResponse response, String appName) throws IOException {
var userId = UUID.fromString(request.getHeader(Constants.USER_ID));
var authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(appName)
.principal(UUIDPrincipal.fromUserId(userId))
.build();
if (isAuthorizationResponse(request)) {
var authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
if (authorizationRequest != null) {
processAuthorizationRequest(request, response);
}
} else {
try {
OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);
if (authorizedClient != null) {
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
System.out.println(accessToken);
}
} catch (ClientAuthorizationException e) {
// in this URL provider name is appended at the end and no way to change this behavior
var authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository,
"/apps/connect");
var authorizationRequest = authorizationRequestResolver.resolve(request);
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
this.redirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri());
}
}
}
private void processAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
var authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
var registrationId = (String) authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
var clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
var params = toMultiMap(request.getParameterMap());
var redirectUri = UrlUtils.buildFullRequestUrl(request);
var authorizationResponse = convert(params, redirectUri);
var authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
OAuth2AuthorizationCodeAuthenticationToken authenticationResult;
try {
var authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
authenticationResult = (OAuth2AuthorizationCodeAuthenticationToken) authenticationManager
.authenticate(authenticationRequest);
} catch (OAuth2AuthorizationException ex) {
OAuth2Error error = ex.getError();
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri())
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
if (!StringUtils.hasText(error.getDescription())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
}
if (!StringUtils.hasText(error.getUri())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_URI, error.getUri());
}
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build().encode().toString());
return;
}
// just copy-paste of original filter - trying to understand what's happening there
Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication();
String principalName = (currentAuthentication != null) ? currentAuthentication.getName() : "anonymousUser";
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
authenticationResult.getClientRegistration(), principalName, authenticationResult.getAccessToken(),
authenticationResult.getRefreshToken());
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request,
response);
String redirectUrl = authorizationRequest.getRedirectUri();
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
}
private static boolean isAuthorizationResponse(HttpServletRequest request) {
return isAuthorizationResponseSuccess(request) || isAuthorizationResponseError(request);
}
private static boolean isAuthorizationResponseSuccess(HttpServletRequest request) {
return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.CODE))
&& StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE));
}
private static boolean isAuthorizationResponseError(HttpServletRequest request) {
return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.ERROR))
&& StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE));
}
// copy paste - not tested this code yet
static MultiValueMap<String, String> toMultiMap(Map<String, String[]> map) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>(map.size());
map.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
params.add(key, value);
}
}
});
return params;
}
static OAuth2AuthorizationResponse convert(MultiValueMap<String, String> request, String redirectUri) {
String code = request.getFirst(OAuth2ParameterNames.CODE);
String errorCode = request.getFirst(OAuth2ParameterNames.ERROR);
String state = request.getFirst(OAuth2ParameterNames.STATE);
if (StringUtils.hasText(code)) {
return OAuth2AuthorizationResponse.success(code).redirectUri(redirectUri).state(state).build();
}
String errorDescription = request.getFirst(OAuth2ParameterNames.ERROR_DESCRIPTION);
String errorUri = request.getFirst(OAuth2ParameterNames.ERROR_URI);
return OAuth2AuthorizationResponse.error(errorCode)
.redirectUri(redirectUri)
.errorDescription(errorDescription)
.errorUri(errorUri)
.state(state)
.build();
}
}
Client service to stored authorized clients in database:
#RequiredArgsConstructor
public class ScyllaOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService {
private final CassandraTemplate cassandraTemplate;
private final ClientRegistrationRepository clientRegistrationRepository;
#Override
#SuppressWarnings("unchecked")
public OAuth2AuthorizedClient loadAuthorizedClient(String clientRegistrationId, String principal) {
var id = BasicMapId.id("userId", principal).with("appCode", clientRegistrationId);
var userApp = cassandraTemplate.selectOneById(id, UserApp.class);
if (userApp != null) {
var clientRegistration = getClientRegistration(clientRegistrationId);
var accessToken = getAccessToken(userApp);
var refreshToken = getRefreshToken(userApp);
return new OAuth2AuthorizedClient(clientRegistration, principal, accessToken, refreshToken);
} else {
return null;
}
}
#Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
Assert.notNull(authorizedClient, "authorizedClient cannot be null");
Assert.notNull(principal, "principal cannot be null");
var userApp = new UserApp();
userApp.setUserId((UUID) principal.getPrincipal());
userApp.setAppCode(authorizedClient.getClientRegistration().getClientId());
if (authorizedClient.getAccessToken() != null) {
userApp.setAccessToken(authorizedClient.getAccessToken().getTokenValue());
userApp.setAccessTokenType(OAuth2AccessToken.TokenType.BEARER.getValue());
userApp.setAccessTokenScopes(authorizedClient.getAccessToken().getScopes());
userApp.setAccessTokenIssuedAt(authorizedClient.getAccessToken().getIssuedAt());
userApp.setAccessTokenExpiresAt(authorizedClient.getAccessToken().getExpiresAt());
}
if (authorizedClient.getRefreshToken() != null) {
userApp.setRefreshToken(authorizedClient.getRefreshToken().getTokenValue());
userApp.setRefreshTokenIssuedAt(authorizedClient.getRefreshToken().getIssuedAt());
userApp.setRefreshTokenExpiresAt(authorizedClient.getRefreshToken().getExpiresAt());
}
cassandraTemplate.insert(userApp);
}
#Override
public void removeAuthorizedClient(String clientRegistrationId, String principal) {
var id = BasicMapId.id("userId", principal).with("appCode", clientRegistrationId);
cassandraTemplate.deleteById(id, UserApp.class);
}
private ClientRegistration getClientRegistration(String clientRegistrationId) {
var clientRegistration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
if (clientRegistration == null) {
throw new DataRetrievalFailureException(
"The ClientRegistration with id '" + clientRegistrationId + "' exists in the data source, "
+ "however, it was not found in the ClientRegistrationRepository.");
}
return clientRegistration;
}
private OAuth2AccessToken getAccessToken(UserApp userApp) {
return new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER,
userApp.getAccessToken(),
userApp.getAccessTokenIssuedAt(),
userApp.getAccessTokenExpiresAt(),
userApp.getAccessTokenScopes());
}
private OAuth2RefreshToken getRefreshToken(UserApp userApp) {
return new OAuth2RefreshToken(userApp.getRefreshToken(), userApp.getRefreshTokenIssuedAt());
}
}
Too much code overwrite. I need to make it as simple as possible.
Currently I'm struggling with storing authorize request in database.
How to do it Spring way but to keep the app architecture given at the beginning of this question?
Any way to configure OAuth2 Client without hardcoded URL like /oauth2/authorization/provider_name?
Maybe it's better to do the whole OAuth2 flow client-side (within SPA) and the SPA should send access and request token to REST API (to store the tokens in order to be able to exchange data with external app)?
In OAuth2 wording, REST APIs are resource-servers, not clients.
What you can do is have
your proxy be transparent to OAuth2 (forward requests with their JWT access-token authorization header and responses status code)
configure each REST API as resource-server. Tutorials there: https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials.
add an OAuth2 client library to your Angular app to handle tokens and authorize requests. My favorite is angular-auth-oidc-client
probably use an intermediate authorization-server for identity federation (Google, Facebook, etc., but also internal DB, LDAP, or whatever is needed), roles management, MFA,... Keycloak is a famous "on premise" solution, but you can search for "OIDC authorization-server" in your favorite search engine and have plenty of alternate choices, including SaaS like Auth0 or Amazon Cognito.
This is fully compatible with distributed architectures and micro-services (session-less is the default configuration for resource-servers in the tutorials I linked).
Two cases for a micro-service delegating some of its processing to another resource-server:
the "child" request is made on behalf of the user who initiated the request => retrieve original access-token from Authentication instance in security-context and forward it (set it as Bearer authorization header for the sub-request)
the "child" request is not made on behalf of a user => client-credentials must be used (the micro-services acquires a new access-token in its own name to authorize the sub request). Refer to spring-boot-oauth2-client and your preferred REST client docs for details (WebClient, #FeignClient, RestTemplate).

Spring Boot Configuration Class isn't Properly passing values from application.properties

I'm creating a spring boot application with the AWS SDK. I am having some troubles with my S3 configuration class. I originally had all of the access keys and secrets keys stored in the code, and am transitioning to having an IAM user credentials in application.properties that has access to secrets manager, and getting all of the credentials for services through that. For some reason, the values are not properly populating. I believe it's possible that the constructor is executing before the value injection. Not sure on the best solution to fix this, thanks in advance for any help.
Here's the code for the S3 config class –
#Configuration
public class S3Config {
private String accessKeyId;
private String secretAccessKey;
#Bean
public AmazonS3 amazonS3Client(#Value("${aws.access-key-id}") String accessKeyId, #Value("${aws.secret-access-key") String secretAccessKey) {
this.accessKeyId = accessKeyId;
this.secretAccessKey = secretAccessKey;
BasicAWSCredentials awsCredentials = getBasicAWSCredentials();
return AmazonS3ClientBuilder
.standard()
.withRegion("xx")
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
private BasicAWSCredentials getBasicAWSCredentials() {
String secretName = "xx";
String region = "xx";
AWSSecretsManager client = AWSSecretsManagerClientBuilder.standard()
.withRegion(region)
.withCredentials(
new AWSStaticCredentialsProvider(
new BasicAWSCredentials(
accessKeyId,
secretAccessKey
)
)
)
.build();
String secret = "";
String decodedBinarySecret;
GetSecretValueRequest getSecretValueRequest = new GetSecretValueRequest()
.withSecretId(secretName);
GetSecretValueResult getSecretValueResult = null;
try {
getSecretValueResult = client.getSecretValue(getSecretValueRequest);
} catch (DecryptionFailureException e) {
// Secrets Manager can't decrypt the protected secret text using the provided KMS key.
// Deal with the exception here, and/or rethrow at your discretion.
throw e;
} catch (InternalServiceErrorException e) {
// An error occurred on the server side.
// Deal with the exception here, and/or rethrow at your discretion.
throw e;
} catch (InvalidParameterException e) {
// You provided an invalid value for a parameter.
// Deal with the exception here, and/or rethrow at your discretion.
throw e;
} catch (InvalidRequestException e) {
// You provided a parameter value that is not valid for the current state of the resource.
// Deal with the exception here, and/or rethrow at your discretion.
throw e;
} catch (ResourceNotFoundException e) {
// We can't find the resource that you asked for.
// Deal with the exception here, and/or rethrow at your discretion.
throw e;
}
if (getSecretValueResult.getSecretString() != null) {
secret = getSecretValueResult.getSecretString();
} else {
decodedBinarySecret = new String(Base64.getDecoder().decode(getSecretValueResult.getSecretBinary()).array());
}
JsonObject secrets = new Gson().fromJson(secret, JsonObject.class);
return new BasicAWSCredentials(secrets.get("accessKey").getAsString(), secrets.get("secretKey").getAsString());
}
And here's an example of me using the S3 config in a service class. This was all working before I introduced the application.properties file and #Value annotations.
#Service
public class FileService {
private AmazonS3 amazonS3Client;
private String bucketName;
private FileRepository fileRepository;
public FileService(AmazonS3 amazonS3Client, FileRepository fileRepository) {
this.amazonS3Client = amazonS3Client;
bucketName = "xx";
this.fileRepository = fileRepository;
}

How the Spring Security AuthenticationManager authenticate() method is able to check if the username and password sent are correct?

I am working on a Spring Boot application that take the username and password of an existing user on the system and then generates a JWT token. I copied it from a tutorial and I changed it in order to work with my specific use cases. The logic is pretty clear to me but I have a big doubt about how the user is authenticated on the system. Following I will try to explain you as this is structured and what is my doubt.
The JWT generation token system is composed by two different micro services, that are:
The GET-USER-WS: this microservice simmply use Hibernate\JPA to retrieve the information of a specific user in the system. Basically it contains a controller class calling a service class that itself calla JPA repository in order to retrieve a specific user information:
#RestController
#RequestMapping("api/users")
#Log
public class UserController {
#Autowired
UserService userService;
#GetMapping(value = "/{email}", produces = "application/json")
public ResponseEntity<User> getUserByEmail(#PathVariable("email") String eMail) throws NotFoundException {
log.info(String.format("****** Get the user with eMail %s *******", eMail) );
User user = userService.getUserByEmail(eMail);
if (user == null)
{
String ErrMsg = String.format("The user with eMail %s was not found", eMail);
log.warning(ErrMsg);
throw new NotFoundException(ErrMsg);
}
return new ResponseEntity<User>(user, HttpStatus.OK);
}
}
As you can see this controller contains an API that use the e-mail parameter (that is the username on the system) and return a JSON containing the information of this user.
Then I have a second microservice (named AUTH-SERVER-JWT) that is the one that call the previous API in order to obtain the user information that will be used to generate the JWT token. To keep the description as simple as possible it contains this controller class:
#RestController
//#CrossOrigin(origins = "http://localhost:4200")
public class JwtAuthenticationRestController {
#Value("${sicurezza.header}")
private String tokenHeader;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Autowired
#Qualifier("customUserDetailsService")
//private UserDetailsService userDetailsService;
private CustomUserDetailsService userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationRestController.class);
#PostMapping(value = "${sicurezza.uri}")
public ResponseEntity<?> createAuthenticationToken(#RequestBody JwtTokenRequest authenticationRequest)
throws AuthenticationException {
logger.info("Autenticazione e Generazione Token");
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
//final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final UserDetailsWrapper userDetailsWrapper = userDetailsService.loadCompleteUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetailsWrapper);
logger.warn(String.format("Token %s", token));
return ResponseEntity.ok(new JwtTokenResponse(token));
}
#RequestMapping(value = "${sicurezza.uri}", method = RequestMethod.GET)
public ResponseEntity<?> refreshAndGetAuthenticationToken(HttpServletRequest request)
throws Exception
{
String authToken = request.getHeader(tokenHeader);
if (authToken == null || authToken.length() < 7)
{
throw new Exception("Token assente o non valido!");
}
final String token = authToken.substring(7);
if (jwtTokenUtil.canTokenBeRefreshed(token))
{
String refreshedToken = jwtTokenUtil.refreshToken(token);
return ResponseEntity.ok(new JwtTokenResponse(refreshedToken));
}
else
{
return ResponseEntity.badRequest().body(null);
}
}
#ExceptionHandler({ AuthenticationException.class })
public ResponseEntity<String> handleAuthenticationException(AuthenticationException e)
{
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}
private void authenticate(String username, String password)
{
Objects.requireNonNull(username);
Objects.requireNonNull(password);
try {
/// ???
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (DisabledException e)
{
logger.warn("UTENTE DISABILITATO");
throw new AuthenticationException("UTENTE DISABILITATO", e);
}
catch (BadCredentialsException e)
{
logger.warn("CREDENZIALI NON VALIDE");
throw new AuthenticationException("CREDENZIALI NON VALIDE", e);
}
}
}
This class contains two method, the first one is used to generate a brand new JWT token and the second one it is used to refresh an existing JWT token. Consider now the first use case (generate a brand new JWT token) related to the createAuthenticationToken() method. This method take as input parmether the information related to the JWT token request: #RequestBody JwtTokenRequest authenticationRequest. Bascailly the JwtTokenRequest is a simple DTO object like this:
#Data
public class JwtTokenRequest implements Serializable
{
private static final long serialVersionUID = -3558537416135446309L;
private String username;
private String password;
}
So the payload in the body request will be something like this:
{
"username": "xxx#gmail.com",
"password": "password"
}
NOTE: in my DB I have a user having this username and password so the user will be retrieved and authenticated on the system.
As you can see the first effective operation that the createAuthenticationToken() method do is:
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
Basically it is calling the authenticate() method defined in the same class passing to it the previous credential ("username": "xxx#gmail.com" and "password": "password").
As you can see this is my authenticate() method
private void authenticate(String username, String password)
{
Objects.requireNonNull(username);
Objects.requireNonNull(password);
try {
/// ???
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (DisabledException e)
{
logger.warn("UTENTE DISABILITATO");
throw new AuthenticationException("UTENTE DISABILITATO", e);
}
catch (BadCredentialsException e)
{
logger.warn("CREDENTIAL ERROR");
throw new AuthenticationException(""CREDENTIAL ERROR", e);
}
}
Basically it is passing these credential to the authenticate() method defined into the injected Spring Security AuthenticationManager instance, by this line:
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
This method seems to be able to authenticate or not these credentials. And it seems to works fine because if I put a wrong username or password it goes into the CREDENTIAL ERROR case and it throw the AuthenticationException exception.
And here my huge doubt: why it works?!?! How is it possible? If you come back to the createAuthenticationToken() controller method you can see that it does these two operation in the following order:
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
final UserDetailsWrapper userDetailsWrapper = userDetailsService.loadCompleteUserByUsername(authenticationRequest.getUsername());
It first perform the authenticate() method (that should check if theusername and password that was sent are correct), then call the service method that retrieve the user information.
Sho how the authenticate() method is able to check if the credential sent in the original payload are correct?
Usually, the implementation of AuthenticationManager is a ProviderManager, which will loop through all the configured AuthenticationProviders and try to authenticate using the credentials provided.
One of the AuthenticationProviders, is DaoAuthenticationProvider, which supports a UsernamePasswordAuthenticationToken and uses the UserDetailsService (you have a customUserDetailsService) to retrieve the user and compare the password using the configured PasswordEncoder.
There is a more detailed explanation in the reference docs about the Authentication Architecture.

How to differentiate headers between two/multiple endpoints in a RequestInterceptor

Hello I'm new to Java and Springboot. I'm currently working with an API where before making a POST request, I would need to generate a Bearer token. In order to generate a Bearer token, I would need to pass in my basic auth credentials to the "/oauth/token" endpoint. My application is having trouble passing my basic auth credentials since by the time I hit the "/v1/some-endpoint", I'm denied authorization because the Bearer token is null.
Here's my initial solution thinking I could check the url in the interceptor, then executing the following line but after debugging, it doesn't seem to be hitting that line.
Is there something I'm missing or not implementing correctly? Am I not implementing the Basic Auth endpoint correctly? Let me know if you need more information. Thanks
#Profile("!offline")
#FeignClient(
value = "someClient",
url = "${someProperty.url}",
configuration = SomeClient.SomeClientConfig.class)
public interface someClient {
#PostMapping("/v1/some-endpoint")
void redeemSomething(someRequestBody data);
#PostMapping("/oauth/token")
static BasicAuthResponse getBasicAuthToken() {
return new BasicAuthResponse();
}
#AllArgsConstructor
class SomeClientConfig extends BaseClientConfig {
private final SomeProperties properties;
private final SomeAuthTokenSupplier tokenSupplier = new SomeAuthTokenSupplier();
#Bean
#Override
public CloseableHttpClient apacheClient() {
return apacheClientFactory(properties.getUseProxy());
}
#Bean
public RequestInterceptor someAuthInterceptor() {
return template -> {
if(template.url().equals("/oauth/token")) {
String authToken = Base64Utils.encodeToString((properties.getCredentials().getUser() + ":" + properties.getCredentials().getUser()).getBytes(Charset.forName("UTF-8")));
template.header("Authorization", authToken);
}
template.header("Authorization", String.format("Bearer %s", tokenSupplier.getToken()));
};
}
private class SomeAuthTokenSupplier {
private volatile String token;
private volatile long retrievedOn = -1L;
String getToken() {
if (updateTokenRequired()) {
synchronized (this) {
if (updateTokenRequired()) {
BasicAuthResponse tokenResponse = getBasicAuthToken();
token = tokenResponse.getAccess_token(); // new token from some api should be assigned here
retrievedOn = Instant.now().toEpochMilli();
}
}
}
return token;
}
private boolean updateTokenRequired() {
return token == null || LocalDateTime.now().minusHours(8L).isAfter(LocalDateTime.ofInstant(Instant.ofEpochMilli(retrievedOn), ZoneId.systemDefault()));
}
}
#Override
public Retryer retryer() {
return new ClientRetry(250L, 2, 3) {
#Override
public void continueOrPropagate(RetryableException e) {
if (e.status() == 401 || e.status() == 403) {
tokenSupplier.token = null;
}
super.continueOrPropagate(e);
}
};
}
}
}
It worth using standard Spring Security OAuth2 Client feature instead in order to support authorization in Feign clients
See docs and code samples: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2client
UPD
See another code sample: https://github.com/int128/feign-oauth2-example
If several service endpoints require different authentication, then it's worth having several Feign clients, each with own configuration

JWT Token verification with Java

I am facing a issue that whenever I am signing a token also I parse it and it is not throwing any signature exception.
You can see the key are different still it giving me the proper response.
public class JwtUtil {
public String parseToken(String token) {
try {
Jws<Claims> jwt = Jwts.parser()
.setSigningKey("Test#12")
.parseClaimsJws(token);
System.out.println(jwt.getBody().getSubject());
return "Valid";
} catch (SignatureException jwtException) {
jwtException.printStackTrace();
return null;
}
}
public String generateToken() {
Claims claim = Jwts.claims();
claim.put("GivenName", "Johnny");
claim.put("Surname", "Rocket");
claim.put("Email", "jrocket#example.com");
return Jwts.builder().setHeaderParam("typ", "JWT").setClaims(claim)
.setIssuer("Online JWT Builder")
.setAudience("www.example.com").setSubject("jrocket#example.com")
.signWith(SignatureAlgorithm.HS256, "Test#123").compact();
}
public static void main(String[] args) {
JwtUtil jwtUtil = new JwtUtil();
String token = jwtUtil.generateToken();
System.out.println(token);
JwtUtil jwtUtil1 = new JwtUtil();
jwtUtil1.parseToken(token);
}
}
Really Test#12 and Test#123 are the same key
It is due to JwtBuilder.signWith(SignatureAlgorithm alg, String base64EncodedSecretKey). assumes that you are providing a key in base64 and your keys are not base64. When the method decodes from base64 to byte[] the java converter used by jjwt provides a representation of the string. Test#12 and Test#123 are encoded with the byte array
See https://stackoverflow.com/a/38269014/6371459
You can test yourself with
System.out.println(
javax.xml.bind.DatatypeConverter.printBase64Binary(
javax.xml.bind.DatatypeConverter.parseBase64Binary("Test#12")));
System.out.println(
javax.xml.bind.DatatypeConverter.printBase64Binary(
javax.xml.bind.DatatypeConverter.parseBase64Binary("Test#123")));
Try a (more) different key and the SignatureException will be thrown

Categories

Resources