BusinessAccountAuthManager.java
/*
* onedrive-java-sdk - A Java SDK to access OneDrive drives and files.
* Copyright © 2023-2025 Andy Miles (andy.miles@amilesend.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.amilesend.onedrive.connection.auth;
import com.amilesend.onedrive.parse.GsonFactory;
import com.amilesend.onedrive.resource.discovery.DiscoverServiceResponse;
import com.amilesend.onedrive.resource.discovery.Service;
import com.google.common.annotations.VisibleForTesting;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.MediaType.FORM_DATA;
/**
* Manager that is responsible for obtaining and refreshing tokens to interact with a business
* OneDrive account for a specific resource. Note: This does not manage the initial stages of the OAUTH request
* flow and instead relies on a provided auth code or a pre-existing refresh token.
*
* <p>Example initializing with an auth code:</p>
* <pre>
* BusinessAccountAuthManager authManager = BusinessAccountAuthManager.builderWithAuthCode()
* .httpClient(client) // the OKHttpClient instance
* .gsonFactory(gsonFactory) // preconfigured Gson instances
* .clientId(clientId) // the client ID of your application
* .clientSecret(clientSecret) // the client secret of your application
* .redirectUrl(redirectUrl) // the redirect URL for OAUTH flow
* .resourceId(resourceId) // the specific resource identifier
* .authCode(authCode) // The obtained auth code from initial OAUTH handshake
* .buildWithAuthCode();
* </pre>
*
* <p>
* Once the BusinessAccountAuthManager is created, the next step is to discover available services to connect to and
* authorize access to a chosen service:
* <pre>
* List<Service> services = authManager.getServices();
* authManager.authorizeService(services.get(0));
* </pre>
*
* <p>Example initializing with an AuthInfo (pre-existing refresh token):</p>
* <pre>
* BusinessAccountAuthManager authManager = BusinessAccountAuthManager.builderWithAuthInfo()
* .httpClient(client)
* .gsonFactory(gsonFactory)
* .clientId(clientId)
* .clientSecret(clientSecret)
* .redirectUrl(redirectUrl)
* .resourceId(resourceId) // the specific resource identifier
* .authInfo(authInfo) // Instead of an auth code, an AuthInfo object is used to obtain the refresh token
* .buildWithAuthInfo();
* </pre>
* Note: Existing persisted authInfo is only valid for a single resource. If the user is to access a different
* resource than the one that that is persisted with the auth tokens, then new access tokens are required and
* should invoke the {@code builderWithAuthCode()} flow.
*
* @see OneDriveAuthInfo
*/
public class BusinessAccountAuthManager implements OneDriveAuthManager {
private static final String ENDPOINT_SUFFIX = "/_api/v2.0";
private static final String RESOURCE_DISCOVERY_URL = "https://api.office.com/discovery/";
private static final String SERVICE_INFO_URL_SUFFIX = "v2.0/me/services";
private static final String RESOURCE_BODY_PARAM = "resource";
/** The client identifier. */
private final String clientId;
/** The client secret. */
private final String clientSecret;
/** The redirect URL. */
private final String redirectUrl;
/** The underlying HTTP client. */
private final OkHttpClient httpClient;
/** The GSON instance used for JSON serialization. */
private final String authBaseTokenUrl;
/** The URL to query for a list of authorized services. */
private final String discoveryBaseTokenUrl;
private final ReentrantLock lock = new ReentrantLock();
@Getter
@Setter(AccessLevel.PACKAGE)
@VisibleForTesting
private String resourceId;
/**
* The current authentication information.
*/
@Setter(AccessLevel.PACKAGE)
@VisibleForTesting
private volatile OneDriveAuthInfo authInfo;
/**
* Used to initialize and manage authentication for a given auth code.
*/
@Builder(builderClassName = "BuilderWithAuthCode",
buildMethodName = "buildWithAuthCode",
builderMethodName = "builderWithAuthCode")
private BusinessAccountAuthManager(
@NonNull final OkHttpClient httpClient,
final String authBaseTokenUrl,
final String discoveryBaseTokenUrl,
final String clientId,
final String clientSecret,
final String redirectUrl,
final String authCode) {
Validate.notBlank(authCode, "authCode must not be blank");
Validate.notBlank(clientId, "clientId must not be blank");
Validate.notBlank(clientSecret, "clientSecret must not be blank");
Validate.notBlank(redirectUrl, "redirectUrl must not be blank");
this.authBaseTokenUrl = StringUtils.isNotBlank(authBaseTokenUrl) ? authBaseTokenUrl : AUTH_TOKEN_URL;
this.discoveryBaseTokenUrl = StringUtils.isNotBlank(discoveryBaseTokenUrl)
? discoveryBaseTokenUrl
: RESOURCE_DISCOVERY_URL;
this.httpClient = httpClient;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.redirectUrl = redirectUrl;
resourceId = this.discoveryBaseTokenUrl;
authInfo = redeemToken(authCode);
}
/**
* Used to manage authentication for an existing AuthInfo that contains a refresh token.
*/
@Builder(builderClassName = "BuilderWithAuthInfo",
builderMethodName = "builderWithAuthInfo",
buildMethodName = "buildWithAuthInfo")
private BusinessAccountAuthManager(
@NonNull final OkHttpClient httpClient,
final String authBaseTokenUrl,
final String discoveryBaseTokenUrl,
final String clientId,
final String clientSecret,
final String redirectUrl,
@NonNull final OneDriveAuthInfo authInfo) {
Validate.notBlank(clientId, "clientId must not be blank");
Validate.notBlank(clientSecret, "clientSecret must not be blank");
Validate.notBlank(redirectUrl, "redirectUrl must not be blank");
Validate.notBlank(authInfo.getResourceId(), "AuthInfo.resourceId must not be blank");
this.authBaseTokenUrl = StringUtils.isNotBlank(authBaseTokenUrl) ? authBaseTokenUrl : AUTH_TOKEN_URL;
this.discoveryBaseTokenUrl = StringUtils.isNotBlank(discoveryBaseTokenUrl)
? discoveryBaseTokenUrl
: RESOURCE_DISCOVERY_URL;
this.httpClient = httpClient;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.redirectUrl = redirectUrl;
this.authInfo = authInfo;
resourceId = authInfo.getResourceId();
refreshToken();
}
@Override
public boolean isAuthenticated() {
return authInfo != null;
}
@Override
public boolean isExpired() {
if (!isAuthenticated()) {
throw new AuthManagerException("Not authenticated");
}
return System.currentTimeMillis() >= authInfo.getExpiresIn();
}
@Override
public OneDriveAuthInfo getAuthInfo() {
refreshIfExpired();
return authInfo;
}
@Override
public String getAuthenticatedEndpoint() {
if (StringUtils.isBlank(resourceId) || RESOURCE_DISCOVERY_URL.equals(resourceId)) {
throw new AuthManagerException("No valid service resource ID is defined. " +
"You must authenticate with a specific service. Was authenticateService() invoked?");
}
return resourceId + ENDPOINT_SUFFIX;
}
@Override
public OneDriveAuthInfo redeemToken(final String authCode) {
Validate.notBlank(authCode, "authCode must not be blank");
lock.lock();
try {
return authInfo =
OneDriveAuthManager.fetchAuthInfo(httpClient, new Request.Builder()
.url(authBaseTokenUrl)
.header(CONTENT_TYPE, FORM_DATA.toString())
.post(new FormBody.Builder()
.add(CLIENT_ID_BODY_PARAM, clientId)
.add(CLIENT_SECRET_BODY_PARAM, clientSecret)
.add(REDIRECT_URI_BODY_PARAM, redirectUrl)
.add(RESOURCE_BODY_PARAM, resourceId)
.add(AUTH_CODE_BODY_ARAM, authCode)
.add(GRANT_TYPE_BODY_PARAM, AUTH_CODE_GRANT_TYPE_BODY_PARAM_VALUE)
.build())
.build())
.copyWithResourceId(resourceId);
} finally {
lock.unlock();
}
}
@Override
public OneDriveAuthInfo refreshToken() {
lock.lock();
try {
return authInfo =
OneDriveAuthManager.fetchAuthInfo(httpClient, new Request.Builder()
.url(authBaseTokenUrl)
.header(CONTENT_TYPE, FORM_DATA_CONTENT_TYPE)
.post(new FormBody.Builder()
.add(CLIENT_ID_BODY_PARAM, clientId)
.add(CLIENT_SECRET_BODY_PARAM, clientSecret)
.add(REDIRECT_URI_BODY_PARAM, redirectUrl)
.add(RESOURCE_BODY_PARAM, resourceId)
.add(REFRESH_TOKEN_BODY_PARAM, authInfo.getRefreshToken())
.add(GRANT_TYPE_BODY_PARAM, REFRESH_TOKEN_GRANT_TYPE_BODY_PARAM_VALUE)
.build())
.build())
.copyWithResourceId(resourceId);
} finally {
lock.unlock();
}
}
/**
* Fetches the list of services available to the authenticated business user. Note:
* Once a service is selected, you must invoke {@link #authenticateService(Service)} prior to
* instantiating a new connection.
*
* @return the list of services
*/
public List<Service> getServices() {
final Request request = new Request.Builder()
.url(discoveryBaseTokenUrl + SERVICE_INFO_URL_SUFFIX)
.header("Authorization", getAuthInfo().getFullToken())
.header("Accept", "application/json;odata=verbose ")
.get()
.build();
try {
try (final Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new AuthManagerException("Unsuccessful service discovery request: " + response);
}
final String json = response.body().string();
return GsonFactory.getInstanceForServiceDiscovery()
.fromJson(json, DiscoverServiceResponse.class)
.getServices();
}
} catch (final AuthManagerException ex) {
throw ex;
} catch (final Exception ex) {
throw new AuthManagerException("Error fetching service info: " + ex.getMessage(), ex);
}
}
/**
* Authenticates with the given {@code service} and refreshes the auth tokens so that a new
* {@code OneDriveConnection} can be used to access the service.
*
* @param service the service to authenticate
*/
public void authenticateService(@NonNull final Service service) {
Validate.notBlank(service.getServiceResourceId(), "service#getServiceResourceId() must not be blank");
resourceId = service.getServiceResourceId();
refreshToken();
}
}