AgeOnce Docs
Integration

Java

AgeOnce integration with Java (Spring Boot, Jakarta EE)

Java Integration

Complete AgeOnce integration example with Java.

Dependencies (Maven)

<dependencies>
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>4.4.0</version>
    </dependency>
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>jwks-rsa</artifactId>
        <version>0.22.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.0</version>
    </dependency>
</dependencies>

Configuration

// AgeOnceConfig.java
@Configuration
public class AgeOnceConfig {
    
    @Value("${ageonce.client-id}")
    private String clientId;
    
    @Value("${ageonce.client-secret}")
    private String clientSecret;
    
    @Value("${ageonce.redirect-uri}")
    private String redirectUri;
    
    @Value("${ageonce.app-url:https://app.ageonce.com}")
    private String appUrl;

    @Value("${ageonce.api-url:https://api.ageonce.com}")
    private String apiUrl;
    
    public String getClientId() { return clientId; }
    public String getClientSecret() { return clientSecret; }
    public String getRedirectUri() { return redirectUri; }
    public String getAppUrl() { return appUrl; }
    public String getApiUrl() { return apiUrl; }
}

AgeOnce service

// AgeOnceService.java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HexFormat;
import java.util.Map;

import com.fasterxml.jackson.databind.ObjectMapper;

@Service
public class AgeOnceService {
    
    private final AgeOnceConfig config;
    private final HttpClient httpClient;
    private final ObjectMapper objectMapper;
    
    public AgeOnceService(AgeOnceConfig config) {
        this.config = config;
        this.httpClient = HttpClient.newHttpClient();
        this.objectMapper = new ObjectMapper();
    }
    
    public record VerifyUrlResult(String url, String state) {}
    
    public VerifyUrlResult generateVerifyUrl() {
        byte[] randomBytes = new byte[16];
        new SecureRandom().nextBytes(randomBytes);
        String state = HexFormat.of().formatHex(randomBytes);
        
        String url = String.format(
            "%s/?client_id=%s&redirect_uri=%s&state=%s",
            config.getAppUrl(),
            URLEncoder.encode(config.getClientId(), StandardCharsets.UTF_8),
            URLEncoder.encode(config.getRedirectUri(), StandardCharsets.UTF_8),
            state
        );
        
        return new VerifyUrlResult(url, state);
    }
    
    public Map<String, Object> exchangeCodeForToken(String code, String state) throws Exception {
        Map<String, String> body = new java.util.HashMap<>();
        body.put("grant_type", "authorization_code");
        body.put("code", code);
        body.put("redirect_uri", config.getRedirectUri());
        if (state != null && !state.isEmpty()) {
            body.put("state", state);
        }

        String credentials = Base64.getEncoder().encodeToString(
            (config.getClientId() + ":" + config.getClientSecret()).getBytes(StandardCharsets.UTF_8)
        );

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(config.getApiUrl() + "/api/oauth/token"))
            .header("Content-Type", "application/json")
            .header("Authorization", "Basic " + credentials)
            .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))
            .build();
        
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() != 200) {
            throw new RuntimeException("Token exchange failed");
        }
        
        return objectMapper.readValue(response.body(), Map.class);
    }
    
    public Map<String, Object> validateToken(String token) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(config.getApiUrl() + "/api/verify/token/" + URLEncoder.encode(token, StandardCharsets.UTF_8)))
            .GET()
            .build();
        
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() != 200) {
            return Map.of("valid", false);
        }
        
        return objectMapper.readValue(response.body(), Map.class);
    }
}

Spring Boot Controller

// AgeOnceController.java
@Controller
public class AgeOnceController {
    
    private final AgeOnceService ageOnceService;
    
    public AgeOnceController(AgeOnceService ageOnceService) {
        this.ageOnceService = ageOnceService;
    }
    
    @GetMapping("/verify")
    public String verify(HttpSession session) {
        var result = ageOnceService.generateVerifyUrl();
        session.setAttribute("oauth_state", result.state());
        return "redirect:" + result.url();
    }
    
    @GetMapping("/callback")
    public String callback(
            @RequestParam String code,
            @RequestParam String state,
            HttpSession session,
            Model model) {
        
        String expectedState = (String) session.getAttribute("oauth_state");
        
        if (!state.equals(expectedState)) {
            model.addAttribute("error", "Invalid state");
            return "error";
        }
        
        try {
            Map<String, Object> tokenData = ageOnceService.exchangeCodeForToken(code, state);
            String accessToken = (String) tokenData.get("access_token");
            
            Map<String, Object> result = ageOnceService.validateToken(accessToken);
            
            if (Boolean.TRUE.equals(result.get("valid")) && Boolean.TRUE.equals(result.get("age_verified"))) {
                session.setAttribute("age_verified", true);
                session.setAttribute("age_over", result.get("age_over"));
                return "redirect:/protected";
            }
            
            model.addAttribute("error", "Age verification failed");
            return "error";
            
        } catch (Exception e) {
            model.addAttribute("error", e.getMessage());
            return "error";
        }
    }
    
    @GetMapping("/protected")
    public String protectedPage(HttpSession session, Model model) {
        if (!Boolean.TRUE.equals(session.getAttribute("age_verified"))) {
            return "redirect:/verify";
        }
        
        return "protected";
    }
}

// AgeVerificationInterceptor.java
@Component
public class AgeVerificationInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        
        HttpSession session = request.getSession();
        
        if (!Boolean.TRUE.equals(session.getAttribute("age_verified"))) {
            response.sendRedirect("/verify");
            return false;
        }
        
        return true;
    }
}

// WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private AgeVerificationInterceptor ageVerificationInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(ageVerificationInterceptor)
            .addPathPatterns("/protected/**");
    }
}
// AgeOnceServlet.java
@WebServlet("/verify")
public class VerifyServlet extends HttpServlet {
    
    @Inject
    private AgeOnceService ageOnceService;
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        var result = ageOnceService.generateVerifyUrl();
        
        HttpSession session = request.getSession();
        session.setAttribute("oauth_state", result.state());
        
        response.sendRedirect(result.url());
    }
}

@WebServlet("/callback")
public class CallbackServlet extends HttpServlet {
    
    @Inject
    private AgeOnceService ageOnceService;
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        String code = request.getParameter("code");
        String state = request.getParameter("state");
        
        HttpSession session = request.getSession();
        String expectedState = (String) session.getAttribute("oauth_state");
        
        if (!state.equals(expectedState)) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid state");
            return;
        }
        
        try {
            Map<String, Object> tokenData = ageOnceService.exchangeCodeForToken(code, state);
            String accessToken = (String) tokenData.get("access_token");

            Map<String, Object> result = ageOnceService.validateToken(accessToken);

            if (Boolean.TRUE.equals(result.get("valid")) && Boolean.TRUE.equals(result.get("age_verified"))) {
                session.setAttribute("age_verified", true);
                response.sendRedirect(request.getContextPath() + "/protected");
                return;
            }
            
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Age verification failed");
            
        } catch (Exception e) {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
        }
    }
}

// AgeVerificationFilter.java
@WebFilter("/protected/*")
public class AgeVerificationFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        HttpSession session = httpRequest.getSession(false);
        
        if (session == null || !Boolean.TRUE.equals(session.getAttribute("age_verified"))) {
            httpResponse.sendRedirect(httpRequest.getContextPath() + "/verify");
            return;
        }
        
        chain.doFilter(request, response);
    }
}

Local JWT validation

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.JwkProviderBuilder;
import java.net.URL;

@Service
public class AgeOnceJwtValidator {
    
    private final JwkProvider jwkProvider;
    
    public AgeOnceJwtValidator(AgeOnceConfig config) {
        this.jwkProvider = new JwkProviderBuilder(new URL(config.getApiUrl() + "/api/.well-known/jwks.json"))
            .cached(true)
            .build();
    }
    
    public record ValidationResult(boolean valid, Map<String, Object> payload, String error) {}
    
    public ValidationResult validate(String token) {
        try {
            // Issued tokens do not set kid. The published JWK id is key_2025_01.
            var jwk = jwkProvider.get("key_2025_01");
            var algorithm = Algorithm.RSA256((java.security.interfaces.RSAPublicKey) jwk.getPublicKey(), null);
            
            var verifier = JWT.require(algorithm)
                .withIssuer("https://ageonce.io")
                .build();
            
            DecodedJWT verified = verifier.verify(token);
            
            Map<String, Object> payload = Map.of(
                "age_verified", verified.getClaim("age_verified").asBoolean(),
                "age_over", verified.getClaim("age_over").asInt()
            );
            
            return new ValidationResult(true, payload, null);
            
        } catch (Exception e) {
            return new ValidationResult(false, null, e.getMessage());
        }
    }
}

Java provides strong typing and broad enterprise environment support.

On this page