Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add the delegating password encoder for apollo-portal simple auth #3804

Merged
merged 13 commits into from
Jul 10, 2021
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Apollo 1.9.0
* [feature: shared session for multi apollo portal](https://github.com/ctripcorp/apollo/pull/3786)
* [feature: add email for select user on apollo portal](https://github.com/ctripcorp/apollo/pull/3797)
* [feature: modify item comment valid size](https://github.com/ctripcorp/apollo/pull/3803)
* [feature: add the delegating password encoder for apollo-portal simple auth](https://github.com/ctripcorp/apollo/pull/3804)
------------------
All issues and pull requests are [here](https://github.com/ctripcorp/apollo/milestone/6?closed=1)

Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
import java.io.IOException;
import java.util.Collections;
import java.util.Properties;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Test;
Expand Down Expand Up @@ -383,7 +383,8 @@ public void testApolloConfigChangeListenerWithInterestedKeyPrefixes() {
}

@Test
public void testApolloConfigChangeListenerWithInterestedKeyPrefixes_fire() {
public void testApolloConfigChangeListenerWithInterestedKeyPrefixes_fire()
throws InterruptedException {
// default mock, useless here
// just for speed up test without waiting
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, mock(Config.class));
Expand Down Expand Up @@ -946,16 +947,16 @@ private static class TestApolloConfigChangeListenerWithInterestedKeyPrefixesBean

static final String SPECIAL_NAMESPACE = "special-namespace-2021";

private final Queue<ConfigChangeEvent> configChangeEventQueue = new ArrayBlockingQueue<>(100);
private final BlockingQueue<ConfigChangeEvent> configChangeEventQueue = new ArrayBlockingQueue<>(100);

@ApolloConfigChangeListener(value = SPECIAL_NAMESPACE, interestedKeyPrefixes = {"number",
"logging.level"})
private void onChange(ConfigChangeEvent changeEvent) {
this.configChangeEventQueue.add(changeEvent);
}

public ConfigChangeEvent getConfigChangeEvent() {
return this.configChangeEventQueue.poll();
public ConfigChangeEvent getConfigChangeEvent() throws InterruptedException {
return this.configChangeEventQueue.take();
vdiskg marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.ctrip.framework.apollo.portal.spi.oidc.OidcLocalUserServiceImpl;
import com.ctrip.framework.apollo.portal.spi.oidc.OidcLogoutHandler;
import com.ctrip.framework.apollo.portal.spi.oidc.OidcUserInfoHolder;
import com.ctrip.framework.apollo.portal.spi.springsecurity.ApolloPasswordEncoderFactory;
import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserInfoHolder;
import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService;
import com.google.common.collect.Maps;
Expand All @@ -64,7 +65,7 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.ldap.authentication.BindAuthenticator;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
Expand Down Expand Up @@ -241,6 +242,12 @@ public SsoHeartbeatHandler defaultSsoHeartbeatHandler() {
return new DefaultSsoHeartbeatHandler();
}

@Bean
@ConditionalOnMissingBean(PasswordEncoder.class)
public static PasswordEncoder passwordEncoder() {
return ApolloPasswordEncoderFactory.createDelegatingPasswordEncoder();
}

@Bean
@ConditionalOnMissingBean(UserInfoHolder.class)
public UserInfoHolder springSecurityUserInfoHolder(UserService userService) {
Expand All @@ -254,10 +261,10 @@ public LogoutHandler logoutHandler() {
}

@Bean
public JdbcUserDetailsManager jdbcUserDetailsManager(AuthenticationManagerBuilder auth,
DataSource datasource) throws Exception {
public static JdbcUserDetailsManager jdbcUserDetailsManager(PasswordEncoder passwordEncoder,
AuthenticationManagerBuilder auth, DataSource datasource) throws Exception {
JdbcUserDetailsManager jdbcUserDetailsManager = auth.jdbcAuthentication()
.passwordEncoder(new BCryptPasswordEncoder()).dataSource(datasource)
.passwordEncoder(passwordEncoder).dataSource(datasource)
.usersByUsernameQuery("select Username,Password,Enabled from `Users` where Username = ?")
.authoritiesByUsernameQuery(
"select Username,Authority from `Authorities` where Username = ?")
Expand All @@ -281,8 +288,10 @@ public JdbcUserDetailsManager jdbcUserDetailsManager(AuthenticationManagerBuilde

@Bean
@ConditionalOnMissingBean(UserService.class)
public UserService springSecurityUserService() {
return new SpringSecurityUserService();
public UserService springSecurityUserService(PasswordEncoder passwordEncoder,
JdbcUserDetailsManager userDetailsManager,
UserRepository userRepository) {
return new SpringSecurityUserService(passwordEncoder, userDetailsManager, userRepository);
}

}
Expand Down Expand Up @@ -471,11 +480,18 @@ public LogoutHandler oidcLogoutHandler() {
return new OidcLogoutHandler();
}

@Bean
@ConditionalOnMissingBean(PasswordEncoder.class)
public PasswordEncoder passwordEncoder() {
return SpringSecurityAuthAutoConfiguration.passwordEncoder();
}

@Bean
@ConditionalOnMissingBean(JdbcUserDetailsManager.class)
public JdbcUserDetailsManager jdbcUserDetailsManager(AuthenticationManagerBuilder auth,
DataSource datasource) throws Exception {
return new SpringSecurityAuthAutoConfiguration().jdbcUserDetailsManager(auth, datasource);
public JdbcUserDetailsManager jdbcUserDetailsManager(PasswordEncoder passwordEncoder,
AuthenticationManagerBuilder auth, DataSource datasource) throws Exception {
return SpringSecurityAuthAutoConfiguration
.jdbcUserDetailsManager(passwordEncoder, auth, datasource);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
Expand All @@ -43,6 +45,10 @@ public class OidcLocalUserServiceImpl implements OidcLocalUserService {
private final Collection<? extends GrantedAuthority> authorities = Collections
.singletonList(new SimpleGrantedAuthority("ROLE_USER"));

private final PasswordEncoder placeholderDelegatingPasswordEncoder = new DelegatingPasswordEncoder(
PlaceholderPasswordEncoder.ENCODING_ID, Collections
.singletonMap(PlaceholderPasswordEncoder.ENCODING_ID, new PlaceholderPasswordEncoder()));

private final JdbcUserDetailsManager userDetailsManager;

private final UserRepository userRepository;
Expand All @@ -58,20 +64,11 @@ public OidcLocalUserServiceImpl(
@Override
public void createLocalUser(UserInfo newUserInfo) {
UserDetails user = new User(newUserInfo.getUserId(),
"{nonsensical}" + this.nonsensicalPassword(), authorities);
this.placeholderDelegatingPasswordEncoder.encode(""), authorities);
userDetailsManager.createUser(user);
this.updateUserInfoInternal(newUserInfo);
}

/**
* generate a random password with no meaning
*/
private String nonsensicalPassword() {
byte[] bytes = new byte[32];
ThreadLocalRandom.current().nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes);
}

private void updateUserInfoInternal(UserInfo newUserInfo) {
UserPO managedUser = userRepository.findByUsername(newUserInfo.getUserId());
if (!StringUtils.isBlank(newUserInfo.getEmail())) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2021 Apollo Authors
*
* 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 com.ctrip.framework.apollo.portal.spi.oidc;

import java.util.Base64;
import java.util.concurrent.ThreadLocalRandom;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @author vdisk <[email protected]>
*/
public class PlaceholderPasswordEncoder implements PasswordEncoder {

public static final String ENCODING_ID = "placeholder";

/**
* generate a random string as a password placeholder.
*/
@Override
public String encode(CharSequence rawPassword) {
byte[] bytes = new byte[32];
ThreadLocalRandom.current().nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes);
}

/**
* placeholder will never matches a password
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2021 Apollo Authors
*
* 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 com.ctrip.framework.apollo.portal.spi.springsecurity;

import com.ctrip.framework.apollo.portal.spi.oidc.PlaceholderPasswordEncoder;
import java.util.HashMap;
import java.util.Map;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;

/**
* @author vdisk <[email protected]>
*/
public final class ApolloPasswordEncoderFactory {

private ApolloPasswordEncoderFactory() {
}

/**
* Creates a {@link DelegatingPasswordEncoder} with default mappings {@link
* PasswordEncoderFactories#createDelegatingPasswordEncoder()}, and add a placeholder encoder for
* oidc {@link PlaceholderPasswordEncoder}
*
* @return the {@link PasswordEncoder} to use
*/
@SuppressWarnings("deprecation")
public static PasswordEncoder createDelegatingPasswordEncoder() {
// copy from PasswordEncoderFactories, and it's should follow the upgrade of the PasswordEncoderFactories
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop",
org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders
.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());

// placeholder encoder for oidc
encoders.put(PlaceholderPasswordEncoder.ENCODING_ID, new PlaceholderPasswordEncoder());
DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(encodingId,
encoders);

// todo: adapt the old password, and it should be removed in the next feature version of the 1.9.x
delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(new PasswordEncoderAdapter(encoders.get(encodingId)));
vdiskg marked this conversation as resolved.
Show resolved Hide resolved
return delegatingPasswordEncoder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2021 Apollo Authors
*
* 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 com.ctrip.framework.apollo.portal.spi.springsecurity;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.StringUtils;

/**
* @author vdisk <[email protected]>
*/
@Deprecated
public class PasswordEncoderAdapter implements PasswordEncoder {

private static final String PREFIX = "{";

private static final String SUFFIX = "}";

private final PasswordEncoder encoder;

public PasswordEncoderAdapter(
PasswordEncoder encoder) {
this.encoder = encoder;
}

@Override
public String encode(CharSequence rawPassword) {
throw new UnsupportedOperationException("encode is not supported");
}

@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
boolean matches = this.encoder.matches(rawPassword, encodedPassword);
if (matches) {
return true;
}
String id = this.extractId(encodedPassword);
if (StringUtils.hasText(id)) {
throw new IllegalArgumentException(
"There is no PasswordEncoder mapped for the id \"" + id + "\"");
}
return false;
}

private String extractId(String prefixEncodedPassword) {
if (prefixEncodedPassword == null) {
return null;
}
int start = prefixEncodedPassword.indexOf(PREFIX);
if (start != 0) {
return null;
}
int end = prefixEncodedPassword.indexOf(SUFFIX, start);
if (end < 0) {
return null;
}
return prefixEncodedPassword.substring(start + 1, end);
}

}
Loading