Skip to content

Java and Kotlin

Package: jp.co.tenon:tenon-auth-sdk:1.0.0, Java 17+.

  • Dependencies: none, except the org.jspecify:jspecify annotations, so Kotlin sees which values may be null.
  • Thread safety: the client is thread-safe; make it a singleton (a Spring @Bean).

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")
}
@Bean
TenonAuthClient 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) and readTimeout (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_…).

@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).

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.number

Every call throws a TenonAuthException subclass:

  • TenonAuthUnavailableException;
  • ApiKeyRejectedException, ForbiddenException;
  • InvalidRequestException, NotFoundException, InvalidCodeException;
  • FactorLockedException, TooManyRequestsException;
  • EmailRefusedException, EmailUnavailableException;
  • ConflictException.

See Errors.