Skip to content

Commit

Permalink
feature: add the delegating password encoder for apollo-portal simple…
Browse files Browse the repository at this point in the history
… auth (#3804)

* DelegatingPasswordEncoder

* sql

* extend Password to 512

* update CHANGES.md

* add an adapter for old password

* fix the unit test NullPointerException

* only throws Exception on password has an id

* mark add prefix for `Users`.`Password` optional

* modify unit test

* remove {bcrypt} prefix on sql
  • Loading branch information
vdiskg authored Jul 10, 2021
1 parent 907dbad commit 72a0498
Show file tree
Hide file tree
Showing 13 changed files with 283 additions and 52 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Apollo 1.9.0
* [feature: modify item comment valid size](https://github.com/ctripcorp/apollo/pull/3803)
* [set default session store-type](https://github.com/ctripcorp/apollo/pull/3812)
* [speed up the stale issue mark and close phase](https://github.com/ctripcorp/apollo/pull/3808)
* [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.poll(5, TimeUnit.SECONDS);
}
}

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)));
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

0 comments on commit 72a0498

Please sign in to comment.