Web Application
Java (Spring Boot)

MojoAuth Hosted Login Page with Java (Spring Boot)

Introduction

Spring Boot is a popular framework that simplifies the development of Java applications, particularly web applications and microservices. It provides a set of pre-configured components and auto-configuration capabilities that make it easy to get started with minimal setup.

This guide demonstrates how to integrate MojoAuth's Hosted Login Page with a Spring Boot application using the Spring Security OAuth2 Client library, which provides robust support for OpenID Connect (OIDC) authentication.

Links:

Prerequisites

Before you begin, make sure you have:

  1. A MojoAuth account with an OIDC application configured
  2. Your OIDC Client ID, Client Secret, and Issuer URL (usually https://your-project.auth.mojoauth.com (opens in a new tab))
  3. Java Development Kit (JDK) 17+ installed
  4. Maven or Gradle build tool installed
  5. Basic knowledge of Java and Spring Boot

Project Setup

Let's start by setting up a new Spring Boot project:

Using Spring Initializr

The easiest way to create a Spring Boot project is to use Spring Initializr (opens in a new tab):

  1. Visit https://start.spring.io/ (opens in a new tab)
  2. Choose the following options:
    • Project: Maven
    • Language: Java
    • Spring Boot: 3.1.x (or the latest stable version)
    • Group: com.example
    • Artifact: mojoauth-spring-demo
    • Description: MojoAuth Spring Boot Demo
    • Packaging: Jar
    • Java: 17
  3. Add dependencies:
    • Spring Web
    • Thymeleaf
    • Spring Security
    • OAuth2 Client
    • Spring Boot DevTools
  4. Click "Generate" to download the project ZIP file
  5. Extract the ZIP file and open it in your favorite IDE

Manual Setup

Alternatively, you can set up the project manually:

  1. Create a new project directory:

    mkdir mojoauth-spring-demo cd mojoauth-spring-demo
  2. Create a pom.xml file with the following content:

    <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">  <modelVersion>4.0.0</modelVersion>  <parent>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-parent</artifactId>  <version>3.1.3</version>  <relativePath/>  </parent>  <groupId>com.example</groupId>  <artifactId>mojoauth-spring-demo</artifactId>  <version>0.0.1-SNAPSHOT</version>  <name>mojoauth-spring-demo</name>  <description>MojoAuth Spring Boot Demo</description>    <properties>  <java.version>17</java.version>  </properties>    <dependencies>  <!-- Spring Web -->  <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-web</artifactId>  </dependency>    <!-- Thymeleaf Template Engine -->  <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-thymeleaf</artifactId>  </dependency>    <!-- Spring Security -->  <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-security</artifactId>  </dependency>    <!-- OAuth2 Client -->  <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-oauth2-client</artifactId>  </dependency>    <!-- Spring Boot DevTools -->  <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-devtools</artifactId>  <scope>runtime</scope>  <optional>true</optional>  </dependency>    <!-- Lombok -->  <dependency>  <groupId>org.projectlombok</groupId>  <artifactId>lombok</artifactId>  <optional>true</optional>  </dependency>    <!-- Test -->  <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-test</artifactId>  <scope>test</scope>  </dependency>  <dependency>  <groupId>org.springframework.security</groupId>  <artifactId>spring-security-test</artifactId>  <scope>test</scope>  </dependency>  </dependencies>    <build>  <plugins>  <plugin>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-maven-plugin</artifactId>  <configuration>  <excludes>  <exclude>  <groupId>org.projectlombok</groupId>  <artifactId>lombok</artifactId>  </exclude>  </excludes>  </configuration>  </plugin>  </plugins>  </build> </project>

Project Structure

Your project structure should look like this:

mojoauth-spring-demo/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── mojoauthspringdemo/ │ │ │ ├── MojoauthSpringDemoApplication.java │ │ │ ├── config/ │ │ │ │ ├── SecurityConfig.java │ │ │ │ └── WebConfig.java │ │ │ ├── controller/ │ │ │ │ ├── HomeController.java │ │ │ │ └── UserController.java │ │ │ └── service/ │ │ │ └── TokenService.java │ │ └── resources/ │ │ ├── application.yml │ │ ├── static/ │ │ │ ├── css/ │ │ │ │ └── styles.css │ │ │ └── js/ │ │ │ └── main.js │ │ └── templates/ │ │ ├── index.html │ │ ├── login.html │ │ └── profile.html │ └── test/ │ └── java/ │ └── com/ │ └── example/ │ └── mojoauthspringdemo/ │ └── MojoauthSpringDemoApplicationTests.java ├── pom.xml └── README.md

Configure OIDC Properties

Create or update the application.yml file in src/main/resources:

spring:  security:  oauth2:  client:  registration:  mojoauth:  client-id: ${MOJOAUTH_CLIENT_ID}  client-secret: ${MOJOAUTH_CLIENT_SECRET}  scope: openid profile email  authorization-grant-type: authorization_code  redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"  provider:  mojoauth:  issuer-uri: ${MOJOAUTH_ISSUER:https://your-project.auth.mojoauth.com}  authorization-uri: ${MOJOAUTH_ISSUER:https://your-project.auth.mojoauth.com}/oauth/authorize  token-uri: ${MOJOAUTH_ISSUER:https://your-project.auth.mojoauth.com}/oauth2/token  user-info-uri: ${MOJOAUTH_ISSUER:https://your-project.auth.mojoauth.com}/oauth/userinfo  jwk-set-uri: ${MOJOAUTH_ISSUER:https://your-project.auth.mojoauth.com}/.well-known/jwks.json  user-name-attribute: sub   server:  port: 8080   logging:  level:  org.springframework.security: INFO  org.springframework.web: INFO

Set Environment Variables

Create a .env file in the root of your project (don't commit this file to your repository):

MOJOAUTH_CLIENT_ID=your-client-id MOJOAUTH_CLIENT_SECRET=your-client-secret MOJOAUTH_ISSUER=https://your-project.auth.mojoauth.com

Spring Boot Application Class

Create the main application class:

package com.example.mojoauthspringdemo;   import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;   @SpringBootApplication public class MojoauthSpringDemoApplication {    public static void main(String[] args) {  SpringApplication.run(MojoauthSpringDemoApplication.class, args);  } }

Security Configuration

Create a SecurityConfig.java file in the config package:

package com.example.mojoauthspringdemo.config;   import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;   import java.net.URI;   @Configuration @EnableWebSecurity public class SecurityConfig {    private final String issuer;  private final String clientId;    public SecurityConfig() {  this.issuer = System.getenv("MOJOAUTH_ISSUER");  this.clientId = System.getenv("MOJOAUTH_CLIENT_ID");  }    @Bean  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  http  .authorizeHttpRequests(authorize -> authorize  .requestMatchers("/", "/css/**", "/js/**", "/error", "/login").permitAll()  .anyRequest().authenticated()  )  .oauth2Login(oauth2 -> oauth2  .loginPage("/login")  .defaultSuccessUrl("/profile", true)  .userInfoEndpoint(userInfo -> userInfo  .oidcUserService(this.oidcUserService())  )  )  .logout(logout -> logout  .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))  .addLogoutHandler(oidcLogoutHandler())  .clearAuthentication(true)  .invalidateHttpSession(true)  .deleteCookies("JSESSIONID")  );    return http.build();  }    @Bean  public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {  return new OidcUserService();  }    @Bean  public LogoutHandler oidcLogoutHandler() {  return (request, response, authentication) -> {  try {  // Build the MojoAuth logout URL with necessary parameters  String logoutUrl = String.format("%s/oauth2/sessions/logout?client_id=%s&post_logout_redirect_uri=%s",  issuer,  clientId,  request.getRequestURL().toString().replace("/logout", "/"));    response.sendRedirect(logoutUrl);  } catch (Exception e) {  // Log error and continue with local logout  System.err.println("Error during OIDC logout: " + e.getMessage());  }  };  } }

Web Configuration

Create a WebConfig.java file in the config package:

package com.example.mojoauthspringdemo.config;   import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;   @Configuration public class WebConfig implements WebMvcConfigurer {    @Override  public void addViewControllers(ViewControllerRegistry registry) {  registry.addViewController("/login").setViewName("login");  }    @Override  public void addResourceHandlers(ResourceHandlerRegistry registry) {  registry.addResourceHandler("/static/**")  .addResourceLocations("classpath:/static/");  } }

Token Service

Create a TokenService.java file in the service package:

package com.example.mojoauthspringdemo.service;   import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.stereotype.Service;   import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Map;   @Service public class TokenService {    private final OAuth2AuthorizedClientService authorizedClientService;    public TokenService(OAuth2AuthorizedClientService authorizedClientService) {  this.authorizedClientService = authorizedClientService;  }    public OAuth2AccessToken getAccessToken(OAuth2AuthenticationToken authentication) {  OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(  authentication.getAuthorizedClientRegistrationId(),  authentication.getName()  );    if (authorizedClient != null) {  return authorizedClient.getAccessToken();  }    return null;  }    public Map<String, Object> getIdTokenClaims(OAuth2AuthenticationToken authentication) {  OidcUser oidcUser = (DefaultOidcUser) authentication.getPrincipal();  return oidcUser.getClaims();  }    public String getIdToken(OAuth2AuthenticationToken authentication) {  OidcUser oidcUser = (DefaultOidcUser) authentication.getPrincipal();  return oidcUser.getIdToken().getTokenValue();  }    public boolean isTokenExpiringSoon(OAuth2AccessToken accessToken) {  if (accessToken == null || accessToken.getExpiresAt() == null) {  return true;  }    // Check if token expires within 5 minutes  Instant now = Instant.now();  Instant expiry = accessToken.getExpiresAt();  long minutesUntilExpiry = now.until(expiry, ChronoUnit.MINUTES);    return minutesUntilExpiry < 5;  } }

Controllers

Home Controller

Create a HomeController.java file in the controller package:

package com.example.mojoauthspringdemo.controller;   import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping;   @Controller public class HomeController {    @GetMapping("/")  public String home(@AuthenticationPrincipal OidcUser oidcUser, Model model) {  if (oidcUser != null) {  model.addAttribute("username", oidcUser.getFullName());  model.addAttribute("authenticated", true);  } else {  model.addAttribute("authenticated", false);  }  return "index";  } }

User Controller

Create a UserController.java file in the controller package:

package com.example.mojoauthspringdemo.controller;   import com.example.mojoauthspringdemo.service.TokenService; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody;   import java.util.HashMap; import java.util.Map;   @Controller public class UserController {    private final TokenService tokenService;    public UserController(TokenService tokenService) {  this.tokenService = tokenService;  }    @GetMapping("/profile")  public String profile(  @AuthenticationPrincipal OidcUser oidcUser,  OAuth2AuthenticationToken authentication,  Model model) {    // Get tokens  OAuth2AccessToken accessToken = tokenService.getAccessToken(authentication);  String idToken = tokenService.getIdToken(authentication);  Map<String, Object> idTokenClaims = tokenService.getIdTokenClaims(authentication);    // Check if token is expiring soon  boolean tokenExpiringSoon = tokenService.isTokenExpiringSoon(accessToken);    // Add attributes to model  model.addAttribute("user", oidcUser);  model.addAttribute("claims", idTokenClaims);  model.addAttribute("accessToken", accessToken != null ? accessToken.getTokenValue() : "");  model.addAttribute("idToken", idToken);  model.addAttribute("tokenExpiringSoon", tokenExpiringSoon);    return "profile";  }    @GetMapping("/api/userinfo")  @ResponseBody  public Map<String, Object> userInfo(@AuthenticationPrincipal OidcUser oidcUser) {  return oidcUser.getClaims();  } }

HTML Templates

index.html

Create src/main/resources/templates/index.html:

<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>MojoAuth Spring Boot Demo</title>  <link rel="stylesheet" href="/css/styles.css"> </head> <body>  <header>  <nav>  <div class="logo">MojoAuth Spring Demo</div>  <div class="nav-links">  <a href="/">Home</a>  <a th:if="${authenticated}" href="/profile">Profile</a>  <a th:if="${authenticated}" href="/logout">Logout</a>  <a th:unless="${authenticated}" href="/login">Login</a>  </div>  </nav>  </header>    <main>  <div class="hero">  <h1>Welcome to MojoAuth Spring Boot Demo</h1>  <p>This application demonstrates how to integrate MojoAuth's Hosted Login Page with a Spring Boot application.</p>    <div th:if="${authenticated}" class="welcome-card">  <h2>Welcome, <span th:text="${username}">User</span>!</h2>  <p>You are successfully logged in with MojoAuth.</p>  <div class="buttons">  <a href="/profile" class="button primary">View Profile</a>  <a href="/logout" class="button secondary">Logout</a>  </div>  </div>    <div th:unless="${authenticated}" class="login-card">  <p>Experience secure passwordless authentication with MojoAuth.</p>  <a href="/login" class="button primary">Login with MojoAuth</a>  </div>  </div>    <div class="features">  <div class="feature">  <h3>Secure Authentication</h3>  <p>OpenID Connect provides a secure, standardized authentication protocol.</p>  </div>  <div class="feature">  <h3>Passwordless Options</h3>  <p>Support for email magic links, SMS OTP, and social login providers.</p>  </div>  <div class="feature">  <h3>Easy Integration</h3>  <p>Spring Security OAuth2 Client makes it easy to integrate with OIDC providers.</p>  </div>  </div>  </main>    <footer>  <p>&copy; 2025 MojoAuth Spring Boot Demo</p>  </footer>    <script src="/js/main.js"></script> </body> </html>

login.html

Create src/main/resources/templates/login.html:

<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Login - MojoAuth Spring Boot Demo</title>  <link rel="stylesheet" href="/css/styles.css"> </head> <body>  <header>  <nav>  <div class="logo">MojoAuth Spring Demo</div>  <div class="nav-links">  <a href="/">Home</a>  </div>  </nav>  </header>    <main>  <div class="login-container">  <h1>Login</h1>  <p>Select your login method to continue.</p>    <div class="oauth-buttons">  <a href="/oauth2/authorization/mojoauth" class="button oauth-button">  <span class="button-text">Continue with MojoAuth</span>  </a>  </div>    <div class="login-footer">  <p>By logging in, you agree to our Terms of Service and Privacy Policy.</p>  <p><a href="/">Return to Home</a></p>  </div>  </div>  </main>    <footer>  <p>&copy; 2025 MojoAuth Spring Boot Demo</p>  </footer> </body> </html>

profile.html

Create src/main/resources/templates/profile.html:

<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Profile - MojoAuth Spring Boot Demo</title>  <link rel="stylesheet" href="/css/styles.css"> </head> <body>  <header>  <nav>  <div class="logo">MojoAuth Spring Demo</div>  <div class="nav-links">  <a href="/">Home</a>  <a href="/profile" class="active">Profile</a>  <a href="/logout">Logout</a>  </div>  </nav>  </header>    <main>  <div class="profile-container">  <h1>User Profile</h1>    <div class="profile-header">  <div class="avatar" th:if="${user.attributes.picture}">  <img th:src="${user.attributes.picture}" alt="Profile Picture">  </div>  <div class="avatar initial" th:unless="${user.attributes.picture}"   th:text="${#strings.substring(user.fullName, 0, 1)}">U</div>    <div class="profile-info">  <h2 th:text="${user.fullName}">User Name</h2>  <p class="email" th:text="${user.email}">[email protected]</p>  <span th:if="${user.emailVerified}" class="badge verified">Email Verified</span>  </div>  </div>    <div th:if="${tokenExpiringSoon}" class="token-alert">  <p>⚠️ Your session is expiring soon. You may need to log in again.</p>  </div>    <div class="profile-section">  <h3>Profile Information</h3>  <table class="profile-table">  <tr>  <th>Subject (ID)</th>  <td th:text="${user.subject}"></td>  </tr>  <tr>  <th>Name</th>  <td th:text="${user.fullName}"></td>  </tr>  <tr>  <th>Email</th>  <td th:text="${user.email}"></td>  </tr>  <tr>  <th>Email Verified</th>  <td th:text="${user.emailVerified ? 'Yes' : 'No'}"></td>  </tr>  </table>  </div>    <div class="profile-section">  <h3>Token Information</h3>    <div class="token-section">  <h4>ID Token</h4>  <div class="token-display" th:text="${idToken}">ID Token</div>  </div>    <div class="token-section">  <h4>Access Token</h4>  <div class="token-display" th:text="${accessToken}">Access Token</div>  </div>  </div>    <div class="profile-section">  <h3>ID Token Claims</h3>  <div class="claims-display">  <pre th:text="${#strings.replace(claims.toString(), ', ', ',\n ')}"></pre>  </div>  </div>    <div class="action-buttons">  <a href="/" class="button secondary">Back to Home</a>  <a href="/logout" class="button warning">Logout</a>  </div>  </div>  </main>    <footer>  <p>&copy; 2025 MojoAuth Spring Boot Demo</p>  </footer> </body> </html>

CSS Styling

Create src/main/resources/static/css/styles.css:

/* Base styles */ :root {  --primary-color: #4f46e5;  --primary-hover: #4338ca;  --secondary-color: #e5e7eb;  --secondary-hover: #d1d5db;  --warning-color: #f59e0b;  --danger-color: #ef4444;  --text-color: #333333;  --text-light: #6b7280;  --bg-color: #f9fafb;  --card-bg: #ffffff;  --border-color: #e5e7eb;  --shadow: 0 2px 10px rgba(0, 0, 0, 0.05); }   * {  margin: 0;  padding: 0;  box-sizing: border-box; }   body {  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;  line-height: 1.6;  color: var(--text-color);  background-color: var(--bg-color); }   a {  color: var(--primary-color);  text-decoration: none; }   a:hover {  text-decoration: underline; }   /* Layout */ header, main, footer {  width: 100%;  max-width: 1200px;  margin: 0 auto;  padding: 0 20px; }   main {  min-height: calc(100vh - 140px);  padding-top: 20px;  padding-bottom: 40px; }   /* Navigation */ header {  padding: 20px; }   nav {  display: flex;  justify-content: space-between;  align-items: center; }   .logo {  font-weight: bold;  font-size: 20px;  color: var(--primary-color); }   .nav-links {  display: flex;  gap: 20px; }   .nav-links a {  color: var(--text-light);  font-weight: 500;  transition: color 0.3s ease; }   .nav-links a:hover, .nav-links a.active {  color: var(--primary-color);  text-decoration: none; }   /* Buttons */ .button {  display: inline-block;  padding: 10px 20px;  border-radius: 6px;  font-weight: 500;  text-align: center;  transition: all 0.3s ease;  cursor: pointer;  text-decoration: none; }   .button.primary {  background-color: var(--primary-color);  color: white; }   .button.primary:hover {  background-color: var(--primary-hover);  text-decoration: none; }   .button.secondary {  background-color: var(--secondary-color);  color: var(--text-color); }   .button.secondary:hover {  background-color: var(--secondary-hover);  text-decoration: none; }   .button.warning {  background-color: var(--warning-color);  color: white; }   .button.warning:hover {  background-color: #d97706;  text-decoration: none; }   .buttons {  display: flex;  gap: 10px;  margin-top: 20px; }   /* Hero section */ .hero {  background-color: var(--card-bg);  border-radius: 8px;  padding: 40px;  text-align: center;  box-shadow: var(--shadow);  margin-bottom: 40px; }   .hero h1 {  margin-bottom: 20px; }   .hero p {  margin-bottom: 30px;  color: var(--text-light);  font-size: 18px; }   .welcome-card, .login-card {  background-color: #f0f9ff;  border-radius: 8px;  padding: 30px;  margin: 30px auto 0;  max-width: 600px; }   /* Features */ .features {  display: grid;  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));  gap: 20px; }   .feature {  background-color: var(--card-bg);  padding: 30px;  border-radius: 8px;  box-shadow: var(--shadow);  transition: transform 0.3s ease, box-shadow 0.3s ease; }   .feature:hover {  transform: translateY(-5px);  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); }   .feature h3 {  margin-bottom: 15px;  color: var(--primary-color); }   /* Login page */ .login-container {  background-color: var(--card-bg);  border-radius: 8px;  padding: 40px;  box-shadow: var(--shadow);  max-width: 500px;  margin: 40px auto;  text-align: center; }   .login-container h1 {  margin-bottom: 15px; }   .login-container p {  margin-bottom: 30px;  color: var(--text-light); }   .oauth-buttons {  display: flex;  flex-direction: column;  gap: 15px;  margin-bottom: 30px; }   .oauth-button {  display: flex;  align-items: center;  justify-content: center;  gap: 10px;  width: 100%;  padding: 12px;  border: 1px solid var(--border-color);  background-color: var(--card-bg);  color: var(--text-color); }   .login-footer {  margin-top: 30px;  font-size: 14px;  color: var(--text-light); }   .login-footer p {  margin-bottom: 10px; }   /* Profile page */ .profile-container {  background-color: var(--card-bg);  border-radius: 8px;  padding: 40px;  box-shadow: var(--shadow);  max-width: 800px;  margin: 0 auto; }   .profile-header {  display: flex;  align-items: center;  margin-bottom: 30px;  padding-bottom: 20px;  border-bottom: 1px solid var(--border-color); }   .avatar {  width: 100px;  height: 100px;  border-radius: 50%;  margin-right: 20px;  overflow: hidden; }   .avatar img {  width: 100%;  height: 100%;  object-fit: cover; }   .avatar.initial {  background-color: var(--primary-color);  color: white;  display: flex;  align-items: center;  justify-content: center;  font-size: 40px;  font-weight: bold; }   .profile-info h2 {  margin-bottom: 5px; }   .profile-info .email {  color: var(--text-light);  margin-bottom: 10px; }   .badge {  display: inline-block;  padding: 4px 10px;  border-radius: 12px;  font-size: 12px;  font-weight: 500; }   .badge.verified {  background-color: #d1fae5;  color: #065f46; }   .token-alert {  background-color: #fffbeb;  border: 1px solid #fef3c7;  color: #92400e;  padding: 15px;  border-radius: 6px;  margin-bottom: 20px;  display: flex;  align-items: center; }   .profile-section {  margin-bottom: 30px; }   .profile-section h3 {  margin-bottom: 15px;  color: var(--primary-color);  font-size: 20px; }   .profile-table {  width: 100%;  border-collapse: collapse; }   .profile-table th, .profile-table td {  padding: 12px 15px;  border-bottom: 1px solid var(--border-color); }   .profile-table th {  text-align: left;  width: 30%;  color: var(--text-light);  font-weight: 500; }   .token-section {  margin-bottom: 20px; }   .token-section h4 {  margin-bottom: 10px;  font-weight: 500; }   .token-display, .claims-display {  background-color: #f8fafc;  padding: 15px;  border-radius: 6px;  overflow-x: auto;  font-family: monospace;  font-size: 14px;  border: 1px solid var(--border-color);  word-break: break-all; }   .claims-display pre {  white-space: pre-wrap; }   .action-buttons {  display: flex;  gap: 10px;  margin-top: 30px; }   /* Footer */ footer {  text-align: center;  padding: 20px;  color: var(--text-light);  font-size: 14px; }   /* Responsive adjustments */ @media (max-width: 768px) {  .profile-header {  flex-direction: column;  align-items: center;  text-align: center;  }    .avatar {  margin-right: 0;  margin-bottom: 20px;  }    .profile-table th, .profile-table td {  padding: 10px;  }    .action-buttons {  flex-direction: column;  }    .action-buttons .button {  width: 100%;  } }   @media (max-width: 576px) {  nav {  flex-direction: column;  gap: 15px;  }    .hero, .profile-container {  padding: 20px;  }    .features {  grid-template-columns: 1fr;  } }

JavaScript File

Create src/main/resources/static/js/main.js:

// Simple JavaScript file for any client-side interactions document.addEventListener('DOMContentLoaded', () => {  // Initialize any client-side functionality here  console.log('MojoAuth Spring Boot Demo loaded');    // Example: Auto-dismiss alerts after 5 seconds  const alerts = document.querySelectorAll('.token-alert');  if (alerts.length > 0) {  setTimeout(() => {  alerts.forEach(alert => {  alert.style.opacity = '0';  alert.style.transition = 'opacity 0.5s ease';  setTimeout(() => alert.style.display = 'none', 500);  });  }, 5000);  } });

Running the Application

To run the application, use:

# Set environment variables for local development export MOJOAUTH_CLIENT_ID=your-client-id export MOJOAUTH_CLIENT_SECRET=your-client-secret export MOJOAUTH_ISSUER=https://your-project.auth.mojoauth.com   # Run the application ./mvnw spring-boot:run

Or, if you're using Windows:

set MOJOAUTH_CLIENT_ID=your-client-id set MOJOAUTH_CLIENT_SECRET=your-client-secret set MOJOAUTH_ISSUER=https://your-project.auth.mojoauth.com   mvnw.cmd spring-boot:run

The application will start on http://localhost:8080.

Testing the Authentication Flow

  1. Open your browser and navigate to http://localhost:8080
  2. Click on the "Login with MojoAuth" button
  3. You'll be redirected to the MojoAuth Hosted Login Page
  4. After successful authentication, you'll be redirected back to your application
  5. You should now see your user profile information

Production Considerations

1. Environment Configuration

For production deployment, configure the application properties in a secure way:

  • Use environment variables in your hosting platform
  • Use Spring Cloud Config Server for centralized configuration
  • Use Azure Key Vault or AWS Secrets Manager for sensitive information

2. HTTPS in Production

Always use HTTPS in production by configuring SSL:

# In application.yml for production server:  port: 443  ssl:  key-store: classpath:keystore.p12  key-store-password: ${KEY_STORE_PASSWORD}  key-store-type: PKCS12  key-alias: tomcat

3. Session Management

Configure session management for better security:

# In application.yml server:  servlet:  session:  cookie:  http-only: true  secure: true  max-age: 3600s  timeout: 3600s

4. CSRF Protection

Ensure CSRF protection is enabled (it's on by default in Spring Security):

@Configuration @EnableWebSecurity public class SecurityConfig {  @Bean  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  http  // ... other configurations  .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));    return http.build();  } }

5. Logging

Configure appropriate logging levels:

# In application.yml logging:  level:  root: INFO  org.springframework.web: INFO  org.springframework.security: INFO  org.springframework.security.oauth2: INFO

Advanced Features

1. Token Refresh

To implement token refresh:

@Service public class TokenRefreshService {    private final OAuth2AuthorizedClientService authorizedClientService;  private final OAuth2AuthorizedClientRepository authorizedClientRepository;  private final ClientRegistrationRepository clientRegistrationRepository;    public TokenRefreshService(  OAuth2AuthorizedClientService authorizedClientService,  OAuth2AuthorizedClientRepository authorizedClientRepository,  ClientRegistrationRepository clientRegistrationRepository) {  this.authorizedClientService = authorizedClientService;  this.authorizedClientRepository = authorizedClientRepository;  this.clientRegistrationRepository = clientRegistrationRepository;  }    public OAuth2AccessToken refreshToken(  OAuth2AuthenticationToken authentication,  HttpServletRequest request,  HttpServletResponse response) {    String registrationId = authentication.getAuthorizedClientRegistrationId();  ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(registrationId);    OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(  registrationId, authentication.getName());    if (authorizedClient != null && authorizedClient.getRefreshToken() != null) {  OAuth2AuthorizedClient refreshedClient =   new DefaultOAuth2AuthorizedClientManager(  clientRegistrationRepository,   authorizedClientRepository  ).authorize(  OAuth2AuthorizeRequest  .withClientRegistrationId(registrationId)  .principal(authentication)  .attribute(HttpServletRequest.class.getName(), request)  .attribute(HttpServletResponse.class.getName(), response)  .build()  );    if (refreshedClient != null) {  return refreshedClient.getAccessToken();  }  }    return null;  } }

2. Role-Based Access Control

Implement role-based access control using OAuth2 claims:

@Configuration @EnableMethodSecurity public class MethodSecurityConfig {    @Bean  public GrantedAuthoritiesMapper userAuthoritiesMapper() {  return (authorities) -> {  Set<GrantedAuthority> mappedAuthorities = new HashSet<>();    authorities.forEach(authority -> {  if (OidcUserAuthority.class.isInstance(authority)) {  OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;  OidcIdToken idToken = oidcUserAuthority.getIdToken();    // Extract roles from the ID token claims  List<String> roles = idToken.getClaim("roles");  if (roles != null) {  roles.forEach(role ->   mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));  }    // Add default role  mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));  }  });    return mappedAuthorities;  };  } }

Use these roles in controllers or methods:

@GetMapping("/admin") @PreAuthorize("hasRole('ADMIN')") public String adminPage() {  return "admin"; }

Reference Links