From 8b5fd884d3cda62a374cc70726fcf1fa4bf16b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BE=D1=84=D0=B8=D1=8F=20=D0=9A=D0=BE=D1=80=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D0=B2=D1=81=D0=BA=D0=B0=D1=8F?= Date: Tue, 28 Jan 2025 15:30:48 +0500 Subject: [PATCH] oidc --- pom.xml | 2 +- src/main/java/Diadoc/Api/DiadocApi.java | 17 ++- .../java/Diadoc/Api/auth/AuthManager.java | 24 +++- .../Diadoc/Api/auth/AuthenticateClient.java | 6 +- .../Api/auth/CredentialsSchemeFactory.java | 25 ++++ .../Diadoc/Api/auth/DiadocAuthScheme.java | 4 +- ...iadocPreemptiveAuthRequestInterceptor.java | 15 ++- .../java/Diadoc/Api/auth/IAuthManager.java | 11 ++ .../Api/auth/oidc/ContainerTokenProvider.java | 34 +++++ .../Api/auth/oidc/DiadocOidcCredentials.java | 35 +++++ .../Diadoc/Api/auth/oidc/OidcAuthManager.java | 37 +++++ .../Diadoc/Api/auth/oidc/OidcAuthScheme.java | 127 ++++++++++++++++++ .../Api/auth/oidc/OidcAuthenticateClient.java | 45 +++++++ .../Diadoc/Api/auth/oidc/TokenProvider.java | 9 ++ src/main/java/Diadoc/Api/helpers/Tools.java | 9 ++ 15 files changed, 384 insertions(+), 16 deletions(-) create mode 100644 src/main/java/Diadoc/Api/auth/CredentialsSchemeFactory.java create mode 100644 src/main/java/Diadoc/Api/auth/IAuthManager.java create mode 100644 src/main/java/Diadoc/Api/auth/oidc/ContainerTokenProvider.java create mode 100644 src/main/java/Diadoc/Api/auth/oidc/DiadocOidcCredentials.java create mode 100644 src/main/java/Diadoc/Api/auth/oidc/OidcAuthManager.java create mode 100644 src/main/java/Diadoc/Api/auth/oidc/OidcAuthScheme.java create mode 100644 src/main/java/Diadoc/Api/auth/oidc/OidcAuthenticateClient.java create mode 100644 src/main/java/Diadoc/Api/auth/oidc/TokenProvider.java diff --git a/pom.xml b/pom.xml index a68d8d7f..ecf7d67a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 ru.kontur.diadoc diadocsdk - 3.26.1 + 3.27.0 jar diff --git a/src/main/java/Diadoc/Api/DiadocApi.java b/src/main/java/Diadoc/Api/DiadocApi.java index 992d6328..3c34ac7c 100644 --- a/src/main/java/Diadoc/Api/DiadocApi.java +++ b/src/main/java/Diadoc/Api/DiadocApi.java @@ -2,6 +2,9 @@ import Diadoc.Api.auth.AuthManager; import Diadoc.Api.auth.AuthenticateClient; +import Diadoc.Api.auth.IAuthManager; +import Diadoc.Api.auth.oidc.OidcAuthManager; +import Diadoc.Api.auth.oidc.TokenProvider; import Diadoc.Api.counteragent.CounteragentClient; import Diadoc.Api.counteragentGroup.CounteragentGroupClient; import Diadoc.Api.department.DepartmentClient; @@ -56,12 +59,20 @@ public class DiadocApi { private final DiadocHttpClient diadocHttpClient; public DiadocApi(String apiClientId, String url, @Nullable HttpHost proxyHost, @Nullable ConnectionSettings connectionSettings) { + this(new AuthManager(apiClientId), url, proxyHost, connectionSettings); + } + + public DiadocApi(TokenProvider tokenProvider, String url, @Nullable HttpHost proxyHost, @Nullable ConnectionSettings connectionSettings) { + this(new OidcAuthManager(tokenProvider), url, proxyHost, connectionSettings); + } + + private DiadocApi(IAuthManager authManager, String url, @Nullable HttpHost proxyHost, @Nullable ConnectionSettings connectionSettings) { if (url == null) { throw new IllegalArgumentException("url"); } - authManager = new AuthManager(apiClientId); + this.authManager = authManager instanceof AuthManager ? (AuthManager) authManager : null; diadocHttpClient = new DiadocHttpClient(authManager.getCredentialsProvider(), url, proxyHost, connectionSettings); - authClient = new AuthenticateClient(authManager, diadocHttpClient); + authClient = authManager.createAuthenticateClient(diadocHttpClient); organizationClient = new OrganizationClient(diadocHttpClient); departmentClient = new DepartmentClient(diadocHttpClient); employeeClient = new EmployeeClient(diadocHttpClient); @@ -83,7 +94,6 @@ public DiadocApi(String apiClientId, String url, @Nullable HttpHost proxyHost, @ employeePowerOfAttorneyClient = new EmployeePowerOfAttorneyClient(diadocHttpClient); documentWorkflowClient = new DocumentWorkflowClient(diadocHttpClient); operatorClient = new OperatorClient(diadocHttpClient); - authManager.setCredentials(null); } public DiadocApi(String apiClientId, String url) { @@ -183,6 +193,7 @@ public DocumentWorkflowClient getDocumentWorkflowClient() { return documentWorkflowClient; } + @Deprecated public AuthManager getAuthManager() { return authManager; } diff --git a/src/main/java/Diadoc/Api/auth/AuthManager.java b/src/main/java/Diadoc/Api/auth/AuthManager.java index 6e44a9ae..33940f82 100644 --- a/src/main/java/Diadoc/Api/auth/AuthManager.java +++ b/src/main/java/Diadoc/Api/auth/AuthManager.java @@ -1,20 +1,32 @@ package Diadoc.Api.auth; +import Diadoc.Api.helpers.Tools; +import Diadoc.Api.httpClient.DiadocHttpClient; import org.apache.http.auth.AuthScope; import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; -public class AuthManager { - private boolean isAuthenticated = false; - private String apiClientId; - private CredentialsProvider credentialsProvider; +public class AuthManager implements IAuthManager { + private volatile boolean isAuthenticated; + private final String apiClientId; + private final CredentialsProvider credentialsProvider; public AuthManager(String apiClientId) { + if (Tools.isNullOrEmpty(apiClientId)) { + throw new IllegalArgumentException("ApiClientId cannot be empty or null"); + } this.apiClientId = apiClientId; + isAuthenticated = false; credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, new DiadocCredentials(apiClientId, null)); } + @Override + public AuthenticateClient createAuthenticateClient(DiadocHttpClient httpClient) { + return new AuthenticateClient(this, httpClient); + } + + @Override public CredentialsProvider getCredentialsProvider() { return credentialsProvider; } @@ -24,11 +36,11 @@ public boolean isAuthenticated() { } public void setCredentials(String authToken) { - isAuthenticated = (authToken != null); + isAuthenticated = authToken != null; credentialsProvider.setCredentials(AuthScope.ANY, new DiadocCredentials(apiClientId, authToken)); } - public void clearCredentials(){ + public void clearCredentials() { isAuthenticated = false; credentialsProvider.setCredentials(AuthScope.ANY, new DiadocCredentials(apiClientId, null)); } diff --git a/src/main/java/Diadoc/Api/auth/AuthenticateClient.java b/src/main/java/Diadoc/Api/auth/AuthenticateClient.java index 1a7da7dd..324628ce 100644 --- a/src/main/java/Diadoc/Api/auth/AuthenticateClient.java +++ b/src/main/java/Diadoc/Api/auth/AuthenticateClient.java @@ -21,8 +21,8 @@ public class AuthenticateClient { private static final String V_3_AUTHENTICATE = "/V3/Authenticate"; - private AuthManager authManager; - private DiadocHttpClient diadocHttpClient; + private final AuthManager authManager; + private final DiadocHttpClient diadocHttpClient; public AuthenticateClient(AuthManager authManager, DiadocHttpClient diadocHttpClient) { this.authManager = authManager; @@ -142,4 +142,4 @@ public ExternalServiceAuthInfo getExternalServiceAuthInfo(String key) throws Dia throw new DiadocSdkException(ex); } } -} +} \ No newline at end of file diff --git a/src/main/java/Diadoc/Api/auth/CredentialsSchemeFactory.java b/src/main/java/Diadoc/Api/auth/CredentialsSchemeFactory.java new file mode 100644 index 00000000..b425dab0 --- /dev/null +++ b/src/main/java/Diadoc/Api/auth/CredentialsSchemeFactory.java @@ -0,0 +1,25 @@ +package Diadoc.Api.auth; + +import Diadoc.Api.auth.oidc.DiadocOidcCredentials; +import Diadoc.Api.auth.oidc.OidcAuthScheme; +import org.apache.http.auth.AuthScheme; +import org.apache.http.auth.Credentials; + +import java.util.Map; +import java.util.function.Supplier; + +public class CredentialsSchemeFactory { + + private static final Map, Supplier> SCHEME_SUPPLIERS = Map.of( + DiadocOidcCredentials.class, OidcAuthScheme::new, + DiadocCredentials.class, DiadocAuthScheme::new + ); + + public AuthScheme createScheme(Credentials credentials) { + Supplier schemeSupplier = SCHEME_SUPPLIERS.get(credentials.getClass()); + if (schemeSupplier == null) { + throw new IllegalArgumentException("Unsupported credentials type: " + credentials.getClass().getName()); + } + return schemeSupplier.get(); + } +} diff --git a/src/main/java/Diadoc/Api/auth/DiadocAuthScheme.java b/src/main/java/Diadoc/Api/auth/DiadocAuthScheme.java index e841ae30..048ee703 100644 --- a/src/main/java/Diadoc/Api/auth/DiadocAuthScheme.java +++ b/src/main/java/Diadoc/Api/auth/DiadocAuthScheme.java @@ -14,7 +14,7 @@ @Contract() public class DiadocAuthScheme extends RFC2617Scheme { - private boolean complete; + private volatile boolean complete; /** * Default constructor for the Diadoc authentication scheme. @@ -121,4 +121,4 @@ public static Header authenticate(final DiadocCredentials credentials) { return new BufferedHeader(buffer); } -} +} \ No newline at end of file diff --git a/src/main/java/Diadoc/Api/auth/DiadocPreemptiveAuthRequestInterceptor.java b/src/main/java/Diadoc/Api/auth/DiadocPreemptiveAuthRequestInterceptor.java index f48f27d9..867e5719 100644 --- a/src/main/java/Diadoc/Api/auth/DiadocPreemptiveAuthRequestInterceptor.java +++ b/src/main/java/Diadoc/Api/auth/DiadocPreemptiveAuthRequestInterceptor.java @@ -3,6 +3,7 @@ import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpRequestInterceptor; +import org.apache.http.auth.AuthScheme; import org.apache.http.auth.AuthScope; import org.apache.http.auth.AuthState; import org.apache.http.auth.Credentials; @@ -13,6 +14,12 @@ public class DiadocPreemptiveAuthRequestInterceptor implements HttpRequestInterceptor { + private final CredentialsSchemeFactory credentialsSchemeMapper; + + public DiadocPreemptiveAuthRequestInterceptor() { + this.credentialsSchemeMapper = new CredentialsSchemeFactory(); + } + public void process(final HttpRequest request, final HttpContext context) { AuthState authState = (AuthState) context.getAttribute(HttpClientContext.TARGET_AUTH_STATE); // If no auth scheme has been initialized yet @@ -24,8 +31,14 @@ public void process(final HttpRequest request, final HttpContext context) { Credentials credentials = credentialsProvider.getCredentials(authScope); // If found, generate BasicScheme preemptively if (credentials != null) { - authState.update(new DiadocAuthScheme(), credentials); + updateAuthState(authState, credentials); } } } + + private void updateAuthState(AuthState authState, Credentials credentials) { + AuthScheme scheme = credentialsSchemeMapper.createScheme(credentials); + authState.update(scheme, credentials); + } + } diff --git a/src/main/java/Diadoc/Api/auth/IAuthManager.java b/src/main/java/Diadoc/Api/auth/IAuthManager.java new file mode 100644 index 00000000..d5ab1d08 --- /dev/null +++ b/src/main/java/Diadoc/Api/auth/IAuthManager.java @@ -0,0 +1,11 @@ +package Diadoc.Api.auth; + +import Diadoc.Api.httpClient.DiadocHttpClient; +import org.apache.http.client.CredentialsProvider; + +public interface IAuthManager { + + AuthenticateClient createAuthenticateClient(DiadocHttpClient httpClient); + + CredentialsProvider getCredentialsProvider(); +} diff --git a/src/main/java/Diadoc/Api/auth/oidc/ContainerTokenProvider.java b/src/main/java/Diadoc/Api/auth/oidc/ContainerTokenProvider.java new file mode 100644 index 00000000..451c0282 --- /dev/null +++ b/src/main/java/Diadoc/Api/auth/oidc/ContainerTokenProvider.java @@ -0,0 +1,34 @@ +package Diadoc.Api.auth.oidc; + +import java.util.Objects; + +/** + * Default implementation + */ +public class ContainerTokenProvider implements TokenProvider { + + private volatile String token; + + @Override + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ContainerTokenProvider that = (ContainerTokenProvider) o; + return Objects.equals(token, that.token); + } + + @Override + public int hashCode() { + return Objects.hashCode(token); + } + +} diff --git a/src/main/java/Diadoc/Api/auth/oidc/DiadocOidcCredentials.java b/src/main/java/Diadoc/Api/auth/oidc/DiadocOidcCredentials.java new file mode 100644 index 00000000..f58e982e --- /dev/null +++ b/src/main/java/Diadoc/Api/auth/oidc/DiadocOidcCredentials.java @@ -0,0 +1,35 @@ +package Diadoc.Api.auth.oidc; + +import org.apache.http.annotation.Contract; +import org.apache.http.annotation.ThreadingBehavior; +import org.apache.http.auth.BasicUserPrincipal; +import org.apache.http.auth.Credentials; + +import java.security.Principal; + +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class DiadocOidcCredentials implements Credentials { + + private final TokenProvider tokenProvider; + private final Principal userPrincipal; + + public DiadocOidcCredentials(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + this.userPrincipal = new BasicUserPrincipal("DefaultUser"); + } + + + public TokenProvider getTokenProvider() { + return tokenProvider; + } + + @Override + public Principal getUserPrincipal() { + return userPrincipal; + } + + @Override + public String getPassword() { + return ""; + } +} diff --git a/src/main/java/Diadoc/Api/auth/oidc/OidcAuthManager.java b/src/main/java/Diadoc/Api/auth/oidc/OidcAuthManager.java new file mode 100644 index 00000000..88e12309 --- /dev/null +++ b/src/main/java/Diadoc/Api/auth/oidc/OidcAuthManager.java @@ -0,0 +1,37 @@ +package Diadoc.Api.auth.oidc; + +import Diadoc.Api.auth.AuthenticateClient; +import Diadoc.Api.auth.IAuthManager; +import Diadoc.Api.httpClient.DiadocHttpClient; +import org.apache.http.auth.AuthScope; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.impl.client.BasicCredentialsProvider; + +import java.util.Objects; + +public class OidcAuthManager implements IAuthManager { + + private final TokenProvider tokenProvider; + private final CredentialsProvider credentialsProvider; + + public OidcAuthManager(TokenProvider tokenProvider) { + Objects.requireNonNull(tokenProvider, "tokenProvider cannot be null"); + this.tokenProvider = tokenProvider; + credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, new DiadocOidcCredentials(tokenProvider)); + } + + @Override + public AuthenticateClient createAuthenticateClient(DiadocHttpClient httpClient) { + return new OidcAuthenticateClient(); + } + + @Override + public CredentialsProvider getCredentialsProvider() { + return credentialsProvider; + } + + public TokenProvider getTokenProvider() { + return tokenProvider; + } +} diff --git a/src/main/java/Diadoc/Api/auth/oidc/OidcAuthScheme.java b/src/main/java/Diadoc/Api/auth/oidc/OidcAuthScheme.java new file mode 100644 index 00000000..5aa85636 --- /dev/null +++ b/src/main/java/Diadoc/Api/auth/oidc/OidcAuthScheme.java @@ -0,0 +1,127 @@ +package Diadoc.Api.auth.oidc; + +import Diadoc.Api.helpers.Tools; +import org.apache.http.Header; +import org.apache.http.HttpRequest; +import org.apache.http.annotation.Contract; +import org.apache.http.auth.AUTH; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.MalformedChallengeException; +import org.apache.http.impl.auth.RFC2617Scheme; +import org.apache.http.message.BufferedHeader; +import org.apache.http.util.CharArrayBuffer; + + +@Contract() +public class OidcAuthScheme extends RFC2617Scheme { + + private volatile boolean complete; + + /** + * Default constructor for the Diadoc authentication scheme. + */ + public OidcAuthScheme() { + super(); + } + + + /** + * Returns textual designation of the OIDC authentication scheme. + * + * @return OIDC + */ + @Override + public String getSchemeName() { + return "OIDC"; + } + + /** + * Processes the challenge. + * + * @param header the challenge header + * @throws org.apache.http.auth.MalformedChallengeException + * is thrown if the authentication challenge + * is malformed + */ + @Override + public void processChallenge(final Header header) throws MalformedChallengeException { + super.processChallenge(header); + complete = true; + } + + /** + * Returns false. OIDC authentication scheme is request based. + * + * @return false. + */ + @Override + public boolean isConnectionBased() { + return false; + } + + /** + * Tests if the authentication process has been completed. + * + * @return true if authorization has been processed, + * false otherwise. + */ + @Override + public boolean isComplete() { + return this.complete; + } + + /** + * Produces standart authorization header for the given set of {@link org.apache.http.auth.Credentials}. + * + * @param credentials The set of credentials to be used for authentication; must be of type DiadocOidcCredentials + * @param request The request being authenticated + * @return a OIDC authorization string + * @throws org.apache.http.auth.AuthenticationException + * if authorization string cannot + * be generated due to an authentication failure + */ + @Override + public Header authenticate(Credentials credentials, HttpRequest request) throws AuthenticationException { + if (credentials == null) { + throw new AuthenticationException("Credentials may not be null"); + } + if (!(credentials instanceof DiadocOidcCredentials)) { + throw new AuthenticationException("Credentials must be of type DiadocOidcCredentials"); + } + if (request == null) { + throw new AuthenticationException("HTTP request may not be null"); + } + + return authenticate((DiadocOidcCredentials) credentials); + } + + /** + * Returns a Diadoc Authorization header value for the given + * {@link DiadocOidcCredentials}. + * + * @param credentials The credentials to encode. + * @return a Diadoc authorization header + */ + private Header authenticate(final DiadocOidcCredentials credentials) throws AuthenticationException { + if (credentials == null) { + throw new IllegalArgumentException("Credentials may not be null"); + } + + String bearerToken = credentials.getTokenProvider().getToken(); + if (Tools.isNullOrEmpty(bearerToken)) { + throw new AuthenticationException("Bearer token may not be null or empty"); + } + + String prefix = ": Bearer "; + int capacity = AUTH.WWW_AUTH_RESP.length() + prefix.length() + bearerToken.length(); + + CharArrayBuffer buffer = new CharArrayBuffer(capacity); + buffer.append(AUTH.WWW_AUTH_RESP); + buffer.append(prefix); + buffer.append(bearerToken); + + return new BufferedHeader(buffer); + } +} + diff --git a/src/main/java/Diadoc/Api/auth/oidc/OidcAuthenticateClient.java b/src/main/java/Diadoc/Api/auth/oidc/OidcAuthenticateClient.java new file mode 100644 index 00000000..f0780b90 --- /dev/null +++ b/src/main/java/Diadoc/Api/auth/oidc/OidcAuthenticateClient.java @@ -0,0 +1,45 @@ +package Diadoc.Api.auth.oidc; + +import Diadoc.Api.Proto.ExternalServiceAuthInfoProtos; +import Diadoc.Api.auth.AuthenticateClient; +import Diadoc.Api.exceptions.DiadocSdkException; + +import java.security.cert.X509Certificate; + +public class OidcAuthenticateClient extends AuthenticateClient { + + public OidcAuthenticateClient() { + super(null, null); + } + + @Override + public void authenticate(String sid) throws DiadocSdkException { + throw new DiadocSdkException("Cannot determine Diadoc authentication operations for OIDC authentication."); + } + + @Override + public void authenticate(String login, String password) throws DiadocSdkException { + throw new DiadocSdkException("Cannot determine Diadoc authentication operations for OIDC authentication."); + } + + @Override + public void authenticate(X509Certificate currentCert, boolean autoConfirm) throws DiadocSdkException { + throw new DiadocSdkException("Cannot determine Diadoc authentication operations for OIDC authentication."); + } + + @Override + public void authenticate(X509Certificate currentCert) throws DiadocSdkException { + throw new DiadocSdkException("Cannot determine Diadoc authentication operations for OIDC authentication."); + } + + @Override + public void confirmAuthenticationByCertificate(X509Certificate currentCert, String token) throws DiadocSdkException { + throw new DiadocSdkException("Cannot determine Diadoc authentication operations for OIDC authentication."); + } + + @Override + public ExternalServiceAuthInfoProtos.ExternalServiceAuthInfo getExternalServiceAuthInfo(String key) throws DiadocSdkException { + throw new DiadocSdkException("Cannot determine Diadoc authentication operations for OIDC authentication."); + } + +} diff --git a/src/main/java/Diadoc/Api/auth/oidc/TokenProvider.java b/src/main/java/Diadoc/Api/auth/oidc/TokenProvider.java new file mode 100644 index 00000000..e0d8cecd --- /dev/null +++ b/src/main/java/Diadoc/Api/auth/oidc/TokenProvider.java @@ -0,0 +1,9 @@ +package Diadoc.Api.auth.oidc; + +/** + * This interface is expected to have a thread-safe implementation. + * Implementations must ensure safe concurrent access when retrieving tokens. + */ +public interface TokenProvider { + String getToken(); +} diff --git a/src/main/java/Diadoc/Api/helpers/Tools.java b/src/main/java/Diadoc/Api/helpers/Tools.java index dcf04064..5f7290f0 100644 --- a/src/main/java/Diadoc/Api/helpers/Tools.java +++ b/src/main/java/Diadoc/Api/helpers/Tools.java @@ -5,12 +5,17 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.Random; import Diadoc.Api.Proto.Forwarding.ForwardedDocumentProtos; +import Diadoc.Api.crypt.TokenDecryptManager; +import Diadoc.Api.crypt.exceptions.TokenDecryptException; import com.google.protobuf.ByteString; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.apache.http.NameValuePair; @@ -101,4 +106,8 @@ public static List getForwardedDocumentIdParameters(ForwardedDocu return params; } + public static String getDecryptedToken(byte[] encryptedToken, X509Certificate currentCert) throws TokenDecryptException { + return StringUtils.newStringUtf8(Base64.encodeBase64(TokenDecryptManager.decryptToken(encryptedToken, currentCert))); + } + }