Social Login

Jmix offers a comprehensive security subsystem that includes user management features out of the box. This built-in functionality allows developers to implement a variety of authentication methods while also providing the flexibility to integrate custom solutions. Among these options, Jmix supports integration with third-party services, such as LDAP/Active Directory servers and OpenID Connect providers.

One notable feature is the ability to implement Social Login, which enables users to authenticate using their existing accounts from popular platforms like GitHub, Facebook, and Google. This approach not only streamlines the registration process for users but also simplifies user management for developers and administrators.

Most Social Login services operate using widely adopted protocols such as OAuth and OpenID Connect, ensuring secure and efficient authentication. This guide will walk you through the steps to implement Social Login in your Jmix applications.

For applications that need to integrate with standards-compliant services or on-premises software like Keycloak, consider also using the OpenID Connect add-on for your Jmix project.

Requirements

To effectively use this guide, you will need the following:

  1. Setup the development environment.

  2. If you don’t want to follow the guide step-by-step, you can download or clone the completed sample project:

    git clone https://github.com/jmix-framework/jmix-social-login-sample.git

    This will allow you to explore the finished implementation and experiment with the functionality right away.

What We Are Going to Build

This guide outlines the process of enhancing an application created using the standard Full-Stack Application (Java) template to support authentication via Google OpenID Connect and GitHub OAuth, alongside the default database-hosted accounts.

Users will have the flexibility to select their preferred authentication method directly from the login screen.

login page

The implementation will be based on the Spring Security OAuth 2.0 Login feature.

Configure Authentication Providers

To enable authentication with a 3rd party service you will have to register Client ID and Client Secret in that service.

Never expose your Client Secret in client-side code or public repositories.

GitHub OAuth

  1. Go to Developer Settings page of your GitHub profile.

    1. Sign in to your GitHub account.

    2. Click on your profile picture in the upper-right corner and select Settings from the dropdown menu.

    3. In the left sidebar, scroll down to Developer settings and click it.

    4. On the Developer settings page, click on OAuth Apps.

  2. Create a new OAuth App

    1. Click on the New OAuth App button to start configuring a new application.

    2. Fill in the registration form with the following details:

      • Application name: Choose a name that represents your application.

      • Homepage URL: Enter http://localhost:8080 if you are running the app locally, or provide the actual application’s base URL if it’s hosted on a server.

      • Authorization callback URL: Use http://localhost:8080/login/oauth2/code/github if running locally. Replace http://localhost:8080 with the actual application’s base URL if it’s hosted on a server.

    3. Click Register application.

  3. Generate Client Secret

    1. In the newly created app settings, click the Generate a new client secret button.

    2. Copy both the Client ID and Client secret that were generated. These credentials will be required to configure OAuth for your application.

  4. Securely store the Client ID and Client secret in a safe location, as you’ll need them for integrating GitHub OAuth authentication with your application.

Google OpenID

  1. Open Google Cloud Console and sign in with your Google account if you aren’t already logged in.

  2. Create a New Project

    1. In the top navigation bar, click on the Project dropdown.

    2. Select New Project and enter a project name.

    3. Enter a project name.

    4. Click Create to initialize your project.

  3. Enable APIs & Services

    1. With your project selected, go to APIs & Services.

    2. Select OAuth consent screen to begin setting up OAuth for your app.

  4. Configure the OAuth Consent Screen

    1. Choose the External option for the user type and click Create.

    2. Fill in the required fields in the form (App name, Support email, etc.), then click Save and Continue.

  5. Add Scopes

    1. In the Scopes section, click Add or Remove Scopes.

    2. Add the following scopes:

      • …​/auth/userinfo.email

      • …​/auth/userinfo.profile

    3. Click Update to add these scopes, then proceed by clicking Save and Continue.

  6. Complete the OAuth Consent Screen

    1. Review the information on the final page of the consent screen setup.

    2. Click Back to Dashboard to save and complete the consent screen configuration.

  7. Create OAuth Credentials

    1. Go to the Credentials section in the left sidebar.

    2. Click Create Credentials and select OAuth Client ID.

  8. Configure the OAuth Client ID

    1. Choose Web Application as the application type.

    2. In the Authorized redirect URIs section, add the following URI: http://localhost:8080/login/oauth2/code/google. Replace http://localhost:8080 with your app’s actual base URL if it is hosted on a server.

  9. Generate the Client ID and Client Secret

    1. Click Create to finish.

    2. A dialog box will appear displaying the Client ID and Client secret. Copy these credentials as they are essential for configuring OpenID authentication in your application.

Configure Application Project

Specify client ids and secrets in the project’s application.properties file:

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

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

Add Spring Boot OAuth2 client starter to the build.gradle dependencies section:

build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

Extend User Entity

In order to be compatible with Spring Security OAuth2 API, the User entity should implement the org.springframework.security.oauth2.core.oidc.user.OidcUser interface.

Add it to the list of interfaces implemented by User:

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 User implements JmixUserDetails, HasTimeZone, OidcUser {
    // ...
    @Override
    public OidcUserInfo getUserInfo() {
        return null;
    }

    @Override
    public OidcIdToken getIdToken() {
        return null;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return null;
    }

    @Override
    public Map<String, Object> getClaims() {
        return null;
    }

The OidcUser methods are not used in the application, so you can leave their implementations empty.

Configure OAuth2 Login

To customize OAuth 2.0 Login provided by Spring Security, you need to create a security configuration class. It should extend the FlowuiVaadinWebSecurity class defined in Jmix and be annotated with @EnableWebSecurity:

@EnableWebSecurity
@Configuration
public class OAuth2SecurityConfiguration extends FlowuiVaadinWebSecurity {

    @Autowired
    private RoleGrantedAuthorityUtils authorityUtils;
    @Autowired
    private UnconstrainedDataManager dataManager;
    // ...

The UnconstrainedDataManager bean should be used instead of the DataManager to allow the configuration code to access the User entity without any restrictions.

Let’s take a closer look at the different parts of the configuration.

The configure(HttpSecurity http) method is an entry point to the security settings configuration. It defines the OAuth2 login process by setting the login page, handling user data from the authentication provider, and managing the post-login behavior:

@Override
protected void configure(HttpSecurity http) throws Exception {
    super.configure(http);
    http.oauth2Login(configurer ->
            configurer
                    .loginPage(getLoginPath())
                    .userInfoEndpoint(userInfoEndpointConfig ->
                            userInfoEndpointConfig
                                    .userService(oauth2UserService())
                                    .oidcUserService(oidcUserService()))
                    .successHandler(this::onAuthenticationSuccess)
    );
}

The onAuthenticationSuccess() method handles successful authentication. It redirects users to the main view after successful login:

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

The oauth2UserService() and oidcUserService() methods are responsible for mapping user information returned by the authentication service to your application’s User entity.

The oidcUserService() method handles GitHub users:

// Returns a method that loads GitHub users
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
    DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
    return (userRequest) -> {
        // Delegate to the default implementation to load an external user
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        // Find or create a user with username corresponding to the GitHub ID
        Integer githubId = oAuth2User.getAttribute("id");
        User jmixUser = loadUserByUsername("github:" + githubId);

        // Update the user with information from GitHub
        jmixUser.setEmail(oAuth2User.getAttribute("email"));
        String nameAttr = oAuth2User.getAttribute("name");
        if (nameAttr != null) {
            int idx = nameAttr.indexOf(" ");
            if (idx > 0) {
                jmixUser.setFirstName(nameAttr.substring(0, idx));
                jmixUser.setLastName(nameAttr.substring(idx + 1));
            } else {
                jmixUser.setLastName(nameAttr);
            }
        }

        // Save the user to the database and assign roles
        User savedJmixUser = dataManager.save(jmixUser);
        savedJmixUser.setAuthorities(getDefaultGrantedAuthorities());
        return savedJmixUser;
    };
}

The oidcUserService() method handles Google users:

// Returns a method that loads Google users
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
    OidcUserService delegate = new OidcUserService();
    return (userRequest) -> {
        // Delegate to the default implementation to load an external user
        OidcUser oidcUser = delegate.loadUser(userRequest);

        // Find or create a user with username corresponding to the Google ID
        String googleId = oidcUser.getSubject();
        User jmixUser = loadUserByUsername("google:" + googleId);

        // Update the user with information from Google
        jmixUser.setEmail(oidcUser.getEmail());
        jmixUser.setFirstName(oidcUser.getAttribute("given_name"));
        jmixUser.setLastName(oidcUser.getAttribute("family_name"));

        // Update the user to the database and assign roles
        User savedJmixUser = dataManager.save(jmixUser);
        savedJmixUser.setAuthorities(getDefaultGrantedAuthorities());
        return savedJmixUser;
    };
}

Both methods depend on the loadUserByUsername() function that loads a user by username from the database or creates a new user if the user does not exist:

// Loads user by username or creates a new user
private User loadUserByUsername(String username) {
    return dataManager.load(User.class)
            .query("e.username = ?1", username)
            .optional()
            .orElseGet(() -> {
                User user = dataManager.create(User.class);
                user.setUsername(username);
                return user;
            });
}

The username attribute of the User entity will store identifiers returned by the authentication services.

The getDefaultGrantedAuthorities() method creates a list of authorities to be assigned to the authenticated user. For demonstration purposes, the getDefaultGrantedAuthorities() method assigns full access rights. In a real-world application, however, it is essential to assign more limited privileges to new users. At minimum, new registrations should be assigned a ui-minimal role, as well as user-specific roles that provide access to relevant business entities, attributes, views, and menu items.

// Builds granted authority list to assign default roles to the user
private Collection<GrantedAuthority> getDefaultGrantedAuthorities() {
    return List.of(
            authorityUtils.createResourceRoleGrantedAuthority(FullAccessRole.CODE)
    );
}

Modify Login View

Open login-view.xml descriptor and add login buttons below the loginForm element:

<h3 text="msg://otherLogin.text" classNames="other-login-header"/>

<vbox classNames="login-wrapper">
    <button id="googleBtn"
            text="Sign in with Google"
            width="100%"
            icon="app-icons:google"
            disableOnClick="true"
            classNames="google-style, other-login-button"/>
    <button id="githubBtn"
            text="Sign in with GitHub"
            width="100%"
            icon="app-icons:github"
            disableOnClick="true"
            classNames="github-style, other-login-button"/>
</vbox>

The button click handlers should redirect to URLs corresponding to the authentication providers:

@Subscribe(id = "googleBtn", subject = "clickListener")
public void onGoogleBtnClick(final ClickEvent<JmixButton> event) {
    UI.getCurrent().getPage().setLocation("/oauth2/authorization/google");
}

@Subscribe(id = "githubBtn", subject = "clickListener")
public void onGithubBtnClick(final ClickEvent<JmixButton> event) {
    UI.getCurrent().getPage().setLocation("/oauth2/authorization/github");
}

Add some styling to /frontend/themes/sample-social-login/sample-social-login.css file to have nice looking social login buttons:

.login-wrapper {
    padding: var(--lumo-space-l);
    max-width: calc(var(--lumo-size-m) * 10);
    background: var(--lumo-base-color) linear-gradient(var(--lumo-tint-5pct), var(--lumo-tint-5pct));
}

.other-login-header {
    color: var(--lumo-contrast-80pct);
    width: calc(var(--lumo-size-m) * 10);
    overflow: hidden;
    text-align: center;
}

.other-login-header::before {
    content: '';
    display: inline-block;
    border-bottom: 1px solid;
    color: var(--lumo-contrast-40pct);
    width: 50%;
    margin: 0 0.5em 0 -55%;
    vertical-align: middle;
}

.other-login-header::after {
    content: '';
    display: inline-block;
    border-bottom: 1px solid;
    color: var(--lumo-contrast-40pct);
    margin: 0 -55% 0 0.5em;
    vertical-align: middle;
    width: 50%;
}

.other-login-button {
    color: var(--lumo-contrast-80pct);
}

.google-style vaadin-icon {
    color: #4285F4;
    width: var(--lumo-icon-size-s);
    height: var(--lumo-icon-size-s);
}

.github-style vaadin-icon {
    color: 	black;
    width: var(--lumo-icon-size-s);
    height: var(--lumo-icon-size-s);
}

This must be enough to test the implemented authentication methods. Run the application and open http://localhost:8080 in the incognito browser tab.

Summary

In this tutorial, you have learned how to implement Social Login in your Jmix application.

You have seen how to configure Google and GitHub as OAuth2 providers, and how to employ Spring Security OAuth2 Login feature.