Skip to content

Commit

Permalink
feat(idp): add multiple login value
Browse files Browse the repository at this point in the history
  • Loading branch information
leleueri authored and tcompiegne committed Apr 22, 2021
1 parent b278865 commit 516f4e6
Show file tree
Hide file tree
Showing 15 changed files with 254 additions and 27 deletions.
Expand Up @@ -105,7 +105,8 @@ public Single<User> authenticate(Client client, Authentication authentication, b
if (lastException instanceof BadCredentialsException) {
return Single.error(new BadCredentialsException("The credentials you entered are invalid", lastException));
} else if (lastException instanceof UsernameNotFoundException) {
return Single.error(new UsernameNotFoundException("Invalid or unknown user"));
// if an IdP return UsernameNotFoundException, convert it as BadCredentials in order to avoid helping attackers
return Single.error(new BadCredentialsException("The credentials you entered are invalid", lastException));
} else if (lastException instanceof AccountStatusException) {
return Single.error(lastException);
} else if (lastException instanceof NegotiateContinueException) {
Expand All @@ -115,7 +116,8 @@ public Single<User> authenticate(Client client, Authentication authentication, b
return Single.error(new InternalAuthenticationServiceException("Unable to validate credentials. The user account you are trying to access may be experiencing a problem.", lastException));
}
} else {
return Single.error(new UsernameNotFoundException("No user found for registered providers"));
// if an IdP return null user, throw BadCredentials in order to avoid helping attackers
return Single.error(new BadCredentialsException("The credentials you entered are invalid"));
}
} else {
// complete user connection
Expand Down
Expand Up @@ -16,7 +16,6 @@
package io.gravitee.am.identityprovider.jdbc.authentication;

import io.gravitee.am.common.exception.authentication.BadCredentialsException;
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.AuthenticationProvider;
Expand All @@ -28,11 +27,13 @@
import io.gravitee.am.identityprovider.jdbc.authentication.spring.JdbcAuthenticationProviderConfiguration;
import io.gravitee.am.identityprovider.jdbc.utils.ColumnMapRowMapper;
import io.gravitee.am.identityprovider.jdbc.utils.ParametersUtils;
import io.r2dbc.spi.Statement;
import io.reactivex.Completable;
import io.reactivex.Flowable;
import io.reactivex.Maybe;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.util.StringUtils;

import java.util.*;

Expand All @@ -54,25 +55,60 @@ public Maybe<User> loadUserByUsername(Authentication authentication) {
final String username = authentication.getPrincipal().toString();
final String presentedPassword = authentication.getCredentials().toString();

return selectUserByUsername(username)
.switchIfEmpty(Maybe.error(new UsernameNotFoundException(username)))
.map(result -> {
return selectUserByMultipleField(username)
.filter(result -> {
// check password
String password = result.get(configuration.getPasswordAttribute()).toString();
if (password == null) {
LOGGER.debug("Authentication failed: password is null");
throw new BadCredentialsException("Invalid account");
return false;
}

if (!passwordEncoder.matches(presentedPassword, password)) {
LOGGER.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException("Bad credentials");
return false;
}

return true;
})
.map(this::createUser)
.toList()
.flatMapMaybe(users -> {
if (users.isEmpty()) {
return Maybe.error(new BadCredentialsException("Bad credentials"));
}
if (users.size() > 1) {
return Maybe.error(new BadCredentialsException("Bad credentials"));
}
// create the user
return createUser(result);
return Maybe.just(users.get(0));
});
}

private Flowable<Map<String, Object>> selectUserByMultipleField(String username) {
String rawQuery = configuration.getSelectUserByMultipleFieldsQuery() != null ? configuration.getSelectUserByMultipleFieldsQuery() : configuration.getSelectUserByUsernameQuery();
String[] args = prepareIndexParameters(rawQuery);
final String sql = String.format(rawQuery, args);
return Flowable.fromPublisher(connectionPool.create())
.flatMap(connection -> {
Statement statement = connection.createStatement(sql);
for (int i = 0; i < args.length; ++i) {
statement = statement.bind(i, username);
}
return Flowable.fromPublisher(statement.execute())
.doFinally(() -> Completable.fromPublisher(connection.close()).subscribe());
})
.flatMap(result -> result.map(ColumnMapRowMapper::mapRow));
}

private String[] prepareIndexParameters(String rawQuery) {
final int variables = StringUtils.countOccurrencesOf(rawQuery, "%s");
String[] idxParameters = new String[variables];
for (int i = 0; i < variables; ++i) {
idxParameters[i] = getIndexParameter("username", i);
}
return idxParameters;
}

@Override
public Maybe<User> loadUserByUsername(String username) {
return selectUserByUsername(username)
Expand Down Expand Up @@ -176,7 +212,11 @@ private boolean roleMappingEnabled() {
}

private String getIndexParameter(String field) {
return ParametersUtils.getIndexParameter(configuration.getProtocol(), 1, field);
return getIndexParameter(field, 0);
}

private String getIndexParameter(String field, int offset) {
return ParametersUtils.getIndexParameter(configuration.getProtocol(), 1 + offset, field);
}

}
Expand Up @@ -40,6 +40,7 @@ public class JdbcIdentityProviderConfiguration implements IdentityProviderConfig
private String user;
private String password;
private String selectUserByUsernameQuery;
private String selectUserByMultipleFieldsQuery;
private String identifierAttribute = FIELD_ID;
private String usernameAttribute = FIELD_USERNAME;
private String passwordAttribute = FIELD_PASSWORD;
Expand Down Expand Up @@ -116,6 +117,14 @@ public void setSelectUserByUsernameQuery(String selectUserByUsernameQuery) {
this.selectUserByUsernameQuery = selectUserByUsernameQuery;
}

public String getSelectUserByMultipleFieldsQuery() {
return selectUserByMultipleFieldsQuery;
}

public void setSelectUserByMultipleFieldsQuery(String selectUserByMultipleFieldsQuery) {
this.selectUserByMultipleFieldsQuery = selectUserByMultipleFieldsQuery;
}

public String getIdentifierAttribute() {
return identifierAttribute;
}
Expand Down
Expand Up @@ -15,6 +15,8 @@
*/
package io.gravitee.am.identityprovider.jdbc.utils;

import io.r2dbc.spi.Statement;

/**
* @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
* @author GraviteeSource Team
Expand Down
Expand Up @@ -47,6 +47,12 @@
"title": "Query to find for a user using its identifier (username)",
"description": "The query which is executed to search for an user using its identifier (username)."
},
"selectUserByMultipleFieldsQuery" : {
"type" : "string",
"widget": "textarea",
"title": "Query to find for a user using multiple fields (username or email)",
"description": "The query which is executed to search for an user using its identifier (username) or another field (email). If this field isn't specified, the findUserByUsernameQuery is used"
},
"identifierAttribute": {
"type": "string",
"title": "User identifier attribute",
Expand Down
Expand Up @@ -22,6 +22,7 @@
import io.gravitee.am.identityprovider.api.AuthenticationProvider;
import io.gravitee.am.identityprovider.api.User;
import io.reactivex.observers.TestObserver;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -63,6 +64,58 @@ public AuthenticationContext getContext() {
testObserver.assertValue(u -> "bob".equals(u.getUsername()));
}

@Test
public void shouldLoadUserByUsername_authentication_multifield_username() {
TestObserver<User> testObserver = authenticationProvider.loadUserByUsername(new Authentication() {
@Override
public Object getCredentials() {
return "user01";
}

@Override
public Object getPrincipal() {
return "user01";
}

@Override
public AuthenticationContext getContext() {
return null;
}
}).test();

testObserver.awaitTerminalEvent();

testObserver.assertComplete();
testObserver.assertNoErrors();
testObserver.assertValue(u -> "user01".equals(u.getUsername()));
}

@Test
public void shouldLoadUserByUsername_authentication_multifield_email() {
TestObserver<User> testObserver = authenticationProvider.loadUserByUsername(new Authentication() {
@Override
public Object getCredentials() {
return "user01";
}

@Override
public Object getPrincipal() {
return "user01@acme.com";
}

@Override
public AuthenticationContext getContext() {
return null;
}
}).test();

testObserver.awaitTerminalEvent();

testObserver.assertComplete();
testObserver.assertNoErrors();
testObserver.assertValue(u -> "user01".equals(u.getUsername()));
}

@Test
public void shouldLoadUserByUsername_authentication_badCredentials() {
TestObserver<User> testObserver = authenticationProvider.loadUserByUsername(new Authentication() {
Expand Down Expand Up @@ -105,6 +158,6 @@ public AuthenticationContext getContext() {
}).test();

testObserver.awaitTerminalEvent();
testObserver.assertError(UsernameNotFoundException.class);
testObserver.assertError(BadCredentialsException.class);
}
}
Expand Up @@ -29,6 +29,7 @@
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -52,9 +53,14 @@ public abstract class JdbcAuthenticationProviderConfigurationTest implements Ini
public void afterPropertiesSet() throws Exception {
// create table users and insert values
Connection connection = connectionPool.create().block();
initData(connection);
Completable.fromPublisher(connection.close()).subscribe();
}

protected void initData(Connection connection) {
Single.fromPublisher(connection.createStatement("create table users(id varchar(256), username varchar(256), password varchar(256), email varchar(256), metadata text)").execute()).subscribe();
Single.fromPublisher(connection.createStatement("insert into users values('1', 'bob', 'bobspassword', null, null)").execute()).subscribe();
Completable.fromPublisher(connection.close()).subscribe();
Single.fromPublisher(connection.createStatement("insert into users values('2', 'user01', 'user01', 'user01@acme.com', null)").execute()).subscribe();
}

@Bean
Expand Down Expand Up @@ -88,6 +94,7 @@ public JdbcIdentityProviderConfiguration configuration() {
configuration.setProtocol(protocol());
configuration.setUsersTable("users");
configuration.setSelectUserByUsernameQuery("select * from users where username = %s");
configuration.setSelectUserByMultipleFieldsQuery("select * from users where username = %s or email = %s ");
configuration.setPasswordEncoder(PasswordEncoder.NONE.getValue());

return configuration;
Expand Down
Expand Up @@ -15,6 +15,10 @@
*/
package io.gravitee.am.identityprovider.jdbc.configuration;

import io.r2dbc.spi.Connection;
import io.r2dbc.spi.Result;
import io.reactivex.Completable;
import io.reactivex.Single;
import org.springframework.context.annotation.Configuration;

/**
Expand All @@ -32,4 +36,18 @@ public String url() {
public String protocol() {
return "sqlserver";
}

@Override
protected void initData(Connection connection) {
Single.fromPublisher(connection.createStatement("create table users(id varchar(256), username varchar(256), password varchar(256), email varchar(256), metadata text)").execute()).blockingGet();
Single.fromPublisher(connection.createStatement("insert into users values('1', 'bob', 'bobspassword', null, null)").execute()).blockingGet();
Single.fromPublisher(connection.createStatement("insert into users(id, username, password, email, metadata) values( @id, @username, @password, @email , @metadata)")
.bind("id", "2")
.bind("username", "user01")
.bind("password", "user01")
.bind("email", "user01@acme.com")
.bindNull("metadata", String.class)
.execute()).flatMap(rp -> Single.fromPublisher(rp.getRowsUpdated()))
.blockingGet();
}
}
Expand Up @@ -37,6 +37,7 @@ public class MongoIdentityProviderConfiguration implements IdentityProviderConfi
private String database;
private String usersCollection;
private String findUserByUsernameQuery;
private String findUserByMultipleFieldsQuery;
private String findUserByEmailQuery = "{email: ?}";
private String usernameField = FIELD_USERNAME;
private String passwordField = FIELD_PASSWORD;
Expand Down Expand Up @@ -135,6 +136,14 @@ public void setFindUserByEmailQuery(String findUserByEmailQuery) {
this.findUserByEmailQuery = findUserByEmailQuery;
}

public String getFindUserByMultipleFieldsQuery() {
return findUserByMultipleFieldsQuery;
}

public void setFindUserByMultipleFieldsQuery(String findUserByMultipleFieldsQuery) {
this.findUserByMultipleFieldsQuery = findUserByMultipleFieldsQuery;
}

public String getUsernameField() {
return usernameField;
}
Expand Down

0 comments on commit 516f4e6

Please sign in to comment.