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 ):
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 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 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 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 There are several ways (or steps?) on how it can be done.
- The least intrusive to 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 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 , 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 ( own refresh server) if using custom credentials. This method will need to pass other parameters into , 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 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.