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:
- MojoAuth Hosted Login Page Documentation (opens in a new tab)
- Spring Boot Documentation (opens in a new tab)
- Spring Security OAuth2 Client Documentation (opens in a new tab)
Prerequisites
Before you begin, make sure you have:
- A MojoAuth account with an OIDC application configured
- Your OIDC Client ID, Client Secret, and Issuer URL (usually https://your-project.auth.mojoauth.com (opens in a new tab))
- Java Development Kit (JDK) 17+ installed
- Maven or Gradle build tool installed
- 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):
- Visit https://start.spring.io/ (opens in a new tab)
- 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
- Add dependencies:
- Spring Web
- Thymeleaf
- Spring Security
- OAuth2 Client
- Spring Boot DevTools
- Click "Generate" to download the project ZIP file
- Extract the ZIP file and open it in your favorite IDE
Manual Setup
Alternatively, you can set up the project manually:
-
Create a new project directory:
mkdir mojoauth-spring-demo cd mojoauth-spring-demo
-
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>© 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>© 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>© 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
- Open your browser and navigate to
http://localhost:8080
- Click on the "Login with MojoAuth" button
- You'll be redirected to the MojoAuth Hosted Login Page
- After successful authentication, you'll be redirected back to your application
- 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"; }