// Advantech SSO + Java / Spring Boot 3 (spring-boot-starter-web) // // application.properties: // sso.client-id=${SSO_CLIENT_ID} // sso.client-secret=${SSO_CLIENT_SECRET} // sso.app-url=${APP_URL} // server.servlet.session.cookie.same-site=lax // Register APP_URL/auth/callback as the Redirect URI of the service. // Put login.html / index.html in src/main/resources/static/. package com.example.myapp; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.HexFormat; import java.util.Map; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestClient; import org.springframework.web.filter.OncePerRequestFilter; @RestController public class SsoController { static final String SSO = "https://ae-sso.advantech.com"; @Value("${sso.client-id}") String clientId; @Value("${sso.client-secret}") String clientSecret; @Value("${sso.app-url}") String appUrl; private final RestClient http = RestClient.create(); String redirectUri() { return appUrl.replaceAll("/$", "") + "/auth/callback"; } static String enc(String s) { return URLEncoder.encode(s, StandardCharsets.UTF_8); } static String safeNext(String v) { return v != null && v.startsWith("/") && !v.startsWith("//") ? v : "/"; } // ---------- 1. sign in ---------- // /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY service, // the SSO returns straight away (no page, no click); otherwise it comes back with // error=login_required and we show login.html. @GetMapping("/auth/login") public void login(@RequestParam(required = false) String next, @RequestParam(required = false) String silent, HttpSession session, HttpServletResponse res) throws IOException { byte[] b = new byte[16]; new SecureRandom().nextBytes(b); String state = HexFormat.of().formatHex(b); // remember several pending sign-ins (two tabs, a double click ...), newest 5 @SuppressWarnings("unchecked") var pending = (java.util.LinkedHashMap) session.getAttribute("pending"); if (pending == null) pending = new java.util.LinkedHashMap<>(); while (pending.size() >= 5) pending.remove(pending.keySet().iterator().next()); pending.put(state, safeNext(next)); session.setAttribute("pending", pending); res.sendRedirect(SSO + "/authorize?client_id=" + enc(clientId) + "&redirect_uri=" + enc(redirectUri()) + "&response_type=code&state=" + state + ("1".equals(silent) ? "&prompt=none" : "")); } @GetMapping("/auth/callback") @SuppressWarnings("unchecked") public void callback(@RequestParam(defaultValue = "") String code, @RequestParam(defaultValue = "") String state, @RequestParam(required = false) String error, HttpServletRequest req, HttpServletResponse res) throws IOException { HttpSession session = req.getSession(); var pending = (Map) session.getAttribute("pending"); String next = pending == null ? null : pending.remove(state); if (next == null) { res.sendRedirect("/login.html?error=state"); return; } // unknown / reused state if (error != null) { // login_required (silent try) / access_denied res.sendRedirect("/login.html?next=" + enc(next) + ("login_required".equals(error) ? "" : "&error=" + enc(error))); return; } var form = new LinkedMultiValueMap(); form.add("grant_type", "authorization_code"); form.add("code", code); form.add("redirect_uri", redirectUri()); form.add("client_id", clientId); form.add("client_secret", clientSecret); Map body; try { body = http.post().uri(SSO + "/token").contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(form).retrieve().body(Map.class); } catch (Exception e) { res.sendRedirect("/login.html?error=token"); return; } req.changeSessionId(); // new session id after sign-in req.getSession().setAttribute("user", body.get("user")); // sub, email, name, department, picture ... res.sendRedirect(next); } @GetMapping("/auth/logout") public void logout(HttpSession session, HttpServletResponse res) throws IOException { session.invalidate(); res.sendRedirect(SSO + "/logout?redirect_uri=" + enc(appUrl.replaceAll("/$", "") + "/")); } // ---------- 3. user data (used by advantech-sso.js) ---------- @GetMapping("/auth/me") public ResponseEntity me(HttpSession session) { Object user = session.getAttribute("user"); return user == null ? ResponseEntity.status(401).body(Map.of("error", "not signed in")) : ResponseEntity.ok(user); } // ---------- your app ---------- @GetMapping("/api/hello") public Map hello(@SessionAttribute("user") Map user) { return Map.of("message", "Hello " + user.get("name")); } } // ---------- 2. is the user signed in? (every request except the public ones) ---------- @Component class RequireLogin extends OncePerRequestFilter { @Override protected boolean shouldNotFilter(HttpServletRequest req) { String p = req.getRequestURI(); return p.startsWith("/auth/") || p.equals("/login.html") || p.equals("/favicon.ico"); } @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { HttpSession s = req.getSession(false); if (s != null && s.getAttribute("user") != null) { chain.doFilter(req, res); return; } if (req.getRequestURI().startsWith("/api/")) { res.sendError(401); return; } String next = req.getRequestURI() + (req.getQueryString() != null ? "?" + req.getQueryString() : ""); // try single sign-on first; login.html is shown only if that fails res.sendRedirect("/auth/login?silent=1&next=" + URLEncoder.encode(next, StandardCharsets.UTF_8)); } }