Skip to content

Commit 0ac26ff

Browse files
committed
oauth2.go: allow overriding of default scope
organization admin and click api require different scopes. Scopes may be specified in OAuth2Config.AuthURL, JWTConfig.Credential and JWTConfig.UserConsentURL. If no scopes specified, "signature" is assumed. fixed getAgreements path in click/click.go
1 parent 3003699 commit 0ac26ff

File tree

3 files changed

+63
-25
lines changed

3 files changed

+63
-25
lines changed

click/click.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func (s *Service) GetAgreements(clickwrapID string) *GetAgreementsOp {
179179
return &GetAgreementsOp{
180180
Credential: s.credential,
181181
Method: "GET",
182-
Path: strings.Join([]string{"clickwrap", clickwrapID, "users"}, "/"),
182+
Path: strings.Join([]string{"clickwraps", clickwrapID, "users"}, "/"),
183183
QueryOpts: make(url.Values),
184184
Version: clickV1,
185185
}

oauth2.go

+44-22
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,7 @@ type OAuth2Config struct {
9090

9191
// codeGrantConfig creates an oauth2 config for refreshing
9292
// and generating a token.
93-
func (c *OAuth2Config) codeGrantConfig() *oauth2.Config {
94-
scopes := []string{"signature"}
95-
if c.ExtendedLifetime {
96-
scopes = []string{"signature", "extended"}
97-
}
93+
func (c *OAuth2Config) codeGrantConfig(scopes ...string) *oauth2.Config {
9894
return &oauth2.Config{
9995
RedirectURL: c.RedirURL,
10096
ClientID: c.IntegratorKey,
@@ -105,14 +101,31 @@ func (c *OAuth2Config) codeGrantConfig() *oauth2.Config {
105101
}
106102
}
107103

104+
func addUnique(scopes []string, scope string) []string {
105+
for _, val := range scopes {
106+
if val == scope {
107+
return scopes
108+
}
109+
}
110+
return append(scopes, scope)
111+
}
112+
108113
// AuthURL returns a URL to DocuSign's OAuth 2.0 consent page with
109114
// all appropriate query parmeters for starting 3-legged OAuth2Flow.
110115
//
116+
// If scopes are empty, {"signature"} is assumed.
117+
//
111118
// State is a token to protect the user from CSRF attacks. You must
112119
// always provide a non-zero string and validate that it matches the
113120
// the state query parameter on your redirect callback.
114-
func (c *OAuth2Config) AuthURL(state string) string {
115-
cfg := c.codeGrantConfig() // client not needed for this action
121+
func (c *OAuth2Config) AuthURL(state string, scopes ...string) string {
122+
if len(scopes) == 0 {
123+
scopes = []string{"signature"}
124+
}
125+
if c.ExtendedLifetime {
126+
scopes = addUnique(scopes, "extended")
127+
}
128+
cfg := c.codeGrantConfig(scopes...)
116129
opts := make([]oauth2.AuthCodeOption, 0)
117130
if c.Prompt {
118131
opts = append(opts, oauth2.SetAuthURLParam("prompt", "login"))
@@ -133,7 +146,7 @@ func (c *OAuth2Config) AuthURL(state string) string {
133146
// The code will be in the *http.Request.FormValue("code"). Before
134147
// calling Exchange, be sure to validate FormValue("state").
135148
func (c *OAuth2Config) Exchange(ctx context.Context, code string) (*OAuth2Credential, error) {
136-
cfg := c.codeGrantConfig()
149+
cfg := c.codeGrantConfig() // scopes are not passed in this step
137150
// oauth2 exchange
138151
tk, err := cfg.Exchange(ctx, code)
139152
if err != nil {
@@ -152,7 +165,7 @@ func (c *OAuth2Config) Exchange(ctx context.Context, code string) (*OAuth2Creden
152165
}
153166

154167
func (c *OAuth2Config) refresher() func(context.Context, *oauth2.Token) (*oauth2.Token, error) {
155-
cfg := c.codeGrantConfig()
168+
cfg := c.codeGrantConfig() // scopes are not passed in this step
156169
return func(ctx context.Context, tk *oauth2.Token) (*oauth2.Token, error) {
157170
if tk == nil || tk.RefreshToken == "" {
158171
return nil, errors.New("codeGrantRefresher: empty refresh token")
@@ -224,24 +237,33 @@ type JWTConfig struct {
224237
}
225238

226239
// UserConsentURL creates a url allowing a user to consent to impersonation
227-
// https://developers.docusign.com/esign-rest-api/guides/authentication/oauth2-jsonwebtoken#step-1-request-the-authorization-code
228-
func (c *JWTConfig) UserConsentURL(redirectURL string) string {
229-
q := make(url.Values)
230-
q.Set("response_type", "code")
231-
q.Set("scope", "signature impersonation")
232-
q.Set("client_id", c.IntegratorKey)
233-
q.Set("redirect_uri", redirectURL)
240+
// https://developers.docusign.com/esign-rest-api/guides/authentication/obtaining-consent#individual-consent
241+
func (c *JWTConfig) UserConsentURL(redirectURL string, scopes ...string) string {
242+
scopeValue := "signature impersonation"
243+
if len(scopes) > 0 {
244+
scopeValue = strings.Join(addUnique(scopes, "impersonation"), " ")
245+
}
234246
// docusign insists upon %20 not + in scope definition
235-
return demoFlag(c.IsDemo).endpoint().AuthURL + "?" + replacePlus(q.Encode())
247+
return demoFlag(c.IsDemo).endpoint().AuthURL + "?" + replacePlus(url.Values{
248+
"response_type": {"code"},
249+
"scope": {scopeValue},
250+
"client_id": {c.IntegratorKey},
251+
"redirect_uri": {redirectURL},
252+
}.Encode())
236253
}
237254

238-
func (c *JWTConfig) jwtRefresher(apiUserName string, signer jws.Signer) func(ctx context.Context, tk *oauth2.Token) (*oauth2.Token, error) {
255+
func (c *JWTConfig) jwtRefresher(apiUserName string, signer jws.Signer, scopes ...string) func(ctx context.Context, tk *oauth2.Token) (*oauth2.Token, error) {
256+
if len(scopes) == 0 {
257+
scopes = []string{"signature", "impersonation"}
258+
} else {
259+
scopes = addUnique(scopes, "impersonation")
260+
}
239261
cfg := &jwt.Config{
240262
Issuer: c.IntegratorKey,
241263
Signer: signer,
242264
Subject: apiUserName,
243265
Options: c.Options,
244-
Scopes: []string{"signature", "impersonation"},
266+
Scopes: scopes,
245267
Audience: demoFlag(c.IsDemo).tokenURI(),
246268
TokenURL: demoFlag(c.IsDemo).endpoint().TokenURL,
247269
HTTPClientFunc: c.HTTPClientFunc,
@@ -252,16 +274,16 @@ func (c *JWTConfig) jwtRefresher(apiUserName string, signer jws.Signer) func(ctx
252274
}
253275

254276
// Credential returns an *OAuth2Credential. The passed token will be refreshed
255-
// as needed.
256-
func (c *JWTConfig) Credential(apiUserName string, token *oauth2.Token, u *UserInfo) (*OAuth2Credential, error) {
277+
// as needed. If no scopes listed, signature is assumed.
278+
func (c *JWTConfig) Credential(apiUserName string, token *oauth2.Token, u *UserInfo, scopes ...string) (*OAuth2Credential, error) {
257279
signer, err := jws.RS256FromPEM([]byte(c.PrivateKey), c.KeyPairID)
258280
if err != nil {
259281
return nil, err
260282
}
261283
return &OAuth2Credential{
262284
accountID: c.AccountID,
263285
cachedToken: token,
264-
refresher: c.jwtRefresher(apiUserName, signer),
286+
refresher: c.jwtRefresher(apiUserName, signer, scopes...),
265287
cacheFunc: c.CacheFunc,
266288
isDemo: demoFlag(c.IsDemo),
267289
userInfo: u,

oauth2_test.go

+18-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ func TestOuauth2Config_AuthURL(t *testing.T) {
6464
authURL := cfg.AuthURL("STATE")
6565
expectedURL := "https://account-d.docusign.com/oauth/auth?client_id=KEY&redirect_uri=https%3A%2F%2Fwww.example.com%2Ftoken&response_type=code&scope=signature&state=STATE"
6666
if authURL != expectedURL {
67-
t.Fatalf("expected %s; got %s", expectedURL, authURL)
67+
t.Errorf("expected %s; got %s", expectedURL, authURL)
68+
return
6869
}
6970

7071
// check for %20 replacement
@@ -74,7 +75,22 @@ func TestOuauth2Config_AuthURL(t *testing.T) {
7475
authURL = cfg.AuthURL("STATE")
7576
expectedURL = "https://account-d.docusign.com/oauth/auth?client_id=KEY&prompt=login&redirect_uri=https%3A%2F%2Fwww.example.com%2Ftoken&response_type=code&scope=signature%20extended&state=STATE&ui_locales=en-us"
7677
if authURL != expectedURL {
77-
t.Fatalf("expected %s; got %s", expectedURL, authURL)
78+
t.Errorf("expected %s; got %s", expectedURL, authURL)
79+
return
80+
}
81+
82+
cfg.UIlocales = nil
83+
cfg.Prompt = false
84+
authURL = cfg.AuthURL("STATE", "ASCOPE", "extended")
85+
expectedURL = "https://account-d.docusign.com/oauth/auth?client_id=KEY&redirect_uri=https%3A%2F%2Fwww.example.com%2Ftoken&response_type=code&scope=ASCOPE%20extended&state=STATE"
86+
if authURL != expectedURL {
87+
t.Errorf("expected %s; got %s", expectedURL, authURL)
88+
return
89+
}
90+
authURL = cfg.AuthURL("STATE", "ASCOPE")
91+
if authURL != expectedURL {
92+
t.Errorf("expected %s; got %s", expectedURL, authURL)
93+
return
7894
}
7995
}
8096

0 commit comments

Comments
 (0)