FakeJwtController.java

package net.hostsharing.hsadminng.config;

import io.micrometer.core.annotation.Timed;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;

import static net.hostsharing.hsadminng.config.JwtFakeBearer.ACCESS_TOKEN_EXPIRES_IN_SECONDS;
import static net.hostsharing.hsadminng.config.JwtFakeBearer.REFRESH_TOKEN_EXPIRES_IN_SECONDS;

@RestController
@Profile("fake-jwt")
@NoSecurityRequirement
@Slf4j
public class FakeJwtController {

    public static final String TOKEN_PATH = "/fake-jwt/token";

    @PostMapping(value = TOKEN_PATH, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    @Timed("app.config.jwt.token")
    public ResponseEntity<Map<String, Object>> token(
            HttpServletRequest request,
            @RequestParam(name = "username", required = false) String username,
            @RequestParam(name = "password", required = false) String password,
            @RequestParam(name = "grant_type", defaultValue = "password") String grantType,
            @RequestParam(name = "refresh_token", required = false) String refreshToken,
            @RequestParam(defaultValue = "openid profile") String scope) {

        if ("refresh_token".equals(grantType)) {
            return tokenResponse(subjectFromRefreshToken(refreshToken), scope);
        }
        if (!"password".equals(grantType)) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "unsupported grant_type");
        }

        log.info("Fake JWT: Issuing token for user: {}", username);

        // Accept any username/password for local testing
        return tokenResponse(username, scope);
    }

    private ResponseEntity<Map<String, Object>> tokenResponse(final String subject, final String scope) {
        // the fake token endpoint has no group source, so it issues tokens without group claims
        final var accessToken = JwtFakeBearer.accessToken(subject, List.of());
        final var refreshToken = JwtFakeBearer.refreshToken(subject);

        return ResponseEntity.ok(Map.of(
                "access_token", accessToken,
                "refresh_token", refreshToken,
                "token_type", "Bearer",
                "expires_in", ACCESS_TOKEN_EXPIRES_IN_SECONDS,
                "refresh_expires_in", REFRESH_TOKEN_EXPIRES_IN_SECONDS,
                "scope", scope
        ));
    }

    private String subjectFromRefreshToken(final String refreshToken) {
        if (refreshToken == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "refresh_token is required");
        }
        try {
            return JwtFakeBearer.subjectFromRefreshToken(refreshToken);
        } catch (final Exception exc) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid refresh_token", exc);
        }
    }
}