Skip to content

Commit 7bb93d1

Browse files
committed
Support both auth styles
1 parent 661f5f0 commit 7bb93d1

6 files changed

Lines changed: 145 additions & 45 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"license": "MIT",
55
"description": "React code example with OAuth Implicit grant and DocuSign API. It redirects to the OAuth IdP.",
66
"homepage_comment": "<homepage> is used by the build process as the build target URL path",
7-
"homepage": "react-oauth-redirect-docusign/build",
7+
"homepage": "react-oauth-docusign/build",
88
"private": true,
99
"dependencies": {
1010
"@testing-library/jest-dom": "^5.11.4",

public/config_example.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,34 @@ var config = {};
44
//
55
// Development: Add to the public directory as config.js.
66
//
7-
// Production: Add to the build directory after the build is complete as config.js.
7+
// Production: Add to the build directory after the build is
8+
// complete as config.js.
9+
10+
// Your app's URL
11+
config.DS_APP_URL='http://localhost:3000/react-oauth-new-tab-docusign/build';
12+
// development url default is 'http://localhost:3000/react-oauth-new-tab-docusign/build';
13+
14+
// If config.DS_REDIRECT_AUTHENTICATION is true, then add a
15+
// redirect URI to the integration key of DS_APP_URL
16+
//
17+
// If config.DS_REDIRECT_AUTHENTICATION is true, then add a
18+
// redirect URI to the integration key of
19+
// DS_APP_URL/oauthResponse.html
820

921
config.DS_CLIENT_ID='xxxx-xxxx-xxxx-xxxx-xxxxxxx';
1022
config.IMPLICIT_SCOPES='signature';
1123

12-
// Your DocuSign eSignature REST API private CORS proxy
13-
config.DS_API_CORS_PROXY='https://your_private_proxy.xxx.xxx';
14-
config.DS_API_CORS_PROXY_FOR='https://demo.docusign.net';
24+
// DocuSign Identity server
25+
config.DS_IDP='https://account-d.docusign.com';
1526

16-
// Your app's URL
17-
config.DS_APP_URL='http://localhost:3000';
27+
// Your private API CORS proxies
28+
config.DS_API_CORS_PROXIES= {
29+
'https://na2.docusign.net' : 'https://xxxproxy.example.com',
30+
'https://demo.docusign.net': 'https://xxxproxy.example.com',
31+
}
32+
33+
// redirect authentication?
34+
// true: the app redirects to the IdP
35+
// false: the app opens a new tab for authentication, then closes it
36+
config.DS_REDIRECT_AUTHENTICATION=true;
1837
config.DS_DEBUG=true;
19-
config.DS_IDP='https://account-d.docusign.com';
20-
config.DS_AUTHENTICATION='https://account-d.docusign.com';

public/oauthResponse.html

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta
6+
name="description"
7+
content="DocuSign OAuth response page"
8+
/>
9+
<title>OAuth response handler</title>
10+
</head>
11+
<body>
12+
<h2>Please close this tab or window.</h2>
13+
<noscript>You need to enable JavaScript to run this app.</noscript>
14+
<script>
15+
const msg = {source: 'oauthResponse', hash: window.location.hash || ''}
16+
if (window.opener) {
17+
window.opener.postMessage(msg, '*');
18+
} else {
19+
window.parent.postMessage(msg, '*');
20+
}
21+
</script>
22+
</body>
23+
</html>

src/App.js

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,56 @@ class App extends React.Component {
4747
this.formEmailChange = this.formEmailChange.bind(this);
4848
this.sendEnvelope = this.sendEnvelope.bind(this);
4949
this.getEnvelope = this.getEnvelope.bind(this);
50+
this.receiveMessage = this.receiveMessage.bind(this);
5051
}
5152

5253
/**
5354
* Starting up--if our URL includes a hash, check it to see if
5455
* it's the OAuth response
5556
*/
5657
async componentDidMount() {
57-
const hash = window.location.hash;
58-
if (!hash) {return}
59-
// possible OAuth response
60-
this.setState({working: true, workingMessage: 'Loggin in'});
61-
await this.oAuthImplicit.completeLogin();
62-
this.setState({working: false});
58+
const config = window.config;
59+
// if the url has a query parameter of ?error=logout_request (from a logout operation)
60+
// then remove it
61+
if (window.location.search && window.location.search === '?error=logout_request') {
62+
window.history.replaceState(null, '', config.DS_APP_URL);
63+
}
64+
65+
if (config.DS_REDIRECT_AUTHENTICATION) {
66+
const hash = window.location.hash;
67+
if (!hash) {return}
68+
// possible OAuth response
69+
this.setState({working: true, workingMessage: 'Logging in'});
70+
await this.oAuthImplicit.receiveHash(hash);
71+
this.setState({working: false});
72+
} else {
73+
// await authentication via the new tab
74+
window.addEventListener("message", this.receiveMessage, false);
75+
}
6376
}
77+
78+
/**
79+
* Receive message from a child .
80+
* This method is only used if authentication is done
81+
* in a new tab. See file public/oauthResponse.html
82+
* @param {object} e
83+
*/
84+
async receiveMessage(e) {
85+
const rawSource = e && e.data && e.data.source
86+
, ignore = {'react-devtools-inject-backend': true,
87+
'react-devtools-content-script': true,
88+
'react-devtools-detector': true,
89+
'react-devtools-bridge': true}
90+
, source = (rawSource && !ignore[rawSource]) ? rawSource : false
91+
;
92+
if (!source) {return}; // Ignore if no source field
93+
if (source === 'oauthResponse') {
94+
this.setState({working: true, workingMessage: 'Logging in'});
95+
const hash = e.data && e.data.hash;
96+
await this.oAuthImplicit.receiveHash(hash);
97+
this.setState ({working: false});
98+
}
99+
}
64100

65101
startAuthentication() {
66102
this.oAuthImplicit.startLogin();

src/DocuSign.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class DocuSign {
8282

8383
try {
8484
const url =
85-
`${window.config.DS_API_CORS_PROXY}${urlFrag}` +
85+
`${this.app.state.baseUri}${urlFrag}` +
8686
`/accounts/${this.app.state.accountId}` +
8787
`/envelopes`;
8888
const response = await fetch(url, {
@@ -152,7 +152,7 @@ class DocuSign {
152152
async getEnvelope() {
153153
try {
154154
const url =
155-
`${window.config.DS_API_CORS_PROXY}${urlFrag}` +
155+
`${this.app.state.baseUri}${urlFrag}` +
156156
`/accounts/${this.app.state.accountId}` +
157157
`/envelopes/${this.app.state.responseEnvelopeId}`;
158158
const response = await fetch(url, {

src/OAuthImplicit.js

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
import { toast } from 'react-toastify';
66

7+
const oauthResponseHtml = 'oauthResponse.html'; // only used for new tab auth
78
const expirationBuffer = 10 * 60; // 10 minute buffer
89
const sdkString = 'codeEg_react';
910
const urlFrag = '/restapi/v2.1'; // DocuSign specific
@@ -34,15 +35,15 @@ class OAuthImplicit {
3435
//
3536
constructor(app) {
3637
this.app = app;
38+
this.oauthWindow = null; // only used for new tab auth
3739
}
3840

3941
/**
4042
* Handle incoming OAuth Implicit grant response
4143
*/
42-
async completeLogin() {
44+
async receiveHash(hash) {
4345
const config = window.config;
44-
const hash = window.location.hash
45-
, accessTokenFound = hash && hash.substring(0,14) === '#access_token=';
46+
const accessTokenFound = hash && hash.substring(0,14) === '#access_token=';
4647
if (!accessTokenFound) {return} // EARLY RETURN
4748

4849
// Avoiding an injection attack: check that the hash only includes expected characters
@@ -70,10 +71,18 @@ class OAuthImplicit {
7071
console.error(`OAuth state mismatch!! Expected state: ${oauthStateValue}; received state: ${incomingState}`);
7172
return // EARLY RETURN
7273
}
73-
// hash was good, so erase it from the browser
74-
window.history.replaceState(null, '', config.DS_APP_URL);
7574
window.localStorage.clear(); // clean up
7675

76+
if (config.DS_REDIRECT_AUTHENTICATION) {
77+
// Using redirect the window authentication:
78+
// hash was good, so erase it from the browser
79+
window.history.replaceState(null, '', config.DS_APP_URL);
80+
} else {
81+
// Using new tab authentication:
82+
// close the tab that was used for authentication
83+
if (this.oauthWindow) {this.oauthWindow.close()}
84+
}
85+
7786
// calculate expires
7887
let expires = new Date()
7988
expires.setTime(expires.getTime() + (expiresIn - expirationBuffer)* 1000)
@@ -96,10 +105,8 @@ class OAuthImplicit {
96105
}
97106
//
98107
// Need to select the right proxy for the API call
99-
let baseUri;
100-
if (defaultAccount.base_uri === config.DS_API_CORS_PROXY_FOR) {
101-
baseUri = config.DS_API_CORS_PROXY;
102-
}
108+
// update the baseUri setting
109+
let baseUri = config.DS_API_CORS_PROXIES[defaultAccount.base_uri];
103110
if (!baseUri) {
104111
const msg = `Problem: no proxy for ${defaultAccount.base_uri}.`;
105112
log(msg);
@@ -118,26 +125,44 @@ class OAuthImplicit {
118125
accountId: defaultAccount.account_id,
119126
externalAccountId,
120127
accountName: defaultAccount.account_name,
121-
baseUri: defaultAccount.base_uri,
128+
baseUri: baseUri,
122129
})
123130
}
124131

125132
/**
126133
* Start the login flow by computing the Implicit grant URL
127-
* and redirecting to the URL for the user.
134+
* and either redirecting to the URL for the user or
135+
* creating a new browser tab for the authentication flow
128136
*/
129137
startLogin() {
138+
const config = window.config;
130139
const oauthStateValue = OAuthImplicit.generateId();
131140
window.localStorage.setItem(oauthState, oauthStateValue); // store for when we come back
132-
const url =
133-
`${window.config.DS_IDP}/oauth/auth?` +
134-
`response_type=token&` +
135-
`scope=${window.config.IMPLICIT_SCOPES}&` +
136-
`client_id=${window.config.DS_CLIENT_ID}&` +
137-
`state=${oauthStateValue}&` +
138-
`redirect_uri=${encodeURIComponent(window.config.DS_APP_URL)}`;
141+
let redirectUrl;
142+
if (config.DS_REDIRECT_AUTHENTICATION) {
143+
// Using redirect the window authentication:
144+
redirectUrl = config.DS_APP_URL;
145+
} else {
146+
// Using new tab authentication
147+
redirectUrl = `${config.DS_APP_URL}/${oauthResponseHtml}`;
148+
}
139149

140-
window.location = url;
150+
const url =
151+
`${window.config.DS_IDP}/oauth/auth?` +
152+
`response_type=token&` +
153+
`scope=${window.config.IMPLICIT_SCOPES}&` +
154+
`client_id=${window.config.DS_CLIENT_ID}&` +
155+
`state=${oauthStateValue}&` +
156+
`redirect_uri=${encodeURIComponent(redirectUrl)}`;
157+
158+
if (config.DS_REDIRECT_AUTHENTICATION) {
159+
// Using redirect the window authentication:
160+
window.location = url;
161+
} else {
162+
// Using new tab authentication:
163+
// Create a new tab for authentication
164+
this.oauthWindow = window.open(url, "_blank");
165+
}
141166
}
142167

143168
/**
@@ -146,15 +171,14 @@ class OAuthImplicit {
146171
* browser back to this app
147172
*/
148173
logout () {
174+
const config = window.config;
149175
const url =
150-
`${window.config.DS_IDP}/logout?` +
151-
//`response_type=code&` +
152-
`response_type=token&` +
153-
`scope=${window.config.IMPLICIT_SCOPES}&` +
154-
`client_id=${window.config.DS_CLIENT_ID}&` +
155-
`redirect_uri=${encodeURIComponent(window.config.DS_APP_URL)}&` +
156-
`response_mode=logout_redirect`;
157-
176+
`${window.config.DS_IDP}/logout?` +
177+
`response_type=token&` +
178+
`scope=${config.IMPLICIT_SCOPES}&` +
179+
`client_id=${config.DS_CLIENT_ID}&` +
180+
`redirect_uri=${encodeURIComponent(config.DS_APP_URL)}&` +
181+
`response_mode=logout_redirect`;
158182
window.location = url;
159183
}
160184

@@ -167,7 +191,7 @@ class OAuthImplicit {
167191
let userInfoResponse
168192
try {
169193
userInfoResponse = await fetch(
170-
`${window.config.DS_AUTHENTICATION}/oauth/userinfo`, {
194+
`${window.config.DS_IDP}/oauth/userinfo`, {
171195
headers: new Headers({
172196
Authorization: `Bearer ${this.accessToken}`,
173197
Accept: `application/json`,

0 commit comments

Comments
 (0)