How to config Google oauth2 in jmix 2.3.1

Am getting “authentication is null” error

package com.company.projectmgt.api.services;

import io.jmix.rest.annotation.RestMethod;
import io.jmix.rest.annotation.RestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.Map;

@Service
@RestService("google")
public class GoogleAuthService {

    private static final Logger log = LoggerFactory.getLogger(GoogleAuthService.class);

    private final OAuth2AuthorizedClientService authorizedClientService;
@Autowired
    public GoogleAuthService(OAuth2AuthorizedClientService authorizedClientService) {
        this.authorizedClientService = authorizedClientService;
    }


    @RestMethod
    public Map<String, Object> getUserInfo(OAuth2AuthenticationToken authentication) {
        if (authentication == null) {
            log.error("Authentication token is null. OAuth2 authentication may not have completed successfully.");
            return Collections.singletonMap("error", "Authentication failed: No valid authentication token");
        }

        Object principal = authentication.getPrincipal();
        if (!(principal instanceof DefaultOidcUser)) {
            log.error("Unexpected principal type: {}", principal != null ? principal.getClass().getName() : "null");
            return Collections.singletonMap("error", "Unexpected authentication type");
        }

        DefaultOidcUser oidcUser = (DefaultOidcUser) principal;
        return oidcUser.getClaims();
    }
}

I would appreciate the support.

regards
mike

Hello Michael,

Please provide more details about your scenario.
What do you want to achive, what requests do you send?
Sample project also will be good.

Regards,
Ivan

Hi @i.gavrilov,

I’m having trouble implementing Google OAuth2 authentication for sign-up and sign-in in my Jmix project, coming from a Spring Boot background where it was easier to implement. Despite trying the OpenID Connect add-on, I haven’t been able to get it working. My goal is to allow users to register and log in to my application using their Google accounts, and I’m looking for guidance on how to achieve this in Jmix.

Regards,
Michael

Hello Michael,

Can you use Keycloak?
If so you can implement social login on Keycloak side (which should be easier) and then make your Jmix application use Keycloak as IDP via OIDC add-on.

Regards,
Ivan

If you need it implemented within Jmix application then you have to make changes according to the following example.

Setup Google OAuth client ID

https://support.google.com/cloud/answer/6158849?hl=en
Use http://localhost:8080/login/oauth2/code/google as Authorized redirect URI.

Dependency

Add implementation 'org.springframework.security:spring-security-oauth2-client' dependency.

Modify your User entity

Add field for Google ID

@Column(name = "GOOGLE_ID")
private String googleId;

Implement OidcUser and add Transient properties

@Transient
private OidcUserInfo userInfo;

@Transient
private OidcIdToken idToken;

@Transient
private Map<String, Object> attributes;

@Transient
private Map<String, Object> claims;

User mapping

Add service to load\store user based on Google ID

@Component
public class OAuth2UserPersistence {

    @Autowired
    private DataManager dataManager;

    @Authenticated
    public User loadUserByGoogleId(String googleId) {
        return dataManager.load(User.class)
                .query("select u from User u where u.googleId = :googleId")
                .parameter("googleId", googleId)
                .optional()
                .orElseGet(() -> {
                    User user = dataManager.create(User.class);
                    user.setGoogleId(googleId);
                    return user;
                });
    }

    @Authenticated
    public User saveUser(User user) {
        return dataManager.save(user);
    }
}

Create/modify your security config to map external user to Jmix user

@EnableWebSecurity
@Configuration
public class OAuth2SecurityConfiguration extends FlowuiVaadinWebSecurity {

    private static final Logger log = LoggerFactory.getLogger(OAuth2SecurityConfiguration.class);

    private final RoleGrantedAuthorityUtils authorityUtils;
    private final OAuth2UserPersistence oidcUserPersistence;

    public OAuth2SecurityConfiguration(RoleGrantedAuthorityUtils authorityUtils,
                                       OAuth2UserPersistence oidcUserPersistence) {
        this.authorityUtils = authorityUtils;
        this.oidcUserPersistence = oidcUserPersistence;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);

        http.oauth2Login(configurer ->
                configurer
                        .loginPage(getLoginPath())
                        .userInfoEndpoint(userInfoEndpointConfig ->
                                userInfoEndpointConfig
                                        .oidcUserService(oidcUserService()))
                        .successHandler(this::onAuthenticationSuccess)
        );
    }

    private void onAuthenticationSuccess(HttpServletRequest request,
                                         HttpServletResponse response,
                                         Authentication authentication) throws IOException {
        // redirect to the main screen after successful authentication using auth provider
        new DefaultRedirectStrategy().sendRedirect(request, response, "/");
    }

    /**
     * Service responsible for loading OIDC users (Google uses OIDC protocol)
     */
    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        OidcUserService delegate = new OidcUserService();
        return (userRequest) -> {
            // Delegate to the default implementation for loading a user
            OidcUser oidcUser = delegate.loadUser(userRequest);

            //find or create user with given Google id
            String googleId = oidcUser.getSubject();

            User jmixUser = oidcUserPersistence.loadUserByGoogleId(googleId);
            jmixUser.setUsername(googleId);
            jmixUser.setGoogleId(googleId);
            jmixUser.setEmail(oidcUser.getEmail());

            User savedJmixUser = oidcUserPersistence.saveUser(jmixUser);
            savedJmixUser.setAuthorities(getDefaultGrantedAuthorities());
            return savedJmixUser;
        };

    }

    /**
     * Builds granted authority list that grants access to the FullAccess role
     */
    private Collection<GrantedAuthority> getDefaultGrantedAuthorities() {
        return List.of(authorityUtils.createResourceRoleGrantedAuthority(FullAccessRole.CODE));
    }
}

UI

Add button to your login screen and in click\action handler execute
UI.getCurrent().getPage().setLocation("/oauth2/authorization/google");

Properties

Fill application properties

spring.security.oauth2.client.registration.google.client-id = <client id>
spring.security.oauth2.client.registration.google.client-secret = <client secret>

Regards,
Ivan

Hello @i.gavrilov
am not using jmix user, instead i have created a different user called member.

regards
micahel kamau

Hello,

If your User entity class has different name - use it in provided code instead of original User.

If it’s not the case - please provide sample project working without Social Login.

Regards,
Ivan