From ddffe0ca885f6682a55658c8edcf5f66cf7098da Mon Sep 17 00:00:00 2001 From: Marcin Wieckowski Date: Tue, 21 Apr 2026 11:06:40 +0200 Subject: [PATCH] Switch from basic to oauth2 authorization + if charset missing in response, assume utf8. --- .../main/java/org/opensky/api/OpenSkyApi.java | 95 ++++++++++++++----- 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/java/src/main/java/org/opensky/api/OpenSkyApi.java b/java/src/main/java/org/opensky/api/OpenSkyApi.java index 27fc909..4689a16 100644 --- a/java/src/main/java/org/opensky/api/OpenSkyApi.java +++ b/java/src/main/java/org/opensky/api/OpenSkyApi.java @@ -14,6 +14,7 @@ import java.net.MalformedURLException; import java.nio.charset.Charset; import java.util.*; +import java.util.concurrent.TimeUnit; /** * Main class of the OpenSky Network API. Instances retrieve data from OpenSky via HTTP @@ -25,6 +26,7 @@ public class OpenSkyApi { private static final String API_ROOT = "https://" + HOST + "/api"; private static final String STATES_URI = API_ROOT + "/states/all"; private static final String MY_STATES_URI = API_ROOT + "/states/own"; + private static final String TOKEN_URL = "https://auth." + HOST + "/auth/realms/opensky-network/protocol/openid-connect/token"; private enum REQUEST_TYPE { GET_STATES, @@ -38,20 +40,65 @@ private enum REQUEST_TYPE { private final OkHttpClient okHttpClient; private final Map lastRequestTime; - private static class BasicAuthInterceptor implements Interceptor { - private final String credentials; + private static class OAuth2Interceptor implements Interceptor { + private final String clientId; + private final String clientSecret; + private final OkHttpClient tokenClient = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(); + private final ObjectMapper tokenMapper = new ObjectMapper(); + + private volatile String accessToken; + private volatile long tokenExpiryMs = 0; + + OAuth2Interceptor(String clientId, String clientSecret) { + this.clientId = clientId; + this.clientSecret = clientSecret; + } - BasicAuthInterceptor(String username, String password) { - credentials = Credentials.basic(username, password); + private synchronized void refreshIfNeeded() throws IOException { + if (accessToken != null && System.currentTimeMillis() < tokenExpiryMs - 60_000) { + return; + } + fetchToken(); + } + + @SuppressWarnings("unchecked") + private synchronized void fetchToken() throws IOException { + RequestBody body = new FormBody.Builder() + .add("grant_type", "client_credentials") + .add("client_id", clientId) + .add("client_secret", clientSecret) + .build(); + Request request = new Request.Builder().url(TOKEN_URL).post(body).build(); + try (Response response = tokenClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("OAuth2 token request failed with HTTP " + response.code()); + } + Map json = tokenMapper.readValue(response.body().byteStream(), Map.class); + accessToken = (String) json.get("access_token"); + long expiresIn = ((Number) json.get("expires_in")).longValue(); + tokenExpiryMs = System.currentTimeMillis() + expiresIn * 1000; + } } @Override public Response intercept(Chain chain) throws IOException { - Request req = chain.request() - .newBuilder() - .header("Authorization", credentials) + refreshIfNeeded(); + Request req = chain.request().newBuilder() + .header("Authorization", "Bearer " + accessToken) .build(); - return chain.proceed(req); + Response response = chain.proceed(req); + if (response.code() == 401) { + response.close(); + fetchToken(); + req = chain.request().newBuilder() + .header("Authorization", "Bearer " + accessToken) + .build(); + return chain.proceed(req); + } + return response; } } @@ -63,27 +110,26 @@ public OpenSkyApi() { } /** - * Create an instance of the API for authenticated access - * @param username an OpenSky username - * @param password an OpenSky password for the given username + * Create an instance of the API for authenticated access using OAuth2 client credentials. + * @param clientId the OAuth2 client ID + * @param clientSecret the OAuth2 client secret */ - public OpenSkyApi(String username, String password) { + public OpenSkyApi(String clientId, String clientSecret) { lastRequestTime = new HashMap<>(); - // set up JSON mapper mapper = new ObjectMapper(); SimpleModule sm = new SimpleModule(); sm.addDeserializer(OpenSkyStates.class, new OpenSkyStatesDeserializer()); mapper.registerModule(sm); - authenticated = username != null && password != null; + authenticated = clientId != null && clientSecret != null; - if (authenticated) { - okHttpClient = new OkHttpClient.Builder() - .addInterceptor(new BasicAuthInterceptor(username, password)) - .build(); - } else { - okHttpClient = new OkHttpClient(); - } + if (authenticated) { + okHttpClient = new OkHttpClient.Builder() + .addInterceptor(new OAuth2Interceptor(clientId, clientSecret)) + .build(); + } else { + okHttpClient = new OkHttpClient(); + } } /** Make the actual HTTP Request and return the parsed response @@ -119,11 +165,10 @@ private OpenSkyStates getResponse(String baseUri, Collection