Creating a Spring MongoDBAuthenticationProvider
Introduction
This short post describes how to implement a spring AuthenticationProvider
that will authenticate against MongoDB directly.
Users will only be authenticated with the application if they are valid users in MongoDB.
An example use-case for this would be for support applications / tooling on top of the database.
This is an alternative to creating and managing your own User
entities via JPA
where user data is usually stored in an application level collection such as users
.
Database Setup
Make sure that you have a secure instance of MongoDB running on the default port at localhost:27017
.
Create a user for testing via the mongo shell:
use admin
db.createUser(
{
user: "jason",
pwd: "jason123",
roles: [ "readWriteAnyDatabase" ]
}
)
Verify access via the shell:
mongo --authenticationDatabase admin -u jason -p jason123 admin --eval 'db.runCommand({listDatabases:1})'
# databases should be printed
The AuthenticationProvider
Lets jump straight into the code:
@Service
public class MongoDBAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
...
public MongoDBAuthenticationProvider(
@Value("${mongo.connection.string:mongodb://localhost:27017/admin}") String connectionString,
@Autowired MongoDBClientFactory clientFactory) {
this.connectionString = connectionString;
this.clientFactory = clientFactory;
}
The constructor has two arguments: the mongoDB connection string and a MongoDBClientFactory
.
The MongoDBClientFactory
is a simple wrapper around the static MongoClients.create
method to make testing easier:
@Configuration
public class MongoDBClientFactory {
public MongoClient create(MongoClientSettings settings) {
return MongoClients.create(settings);
}
}
The interesting code is implemented in the retrieveUser
method.
MongoClientSettings
is first created from the connectionString and credentials (username and password).- Then a
MongoClient
is created. - Using the client an attempt is made to list the database names and retrieve the first one. If the user is authenticated this should work.
- Otherwise a
MongoSecurityException
is thrown from the driver. This is caught, wrapped in aBadCredentialsException
and re-thrown:
@Override
protected UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
ConnectionString connString = new ConnectionString(connectionString);
MongoCredential credential = createCredential(username, authentication, connString);
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(connString)
.credential(credential)
.build();
try(MongoClient mongoClient = clientFactory.create(settings)) {
log.info("Attempting to authenticate user '{}'", username);
mongoClient.listDatabaseNames().first();
log.info("Successfully authenticated user '{}'", username);
} catch(MongoSecurityException mse) {
String message = String.format("User '%s' was not authenticated.", username);
log.warn(message, mse);
throw new BadCredentialsException(message, mse);
}
return new User(username, authentication.getCredentials().toString(), Collections.emptyList());
}
private MongoCredential createCredential(String username,
UsernamePasswordAuthenticationToken authentication,
ConnectionString connString) {
char[] chars = authentication.getCredentials().toString().toCharArray();
return MongoCredential.createCredential(username, connString.getDatabase(), chars);
}
- If the list databases command worked, a
User
is created and returned. - This is cached by the spring framework: so this method is not called upon every request.
Summary
This short post demonstrated how write a Spring AuthenticationProvider
where authentication is done against users defined in MongoDB directly.
The code is available as part of a simple web-application on github here: MongoDBAuthenticationProvider.java