Java and Kotlin
Package: jp.co.tenon:tenon-auth-sdk:1.0.0, Java 17+.
- Dependencies: none, except the
org.jspecify:jspecifyannotations, so Kotlin sees which values may be null. - Thread safety: the client is thread-safe; make it a singleton (a Spring
@Bean).
Install
Section titled “Install”Download the jar from Downloads, check its SHA-256, and add it:
dependencies { implementation(files("libs/tenon-auth-sdk-1.0.0.jar")) implementation("org.jspecify:jspecify:1.0.0")}Configure (Spring Boot)
Section titled “Configure (Spring Boot)”@BeanTenonAuthClient tenonAuth(@Value("${tenon-auth.url}") String url, @Value("${tenon-auth.api-key}") String apiKey) { return TenonAuthClient.builder().baseUrl(url).apiKey(apiKey).build();}Options:
connectTimeout(5 s) andreadTimeout(30 s);header(name, value), e.g. for a proxy;httpClient(HttpClient)for your own proxy or company CA.
The builder refuses http:// except to localhost, and any key that isn’t an application key (tnk_…).
Sign in (Spring MVC)
Section titled “Sign in (Spring MVC)”@PostMapping("/login")String login(LoginForm form, HttpServletRequest request, HttpSession session) { Account account = accounts.checkPassword(form.username(), form.password()); // yours SecondStep step; try { step = tenonAuth.beginSecondStep(account.id(), new SignInContext(request.getRemoteAddr(), request.getHeader("User-Agent")), cookieValue(request, "tenon_device"), "ja"); } catch (TenonAuthUnavailableException e) { return emergency.allows(account) ? signIn(session, account) : "login-unavailable"; // fail closed } session.setAttribute("secondStep", step); // SecondStep is Serializable return switch (step.kind()) { case CODE_ENTRY -> "redirect:/login/code"; case PUSH_WAIT -> "redirect:/login/approve"; case RECOVERY_ONLY -> "redirect:/login/recover"; case TRUSTED, NO_METHOD -> signIn(session, account); };}
@PostMapping("/login/code")String code(@RequestParam String code, HttpSession session) { SecondStep step = (SecondStep) session.getAttribute("secondStep"); SecondStepResult result = tenonAuth.completeWithCode(step, code); return switch (result.status()) { case APPROVED -> signIn(session, step); case WRONG_CODE -> "redirect:/login/code?wrong=" + result.attemptsRemaining(); default -> "redirect:/login"; // FAILED, EXPIRED: start again };}The waiting page calls tenonAuth.pollApproval(step) every 2 seconds. The recovery page calls tenonAuth.completeWithRecoveryCode(step, code).
Kotlin
Section titled “Kotlin”Records read as properties (step.kind, user.id), and nullability is visible:
val step = tenonAuth.beginSecondStep(account.id, SignInContext(ip, userAgent))val number: Int? = step.numberErrors
Section titled “Errors”Every call throws a TenonAuthException subclass:
TenonAuthUnavailableException;ApiKeyRejectedException,ForbiddenException;InvalidRequestException,NotFoundException,InvalidCodeException;FactorLockedException,TooManyRequestsException;EmailRefusedException,EmailUnavailableException;ConflictException.
See Errors.