MojoAuth Hosted Login Page with Java (Quarkus)
Introduction
Quarkus is a Kubernetes-native Java framework tailored for GraalVM and HotSpot, designed to optimize Java specifically for containers. It offers fast startup time, low memory utilization, and effective developer productivity, making it an excellent choice for cloud-native applications.
This guide demonstrates how to integrate MojoAuth's Hosted Login Page with a Quarkus application using the OpenID Connect (OIDC) extension, which provides comprehensive support for authenticating users through MojoAuth's OIDC provider.
Links:
- MojoAuth Hosted Login Page Documentation (opens in a new tab)
- Quarkus Documentation (opens in a new tab)
- Quarkus OpenID Connect 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
- Apache Maven 3.8.1+ or Gradle 7.0+ installed
- Basic knowledge of Java and Quarkus
Project Setup
Let's start by creating a new Quarkus project:
Using Quarkus CLI
If you have the Quarkus CLI installed:
quarkus create app com.example:mojoauth-quarkus-demo cd mojoauth-quarkus-demo quarkus extension add quarkus-oidc quarkus-qute quarkus-resteasy-reactive-jackson
Using Maven
Alternatively, you can use Maven:
mvn io.quarkus:quarkus-maven-plugin:3.2.0.Final:create \ -DprojectGroupId=com.example \ -DprojectArtifactId=mojoauth-quarkus-demo \ -DclassName="com.example.mojoauthquarkusdemo.GreetingResource" \ -Dpath="/hello" cd mojoauth-quarkus-demo mvn quarkus:add-extension -Dextensions="quarkus-oidc,quarkus-qute,quarkus-resteasy-reactive-jackson"
Project Structure
Your project structure should look like this:
mojoauth-quarkus-demo/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── mojoauthquarkusdemo/ │ │ │ ├── GreetingResource.java │ │ │ ├── HomeResource.java │ │ │ ├── UserResource.java │ │ │ ├── model/ │ │ │ │ └── TokenInfo.java │ │ │ └── service/ │ │ │ └── TokenService.java │ │ └── resources/ │ │ ├── application.properties │ │ ├── META-INF/ │ │ │ └── resources/ │ │ │ ├── css/ │ │ │ │ └── styles.css │ │ │ └── js/ │ │ │ └── main.js │ │ └── templates/ │ │ ├── base.html │ │ ├── index.html │ │ ├── login.html │ │ └── profile.html │ └── test/ │ └── java/ │ └── com/ │ └── example/ │ └── mojoauthquarkusdemo/ │ └── GreetingResourceTest.java ├── mvnw ├── mvnw.cmd ├── pom.xml └── README.md
Configure OIDC Properties
Edit the src/main/resources/application.properties
file:
# Server configuration quarkus.http.port=8080 # Production profile %prod.quarkus.http.ssl-port=8443 %prod.quarkus.http.ssl.certificate.key-store-file=keystore.jks %prod.quarkus.http.ssl.certificate.key-store-password=${KEY_STORE_PASSWORD:changeit} # Application name quarkus.application.name=MojoAuth Quarkus Demo # Default locale quarkus.default-locale=en-US # OIDC Configuration quarkus.oidc.auth-server-url=${MOJOAUTH_ISSUER:https://your-project.auth.mojoauth.com} quarkus.oidc.client-id=${MOJOAUTH_CLIENT_ID:your-client-id} quarkus.oidc.credentials.secret=${MOJOAUTH_CLIENT_SECRET:your-client-secret} quarkus.oidc.authentication.redirect-path=/callback quarkus.oidc.authentication.restore-path-after-redirect=false quarkus.oidc.authentication.extra-params.scope=openid profile email quarkus.oidc.token.refresh-expired=true quarkus.oidc.token.refresh-token-time-skew=5M quarkus.oidc.application-type=web-app # HTTP security configuration quarkus.http.auth.permission.authenticated.paths=/profile,/api/* quarkus.http.auth.permission.authenticated.policy=authenticated # Logging quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n quarkus.log.console.level=INFO quarkus.log.category."io.quarkus.oidc".level=INFO quarkus.log.category."io.quarkus.security".level=INFO # Static resources quarkus.http.static-resources.enable=true
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
To load these environment variables when running in development mode:
export MOJOAUTH_CLIENT_ID=your-client-id export MOJOAUTH_CLIENT_SECRET=your-client-secret export MOJOAUTH_ISSUER=https://your-project.auth.mojoauth.com
Model Class
Create a TokenInfo.java
file in the model
package:
package com.example.mojoauthquarkusdemo.model; import io.quarkus.runtime.annotations.RegisterForReflection; import java.time.Instant; @RegisterForReflection public class TokenInfo { private String accessToken; private String idToken; private Instant expiresAt; private boolean isExpiringSoon; public TokenInfo() { // Default constructor for reflection } public TokenInfo(String accessToken, String idToken, Instant expiresAt, boolean isExpiringSoon) { this.accessToken = accessToken; this.idToken = idToken; this.expiresAt = expiresAt; this.isExpiringSoon = isExpiringSoon; } public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public String getIdToken() { return idToken; } public void setIdToken(String idToken) { this.idToken = idToken; } public Instant getExpiresAt() { return expiresAt; } public void setExpiresAt(Instant expiresAt) { this.expiresAt = expiresAt; } public boolean isExpiringSoon() { return isExpiringSoon; } public void setExpiringSoon(boolean expiringSoon) { this.isExpiringSoon = expiringSoon; } }
Token Service
Create a TokenService.java
file in the service
package:
package com.example.mojoauthquarkusdemo.service; import com.example.mojoauthquarkusdemo.model.TokenInfo; import io.quarkus.oidc.IdToken; import io.quarkus.oidc.OidcConfigurationMetadata; import io.quarkus.oidc.RefreshToken; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.client.OidcClient; import io.smallrye.jwt.auth.principal.DefaultJWTParser; import io.smallrye.jwt.auth.principal.JWTParser; import io.smallrye.jwt.auth.principal.ParseException; import io.smallrye.mutiny.Uni; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; import org.jose4j.jwt.JwtClaims; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.Map; import java.util.Optional; @ApplicationScoped public class TokenService { @Inject @IdToken JsonWebToken idToken; @Inject RefreshToken refreshToken; @Inject TokenIntrospection tokenIntrospection; @Inject OidcConfigurationMetadata configurationMetadata; @ConfigProperty(name = "quarkus.oidc.client-id") String clientId; @ConfigProperty(name = "quarkus.oidc.credentials.secret") String clientSecret; @ConfigProperty(name = "quarkus.oidc.auth-server-url") String authServerUrl; private final JWTParser parser = new DefaultJWTParser(); @Inject OidcClient oidcClient; public TokenInfo getTokenInfo() { // Get access token expiry Instant expiresAt = null; boolean isExpiringSoon = false; try { // Get the access token expiry from the TokenIntrospection if (tokenIntrospection != null) { Long exp = tokenIntrospection.getExpiresAt(); if (exp != null) { expiresAt = Instant.ofEpochSecond(exp); isExpiringSoon = isTokenExpiring(expiresAt); } } // Get the access token (we only have direct access to the ID token) String accessToken = extractAccessTokenFromIntrospection(); // Return the token info return new TokenInfo( accessToken, idToken.getRawToken(), expiresAt, isExpiringSoon ); } catch (Exception e) { throw new RuntimeException("Error retrieving token information", e); } } private String extractAccessTokenFromIntrospection() { // This is a workaround since we don't have direct access to the access token // A better approach would be to store it in the session when obtained Map<String, Object> allClaims = tokenIntrospection.getAll(); return (String) allClaims.get("access_token"); } private boolean isTokenExpiring(Instant expiresAt) { if (expiresAt == null) { return true; } Instant now = Instant.now(); // Consider token expiring if it's less than 5 minutes from expiry return now.plus(5, ChronoUnit.MINUTES).isAfter(expiresAt); } public Uni<TokenInfo> refreshTokens() { if (refreshToken == null || refreshToken.getToken() == null) { return Uni.createFrom().failure(new IllegalStateException("No refresh token available")); } return oidcClient.refreshTokens(refreshToken.getToken()) .onItem().transform(tokens -> { try { JsonWebToken newIdToken = parser.parse(tokens.getIdToken()); Long exp = newIdToken.getClaim(Claims.exp.name()); Instant expiresAt = exp != null ? Instant.ofEpochSecond(exp) : null; return new TokenInfo( tokens.getAccessToken(), tokens.getIdToken(), expiresAt, isTokenExpiring(expiresAt) ); } catch (ParseException e) { throw new RuntimeException("Failed to parse new ID token", e); } }); } public Map<String, Object> decodeJwtPayload(String jwt) { try { String[] parts = jwt.split("\\."); if (parts.length != 3) { return Map.of("error", "Invalid JWT format"); } String payload = parts[1]; Base64.Decoder decoder = Base64.getUrlDecoder(); String decodedPayload = new String(decoder.decode(payload)); // Parse JSON string JwtClaims claims = JwtClaims.parse(decodedPayload); return claims.getClaimsMap(); } catch (Exception e) { return Map.of("error", "Failed to decode JWT: " + e.getMessage()); } } public String getLogoutUrl(String redirectUri) { Optional<String> endSessionEndpoint = configurationMetadata.getEndSessionEndpoint(); if (endSessionEndpoint.isPresent()) { return endSessionEndpoint.get() + "?client_id=" + clientId + "&post_logout_redirect_uri=" + redirectUri; } else { // Fallback if the end session endpoint is not found in the metadata return authServerUrl + "/oauth2/sessions/logout" + "?client_id=" + clientId + "&post_logout_redirect_uri=" + redirectUri; } } }
Resource Classes
Home Resource
Create a HomeResource.java
file:
package com.example.mojoauthquarkusdemo; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import io.quarkus.security.identity.SecurityIdentity; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @Path("/") public class HomeResource { @Inject SecurityIdentity securityIdentity; @CheckedTemplate public static class Templates { public static native TemplateInstance index(boolean authenticated, String username); public static native TemplateInstance login(); } @GET @Produces(MediaType.TEXT_HTML) public TemplateInstance index() { boolean authenticated = securityIdentity.isAnonymous() == false; String username = authenticated ? securityIdentity.getPrincipal().getName() : ""; return Templates.index(authenticated, username); } @GET @Path("/login") @Produces(MediaType.TEXT_HTML) public TemplateInstance login() { return Templates.login(); } }
User Resource
Create a UserResource.java
file:
package com.example.mojoauthquarkusdemo; import com.example.mojoauthquarkusdemo.model.TokenInfo; import com.example.mojoauthquarkusdemo.service.TokenService; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; import io.smallrye.mutiny.Uni; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; import org.eclipse.microprofile.jwt.JsonWebToken; import java.net.URI; import java.util.Map; @Path("/profile") @Authenticated public class UserResource { @Inject SecurityIdentity securityIdentity; @Inject JsonWebToken idToken; @Inject TokenService tokenService; @CheckedTemplate public static class Templates { public static native TemplateInstance profile(String name, String email, String picture, boolean emailVerified, String subject, String accessToken, String idToken, Map<String, Object> claims, boolean tokenExpiringSoon); } @GET @Produces(MediaType.TEXT_HTML) public TemplateInstance getProfile() { TokenInfo tokenInfo = tokenService.getTokenInfo(); Map<String, Object> claims = tokenService.decodeJwtPayload(idToken.getRawToken()); return Templates.profile( idToken.getClaim("name"), idToken.getClaim("email"), idToken.getClaim("picture"), idToken.getClaim("email_verified"), idToken.getSubject(), tokenInfo.getAccessToken(), tokenInfo.getIdToken(), claims, tokenInfo.isExpiringSoon() ); } @GET @Path("/refresh-token") @Produces(MediaType.TEXT_HTML) public Uni<Response> refreshToken() { return tokenService.refreshTokens() .onItem().transform(tokenInfo -> { // Redirect to profile page after token refresh return Response.seeOther(URI.create("/profile")).build(); }) .onFailure().recoverWithItem(error -> { // In case of failure, redirect to login return Response.seeOther(URI.create("/login")).build(); }); } @GET @Path("/logout") public Response logout(@Context UriInfo uriInfo) { // Get the base URL for the logout redirect String baseUrl = uriInfo.getBaseUri().toString(); if (baseUrl.endsWith("/")) { baseUrl = baseUrl.substring(0, baseUrl.length() - 1); } // Get the OIDC logout URL String logoutUrl = tokenService.getLogoutUrl(baseUrl); // Redirect to the OIDC logout URL return Response.seeOther(URI.create(logoutUrl)).build(); } @GET @Path("/api/userinfo") @Produces(MediaType.APPLICATION_JSON) public Map<String, Object> userInfo() { return tokenService.decodeJwtPayload(idToken.getRawToken()); } }
Greeting Resource (Optional)
Update the default GreetingResource.java
file (optional):
package com.example.mojoauthquarkusdemo; import io.quarkus.security.Authenticated; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @Path("/hello") public class GreetingResource { @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { return "Welcome to MojoAuth Quarkus Demo!"; } @GET @Path("/secure") @Authenticated @Produces(MediaType.TEXT_PLAIN) public String secureHello() { return "Welcome, authenticated user!"; } }
HTML Templates
Create the following template files under src/main/resources/templates
:
base.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{#insert title}MojoAuth Quarkus Demo{/}</title> <link rel="stylesheet" href="/css/styles.css"> </head> <body> <header> <nav> <div class="logo">MojoAuth Quarkus Demo</div> <div class="nav-links"> <a href="/">Home</a> {#if authenticated} <a href="/profile">Profile</a> <a href="/profile/logout">Logout</a> {#else} <a href="/login">Login</a> {/if} </div> </nav> </header> <main> {#insert content} <!-- Content will be inserted here --> {/insert} </main> <footer> <p>© 2025 MojoAuth Quarkus Demo</p> </footer> <script src="/js/main.js"></script> </body> </html>
index.html
{#include base.html} {#title}Home - MojoAuth Quarkus Demo{/title} {#content} <div class="hero"> <h1>Welcome to MojoAuth Quarkus Demo</h1> <p>This application demonstrates how to integrate MojoAuth's Hosted Login Page with a Quarkus application.</p> {#if authenticated} <div class="welcome-card"> <h2>Welcome, {username}!</h2> <p>You are successfully logged in with MojoAuth.</p> <div class="buttons"> <a href="/profile" class="button primary">View Profile</a> <a href="/profile/logout" class="button secondary">Logout</a> </div> </div> {#else} <div class="login-card"> <p>Experience secure passwordless authentication with MojoAuth.</p> <a href="/login" class="button primary">Login with MojoAuth</a> </div> {/if} </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>Cloud Native</h3> <p>Quarkus is optimized for cloud deployments with minimal footprint.</p> </div> </div> {/content} {/include}
login.html
{#include base.html} {#title}Login - MojoAuth Quarkus Demo{/title} {#content} <div class="login-container"> <h1>Login</h1> <p>Select your login method to continue.</p> <div class="oauth-buttons"> <a href="/q/oidc/login" 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> {/content} {/include}
profile.html
{#include base.html} {#title}Profile - MojoAuth Quarkus Demo{/title} {#content} <div class="profile-container"> <h1>User Profile</h1> <div class="profile-header"> {#if picture} <div class="avatar"> <img src="{picture}" alt="Profile Picture"> </div> {#else} <div class="avatar initial"> {name.charAt(0)} </div> {/if} <div class="profile-info"> <h2>{name}</h2> <p class="email">{email}</p> {#if emailVerified} <span class="badge verified">Email Verified</span> {/if} </div> </div> {#if tokenExpiringSoon} <div class="token-alert"> <p>⚠️ Your session is expiring soon. You may need to log in again.</p> <a href="/profile/refresh-token" class="button secondary">Refresh Token</a> </div> {/if} <div class="profile-section"> <h3>Profile Information</h3> <table class="profile-table"> <tr> <th>Subject (ID)</th> <td>{subject}</td> </tr> <tr> <th>Name</th> <td>{name}</td> </tr> <tr> <th>Email</th> <td>{email}</td> </tr> <tr> <th>Email Verified</th> <td>{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">{idToken}</div> </div> <div class="token-section"> <h4>Access Token</h4> <div class="token-display">{accessToken}</div> </div> </div> <div class="profile-section"> <h3>ID Token Claims</h3> <div class="claims-display"> <pre>{claims}</pre> </div> </div> <div class="action-buttons"> <a href="/" class="button secondary">Back to Home</a> <a href="/profile/logout" class="button warning">Logout</a> </div> </div> {/content} {/include}
CSS Styling
Create a CSS file at src/main/resources/META-INF/resources/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; justify-content: space-between; 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; } .token-alert { flex-direction: column; gap: 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 a JavaScript file at src/main/resources/META-INF/resources/js/main.js
:
// Simple JavaScript file for any client-side interactions document.addEventListener('DOMContentLoaded', () => { // Initialize any client-side functionality here console.log('MojoAuth Quarkus 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 in development mode:
# Set environment variables export MOJOAUTH_CLIENT_ID=your-client-id export MOJOAUTH_CLIENT_SECRET=your-client-secret export MOJOAUTH_ISSUER=https://your-project.auth.mojoauth.com # Run Quarkus in dev mode ./mvnw quarkus:dev
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 Build
To build the application for production:
./mvnw package
This will create a runnable JAR file in the target/quarkus-app
directory.
Running in Production
To run the application in production:
java -jar target/quarkus-app/quarkus-run.jar
Creating a Native Executable
To create a native executable (requires GraalVM installed):
./mvnw package -Pnative
To run the native executable:
./target/mojoauth-quarkus-demo-1.0.0-SNAPSHOT-runner
Production Considerations
1. SSL Configuration
For production, always enable HTTPS:
# In application.properties quarkus.http.ssl.certificate.key-store-file=keystore.jks quarkus.http.ssl.certificate.key-store-password=${KEY_STORE_PASSWORD:changeit} quarkus.http.ssl.port=8443 quarkus.http.insecure-requests=redirect
2. Handling Secrets
Use environment variables or Kubernetes secrets:
# In application.properties quarkus.oidc.client-id=${MOJOAUTH_CLIENT_ID} quarkus.oidc.credentials.secret=${MOJOAUTH_CLIENT_SECRET}
3. Securing Endpoints
Use role-based access control:
# In application.properties quarkus.http.auth.policy.role-policy1.roles-allowed=admin quarkus.http.auth.permission.roles1.paths=/api/admin/* quarkus.http.auth.permission.roles1.policy=role-policy1
4. Health Checks
Add health checks for better monitoring:
<!-- In pom.xml --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-health</artifactId> </dependency>
Create a custom health check:
@Liveness @ApplicationScoped public class OidcHealthCheck implements HealthCheck { @ConfigProperty(name = "quarkus.oidc.auth-server-url") String authServerUrl; @Override public HealthCheckResponse call() { // Perform a simple connectivity check try { URL url = new URL(authServerUrl + "/.well-known/openid-configuration"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.connect(); int responseCode = connection.getResponseCode(); if (responseCode == 200) { return HealthCheckResponse.up("OIDC provider is available"); } else { return HealthCheckResponse.down("OIDC provider returned HTTP code: " + responseCode); } } catch (Exception e) { return HealthCheckResponse.down("OIDC provider check failed: " + e.getMessage()); } } }
Advanced Features
1. Token Refresh
Quarkus already has built-in support for token refresh. To explicitly refresh tokens, we've implemented the refresh token endpoint:
@GET @Path("/refresh-token") @Produces(MediaType.TEXT_HTML) public Uni<Response> refreshToken() { return tokenService.refreshTokens() .onItem().transform(tokenInfo -> { // Redirect to profile page after token refresh return Response.seeOther(URI.create("/profile")).build(); }) .onFailure().recoverWithItem(error -> { // In case of failure, redirect to login return Response.seeOther(URI.create("/login")).build(); }); }
2. Role-Based Access Control
To implement role-based access control:
- Make sure your MojoAuth application is configured to include roles in the tokens
- Configure Quarkus to map claims to roles:
# In application.properties quarkus.oidc.roles.source=userinfo quarkus.oidc.roles.role-claim-path=roles
- Use the
@RolesAllowed
annotation for role-based access:
@GET @Path("/admin") @RolesAllowed("admin") @Produces(MediaType.TEXT_HTML) public TemplateInstance adminPage() { // Only users with the 'admin' role will reach here return Templates.admin(securityIdentity.getPrincipal().getName()); }
3. Custom Claims Validator
To validate specific claims in the token:
@ApplicationScoped public class CustomClaimsValidator implements OidcClaimsValidator { @Override public CompletionStage<Boolean> validate(JsonObject claims) { // Check if the email is verified Boolean emailVerified = claims.getBoolean("email_verified"); if (emailVerified == null || !emailVerified) { return CompletableFuture.completedFuture(false); } return CompletableFuture.completedFuture(true); } }
Register the validator:
# In application.properties quarkus.oidc.claims-validator=com.example.mojoauthquarkusdemo.CustomClaimsValidator