From 810197cc05d81207fbfda86ba8fd1d248d6c3943 Mon Sep 17 00:00:00 2001 From: abraunegg Date: Tue, 3 Jun 2025 07:47:56 +1000 Subject: [PATCH] Implement FR #2693: Implement OAuth2 Device Authorisation Flow (#3313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/actions/spelling/allow.txt | 11 +- config | 3 + docs/application-config-options.md | 15 ++ docs/usage.md | 52 +++- readme.md | 1 + src/config.d | 4 + src/onedrive.d | 408 ++++++++++++++++++++--------- 7 files changed, 367 insertions(+), 127 deletions(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 8482c5d6..ba5b3a39 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -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 diff --git a/config b/config index c897ec3e..9af06bb3 100644 --- a/config +++ b/config @@ -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" diff --git a/docs/application-config-options.md b/docs/application-config-options.md index 5d80f358..d7bec990 100644 --- a/docs/application-config-options.md +++ b/docs/application-config-options.md @@ -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. diff --git a/docs/usage.md b/docs/usage.md index fac3dc41..7cc7bfa7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -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 ``` diff --git a/readme.md b/readme.md index 4f558906..c62dfa45 100644 --- a/readme.md +++ b/readme.md @@ -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 diff --git a/src/config.d b/src/config.d index 57d8a993..954b0e43 100644 --- a/src/config.d +++ b/src/config.d @@ -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"))); diff --git a/src/onedrive.d b/src/onedrive.d index d44d4ec6..a5b956f9 100644 --- a/src/onedrive.d +++ b/src/onedrive.d @@ -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"]);}