Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion src/main/java/Diadoc/Api/DiadocApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import Diadoc.Api.auth.*;
import Diadoc.Api.auth.oidc.OidcAuthManager;
import Diadoc.Api.auth.oidc.OidcRefreshTokenProvider;
import Diadoc.Api.auth.oidc.TokenProvider;
import Diadoc.Api.counteragent.CounteragentClient;
import Diadoc.Api.counteragentGroup.CounteragentGroupClient;
Expand Down Expand Up @@ -105,6 +106,40 @@ public DiadocApi(String apiClientId, String url) {
this(apiClientId, url, null, null);
}

/**
"Quick start" of authentication according to the OpenID Connect scheme: the access token will be automatically
* received (and cached) by the integrator's refresh token when the API was first accessed. Address
* identity.kontur.ru server is used by default.
*
* @param apiClientId the integrator's client identifier (Client Id from your personal account), also used as OIDC client_id
* @param oidcClientSecret secret key of the integrator application (Client Secret)
* @param oidcRefreshToken Refresh Integrator Token
* @param url address of the Diadoc server
*/
public static DiadocApi createWithOidcRefreshToken(String apiClientId, String oidcClientSecret, String oidcRefreshToken, String url) {
return createWithOidcRefreshToken(apiClientId, oidcClientSecret, oidcRefreshToken, url, null, null, null);
}

/**
* "Quick start" of authentication according to the OpenID Connect scheme with advanced settings.
* See. {@link #createWithOidcRefreshToken(String, String, String, String)}.
*
* @param oidcBaseUrl адрес сервера OpenID Connect, например {@code "https://identity.kontur.ru"}.
* Если {@code null}, используется адрес по умолчанию.
*/
public static DiadocApi createWithOidcRefreshToken(
String apiClientId,
String oidcClientSecret,
String oidcRefreshToken,
String url,
@Nullable String oidcBaseUrl,
@Nullable HttpHost proxyHost,
@Nullable ConnectionSettings connectionSettings) {
TokenProvider tokenProvider = new OidcRefreshTokenProvider(
apiClientId, oidcClientSecret, oidcRefreshToken, oidcBaseUrl, proxyHost, connectionSettings);
return new DiadocApi(tokenProvider, url, proxyHost, connectionSettings);
}

public AuthenticateClient getAuthClient() {
return authClient;
}
Expand Down Expand Up @@ -225,4 +260,4 @@ public DiadocHttpClient getDiadocHttpClient() {
public void setSolutionInfo(String solutionInfo) {
diadocHttpClient.setSolutionInfo(solutionInfo);
}
}
}
118 changes: 118 additions & 0 deletions src/main/java/Diadoc/Api/auth/oidc/OidcAuthenticator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package Diadoc.Api.auth.oidc;

import Diadoc.Api.ConnectionSettings;
import Diadoc.Api.exceptions.DiadocSdkException;
import Diadoc.Api.helpers.Tools;
import com.google.gson.Gson;
import org.apache.http.HttpHost;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

public final class OidcAuthenticator {

private static final String DEFAULT_OIDC_BASE_URL = "https://identity.kontur.ru";
private static final String OIDC_TOKEN_ENDPOINT_PATH = "/connect/token";

private OidcAuthenticator() {
}

public static OidcTokenResponse authenticateWithOidc(String clientId, String clientSecret, String refreshToken) throws DiadocSdkException {
return authenticateWithOidc(clientId, clientSecret, refreshToken, null, null, null);
}

public static OidcTokenResponse authenticateWithOidc(String clientId, String clientSecret, String refreshToken, @Nullable String oidcBaseUrl) throws DiadocSdkException {
return authenticateWithOidc(clientId, clientSecret, refreshToken, oidcBaseUrl, null, null);
}

public static OidcTokenResponse authenticateWithOidc(
String clientId,
String clientSecret,
String refreshToken,
@Nullable String oidcBaseUrl,
@Nullable HttpHost proxyHost,
@Nullable ConnectionSettings connectionSettings) throws DiadocSdkException {
if (Tools.isNullOrEmpty(clientId)) {
throw new IllegalArgumentException("clientId cannot be empty or null");
}
if (Tools.isNullOrEmpty(clientSecret)) {
throw new IllegalArgumentException("clientSecret cannot be empty or null");
}
if (Tools.isNullOrEmpty(refreshToken)) {
throw new IllegalArgumentException("refreshToken cannot be empty or null");
}

String baseUrl = Tools.isNullOrEmpty(oidcBaseUrl) ? DEFAULT_OIDC_BASE_URL : oidcBaseUrl;
return performOidcTokenRequest(baseUrl, clientId, clientSecret, refreshToken, proxyHost, connectionSettings);
}

private static OidcTokenResponse performOidcTokenRequest(
String baseUrl,
String clientId,
String clientSecret,
String refreshToken,
@Nullable HttpHost proxyHost,
@Nullable ConnectionSettings connectionSettings) throws DiadocSdkException {
List<NameValuePair> parameters = new ArrayList<>();
parameters.add(new BasicNameValuePair("grant_type", "refresh_token"));
parameters.add(new BasicNameValuePair("refresh_token", refreshToken));
parameters.add(new BasicNameValuePair("client_id", clientId));
parameters.add(new BasicNameValuePair("client_secret", clientSecret));

HttpPost request = new HttpPost(baseUrl + OIDC_TOKEN_ENDPOINT_PATH);
request.setEntity(new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8));

try (CloseableHttpClient httpClient = buildHttpClient(proxyHost, connectionSettings);
CloseableHttpResponse httpResponse = httpClient.execute(request)) {
String json = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8);

int statusCode = httpResponse.getStatusLine().getStatusCode();
if (statusCode != 200) {
throw new DiadocSdkException(
"OIDC token endpoint returned status " + statusCode + ". Response body: " + json);
}

return parseOidcTokenResponse(json);
} catch (IOException e) {
throw new DiadocSdkException(e);
}
}

private static CloseableHttpClient buildHttpClient(@Nullable HttpHost proxyHost, @Nullable ConnectionSettings connectionSettings) {
HttpClientBuilder httpClientBuilder = HttpClients.custom();

if (connectionSettings != null) {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(connectionSettings.getMaxTotalConnections());
connectionManager.setDefaultMaxPerRoute(connectionSettings.getMaxConnectionsPerRoute());
httpClientBuilder.setConnectionManager(connectionManager);
}

if (proxyHost != null) {
httpClientBuilder.setProxy(proxyHost);
}

return httpClientBuilder.build();
}

private static OidcTokenResponse parseOidcTokenResponse(String json) throws DiadocSdkException {
OidcTokenResponse result = new Gson().fromJson(json, OidcTokenResponse.class);
if (result == null || Tools.isNullOrEmpty(result.getAccessToken())) {
throw new DiadocSdkException("OIDC token endpoint returned response without access_token. Response body: " + json);
}
return result;
}
}
102 changes: 102 additions & 0 deletions src/main/java/Diadoc/Api/auth/oidc/OidcRefreshTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package Diadoc.Api.auth.oidc;

import Diadoc.Api.ConnectionSettings;
import Diadoc.Api.exceptions.DiadocSdkException;
import org.apache.http.HttpHost;
import org.jetbrains.annotations.Nullable;

public class OidcRefreshTokenProvider implements TokenProvider {

private static final long EXPIRATION_SAFETY_MARGIN_MILLIS = 60_000;

private final String clientId;
private final String clientSecret;
private final String refreshToken;
private final String oidcBaseUrl;
private final HttpHost proxyHost;
private final ConnectionSettings connectionSettings;
private final Object tokenRefreshLock = new Object();

private volatile CachedToken cachedToken;

public OidcRefreshTokenProvider(String clientId, String clientSecret, String refreshToken) {
this(clientId, clientSecret, refreshToken, null, null, null);
}

public OidcRefreshTokenProvider(String clientId, String clientSecret, String refreshToken, @Nullable String oidcBaseUrl) {
this(clientId, clientSecret, refreshToken, oidcBaseUrl, null, null);
}

public OidcRefreshTokenProvider(
String clientId,
String clientSecret,
String refreshToken,
@Nullable String oidcBaseUrl,
@Nullable HttpHost proxyHost,
@Nullable ConnectionSettings connectionSettings) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.refreshToken = refreshToken;
this.oidcBaseUrl = oidcBaseUrl;
this.proxyHost = proxyHost;
this.connectionSettings = connectionSettings;
}

@Override
public String getToken() {
CachedToken token = cachedToken;
if (isValid(token)) {
return token.accessToken();
}
synchronized (tokenRefreshLock) {
token = cachedToken;
if (isValid(token)) {
return token.accessToken();
}
return fetchAndCacheToken();
}
}

public String refresh() {
synchronized (tokenRefreshLock) {
return fetchAndCacheToken();
}
}

private static boolean isValid(@Nullable CachedToken token) {
return token != null && System.currentTimeMillis() < token.expiresAtMillis();
}

private String fetchAndCacheToken() {
try {
OidcTokenResponse response = OidcAuthenticator.authenticateWithOidc(
clientId, clientSecret, refreshToken, oidcBaseUrl, proxyHost, connectionSettings);
long expiresAtMillis = System.currentTimeMillis()
+ response.getExpiresIn() * 1000L
- EXPIRATION_SAFETY_MARGIN_MILLIS;
CachedToken token = new CachedToken(response.getAccessToken(), expiresAtMillis);
cachedToken = token;
return token.accessToken();
} catch (DiadocSdkException e) {
throw new OidcTokenRefreshException(e);
}
}

private static final class CachedToken {
private final String accessToken;
private final long expiresAtMillis;

private CachedToken(String accessToken, long expiresAtMillis) {
this.accessToken = accessToken;
this.expiresAtMillis = expiresAtMillis;
}

private String accessToken() {
return accessToken;
}

private long expiresAtMillis() {
return expiresAtMillis;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package Diadoc.Api.auth.oidc;

public class OidcTokenRefreshException extends RuntimeException {

public OidcTokenRefreshException(Throwable cause) {
super("Can't get access-token by refresh-token", cause);
}
}
23 changes: 23 additions & 0 deletions src/main/java/Diadoc/Api/auth/oidc/OidcTokenResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package Diadoc.Api.auth.oidc;

import com.google.gson.annotations.SerializedName;

public class OidcTokenResponse {

@SerializedName("access_token")
private String accessToken;

@SerializedName("token_type")
private String tokenType;

@SerializedName("expires_in")
private int expiresIn;

@SerializedName("refresh_token")
private String refreshToken;

public String getAccessToken() { return accessToken; }
public String getTokenType() { return tokenType; }
public int getExpiresIn() { return expiresIn; }
public String getRefreshToken() { return refreshToken; }
}