Implement FR #2693: Implement OAuth2 Device Authorisation Flow (#3313)

Adds support for Microsoft’s OAuth2 Device Authorisation Flow, enabling the client to authenticate using device and user codes on a second device. This is particularly useful for headless or limited-input environments where interactive browser login is not possible.

Includes:
- Initiating device code requests and displaying user instructions
- Polling token endpoint until user authorises the device or the code expires
- Error handling for pending, declined, and expired authorisation states
- Countdown display showing remaining authorisation window

This feature is enabled via the `use_device_auth` config option
This commit is contained in:
abraunegg 2025-06-03 07:47:56 +10:00 committed by GitHub
commit 810197cc05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 366 additions and 126 deletions

View file

@ -7,15 +7,21 @@ abraunegg
accrights
accrightslen
adamdruppe
ADand
addrepo
Adevice
adr
Agrant
ags
Aietf
aip
alex
alpinelinux
annobin
antix
Aoauth
aothmane
Aparams
apng
archlinux
ARequest
@ -83,6 +89,7 @@ debian
dechunk
Deepin
deimos
devicecode
devuan
dhparams
dirmask
@ -108,6 +115,7 @@ eis
ele
endinaness
enduml
Entra
envp
epfd
eselect
@ -154,13 +162,14 @@ gshared
GVariant
hideonindex
hnsecs
hotmail
howto
hskrieg
htons
ietf
idk
idlol
idup
ietf
ifrom
includedir
ine

3
config
View file

@ -178,6 +178,9 @@
## Only upload changes to OneDrive, do not download from cloud.
#upload_only = "false"
## Authenticate using the Microsoft OAuth2 Device Authorisation Flow
#use_device_auth = "true"
## Single Sign-On (SSO) via Intune using the Microsoft Identity Device Broker
#use_intune_sso = "true"

View file

@ -64,6 +64,7 @@ Before reading this document, please ensure you are running application version
- [threads](#threads)
- [transfer_order](#transfer_order)
- [upload_only](#upload_only)
- [use_device_auth](#use_device_auth)
- [use_intune_sso](#use_intune_sso)
- [use_recycle_bin](#use_recycle_bin)
- [user_agent](#user_agent)
@ -1018,6 +1019,20 @@ _**CLI Option Use:**_ `--upload-only`
> [!IMPORTANT]
> To ensure that data deleted locally remains accessible online, you can use the 'no_remote_delete' option. If you want to delete the data from your local storage after a successful upload to Microsoft OneDrive, you can use the 'remove_source_files' option.
### use_device_auth
_**Description:**_ Enable this option to authenticate using the Microsoft OAuth2 Device Authorisation Flow (`device_code` grant). This flow allows the client to initiate a sign-in process without launching a web browser directly — ideal for headless systems or remote sessions. A short code and URL will be provided for the user to complete authentication via a separate browser-enabled device.
_**Value Type:**_ Boolean
_**Default Value:**_ False
_**Config Example:**_ `use_device_auth = "false"` or `use_device_auth = "true"`
_**CLI Option Use:**_ *None - this is a config file option only*
> [!IMPORTANT]
> This option is fully supported for Microsoft Entra ID (Work/School) accounts. For personal Microsoft accounts (e.g., @outlook.com or @hotmail.com), this method of authentication is not supported. Please use the interactive interactive authentication method (default) to authenticate this application.
### use_intune_sso
_**Description:**_ Enable this option to authenticate using Intune Single Sign-On (SSO) via the Microsoft Identity Device Broker over D-Bus. This method is suitable for environments where the system is Intune-enrolled and allows seamless token retrieval without requiring browser interaction.

View file

@ -336,6 +336,7 @@ If you explicitly want to use HTTP/1.1, you can do so by using the `--force-http
> If you continue to use a curl/libcurl version with known HTTP/2 bugs you will experience application runtime issues such as randomly exiting for zero reason or incomplete download/upload of your data.
## First Steps
### Authorise the Application with Your Microsoft OneDrive Account
Once you've installed the application, you'll need to authorise it using your Microsoft OneDrive Account. This can be done by simply running the application without any additional command switches.
@ -344,6 +345,7 @@ Please be aware that some companies may require you to explicitly add this app t
This client supports the following methods to authenticate the application with Microsoft OneDrive:
* Supports interactive browser-based authentication using OAuth2 and a response URI
* Supports seamless Single Sign-On (SSO) via Intune using the Microsoft Identity Device Broker D-Bus interface
* Supports OAuth2 Device Authorisation Flow for Microsoft Entra ID accounts
#### Interactive Authentication using OAuth2 and a response URI
When you run the application for the first time, you'll be prompted to open a specific URL using your web browser, where you'll need to log in to your Microsoft Account and grant the application permission to access your files. After granting permission to the application, you'll be redirected to a blank page. Simply copy the URI from the blank page and paste it into the application.
@ -380,6 +382,54 @@ Intune SSO via Microsoft Identity Broker dbus session usage criteria met - will
> [!NOTE]
> The installation and configuration of Intune for your platform is beyond the scope of this documentation.
#### OAuth2 Device Authorisation Flow for Microsoft Entra ID accounts
To use this method of authentication, you must add the following configuration to your 'config' file:
```
use_device_auth = "true"
```
You will be required to open a URL using a web browser, and enter the code that this application presents:
```
Configuring Global Azure AD Endpoints
Authorise this application by visiting:
https://microsoft.com/devicelogin
Enter the following code when prompted: ABCDEFGHI
This code expires at: 2025-Jun-02 15:27:30
```
You will have ~15 minutes before the code expires.
> [!IMPORTANT]
> #### Limitation: OAuth2 Device Authorization Flow and Personal Microsoft Accounts
>
> While the OneDrive Client for Linux fully supports OAuth2 Device Authorisation Flow (`device_code` grant) for **Microsoft Entra ID (Work/School)** accounts, **Microsoft currently does not allow this flow to be used with personal Microsoft accounts (MSA)** unless the application is explicitly authorised by Microsoft.
>
> **Application Configuration Summary:**
>
> - `signInAudience`: `AzureADandPersonalMicrosoftAccount`
> - `allowPublicClient`: `true`
> - Uses Microsoft Identity Platform v2.0 endpoints (`/devicecode`, `/token`, etc.)
> - Microsoft Graph scopes properly defined
>
> Despite this correct configuration, users signing in with a personal Microsoft account will see the following error:
>
> > **"The code you entered has expired. Get a new code from the device you're trying to sign in to and try again."**
>
> This occurs even if the code is entered immediately. Microsoft redirects the user to:
>
> ```
> https://login.live.com/ppsecure/post.srf?username=......
> ```
>
> This behaviour confirms that Microsoft **blocks the `device_code` grant flow for MSA accounts** on unapproved apps.
>
> **Recommendation:**
> If using a personal Microsoft account (e.g., @outlook.com or @hotmail.com), please complete authentication using the interactive authentication method detailed above.
>
> **Further Reading:**
> 📚 [Microsoft Documentation — OAuth 2.0 device authorization grant](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code)
### Display Your Applicable Runtime Configuration
To verify the configuration that the application will use, use the following command:
@ -1259,7 +1309,7 @@ journalctl --unit=onedrive -f
#### OneDrive service running as a non-root user via systemd (All Linux Distributions)
In some instances, it is preferable to run the OneDrive client as a service without the 'root' user. Follow the instructions below to configure the service for your regular user login.
1. As the user who will run the service, launch the application in standalone mode, authorize it for use, and verify that synchronization is functioning as expected:
1. As the user who will run the service, launch the application in standalone mode, authorise it for use, and verify that synchronisation is functioning as expected:
```text
onedrive --sync --verbose
```

View file

@ -25,6 +25,7 @@ Since forking in early 2018, this client has evolved into a clean re-imagining o
* Supports seamless access to shared folders and files across both OneDrive Personal and OneDrive for Business accounts
* Supports single-tenant and multi-tenant applications
* Supports Intune Single Sign-On (SSO) authentication via the Microsoft Identity Device Broker (D-Bus interface)
* Supports OAuth2 Device Authorisation Flow for Microsoft Entra ID accounts
* Supports national cloud deployments including Microsoft Cloud for US Government, Microsoft Cloud Germany, and Azure/Office 365 operated by VNET in China
* Provides rules for client-side filtering to select data for syncing with Microsoft OneDrive accounts
* Protects against significant data loss on OneDrive after configuration changes

View file

@ -420,6 +420,9 @@ class ApplicationConfig {
// Use authentication via Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session
boolValues["use_intune_sso"] = false;
// Use authentication via OAuth2 Device Authorisation Flow
boolValues["use_device_auth"] = false;
// EXPAND USERS HOME DIRECTORY
// Determine the users home directory.
// Need to avoid using ~ here as expandTilde() below does not interpret correctly when running under init.d or systemd scripts
@ -1557,6 +1560,7 @@ class ApplicationConfig {
// authentication
addLogEntry("Config option 'use_intune_sso' = " ~ to!string(getValueBool("use_intune_sso")));
addLogEntry("Config option 'use_device_auth' = " ~ to!string(getValueBool("use_device_auth")));
// logging and notifications
addLogEntry("Config option 'enable_logging' = " ~ to!string(getValueBool("enable_logging")));

View file

@ -32,24 +32,29 @@ import curlEngine;
import intune;
// Define the 'OneDriveException' class
class OneDriveException: Exception {
class OneDriveException : Exception {
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/errors
int httpStatusCode;
const CurlResponse response;
JSONValue error;
private JSONValue _error;
// Public property to access the JSON error
@property JSONValue error() const {
return _error;
}
this(int httpStatusCode, string reason, const CurlResponse response, string file = __FILE__, size_t line = __LINE__) {
this.httpStatusCode = httpStatusCode;
this.response = response;
this.error = response.json();
string msg = format("HTTP request returned status code %d (%s)\n%s", httpStatusCode, reason, toJSON(error, true));
this._error = response.json();
string msg = format("HTTP request returned status code %d (%s)\n%s", httpStatusCode, reason, toJSON(_error, true));
super(msg, file, line);
}
this(int httpStatusCode, string reason, string file = __FILE__, size_t line = __LINE__) {
this.httpStatusCode = httpStatusCode;
this.response = null;
super(msg, file, line, null);
super(msg, file, line);
}
}
@ -71,6 +76,7 @@ class OneDriveApi {
string clientId = "";
string companyName = "";
string authUrl = "";
string deviceAuthUrl = "";
string redirectUrl = "";
string tokenUrl = "";
string driveUrl = "";
@ -173,10 +179,18 @@ class OneDriveApi {
// Configure the authentication scope
if (appConfig.getValueBool("read_only_auth_scope")) {
// read-only authentication scopes has been requested
authScope = "&scope=Files.Read%20Files.Read.All%20Sites.Read.All%20offline_access&response_type=code&prompt=login&redirect_uri=";
if (appConfig.getValueBool("use_device_auth")) {
authScope = "&scope=Files.Read%20Files.Read.All%20Sites.Read.All%20offline_access";
} else {
authScope = "&scope=Files.Read%20Files.Read.All%20Sites.Read.All%20offline_access&response_type=code&prompt=login&redirect_uri=";
}
} else {
// read-write authentication scopes will be used (default)
authScope = "&scope=Files.ReadWrite%20Files.ReadWrite.All%20Sites.ReadWrite.All%20offline_access&response_type=code&prompt=login&redirect_uri=";
if (appConfig.getValueBool("use_device_auth")) {
authScope = "&scope=Files.ReadWrite%20Files.ReadWrite.All%20Sites.ReadWrite.All%20offline_access";
} else {
authScope = "&scope=Files.ReadWrite%20Files.ReadWrite.All%20Sites.ReadWrite.All%20offline_access&response_type=code&prompt=login&redirect_uri=";
}
}
// Configure Azure AD endpoints if 'azure_ad_endpoint' is configured
@ -190,6 +204,7 @@ class OneDriveApi {
}
// Authentication
authUrl = appConfig.globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize";
deviceAuthUrl = appConfig.globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/devicecode";
redirectUrl = appConfig.globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
tokenUrl = appConfig.globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token";
break;
@ -197,6 +212,7 @@ class OneDriveApi {
if (!appConfig.apiWasInitialised) addLogEntry("Configuring Azure AD for US Government Endpoints");
// Authentication
authUrl = appConfig.usl4AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize";
deviceAuthUrl = appConfig.usl4AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/devicecode";
tokenUrl = appConfig.usl4AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token";
if (clientId == appConfig.defaultApplicationId) {
// application_id == default
@ -225,6 +241,7 @@ class OneDriveApi {
if (!appConfig.apiWasInitialised) addLogEntry("Configuring Azure AD for US Government Endpoints (DOD)");
// Authentication
authUrl = appConfig.usl5AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize";
deviceAuthUrl = appConfig.usl5AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/devicecode";
tokenUrl = appConfig.usl5AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token";
if (clientId == appConfig.defaultApplicationId) {
// application_id == default
@ -253,6 +270,7 @@ class OneDriveApi {
if (!appConfig.apiWasInitialised) addLogEntry("Configuring Azure AD Germany");
// Authentication
authUrl = appConfig.deAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize";
deviceAuthUrl = appConfig.deAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/devicecode";
tokenUrl = appConfig.deAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token";
if (clientId == appConfig.defaultApplicationId) {
// application_id == default
@ -281,6 +299,7 @@ class OneDriveApi {
if (!appConfig.apiWasInitialised) addLogEntry("Configuring AD China operated by VNET");
// Authentication
authUrl = appConfig.cnAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize";
deviceAuthUrl = appConfig.cnAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/devicecode";
tokenUrl = appConfig.cnAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token";
if (clientId == appConfig.defaultApplicationId) {
// application_id == default
@ -410,7 +429,7 @@ class OneDriveApi {
GC.collect();
}
// Authenticate this client against Microsoft OneDrive API
// Authenticate this client against Microsoft OneDrive API using one of the 3 authentication methods this client supports
bool authorise() {
// Has the client been configured to use Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session
if (appConfig.getValueBool("use_intune_sso")) {
@ -493,94 +512,220 @@ class OneDriveApi {
}
}
} else {
// Normal authentication method
char[] response;
// What URL should be presented to the user to access
string url = authUrl ~ "?client_id=" ~ clientId ~ authScope ~ redirectUrl;
// Configure automated authentication if --auth-files authUrl:responseUrl is being used
string authFilesString = appConfig.getValueString("auth_files");
string authResponseString = appConfig.getValueString("auth_response");
if (!authResponseString.empty) {
// read the response from authResponseString
response = cast(char[]) authResponseString;
} else if (authFilesString != "") {
string[] authFiles = authFilesString.split(":");
string authUrl = authFiles[0];
string responseUrl = authFiles[1];
// There are 2 options here for normal authentication flow
// 1. Use OAuth2 Device Authorisation Flow
// 2. Use OAuth2 Interactive Authorisation Flow (application default)
if (appConfig.getValueBool("use_device_auth")) {
// Use OAuth2 Device Authorisation Flow
// * deviceAuthUrl: Should already be configured based on client configuration
// * tokenUrl: Should already be configured based on client configuration
// * authScope: Should already be configured with the correct auth scopes
string deviceAuthPostData = "client_id=" ~ clientId ~ authScope;
try {
auto authUrlFile = File(authUrl, "w");
authUrlFile.write(url);
authUrlFile.close();
} catch (FileException exception) {
// There was a file system error
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
// Must force exit here, allow logging to be done
forceExit();
} catch (ErrnoException exception) {
// There was a file system error
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
// Must force exit here, allow logging to be done
forceExit();
}
addLogEntry("Client requires authentication before proceeding. Waiting for --auth-files elements to be available.");
// Initiating Device Code Request
JSONValue deviceAuthResponse = initiateDeviceAuthorisation(deviceAuthPostData);
while (!exists(responseUrl)) {
Thread.sleep(dur!("msecs")(100));
}
// Was a valid JSON response provided?
if (deviceAuthResponse.type() == JSONType.object) {
// A valid JSON was returned
// Extract required values
string deviceCode = deviceAuthResponse["device_code"].str;
string deviceAuthUrl = deviceAuthResponse["verification_uri"].str;
string userCode = deviceAuthResponse["user_code"].str;
long expiresIn = deviceAuthResponse["expires_in"].integer;
long pollInterval = deviceAuthResponse["interval"].integer;
SysTime expiresAt = Clock.currTime + dur!"seconds"(expiresIn);
expiresAt.fracSecs = Duration.zero;
// Display the required items for the user to action
addLogEntry();
addLogEntry("Authorise this application by visiting:\n", ["consoleOnly"]);
addLogEntry(deviceAuthUrl ~ "\n", ["consoleOnly"]);
addLogEntry("Enter the following code when prompted: " ~ userCode, ["consoleOnly"]);
addLogEntry();
addLogEntry("This code expires at: " ~ to!string(expiresAt), ["consoleOnly"]);
addLogEntry();
// JSON value to store the poll response data
JSONValue deviceAuthPollResponse;
// Construct the polling post submission data
string pollPostData = format(
"client_id=%s&grant_type=urn%%3Aietf%%3Aparams%%3Aoauth%%3Agrant-type%%3Adevice_code&device_code=%s",
clientId,
deviceCode
);
// Poll Microsoft API for authentication to be performed, until the expiry of this device authentication request
while (Clock.currTime < expiresAt) {
// Try the post to poll if the authentication has been done
try {
deviceAuthPollResponse = post(tokenUrl, pollPostData, null, true, "application/x-www-form-urlencoded");
// No error ... break out of the loop so the returned data can be processed
break;
} catch (OneDriveException e) {
// Get the polling error JSON response
JSONValue errorResponse = e.error;
string errorType;
// read response from provided from OneDrive
try {
response = cast(char[]) read(responseUrl);
} catch (OneDriveException exception) {
// exception generated
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
if ("error" in errorResponse) {
errorType = errorResponse["error"].str;
if (errorType == "authorization_pending") {
// Calculate remaining time
Duration timeRemaining = expiresAt - Clock.currTime;
long minutes = timeRemaining.total!"minutes"();
long seconds = timeRemaining.total!"seconds"() % 60;
// Log countdown and status
addLogEntry(format("[%02dm %02ds remaining] Still pending authorisation ...", minutes, seconds));
} else if (errorType == "authorization_declined") {
addLogEntry("Authorisation was declined by the user.");
// return false if we get to this point
// set 'use_device_auth' to false to fall back to interactive authentication flow
appConfig.setValueBool("use_device_auth" , false);
return false;
} else if (errorType == "expired_token") {
addLogEntry("Device code expired before authorisation was completed.");
// return false if we get to this point
// set 'use_device_auth' to false to fall back to interactive authentication flow
appConfig.setValueBool("use_device_auth" , false);
return false;
} else {
addLogEntry("Unhandled error during polling: " ~ errorType);
// return false if we get to this point
// set 'use_device_auth' to false to fall back to interactive authentication flow
appConfig.setValueBool("use_device_auth" , false);
return false;
}
} else {
addLogEntry("Unexpected error response from token polling.");
// return false if we get to this point
// set 'use_device_auth' to false to fall back to interactive authentication flow
appConfig.setValueBool("use_device_auth" , false);
return false;
}
}
// Sleep until next polling interval
Thread.sleep(dur!"seconds"(pollInterval));
}
// Broken out of the polling loop
// Was a valid JSON response provided?
if (deviceAuthPollResponse.type() == JSONType.object) {
// is the required 'access_token' available?
if ("access_token" in deviceAuthPollResponse) {
// We got the applicable access token
addLogEntry("Access token acquired!");
// Process this JSON data
processAuthenticationJSON(deviceAuthPollResponse);
// Return that we are authorised
return true;
}
}
// return false if we get to this point
// set 'use_device_auth' to false to fall back to interactive authentication flow
appConfig.setValueBool("use_device_auth" , false);
return false;
}
// try to remove old files
try {
std.file.remove(authUrl);
std.file.remove(responseUrl);
} catch (FileException exception) {
addLogEntry("Cannot remove files " ~ authUrl ~ " " ~ responseUrl);
} else {
// No valid JSON response was returned
// set 'use_device_auth' to false to fall back to interactive authentication flow
appConfig.setValueBool("use_device_auth" , false);
return false;
}
} else {
// Are we in a --dry-run scenario?
if (!appConfig.getValueBool("dry_run")) {
// No --dry-run is being used
addLogEntry("Authorise this application by visiting:\n", ["consoleOnly"]);
addLogEntry(url ~ "\n", ["consoleOnly"]);
addLogEntry("Enter the response uri from your browser: ", ["consoleOnlyNoNewLine"]);
readln(response);
appConfig.applicationAuthorizeResponseUri = true;
} else {
// The application cannot be authorised when using --dry-run as we have to write out the authentication data, which negates the whole 'dry-run' process
addLogEntry();
addLogEntry("The application requires authorisation, which involves saving authentication data on your system. Application authorisation cannot be completed when using the '--dry-run' option.");
addLogEntry();
addLogEntry("To authorise the application please use your original command without '--dry-run'.");
addLogEntry();
addLogEntry("To exclusively authorise the application without performing any additional actions, do not add '--sync' or '--monitor' to your command line.");
addLogEntry();
forceExit();
}
}
// Use OAuth2 Interactive Authorisation Flow (application default)
char[] response;
// What URL should be presented to the user to access
string url = authUrl ~ "?client_id=" ~ clientId ~ authScope ~ redirectUrl;
// Configure automated authentication if --auth-files authUrl:responseUrl is being used
string authFilesString = appConfig.getValueString("auth_files");
string authResponseString = appConfig.getValueString("auth_response");
// match the authorisation code
auto c = matchFirst(response, r"(?:[\?&]code=)([\w\d-.]+)");
if (c.empty) {
addLogEntry("An empty or invalid response uri was entered");
return false;
if (!authResponseString.empty) {
// read the response from authResponseString
response = cast(char[]) authResponseString;
} else if (authFilesString != "") {
string[] authFiles = authFilesString.split(":");
string authUrl = authFiles[0];
string responseUrl = authFiles[1];
try {
auto authUrlFile = File(authUrl, "w");
authUrlFile.write(url);
authUrlFile.close();
} catch (FileException exception) {
// There was a file system error
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
// Must force exit here, allow logging to be done
forceExit();
} catch (ErrnoException exception) {
// There was a file system error
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
// Must force exit here, allow logging to be done
forceExit();
}
addLogEntry("Client requires authentication before proceeding. Waiting for --auth-files elements to be available.");
while (!exists(responseUrl)) {
Thread.sleep(dur!("msecs")(100));
}
// read response from provided from OneDrive
try {
response = cast(char[]) read(responseUrl);
} catch (OneDriveException exception) {
// exception generated
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
return false;
}
// try to remove old files
try {
std.file.remove(authUrl);
std.file.remove(responseUrl);
} catch (FileException exception) {
addLogEntry("Cannot remove files " ~ authUrl ~ " " ~ responseUrl);
return false;
}
} else {
// Are we in a --dry-run scenario?
if (!appConfig.getValueBool("dry_run")) {
// No --dry-run is being used
addLogEntry("Authorise this application by visiting:\n", ["consoleOnly"]);
addLogEntry(url ~ "\n", ["consoleOnly"]);
addLogEntry("Enter the response uri from your browser: ", ["consoleOnlyNoNewLine"]);
readln(response);
appConfig.applicationAuthorizeResponseUri = true;
} else {
// The application cannot be authorised when using --dry-run as we have to write out the authentication data, which negates the whole 'dry-run' process
addLogEntry();
addLogEntry("The application requires authorisation, which involves saving authentication data on your system. Application authorisation cannot be completed when using the '--dry-run' option.");
addLogEntry();
addLogEntry("To authorise the application please use your original command without '--dry-run'.");
addLogEntry();
addLogEntry("To exclusively authorise the application without performing any additional actions, do not add '--sync' or '--monitor' to your command line.");
addLogEntry();
forceExit();
}
}
// match the authorisation code
auto c = matchFirst(response, r"(?:[\?&]code=)([\w\d-.]+)");
if (c.empty) {
addLogEntry("An empty or invalid response uri was entered");
return false;
}
c.popFront(); // skip the whole match
redeemToken(c.front);
return true;
}
c.popFront(); // skip the whole match
redeemToken(c.front);
return true;
}
}
@ -617,6 +762,12 @@ class OneDriveApi {
}
}
// Initiate OAuth2 Device Authorisation
JSONValue initiateDeviceAuthorisation(string deviceAuthPostData) {
// Device OAuth2 Device Authorisation requires a HTTP POST
return post(deviceAuthUrl, deviceAuthPostData, null, true, "application/x-www-form-urlencoded");
}
// Do we print the current access token
void debugOutputAccessToken() {
if (appConfig.verbosityCount > 1) {
@ -1065,44 +1216,8 @@ class OneDriveApi {
}
if ("access_token" in response) {
appConfig.accessToken = "bearer " ~ strip(response["access_token"].str);
// Do we print the current access token
debugOutputAccessToken();
// Obtain the 'refresh_token' and its expiry
refreshToken = strip(response["refresh_token"].str);
appConfig.accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer());
// Debug this response
if (debugLogging) {addLogEntry("appConfig.accessTokenExpiration = " ~ to!string(appConfig.accessTokenExpiration), ["debug"]);}
if (!dryRun) {
// Update the refreshToken in appConfig so that we can reuse it
if (appConfig.refreshToken.empty) {
// The access token is empty
if (debugLogging) {addLogEntry("Updating appConfig.refreshToken with new refreshToken as appConfig.refreshToken is empty", ["debug"]);}
appConfig.refreshToken = refreshToken;
} else {
// Is the access token different?
if (appConfig.refreshToken != refreshToken) {
// Update the memory version
if (debugLogging) {addLogEntry("Updating appConfig.refreshToken with updated refreshToken", ["debug"]);}
appConfig.refreshToken = refreshToken;
}
}
// try and update the 'refresh_token' file on disk
try {
if (debugLogging) {addLogEntry("Updating 'refresh_token' on disk", ["debug"]);}
std.file.write(appConfig.refreshTokenFilePath, refreshToken);
if (debugLogging) {addLogEntry("Setting file permissions for: " ~ appConfig.refreshTokenFilePath, ["debug"]);}
appConfig.refreshTokenFilePath.setAttributes(appConfig.returnSecureFilePermission());
} catch (FileException exception) {
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
}
}
// Process the response JSON
processAuthenticationJSON(response);
} else {
// Release curl engine
releaseCurlEngine();
@ -1119,6 +1234,49 @@ class OneDriveApi {
forceExit();
}
}
// Process the authentication JSON
private void processAuthenticationJSON(JSONValue response) {
// Use 'access_token' and set in the application configuration
appConfig.accessToken = "bearer " ~ strip(response["access_token"].str);
// Do we print the current access token
debugOutputAccessToken();
// Obtain the 'refresh_token' and its expiry
refreshToken = strip(response["refresh_token"].str);
appConfig.accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer());
// Debug this response
if (debugLogging) {addLogEntry("appConfig.accessTokenExpiration = " ~ to!string(appConfig.accessTokenExpiration), ["debug"]);}
if (!dryRun) {
// Update the refreshToken in appConfig so that we can reuse it
if (appConfig.refreshToken.empty) {
// The access token is empty
if (debugLogging) {addLogEntry("Updating appConfig.refreshToken with new refreshToken as appConfig.refreshToken is empty", ["debug"]);}
appConfig.refreshToken = refreshToken;
} else {
// Is the access token different?
if (appConfig.refreshToken != refreshToken) {
// Update the memory version
if (debugLogging) {addLogEntry("Updating appConfig.refreshToken with updated refreshToken", ["debug"]);}
appConfig.refreshToken = refreshToken;
}
}
// try and update the 'refresh_token' file on disk
try {
if (debugLogging) {addLogEntry("Updating 'refresh_token' on disk", ["debug"]);}
std.file.write(appConfig.refreshTokenFilePath, refreshToken);
if (debugLogging) {addLogEntry("Setting file permissions for: " ~ appConfig.refreshTokenFilePath, ["debug"]);}
appConfig.refreshTokenFilePath.setAttributes(appConfig.returnSecureFilePermission());
} catch (FileException exception) {
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
}
}
}
private void generateNewAccessToken() {
if (debugLogging) {addLogEntry("Need to generate a new access token for Microsoft OneDrive", ["debug"]);}