io.gravitee.am.identityprovider.mongo.authentication.MongoAuthenticationProvider Maven / Gradle / Ivy
/**
* Copyright (C) 2015 The Gravitee team (http://gravitee.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.gravitee.am.identityprovider.mongo.authentication;
import com.mongodb.reactivestreams.client.MongoCollection;
import io.gravitee.am.common.exception.authentication.BadCredentialsException;
import io.gravitee.am.common.exception.authentication.InternalAuthenticationServiceException;
import io.gravitee.am.common.exception.authentication.UsernameNotFoundException;
import io.gravitee.am.common.oidc.StandardClaims;
import io.gravitee.am.identityprovider.api.Authentication;
import io.gravitee.am.identityprovider.api.AuthenticationContext;
import io.gravitee.am.identityprovider.api.AuthenticationProvider;
import io.gravitee.am.identityprovider.api.DefaultUser;
import io.gravitee.am.identityprovider.api.IdentityProviderMapper;
import io.gravitee.am.identityprovider.api.IdentityProviderRoleMapper;
import io.gravitee.am.identityprovider.api.SimpleAuthenticationContext;
import io.gravitee.am.identityprovider.api.User;
import io.gravitee.am.identityprovider.api.UserCredentialEvaluation;
import io.gravitee.am.identityprovider.mongo.MongoAbstractProvider;
import io.gravitee.am.identityprovider.mongo.authentication.spring.MongoAuthenticationProviderConfiguration;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Observable;
import org.bson.BsonDocument;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Optional.ofNullable;
/**
* @author David BRASSELY (david.brassely at graviteesource.com)
* @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
* @author GraviteeSource Team
*/
@Import({MongoAuthenticationProviderConfiguration.class})
public class MongoAuthenticationProvider extends MongoAbstractProvider implements AuthenticationProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(MongoAuthenticationProvider.class);
private static final String FIELD_ID = "_id";
private static final String FIELD_USERNAME = "username";
private static final String FIELD_CREATED_AT = "createdAt";
private static final String FIELD_UPDATED_AT = "updatedAt";
@Autowired
private IdentityProviderMapper mapper;
@Autowired
private IdentityProviderRoleMapper roleMapper;
@Override
public AuthenticationProvider stop() throws Exception {
if (this.clientWrapper != null) {
this.clientWrapper.releaseClient();
}
return this;
}
public Maybe loadUserByUsername(Authentication authentication) {
String username = getSafeUsername((String) authentication.getPrincipal());
return findUserByMultipleField(username)
.toList()
.flatMapPublisher(users -> {
if (users.isEmpty()) {
return Flowable.error(new UsernameNotFoundException(username));
}
return Flowable.fromIterable(users);
})
.map(user -> {
String password = user.getString(this.configuration.getPasswordField());
String presentedPassword = authentication.getCredentials().toString();
if (password == null) {
LOGGER.debug("Authentication failed: password is null");
return new UserCredentialEvaluation<>(false, user);
}
if (configuration.isUseDedicatedSalt()) {
String hash = user.getString(configuration.getPasswordSaltAttribute());
if (!passwordEncoder.matches(presentedPassword, password, hash)) {
LOGGER.debug("Authentication failed: password does not match stored value");
return new UserCredentialEvaluation<>(false, user);
}
} else {
if (!passwordEncoder.matches(presentedPassword, password)) {
LOGGER.debug("Authentication failed: password does not match stored value");
return new UserCredentialEvaluation<>(false, user);
}
}
return new UserCredentialEvaluation<>(true, user);
})
.toList()
.flatMapMaybe(userEvaluations -> {
final var validUsers = userEvaluations.stream().filter(UserCredentialEvaluation::isPasswordValid).collect(Collectors.toList());
if (validUsers.size() > 1) {
LOGGER.debug("Authentication failed: multiple accounts with same credentials");
return Maybe.error(new BadCredentialsException("Bad credentials"));
}
var userEvaluation = !validUsers.isEmpty() ? validUsers.get(0) : userEvaluations.get(0);
var user = this.createUser(authentication.getContext(), userEvaluation.getUser());
ofNullable(authentication.getContext()).ifPresent(auth -> auth.set(ACTUAL_USERNAME, user.getUsername()));
return userEvaluation.isPasswordValid() ?
Maybe.just(user) :
Maybe.error(new BadCredentialsException("Bad credentials"));
});
}
private Flowable findUserByMultipleField(String value) {
MongoCollection usersCol = this.mongoClient.getDatabase(this.configuration.getDatabase()).getCollection(this.configuration.getUsersCollection());
String findQuery = this.configuration.getFindUserByMultipleFieldsQuery() != null ? this.configuration.getFindUserByMultipleFieldsQuery() : this.configuration.getFindUserByUsernameQuery();
String jsonQuery = convertToJsonString(findQuery)
.replaceAll("\\?", value);
BsonDocument query = BsonDocument.parse(jsonQuery);
return Flowable.fromPublisher(usersCol.find(query));
}
public Maybe loadUserByUsername(String username) {
final String encodedUsername = getSafeUsername(username);
return findUserByUsername(encodedUsername)
.map(document -> createUser(new SimpleAuthenticationContext(), document));
}
private Maybe findUserByUsername(String encodedUsername) {
MongoCollection usersCol = this.mongoClient.getDatabase(this.configuration.getDatabase())
.getCollection(this.configuration.getUsersCollection());
String rawQuery = this.configuration.getFindUserByUsernameQuery().replaceAll("\\?", encodedUsername);
String jsonQuery = convertToJsonString(rawQuery);
BsonDocument query = BsonDocument.parse(jsonQuery);
return Observable.fromPublisher(usersCol.find(query).first()).firstElement();
}
private User createUser(AuthenticationContext authContext, Document document) {
// get sub
String sub = getClaim(document, FIELD_ID, null);
// get username
String username = getClaim(document, configuration.getUsernameField(), sub);
// create the user
DefaultUser user = new DefaultUser(username);
// set technical id
user.setId(sub);
// set user roles
user.setRoles(applyRoleMapping(authContext, document));
// set additional information
Map additionalInformation = new HashMap<>();
additionalInformation.put(StandardClaims.SUB, sub);
additionalInformation.put(StandardClaims.PREFERRED_USERNAME, username);
// apply user mapping
Map mappedAttributes = applyUserMapping(authContext, document);
additionalInformation.putAll(mappedAttributes);
// update sub if user mapping has been changed
if (additionalInformation.get(StandardClaims.SUB) != null) {
user.setId(additionalInformation.get(StandardClaims.SUB).toString());
}
// update username if user mapping has been changed
if (additionalInformation.get(StandardClaims.PREFERRED_USERNAME) != null) {
user.setUsername(additionalInformation.get(StandardClaims.PREFERRED_USERNAME).toString());
}
// remove reserved claims
additionalInformation.remove(FIELD_ID);
additionalInformation.remove(configuration.getUsernameField());
additionalInformation.remove(configuration.getPasswordField());
additionalInformation.remove(FIELD_CREATED_AT);
if (configuration.isUseDedicatedSalt()) {
additionalInformation.remove(configuration.getPasswordSaltAttribute());
}
if (additionalInformation.containsKey(FIELD_UPDATED_AT)) {
additionalInformation.put(StandardClaims.UPDATED_AT, document.get(FIELD_UPDATED_AT));
additionalInformation.remove(FIELD_UPDATED_AT);
}
if (additionalInformation.isEmpty() || additionalInformation.get(StandardClaims.SUB) == null) {
throw new InternalAuthenticationServiceException("The 'sub' claim for the user is required");
}
user.setAdditionalInformation(additionalInformation);
return user;
}
private String getClaim(Map claims, String userAttribute, String defaultValue) {
return claims.containsKey(userAttribute) ? claims.get(userAttribute).toString() : defaultValue;
}
private String convertToJsonString(String rawString) {
rawString = rawString.replaceAll("[^" + JSON_SPECIAL_CHARS + "\\s]+", "\"$0\"").replaceAll("\\s+", "");
return rawString;
}
private Map applyUserMapping(AuthenticationContext authContext, Map attributes) {
if (!mappingEnabled()) {
return attributes;
}
return this.mapper.apply(authContext, attributes);
}
private List applyRoleMapping(AuthenticationContext authContext, Map attributes) {
if (!roleMappingEnabled()) {
return Collections.emptyList();
}
return roleMapper.apply(authContext, attributes);
}
private boolean mappingEnabled() {
return this.mapper != null;
}
private boolean roleMappingEnabled() {
return this.roleMapper != null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy