mirror of
https://github.com/rclone/rclone.git
synced 2024-11-23 00:43:49 +01:00
box: add options to get access token via JWT auth
This commit is contained in:
parent
1934426789
commit
4788545b05
@ -202,3 +202,23 @@ type CommitUpload struct {
|
|||||||
ContentModifiedAt Time `json:"content_modified_at"`
|
ContentModifiedAt Time `json:"content_modified_at"`
|
||||||
} `json:"attributes"`
|
} `json:"attributes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfigJSON defines the shape of a box config.json
|
||||||
|
type ConfigJSON struct {
|
||||||
|
BoxAppSettings AppSettings `json:"boxAppSettings"`
|
||||||
|
EnterpriseID string `json:"enterpriseID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppSettings defines the shape of the boxAppSettings within box config.json
|
||||||
|
type AppSettings struct {
|
||||||
|
ClientID string `json:"clientID"`
|
||||||
|
ClientSecret string `json:"clientSecret"`
|
||||||
|
AppAuth AppAuth `json:"appAuth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppAuth defines the shape of the appAuth within boxAppSettings in config.json
|
||||||
|
type AppAuth struct {
|
||||||
|
PublicKeyID string `json:"publicKeyID"`
|
||||||
|
PrivateKey string `json:"privateKey"`
|
||||||
|
Passphrase string `json:"passphrase"`
|
||||||
|
}
|
||||||
|
@ -11,8 +11,12 @@ package box
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -21,6 +25,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/lib/jwtutil"
|
||||||
|
|
||||||
|
"github.com/youmark/pkcs8"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rclone/rclone/backend/box/api"
|
"github.com/rclone/rclone/backend/box/api"
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
@ -29,12 +37,14 @@ import (
|
|||||||
"github.com/rclone/rclone/fs/config/configstruct"
|
"github.com/rclone/rclone/fs/config/configstruct"
|
||||||
"github.com/rclone/rclone/fs/config/obscure"
|
"github.com/rclone/rclone/fs/config/obscure"
|
||||||
"github.com/rclone/rclone/fs/fserrors"
|
"github.com/rclone/rclone/fs/fserrors"
|
||||||
|
"github.com/rclone/rclone/fs/fshttp"
|
||||||
"github.com/rclone/rclone/fs/hash"
|
"github.com/rclone/rclone/fs/hash"
|
||||||
"github.com/rclone/rclone/lib/dircache"
|
"github.com/rclone/rclone/lib/dircache"
|
||||||
"github.com/rclone/rclone/lib/oauthutil"
|
"github.com/rclone/rclone/lib/oauthutil"
|
||||||
"github.com/rclone/rclone/lib/pacer"
|
"github.com/rclone/rclone/lib/pacer"
|
||||||
"github.com/rclone/rclone/lib/rest"
|
"github.com/rclone/rclone/lib/rest"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/jws"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -49,6 +59,7 @@ const (
|
|||||||
listChunks = 1000 // chunk size to read directory listings
|
listChunks = 1000 // chunk size to read directory listings
|
||||||
minUploadCutoff = 50000000 // upload cutoff can be no lower than this
|
minUploadCutoff = 50000000 // upload cutoff can be no lower than this
|
||||||
defaultUploadCutoff = 50 * 1024 * 1024
|
defaultUploadCutoff = 50 * 1024 * 1024
|
||||||
|
tokenURL = "https://api.box.com/oauth2/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Globals
|
// Globals
|
||||||
@ -73,10 +84,35 @@ func init() {
|
|||||||
Description: "Box",
|
Description: "Box",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(name string, m configmap.Mapper) {
|
Config: func(name string, m configmap.Mapper) {
|
||||||
err := oauthutil.Config("box", name, m, oauthConfig)
|
jsonFile, ok := m.Get("box_config_file")
|
||||||
|
boxSubType, boxSubTypeOk := m.Get("box_sub_type")
|
||||||
|
var err error
|
||||||
|
if ok && boxSubTypeOk && jsonFile != "" && boxSubType != "" {
|
||||||
|
boxConfig, err := getBoxConfig(jsonFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token: %v", err)
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
}
|
}
|
||||||
|
privateKey, err := getDecryptedPrivateKey(boxConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
|
}
|
||||||
|
claims, err := getClaims(boxConfig, boxSubType)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
|
}
|
||||||
|
signingHeaders := getSigningHeaders(boxConfig)
|
||||||
|
queryParams := getQueryParams(boxConfig)
|
||||||
|
client := fshttp.NewClient(fs.Config)
|
||||||
|
err = jwtutil.Config("box", name, claims, signingHeaders, queryParams, privateKey, m, client)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to configure token with jwt authentication: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = oauthutil.Config("box", name, m, oauthConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to configure token with oauth authentication: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Options: []fs.Option{{
|
Options: []fs.Option{{
|
||||||
Name: config.ConfigClientID,
|
Name: config.ConfigClientID,
|
||||||
@ -84,6 +120,19 @@ func init() {
|
|||||||
}, {
|
}, {
|
||||||
Name: config.ConfigClientSecret,
|
Name: config.ConfigClientSecret,
|
||||||
Help: "Box App Client Secret\nLeave blank normally.",
|
Help: "Box App Client Secret\nLeave blank normally.",
|
||||||
|
}, {
|
||||||
|
Name: "box_config_file",
|
||||||
|
Help: "Box App config.json location\nLeave blank normally.",
|
||||||
|
}, {
|
||||||
|
Name: "box_sub_type",
|
||||||
|
Default: "user",
|
||||||
|
Examples: []fs.OptionExample{{
|
||||||
|
Value: "user",
|
||||||
|
Help: "Rclone should act on behalf of a user",
|
||||||
|
}, {
|
||||||
|
Value: "enterprise",
|
||||||
|
Help: "Rclone should act on behalf of a service account",
|
||||||
|
}},
|
||||||
}, {
|
}, {
|
||||||
Name: "upload_cutoff",
|
Name: "upload_cutoff",
|
||||||
Help: "Cutoff for switching to multipart upload (>= 50MB).",
|
Help: "Cutoff for switching to multipart upload (>= 50MB).",
|
||||||
@ -98,6 +147,74 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getBoxConfig(configFile string) (boxConfig *api.ConfigJSON, err error) {
|
||||||
|
file, err := ioutil.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "box: failed to read Box config")
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(file, &boxConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "box: failed to parse Box config")
|
||||||
|
}
|
||||||
|
return boxConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClaims(boxConfig *api.ConfigJSON, boxSubType string) (claims *jws.ClaimSet, err error) {
|
||||||
|
val, err := jwtutil.RandomHex(20)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "box: failed to generate random string for jti")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims = &jws.ClaimSet{
|
||||||
|
Iss: boxConfig.BoxAppSettings.ClientID,
|
||||||
|
Sub: boxConfig.EnterpriseID,
|
||||||
|
Aud: tokenURL,
|
||||||
|
Iat: time.Now().Unix(),
|
||||||
|
Exp: time.Now().Add(time.Second * 45).Unix(),
|
||||||
|
PrivateClaims: map[string]interface{}{
|
||||||
|
"box_sub_type": boxSubType,
|
||||||
|
"aud": tokenURL,
|
||||||
|
"jti": val,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSigningHeaders(boxConfig *api.ConfigJSON) *jws.Header {
|
||||||
|
signingHeaders := &jws.Header{
|
||||||
|
Algorithm: "RS256",
|
||||||
|
Typ: "JWT",
|
||||||
|
KeyID: boxConfig.BoxAppSettings.AppAuth.PublicKeyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return signingHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
func getQueryParams(boxConfig *api.ConfigJSON) map[string]string {
|
||||||
|
queryParams := map[string]string{
|
||||||
|
"client_id": boxConfig.BoxAppSettings.ClientID,
|
||||||
|
"client_secret": boxConfig.BoxAppSettings.ClientSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDecryptedPrivateKey(boxConfig *api.ConfigJSON) (key *rsa.PrivateKey, err error) {
|
||||||
|
|
||||||
|
block, rest := pem.Decode([]byte(boxConfig.BoxAppSettings.AppAuth.PrivateKey))
|
||||||
|
if len(rest) > 0 {
|
||||||
|
return nil, errors.Wrap(err, "box: extra data included in private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
rsaKey, err := pkcs8.ParsePKCS8PrivateKey(block.Bytes, []byte(boxConfig.BoxAppSettings.AppAuth.Passphrase))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "box: failed to decrypt private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rsaKey.(*rsa.PrivateKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Options defines the configuration for this backend
|
// Options defines the configuration for this backend
|
||||||
type Options struct {
|
type Options struct {
|
||||||
UploadCutoff fs.SizeSuffix `config:"upload_cutoff"`
|
UploadCutoff fs.SizeSuffix `config:"upload_cutoff"`
|
||||||
|
@ -12,7 +12,8 @@ Paths are specified as `remote:path`
|
|||||||
Paths may be as deep as required, eg `remote:directory/subdirectory`.
|
Paths may be as deep as required, eg `remote:directory/subdirectory`.
|
||||||
|
|
||||||
The initial setup for Box involves getting a token from Box which you
|
The initial setup for Box involves getting a token from Box which you
|
||||||
need to do in your browser. `rclone config` walks you through it.
|
can do either in your browser, or with a config.json downloaded from Box
|
||||||
|
to use JWT authentication. `rclone config` walks you through it.
|
||||||
|
|
||||||
Here is an example of how to make a remote called `remote`. First run:
|
Here is an example of how to make a remote called `remote`. First run:
|
||||||
|
|
||||||
@ -38,6 +39,13 @@ Box App Client Id - leave blank normally.
|
|||||||
client_id>
|
client_id>
|
||||||
Box App Client Secret - leave blank normally.
|
Box App Client Secret - leave blank normally.
|
||||||
client_secret>
|
client_secret>
|
||||||
|
Box App config.json location
|
||||||
|
Leave blank normally.
|
||||||
|
Enter a string value. Press Enter for the default ("").
|
||||||
|
config_json>
|
||||||
|
'enterprise' or 'user' depending on the type of token being requested.
|
||||||
|
Enter a string value. Press Enter for the default ("user").
|
||||||
|
box_sub_type>
|
||||||
Remote config
|
Remote config
|
||||||
Use auto config?
|
Use auto config?
|
||||||
* Say Y if not sure
|
* Say Y if not sure
|
||||||
|
Loading…
Reference in New Issue
Block a user