OneDrive using own credentials

Is it possible to use own credentials with OneDrive backend? Can’t find anything relevant on the forum. We can do that with Google drive, but I am not sure how to do that with OD. I can set up working credentials for RClone, and copy token data from there, and it works right away. However, it seems that :d: does not refresh the token, so it expires quickly.

Is there anything that can be done about it?

The problem here is facilitation of token renewals.

The token file generated by duplicacy.com/start_xxx contains URL of a script that is hosted on the same server called /renew_xxx or something along those lines, that is being used in periodic token renewal workflow.

If you issue token from elsewhere – you need to create all the infrastructure for renewals yourself. There is an example in google playgrounds btw if you want to play with it. You can then run it on your own cloud instance.

Google Workspace allows to create “service” accounts that can get permanent credentials, and gchen added support for this types to authentication to duplicacy – so it also “just works” since these don’t require renewals.

I’m not familiar with Microsoft offerings, but I’d assume it won’t be too different, in the sense that unless you can create permanent set of credentials, you would need to facilitate OAUTH token renewals somewhow, and if you don’t want to rely on the web service gchen maintains – you would need to maintain your own.

Well, the thing is, rclone can do token refresh of these credentials without doing anything extra (like creating your own infrastructure for refresh). I haven’t looked at how they’ve done it, but it seems that it should be possible, unless I am missing something.

I mean, I can rig a script that would sync a token from rclone.conf into the odb token file that :d: uses, and it would probably work unless operation duration exceeds token lifetime.

I assume that Microsoft’s Enterprise Applications is a correspondent concept to Google’s Service Accounts, so authorization likely operates similarly.

Yep, rclone starts webservice on a localhost for a duration of refresh.

Perhaps duplicacy could do the same, since it’s already is running a webservice anyway for UI

Don’t both GCD and ODB use the same oauth2 mechanism for tokens? I think you only need webservice for initial authorization of the app, once this is done one way or another (not necessarily in :d:), all you need is refresh tokens, and these are already in the token json files. I think the only thing that is missing is passing through user defined client_id/client_secret into the regular oauth2 refresh…

This is the code that you need in order to run your own authorization server:

This is great, will try it out as it can provide a workaround. But if you already have this code, and it doesn’t look extremely elaborate, is there any reason why it is not incorporated into the main :d: codebase (outside of purely required effort)?

Outside of unnecessary dependency on duplicacy.com to work, current setup potentially constricts usage of OD backend across all :d: users due to throttling, as all of them use the same app id. IIRC MS Graph limits any single app to something like 2000 requests per second across all tenants, there are other per-app limits as well.

1 Like

OK, this did take some finagling, but I managed to get it to work. Here is the standalone go app that can facilitate using own credentials for OneDrive (at the end of this post there are suggestions/requests on how some of this can be incorporated into :d:):

package main

import (
    "fmt"
    "strings"
    "encoding/json"
    "net/http"

    "golang.org/x/oauth2"
)

///////////////////////////
// Config start
//
// From Azure->Enterprise Applications->Overview
const CLIENTID 		string = "xxxxxxxxxxxxxxxxxxxx"
// From Azure->Enterprise Applications->Certificates & secrets
const CLIENTSECRET 	string = "xxxxxxxxxxxxxxxxxxxx"
// localhost if you don't have certificates; need to connect via browser
//const URLBASE		string = "https://your.own.server.FQDN"
const URLBASE		string = "http://localhost"
// Make sure to add URLBASE:SERVERPORT to 
// Azure->Enterprise Applications->Authentication->Web->Redirect URIs
const SERVERPORT	string = "33333"

const CRTFILE		string = "xxxxx.crt" // Only needed for https redirects
const KEYFILE		string = "xxxxx.key" // Only needed for https redirects
//
// Config end
///////////////////////////

const STARTPAGE		string = `
<html lang="en">
  <head>
    <title>OneDrive for Duplicacy</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet">
  </head>
  <body>
    <div class="container">
      <br>
      <p class="lead">
        This web page allows you to download Microsoft OneDrive credentials
        (access token and refresh token) to be used with Duplicacy.
      </p>
      <p class="lead">
        When you're ready to proceed, please click on "Download my credentials"
        below. It will take you to the OneDrive website and ask you to log in to OneDrive and grant
        Duplicacy the permission to back up files to your OneDrive.
      </p>
      <p class="lead">
        You will then receive a file named
        <code>%v</code> which can be supplied to Duplicacy when prompted.
        This web page never logs or saves any information, or in
       anyway interacts with OneDrive on your behalf beyond providing you with a
       token and refreshing that token for you.
      </p>
      <br><br>
      <p class="text-center">
        <a href="%v" type="button" class="btn btn-lg btn-info btn-fill">
          Download my credentials
        </a>
      </p>
    </div>
  </body>
</html>
`

var (
    oneOauthConfig = oauth2.Config{
        ClientID: 		CLIENTID,
        ClientSecret: 	CLIENTSECRET,
        RedirectURL:  	URLBASE + ":" + SERVERPORT + "/odb_oauth",
        Scopes:       	[]string{"Files.ReadWrite", "offline_access"},
        Endpoint: oauth2.Endpoint{
            AuthURL:  "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
            TokenURL: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
        },
    }

	odb_file_name = "odb-" + CLIENTID + "-token.json"
)

// Start page to authorize and download initial token...
func odbStartHandler(w http.ResponseWriter, r *http.Request) {
	url := oneOauthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
    fmt.Fprint(w, fmt.Sprintf(STARTPAGE+"\n", odb_file_name, url))
}

// OauthHandler ...
func odbOauthHandler(w http.ResponseWriter, r *http.Request) {
    token, err := oneOauthConfig.Exchange(r.Context(), r.URL.Query().Get("code"))
    if err != nil {
        http.Error(w, fmt.Sprintf("Error exchanging the code for an access token: %v", err), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.Header().Set("Content-Disposition", "attachment; filename="+odb_file_name)
    if err := json.NewEncoder(w).Encode(token); err != nil {
        http.Error(w, fmt.Sprintf("Error encoding the token in JSON: %v", err), http.StatusInternalServerError)
        return
    }
}

// RefreshHandler ...
func odbRefreshHandler(w http.ResponseWriter, r *http.Request) {
    var token oauth2.Token
    defer r.Body.Close()

    if err := json.NewDecoder(r.Body).Decode(&token); err != nil {
       http.Error(w, fmt.Sprintf("Error decoding the token from the request: %v", err), http.StatusBadRequest)
       return
    }

    tokenSource := oneOauthConfig.TokenSource(r.Context(), &token)
    newToken, err := tokenSource.Token()
    if err != nil {
        http.Error(w, fmt.Sprintf("Error fetching a new token: %v", err), http.StatusBadRequest)
        return
    }

    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    if err := json.NewEncoder(w).Encode(newToken); err != nil {
        http.Error(w, fmt.Sprintf("Error encoding the token in JSON: %v", err), http.StatusInternalServerError)
        return
    }
}


func main() {
	http.HandleFunc("/odb_start",   odbStartHandler)
	http.HandleFunc("/odb_oauth",   odbOauthHandler)
    http.HandleFunc("/odb_refresh", odbRefreshHandler)

    var err error = nil

    if strings.HasPrefix(URLBASE, "https://") {
    	err = http.ListenAndServeTLS(":" + SERVERPORT, CRTFILE, KEYFILE, nil)
    } else {
    	err = http.ListenAndServe(":" + SERVERPORT, nil)
    }
    if err != nil {
        fmt.Printf("Failed to start the server: %v\n", err)
    }
}

Start page is stolen from duplicacy.com/odb_start :wink: Obviously heavily based on the code snipped posted by @gchen above, but that one didn’t work out of the box. More importantly, I ditched autocert (and potentially the whole HTTPS redirect) - which means that you don’t need to run it on a server that is resolvable on the internet. You can run it on your local network or localhost (obviously outgoing connection is still needed).

If going with localhost, need to start with visiting http://localhost:33333/odb_start, it does the same things as the one at duplicacy.com, but will download token for your own application.

Now, all this is for naught if :d: won’t use the new URL for refresh. For that, client.RefreshTokenURL in duplicacy_oneclient.go needs to be changed from https://duplicacy.com/odb_refresh to http://localhost:33333/odb_refresh (if going with localhost). This is possible to accomplish via DNS/iptables manupulation, but really, this is something that needs to be accomodated in the code base. Realistically, odb_refresh is the only endpoint needed during normal :d: operation, as creation of initial token file can be handled externally. One can create a new token via rclone for instance and skip the whole odb_start/odb_auth.

OK, so it would be great to see support for custom credentials for OD incorporated into :d: There are several ways (or steps?) on how it can be done.

  • The least intrusive to :d: code base would be to introduce customization to refresh URLs. Probably along the lines of environment variable per storage, e.g. DUPLICACY_storagename_ODB_REFRESH_URL=http://localhost:33333/odb_refresh. Then the whole server infrastructure can be kept as per above (i.e. separate), while :d: would still be able to refresh tokens properly. Not super user friendly as you’d need to run a separate server.
  • odb_refresh part of the server can be incorporated into :d:, to be run concurrently with the rest of the application. In this case, there is no real need to customize refresh URLs as these will always point to localhost (:d: own refresh server) if using custom credentials. This method will need to pass other parameters into :d:, namely client_id and client_secret, probably using the same environment variable mechanism. But this way it looks much better from the user perspective as they don’t need to run a standalone refresh server at all times.

Initial token creation is probably better to leave out of :d: completely as there is no obvious place for it right now. Initial token creation can always be done with rclone or a standalone app such as above.

1 Like

Actually, it seems that refresh functionality is rather straightforward to support, no need for standalone servers and such. I’ve submitted a pull request with implementation of support for custom credentials for OneDrive: https://github.com/gilbertchen/duplicacy/pull/632 It should be completely transparent for anyone not using custom credentials and backwards compatible with existing functionality (including using duplicacy.com token refresh if no custom credentials are specified).

Hopefully, it can be merged into the main codebase.