From 410ef5e0594e95bb19953c96f7bcc60686a9fd5f Mon Sep 17 00:00:00 2001 From: Jean Aurambault Date: Fri, 13 Sep 2024 00:56:01 -0700 Subject: [PATCH] Support user creation for Oicd authentication This is similar to UserDetailImplOAuth2UserService though in that case it is simpler and just reuses the parent class to get an OicdUser and then convert it into a UserDetailsImpl. Add a username-from-email config. In case the principal ID form the auth provider is a UUID, it is of little use on the Mojito side for tasks such as sending notifications, associating with Git commit, PRs, etc. With this option, the username is extracted from the email. This assumes a 1:1 mapping between the email and the Unix username Considered saving the email in the user, but for simplicity we just need a username that is unix like for now. Note: UserDetailImplOAuth2UserService probably duplicated the code to support the "unwrapping" of the user attribute. The logic was probably failing before being able to do the conversion --- .../mojito/security/OidcUserDetailsImpl.java | 42 ++++++++++++++++ .../l10n/mojito/security/SecurityConfig.java | 19 +++++++ .../UserDetailImplOidcUserService.java | 50 +++++++++++++++++++ .../mojito/security/WebSecurityConfig.java | 3 ++ 4 files changed, 114 insertions(+) create mode 100644 webapp/src/main/java/com/box/l10n/mojito/security/OidcUserDetailsImpl.java create mode 100644 webapp/src/main/java/com/box/l10n/mojito/security/UserDetailImplOidcUserService.java diff --git a/webapp/src/main/java/com/box/l10n/mojito/security/OidcUserDetailsImpl.java b/webapp/src/main/java/com/box/l10n/mojito/security/OidcUserDetailsImpl.java new file mode 100644 index 0000000000..87f14d247b --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/security/OidcUserDetailsImpl.java @@ -0,0 +1,42 @@ +package com.box.l10n.mojito.security; + +import com.box.l10n.mojito.entity.security.user.User; +import java.util.Map; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +public class OidcUserDetailsImpl extends UserDetailsImpl implements OidcUser { + + private final OidcUser oidcUser; + + public OidcUserDetailsImpl(User user, OidcUser oidcUser) { + super(user); + this.oidcUser = oidcUser; + } + + @Override + public Map getClaims() { + return oidcUser.getClaims(); + } + + @Override + public OidcUserInfo getUserInfo() { + return oidcUser.getUserInfo(); + } + + @Override + public OidcIdToken getIdToken() { + return oidcUser.getIdToken(); + } + + @Override + public Map getAttributes() { + return oidcUser.getAttributes(); + } + + @Override + public String getName() { + return oidcUser.getName(); + } +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/security/SecurityConfig.java b/webapp/src/main/java/com/box/l10n/mojito/security/SecurityConfig.java index 8863f92e22..db3388374a 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/security/SecurityConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/security/SecurityConfig.java @@ -72,6 +72,17 @@ public static class OAuth2 { String unwrapUserAttributes; + /** + * In case the principal ID form the auth provider is a UUID, it is of little use on the Mojito + * side for tasks such as sending notifications, associating with Git commit, PRs, etc. + * + *

With this option, the username is extracted from the email. This assumes a 1:1 mapping + * between the email and the Unix username + * + *

Currently only implement in {@link UserDetailImplOidcUserService} + */ + boolean usernameFromEmail = false; + String givenNameAttribute = "first_name"; String surnameAttribute = "last_name"; String commonNameAttribute = "name"; @@ -115,5 +126,13 @@ public String getUnwrapUserAttributes() { public void setUnwrapUserAttributes(String unwrapUserAttributes) { this.unwrapUserAttributes = unwrapUserAttributes; } + + public boolean isUsernameFromEmail() { + return usernameFromEmail; + } + + public void setUsernameFromEmail(boolean usernameFromEmail) { + this.usernameFromEmail = usernameFromEmail; + } } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/security/UserDetailImplOidcUserService.java b/webapp/src/main/java/com/box/l10n/mojito/security/UserDetailImplOidcUserService.java new file mode 100644 index 0000000000..54665fcf77 --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/security/UserDetailImplOidcUserService.java @@ -0,0 +1,50 @@ +package com.box.l10n.mojito.security; + +import com.box.l10n.mojito.entity.security.user.User; +import com.box.l10n.mojito.service.security.user.UserService; +import java.util.Objects; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +class UserDetailImplOidcUserService extends OidcUserService { + + SecurityConfig securityConfig; + UserService userService; + + public UserDetailImplOidcUserService(SecurityConfig securityConfig, UserService userService) { + this.securityConfig = Objects.requireNonNull(securityConfig); + this.userService = Objects.requireNonNull(userService); + } + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + OidcUser oidcUser = super.loadUser(userRequest); + + SecurityConfig.OAuth2 securityConfigOAuth2 = + securityConfig + .getoAuth2() + .getOrDefault( + userRequest.getClientRegistration().getRegistrationId(), + new SecurityConfig.OAuth2()); + + String username; + if (securityConfigOAuth2.usernameFromEmail) { + String email = oidcUser.getEmail(); + if (email == null) { + throw new RuntimeException( + "OidcUser's email must not be null since it used to extract username"); + } + username = email.split("@")[0]; + } else { + username = oidcUser.getName(); + } + + User user = + userService.getOrCreateOrUpdateBasicUser( + username, oidcUser.getGivenName(), oidcUser.getFamilyName(), oidcUser.getFullName()); + + return new OidcUserDetailsImpl(user, oidcUser); + } +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/security/WebSecurityConfig.java b/webapp/src/main/java/com/box/l10n/mojito/security/WebSecurityConfig.java index 1c05bbf271..9f0b1c3f2f 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/security/WebSecurityConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/security/WebSecurityConfig.java @@ -268,6 +268,9 @@ public SecurityFilterChain configure(HttpSecurity http) throws Exception { userInfoEndpoint -> { userInfoEndpoint.userService( new UserDetailImplOAuth2UserService(securityConfig, userService)); + + userInfoEndpoint.oidcUserService( + new UserDetailImplOidcUserService(securityConfig, userService)); }); }); break;