OAuthConfiguration.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.arrow.driver.jdbc.client.oauth;
import com.nimbusds.oauth2.sdk.GrantType;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.SQLException;
import java.util.Locale;
import java.util.Objects;
import org.checkerframework.checker.nullness.qual.Nullable;
/** Configuration class for OAuth settings parsed from connection properties. */
public class OAuthConfiguration {
private final GrantType grantType;
private final URI tokenUri;
private final @Nullable String clientId;
private final @Nullable String clientSecret;
private final @Nullable String scope;
private final @Nullable String subjectToken;
private final @Nullable String subjectTokenType;
private final @Nullable String actorToken;
private final @Nullable String actorTokenType;
private final @Nullable String audience;
private final @Nullable String resource;
private final @Nullable String requestedTokenType;
private OAuthConfiguration(Builder builder) throws SQLException {
this.grantType = builder.grantType;
this.tokenUri = builder.tokenUri;
this.clientId = builder.clientId;
this.clientSecret = builder.clientSecret;
this.scope = builder.scope;
this.subjectToken = builder.subjectToken;
this.subjectTokenType = builder.subjectTokenType;
this.actorToken = builder.actorToken;
this.actorTokenType = builder.actorTokenType;
this.audience = builder.audience;
this.resource = builder.resource;
this.requestedTokenType = builder.requestedTokenType;
validate();
}
private void validate() throws SQLException {
Objects.requireNonNull(grantType, "OAuth grant type is required");
Objects.requireNonNull(tokenUri, "Token URI is required");
if (GrantType.CLIENT_CREDENTIALS.equals(grantType)) {
if (clientId == null || clientId.isEmpty()) {
throw new SQLException("clientId is required for client_credentials flow");
}
if (clientSecret == null || clientSecret.isEmpty()) {
throw new SQLException("clientSecret is required for client_credentials flow");
}
} else if (GrantType.TOKEN_EXCHANGE.equals(grantType)) {
if (subjectToken == null || subjectToken.isEmpty()) {
throw new SQLException("subjectToken is required for token_exchange flow");
}
if (subjectTokenType == null || subjectTokenType.isEmpty()) {
throw new SQLException("subjectTokenType is required for token_exchange flow");
}
} else {
throw new SQLException("Unsupported OAuth grant type: " + grantType);
}
}
/**
* Creates an OAuthTokenProvider based on the configured grant type.
*
* @return the token provider
* @throws SQLException if the grant type is not supported or configuration is invalid
*/
public OAuthTokenProvider createTokenProvider() throws SQLException {
if (GrantType.CLIENT_CREDENTIALS.equals(grantType)) {
return OAuthTokenProviders.clientCredentials()
.tokenUri(tokenUri)
.clientId(clientId)
.clientSecret(clientSecret)
.scope(scope)
.build();
} else if (GrantType.TOKEN_EXCHANGE.equals(grantType)) {
OAuthTokenProviders.TokenExchangeBuilder builder =
OAuthTokenProviders.tokenExchange()
.tokenUri(tokenUri)
.subjectToken(subjectToken)
.subjectTokenType(subjectTokenType)
.actorToken(actorToken)
.actorTokenType(actorTokenType)
.audience(audience)
.requestedTokenType(requestedTokenType)
.scope(scope)
.resource(resource);
if (clientId != null && clientSecret != null) {
builder.clientCredentials(clientId, clientSecret);
}
return builder.build();
} else {
throw new SQLException("Unsupported OAuth grant type: " + grantType);
}
}
/** Builder for OAuthConfiguration. */
public static class Builder {
private GrantType grantType;
private URI tokenUri;
private @Nullable String clientId;
private @Nullable String clientSecret;
private @Nullable String scope;
private @Nullable String subjectToken;
private @Nullable String subjectTokenType;
private @Nullable String actorToken;
private @Nullable String actorTokenType;
private @Nullable String audience;
private @Nullable String resource;
private @Nullable String requestedTokenType;
/**
* Sets the OAuth grant type from a string value.
*
* <p>Accepts either user-friendly names ("client_credentials", "token_exchange") or the full
* URN format as defined in RFC 6749 and RFC 8693.
*
* @param flowStr the flow type string (e.g., "client_credentials", "token_exchange")
* @return this builder
* @throws SQLException if the flow string is invalid
*/
public Builder flow(String flowStr) throws SQLException {
if (flowStr == null || flowStr.isEmpty()) {
throw new SQLException("OAuth flow cannot be null or empty");
}
try {
String normalized = flowStr.toLowerCase(Locale.ROOT);
// Map user-friendly names to URN format for token_exchange
if ("token_exchange".equals(normalized)) {
normalized = GrantType.TOKEN_EXCHANGE.getValue();
}
GrantType parsed = GrantType.parse(normalized);
if (!parsed.equals(GrantType.CLIENT_CREDENTIALS)
&& !parsed.equals(GrantType.TOKEN_EXCHANGE)) {
throw new SQLException("Unsupported OAuth flow: " + flowStr);
}
this.grantType = parsed;
} catch (com.nimbusds.oauth2.sdk.ParseException e) {
throw new SQLException("Invalid OAuth flow: " + flowStr, e);
}
return this;
}
/**
* Sets the token URI.
*
* @param tokenUri the OAuth token endpoint URI
* @return this builder
* @throws SQLException if the URI is invalid
*/
public Builder tokenUri(String tokenUri) throws SQLException {
if (tokenUri == null || tokenUri.isEmpty()) {
throw new SQLException("Token URI cannot be null or empty");
}
try {
this.tokenUri = new URI(tokenUri);
} catch (URISyntaxException e) {
throw new SQLException("Invalid token URI: " + tokenUri, e);
}
return this;
}
public Builder clientId(@Nullable String clientId) {
this.clientId = clientId;
return this;
}
public Builder clientSecret(@Nullable String clientSecret) {
this.clientSecret = clientSecret;
return this;
}
public Builder scope(@Nullable String scope) {
this.scope = scope;
return this;
}
public Builder subjectToken(@Nullable String subjectToken) {
this.subjectToken = subjectToken;
return this;
}
public Builder subjectTokenType(@Nullable String subjectTokenType) {
this.subjectTokenType = subjectTokenType;
return this;
}
public Builder actorToken(@Nullable String actorToken) {
this.actorToken = actorToken;
return this;
}
public Builder actorTokenType(@Nullable String actorTokenType) {
this.actorTokenType = actorTokenType;
return this;
}
public Builder audience(@Nullable String audience) {
this.audience = audience;
return this;
}
public Builder resource(@Nullable String resource) {
this.resource = resource;
return this;
}
public Builder requestedTokenType(@Nullable String requestedTokenType) {
this.requestedTokenType = requestedTokenType;
return this;
}
public OAuthConfiguration build() throws SQLException {
return new OAuthConfiguration(this);
}
}
}