-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support binding OAuth2 user automatically (#6702)
#### What type of PR is this? /kind improvement /area core /milestone 2.20.x #### What this PR does / why we need it: This PR add support for binding OAuth2 user automatically. So we can remove the user-binding page. Please note that those changes may break the OAuth2 and SocialLogin plugins. #### Special notes for your reviewer: Build OAuth2 plugin from <halo-sigs/plugin-oauth2#64> or use [plugin-oauth2-1.0.4-SNAPSHOT.zip](https://github.com/user-attachments/files/17177592/plugin-oauth2-1.0.4-SNAPSHOT.zip) I built. - Bind after logging in 1. Log in Halo with username and password method 2. Try to unbind OAuth2 user 3. Bind OAuth2 user again - Initially bind without logging in 1. Go to login page 2. Log in with OAuth2 method and you will be redirected to login page 3. Log in with username and password method 4. See the result of binding - Log in with OAuth2 method after binding 1. Go to login page 2. Log in with OAuth2 method and you will be redirected to uc page directly #### Does this PR introduce a user-facing change? ```release-note 支持自动绑定 OAuth2 登录用户 ```
- Loading branch information
Showing
39 changed files
with
936 additions
and
136 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
api/src/main/java/run/halo/app/security/HttpBasicSecurityWebFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package run.halo.app.security; | ||
|
||
import org.pf4j.ExtensionPoint; | ||
import org.springframework.web.server.WebFilter; | ||
|
||
/** | ||
* Security web filter for HTTP basic. | ||
* | ||
* @author johnniang | ||
* @since 2.20.0 | ||
*/ | ||
public interface HttpBasicSecurityWebFilter extends WebFilter, ExtensionPoint { | ||
|
||
} |
14 changes: 14 additions & 0 deletions
14
api/src/main/java/run/halo/app/security/OAuth2AuthorizationCodeSecurityWebFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package run.halo.app.security; | ||
|
||
import org.pf4j.ExtensionPoint; | ||
import org.springframework.web.server.WebFilter; | ||
|
||
/** | ||
* Security web filter for OAuth2 authorization code. | ||
* | ||
* @author johnniang | ||
* @since 2.20.0 | ||
*/ | ||
public interface OAuth2AuthorizationCodeSecurityWebFilter extends WebFilter, ExtensionPoint { | ||
|
||
} |
96 changes: 96 additions & 0 deletions
96
.../main/java/run/halo/app/security/authentication/oauth2/HaloOAuth2AuthenticationToken.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package run.halo.app.security.authentication.oauth2; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import lombok.Getter; | ||
import org.springframework.security.authentication.AbstractAuthenticationToken; | ||
import org.springframework.security.core.GrantedAuthority; | ||
import org.springframework.security.core.userdetails.UserDetails; | ||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; | ||
import org.springframework.security.oauth2.core.user.OAuth2User; | ||
|
||
/** | ||
* Halo OAuth2 authentication token which combines {@link UserDetails} and original | ||
* {@link OAuth2AuthenticationToken}. | ||
* | ||
* @author johnniang | ||
* @since 2.20.0 | ||
*/ | ||
public class HaloOAuth2AuthenticationToken extends AbstractAuthenticationToken { | ||
|
||
@Getter | ||
private final UserDetails userDetails; | ||
|
||
@Getter | ||
private final OAuth2AuthenticationToken original; | ||
|
||
/** | ||
* Constructs an {@code HaloOAuth2AuthenticationToken} using {@link UserDetails} and original | ||
* {@link OAuth2AuthenticationToken}. | ||
* | ||
* @param userDetails the {@link UserDetails} | ||
* @param original the original {@link OAuth2AuthenticationToken} | ||
*/ | ||
public HaloOAuth2AuthenticationToken(UserDetails userDetails, | ||
OAuth2AuthenticationToken original) { | ||
super(combineAuthorities(userDetails, original)); | ||
this.userDetails = userDetails; | ||
this.original = original; | ||
setAuthenticated(true); | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return userDetails.getUsername(); | ||
} | ||
|
||
@Override | ||
public Collection<GrantedAuthority> getAuthorities() { | ||
var originalAuthorities = super.getAuthorities(); | ||
var userDetailsAuthorities = getUserDetails().getAuthorities(); | ||
var authorities = new ArrayList<GrantedAuthority>( | ||
originalAuthorities.size() + userDetailsAuthorities.size() | ||
); | ||
authorities.addAll(originalAuthorities); | ||
authorities.addAll(userDetailsAuthorities); | ||
return Collections.unmodifiableList(authorities); | ||
} | ||
|
||
@Override | ||
public Object getCredentials() { | ||
return ""; | ||
} | ||
|
||
@Override | ||
public OAuth2User getPrincipal() { | ||
return original.getPrincipal(); | ||
} | ||
|
||
/** | ||
* Creates an authenticated {@link HaloOAuth2AuthenticationToken} using {@link UserDetails} and | ||
* original {@link OAuth2AuthenticationToken}. | ||
* | ||
* @param userDetails the {@link UserDetails} | ||
* @param original the original {@link OAuth2AuthenticationToken} | ||
* @return an authenticated {@link HaloOAuth2AuthenticationToken} | ||
*/ | ||
public static HaloOAuth2AuthenticationToken authenticated( | ||
UserDetails userDetails, OAuth2AuthenticationToken original | ||
) { | ||
return new HaloOAuth2AuthenticationToken(userDetails, original); | ||
} | ||
|
||
private static Collection<? extends GrantedAuthority> combineAuthorities( | ||
UserDetails userDetails, OAuth2AuthenticationToken original) { | ||
var userDetailsAuthorities = userDetails.getAuthorities(); | ||
var originalAuthorities = original.getAuthorities(); | ||
var authorities = new ArrayList<GrantedAuthority>( | ||
originalAuthorities.size() + userDetailsAuthorities.size() | ||
); | ||
authorities.addAll(originalAuthorities); | ||
authorities.addAll(userDetailsAuthorities); | ||
return Collections.unmodifiableList(authorities); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
application/src/main/java/run/halo/app/core/user/service/UserConnectionService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package run.halo.app.core.user.service; | ||
|
||
import org.springframework.security.oauth2.core.user.OAuth2User; | ||
import reactor.core.publisher.Mono; | ||
import run.halo.app.core.extension.UserConnection; | ||
|
||
public interface UserConnectionService { | ||
|
||
/** | ||
* Create user connection. | ||
* | ||
* @param username Username | ||
* @param registrationId Registration id | ||
* @param oauth2User OAuth2 user | ||
* @return Created user connection | ||
*/ | ||
Mono<UserConnection> createUserConnection( | ||
String username, | ||
String registrationId, | ||
OAuth2User oauth2User | ||
); | ||
|
||
/** | ||
* Update the user connection if present. | ||
* If found, update updatedAt timestamp of the user connection. | ||
* | ||
* @param registrationId Registration id | ||
* @param oauth2User OAuth2 user | ||
* @return Updated user connection or empty | ||
*/ | ||
Mono<UserConnection> updateUserConnectionIfPresent( | ||
String registrationId, OAuth2User oauth2User | ||
); | ||
|
||
} |
103 changes: 103 additions & 0 deletions
103
application/src/main/java/run/halo/app/core/user/service/impl/UserConnectionServiceImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package run.halo.app.core.user.service.impl; | ||
|
||
import static run.halo.app.extension.ExtensionUtil.defaultSort; | ||
import static run.halo.app.extension.index.query.QueryFactory.and; | ||
import static run.halo.app.extension.index.query.QueryFactory.equal; | ||
|
||
import java.time.Clock; | ||
import java.util.HashMap; | ||
import java.util.Optional; | ||
import org.springframework.security.oauth2.core.user.OAuth2User; | ||
import org.springframework.stereotype.Service; | ||
import reactor.core.publisher.Mono; | ||
import run.halo.app.core.extension.UserConnection; | ||
import run.halo.app.core.extension.UserConnection.UserConnectionSpec; | ||
import run.halo.app.core.user.service.UserConnectionService; | ||
import run.halo.app.extension.ListOptions; | ||
import run.halo.app.extension.Metadata; | ||
import run.halo.app.extension.MetadataOperator; | ||
import run.halo.app.extension.ReactiveExtensionClient; | ||
import run.halo.app.infra.exception.OAuth2UserAlreadyBoundException; | ||
import run.halo.app.infra.utils.JsonUtils; | ||
|
||
@Service | ||
public class UserConnectionServiceImpl implements UserConnectionService { | ||
|
||
private final ReactiveExtensionClient client; | ||
|
||
private Clock clock = Clock.systemDefaultZone(); | ||
|
||
public UserConnectionServiceImpl(ReactiveExtensionClient client) { | ||
this.client = client; | ||
} | ||
|
||
void setClock(Clock clock) { | ||
this.clock = clock; | ||
} | ||
|
||
@Override | ||
public Mono<UserConnection> createUserConnection( | ||
String username, | ||
String registrationId, | ||
OAuth2User oauth2User | ||
) { | ||
return getUserConnection(registrationId, username) | ||
.flatMap(connection -> Mono.<UserConnection>error( | ||
() -> new OAuth2UserAlreadyBoundException(connection)) | ||
) | ||
.switchIfEmpty(Mono.defer(() -> { | ||
var connection = new UserConnection(); | ||
connection.setMetadata(new Metadata()); | ||
var metadata = connection.getMetadata(); | ||
updateUserInfo(metadata, oauth2User); | ||
metadata.setGenerateName(username + "-"); | ||
connection.setSpec(new UserConnectionSpec()); | ||
var spec = connection.getSpec(); | ||
spec.setUsername(username); | ||
spec.setProviderUserId(oauth2User.getName()); | ||
spec.setRegistrationId(registrationId); | ||
spec.setUpdatedAt(clock.instant()); | ||
return client.create(connection); | ||
})); | ||
} | ||
|
||
private Mono<UserConnection> updateUserConnection(UserConnection connection, | ||
OAuth2User oauth2User) { | ||
connection.getSpec().setUpdatedAt(clock.instant()); | ||
updateUserInfo(connection.getMetadata(), oauth2User); | ||
return client.update(connection); | ||
} | ||
|
||
private Mono<UserConnection> getUserConnection(String registrationId, String username) { | ||
var listOptions = ListOptions.builder() | ||
.fieldQuery(and( | ||
equal("spec.registrationId", registrationId), | ||
equal("spec.username", username) | ||
)) | ||
.build(); | ||
return client.listAll(UserConnection.class, listOptions, defaultSort()).next(); | ||
} | ||
|
||
@Override | ||
public Mono<UserConnection> updateUserConnectionIfPresent(String registrationId, | ||
OAuth2User oauth2User) { | ||
var listOptions = ListOptions.builder() | ||
.fieldQuery(and( | ||
equal("spec.registrationId", registrationId), | ||
equal("spec.providerUserId", oauth2User.getName()) | ||
)) | ||
.build(); | ||
return client.listAll(UserConnection.class, listOptions, defaultSort()).next() | ||
.flatMap(connection -> updateUserConnection(connection, oauth2User)); | ||
} | ||
|
||
private void updateUserInfo(MetadataOperator metadata, OAuth2User oauth2User) { | ||
var annotations = Optional.ofNullable(metadata.getAnnotations()) | ||
.orElseGet(HashMap::new); | ||
metadata.setAnnotations(annotations); | ||
annotations.put( | ||
"auth.halo.run/oauth2-user-info", | ||
JsonUtils.objectToJson(oauth2User.getAttributes()) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.