Skip to content

Commit ce2d005

Browse files
committed
Updating to support PKCE for Authorization Code Grant
1 parent 6cf09c9 commit ce2d005

10 files changed

+379
-116
lines changed

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ For a list of code examples that use the Web Forms API, see the [How-to guides o
7777
### Prerequisites
7878
**Note:** If you downloaded this code using [Quickstart](https://developers.docusign.com/docs/esign-rest-api/quickstart/) from the Docusign Developer Center, skip items 1 and 2 as they were automatically performed for you.
7979

80-
1. A free [Docusign developer account](https://www.docusign.com/developers/sandbox); create one if you don't already have one.
80+
1. A free [Docusign developer account](https://go.docusign.com/o/sandbox/); create one if you don't already have one.
8181
1. A Docusign app and integration key that is configured to use either [Authorization Code Grant](https://developers.docusign.com/platform/auth/authcode/) or [JWT Grant](https://developers.docusign.com/platform/auth/jwt/) authentication.
8282

8383
This [video](https://www.youtube.com/watch?v=eiRI4fe5HgM) demonstrates how to obtain an integration key.

Diff for: pom.xml

+3-3
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030
<rooms.version>1.4.3</rooms.version>
3131
<click.version>1.5.0</click.version>
3232
<monitor.version>1.4.0</monitor.version>
33-
<admin.version>2.0.0-RC1</admin.version>
34-
<webforms.version>1.0.2-RC12</webforms.version>
35-
<maestro.version>2.0.0-RC1</maestro.version>
33+
<admin.version>2.0.0-RC2</admin.version>
34+
<webforms.version>2.0.0-RC1</webforms.version>
35+
<maestro.version>2.0.0</maestro.version>
3636
<swagger-core-version>2.2.22</swagger-core-version>
3737
<jackson-version>2.17.2</jackson-version>
3838
<jersey2.version>3.1.8</jersey2.version>

Diff for: src/main/java/com/docusign/DSConfiguration.java

+13-3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ public class DSConfiguration {
7070
@Value("${spring.security.oauth2.client.registration.jwt.client-id}")
7171
private String userId;
7272

73+
@Value("${spring.security.oauth2.client.registration.acg.client-secret}")
74+
private String secretUserId;
75+
76+
@Value("${spring.security.oauth2.client.provider.acg.token-uri}")
77+
private String tokenEndpoint;
78+
79+
@Value("${spring.security.oauth2.client.provider.acg.authorization-uri}")
80+
private String authorizationEndpoint;
81+
7382
@Value("${jwt.grant.sso.redirect-url}")
7483
private String jwtRedirectURL;
7584

@@ -158,7 +167,8 @@ public ManifestStructure getCodeExamplesText() {
158167
}
159168

160169
try {
161-
codeExamplesText = new ObjectMapper().readValue(loadFileData(codeExamplesManifest), ManifestStructure.class);
170+
codeExamplesText = new ObjectMapper().readValue(loadFileData(codeExamplesManifest),
171+
ManifestStructure.class);
162172
} catch (Exception e) {
163173
e.printStackTrace();
164174
}
@@ -172,8 +182,8 @@ public String loadFileData(String linkToManifestFile) throws Exception {
172182
httpConnection.setRequestMethod(HttpMethod.GET);
173183

174184
httpConnection.setRequestProperty(
175-
HttpHeaders.CONTENT_TYPE,
176-
String.valueOf(MediaType.APPLICATION_JSON));
185+
HttpHeaders.CONTENT_TYPE,
186+
String.valueOf(MediaType.APPLICATION_JSON));
177187

178188
int responseCode = httpConnection.getResponseCode();
179189

Diff for: src/main/java/com/docusign/WebSecurityConfig.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
1111
import org.springframework.security.web.savedrequest.RequestCache;
1212

13+
import com.docusign.core.security.CustomAuthenticationFailureHandler;
14+
1315
@EnableWebSecurity
1416
public class WebSecurityConfig {
1517

@@ -28,24 +30,22 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
2830
try {
2931
authorize
3032
.antMatchers("/", "/error**", "/assets/**", "/ds/mustAuthenticate**",
31-
"/ds/authenticate**", "/ds/selectApi**", "/con001")
33+
"/ds/authenticate**", "/ds/selectApi**", "/con001", "/pkce")
3234
.permitAll()
3335
.anyRequest().authenticated()
3436
.and()
3537
.exceptionHandling()
3638
.authenticationEntryPoint(
37-
new LoginUrlAuthenticationEntryPoint("/ds/mustAuthenticate")
38-
);
39+
new LoginUrlAuthenticationEntryPoint("/ds/mustAuthenticate"));
3940
} catch (Exception e) {
4041
throw new RuntimeException(e);
4142
}
4243
})
4344
.requestCache().requestCache(requestCache()).and()
44-
.oauth2Login(Customizer.withDefaults())
45+
.oauth2Login(login -> login.failureHandler(new CustomAuthenticationFailureHandler()))
4546
.oauth2Client(Customizer.withDefaults())
4647
.logout(logout -> logout
47-
.logoutSuccessUrl("/")
48-
)
48+
.logoutSuccessUrl("/"))
4949
.csrf().disable();
5050

5151
return http.build();

Diff for: src/main/java/com/docusign/core/controller/IndexController.java

+37-14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.docusign.core.model.AuthType;
88
import com.docusign.core.model.Session;
99
import com.docusign.core.model.User;
10+
import com.docusign.core.security.acg.ACGAuthenticationMethod;
1011
import com.docusign.core.security.jwt.JWTAuthenticationMethod;
1112
import org.apache.commons.lang3.StringUtils;
1213
import org.springframework.beans.factory.annotation.Autowired;
@@ -94,7 +95,7 @@ public String index(ModelMap model, HttpServletResponse response) throws Excepti
9495
}
9596

9697
if (config.getQuickstart().equals("true") && config.getSelectedApiIndex().equals(ApiIndex.ESIGNATURE) &&
97-
!(SecurityContextHolder.getContext().getAuthentication() instanceof OAuth2AuthenticationToken)) {
98+
!(SecurityContextHolder.getContext().getAuthentication() instanceof OAuth2AuthenticationToken)) {
9899
String site = ApiIndex.ESIGNATURE.getPathOfFirstExample();
99100
response.setStatus(response.SC_MOVED_TEMPORARILY);
100101
response.setHeader(LOCATION_HEADER, site);
@@ -112,7 +113,8 @@ public String index(ModelMap model, HttpServletResponse response) throws Excepti
112113
}
113114

114115
@GetMapping(path = "/ds/mustAuthenticate")
115-
public ModelAndView mustAuthenticateController(ModelMap model, HttpServletRequest req, HttpServletResponse resp) throws IOException {
116+
public ModelAndView mustAuthenticateController(ModelMap model, HttpServletRequest req, HttpServletResponse resp)
117+
throws IOException {
116118
model.addAttribute(LAUNCHER_TEXTS, config.getCodeExamplesText().SupportingTexts);
117119
model.addAttribute(ATTR_TITLE, config.getCodeExamplesText().SupportingTexts.LoginPage.LoginButton);
118120

@@ -125,7 +127,8 @@ public ModelAndView mustAuthenticateController(ModelMap model, HttpServletReques
125127
return new ModelAndView(new JWTAuthenticationMethod().loginUsingJWT(config, session, redirectURL));
126128
}
127129

128-
boolean isRedirectToMonitor = redirectURL.toLowerCase().contains("/m") && !redirectURL.toLowerCase().contains("/mae");
130+
boolean isRedirectToMonitor = redirectURL.toLowerCase().contains("/m") &&
131+
!redirectURL.toLowerCase().contains("/mae");
129132
if (session.isRefreshToken() || config.getQuickstart().equals("true")) {
130133
config.setQuickstart("false");
131134

@@ -148,32 +151,52 @@ private ModelAndView checkForMonitorRedirects(String redirectURL) {
148151
return new ModelAndView(new JWTAuthenticationMethod().loginUsingJWT(config, session, redirectURL));
149152
}
150153

154+
@GetMapping("/pkce")
155+
public RedirectView pkce(String code, String state, HttpServletRequest req, HttpServletResponse resp)
156+
throws Exception {
157+
String redirectURL = getRedirectURLForJWTAuthentication(req, resp);
158+
RedirectView redirect;
159+
try {
160+
redirect = new ACGAuthenticationMethod().exchangeCodeForToken(code, config, session, redirectURL);
161+
} catch (Exception e) {
162+
redirect = getRedirectView(AuthType.AGC);
163+
this.session.setIsPKCEWorking(false);
164+
}
165+
166+
return redirect;
167+
}
168+
151169
@PostMapping("/ds/authenticate")
152-
public RedirectView authenticate(ModelMap model, @RequestBody MultiValueMap<String, String> formParams, HttpServletRequest req, HttpServletResponse resp) throws IOException {
170+
public RedirectView authenticate(ModelMap model, @RequestBody MultiValueMap <String, String> formParams,
171+
HttpServletRequest req, HttpServletResponse resp) throws Exception {
153172
if (!formParams.containsKey("selectAuthType")) {
154173
model.addAttribute("message", "Select option with selectAuthType name must be provided.");
155174
return new RedirectView("pages/error");
156175
}
157176

158177
String redirectURL = getRedirectURLForJWTAuthentication(req, resp);
159178

160-
List<String> selectAuthTypeObject = formParams.get("selectAuthType");
179+
List <String> selectAuthTypeObject = formParams.get("selectAuthType");
161180
AuthType authTypeSelected = AuthType.valueOf(selectAuthTypeObject.get(0));
162181

163182
if (authTypeSelected.equals(AuthType.JWT)) {
164183
this.session.setAuthTypeSelected(AuthType.JWT);
165184
return new JWTAuthenticationMethod().loginUsingJWT(config, session, redirectURL);
166185
} else {
167186
this.session.setAuthTypeSelected(AuthType.AGC);
168-
return getRedirectView(authTypeSelected);
187+
if (this.session.getIsPKCEWorking()) {
188+
return new ACGAuthenticationMethod().initiateAuthorization(config);
189+
} else {
190+
return getRedirectView(authTypeSelected);
191+
}
169192
}
170193
}
171194

172195
private String getRedirectURLForJWTAuthentication(HttpServletRequest req, HttpServletResponse resp) {
173196
SavedRequest savedRequest = requestCache.getRequest(req, resp);
174197

175-
String[] examplesCodes = new String[]{
176-
ApiIndex.CLICK.getExamplesPathCode(),
198+
String[] examplesCodes = new String[] {
199+
ApiIndex.CLICK.getExamplesPathCode(),
177200
ApiIndex.ESIGNATURE.getExamplesPathCode(),
178201
ApiIndex.MONITOR.getExamplesPathCode(),
179202
ApiIndex.ADMIN.getExamplesPathCode(),
@@ -185,10 +208,10 @@ private String getRedirectURLForJWTAuthentication(HttpServletRequest req, HttpSe
185208
Integer indexOfExampleCodeInRedirect = StringUtils.indexOfAny(savedRequest.getRedirectUrl(), examplesCodes);
186209

187210
if (indexOfExampleCodeInRedirect != -1) {
188-
Boolean hasNumbers = savedRequest.getRedirectUrl().substring(indexOfExampleCodeInRedirect).matches(".*\\d.*");
211+
Boolean hasNumbers = savedRequest.getRedirectUrl().substring(indexOfExampleCodeInRedirect)
212+
.matches(".*\\d.*");
189213

190-
return "GET".equals(savedRequest.getMethod()) && hasNumbers ?
191-
savedRequest.getRedirectUrl() : "/";
214+
return "GET".equals(savedRequest.getMethod()) && hasNumbers ? savedRequest.getRedirectUrl() : "/";
192215
}
193216
}
194217

@@ -197,8 +220,8 @@ private String getRedirectURLForJWTAuthentication(HttpServletRequest req, HttpSe
197220

198221
@GetMapping(path = "/ds-return")
199222
public String returnController(@RequestParam(value = ATTR_STATE, required = false) String state,
200-
@RequestParam(value = ATTR_EVENT, required = false) String event,
201-
@RequestParam(required = false) String envelopeId, ModelMap model) {
223+
@RequestParam(value = ATTR_EVENT, required = false) String event,
224+
@RequestParam(required = false) String envelopeId, ModelMap model) {
202225
model.addAttribute(LAUNCHER_TEXTS, config.getCodeExamplesText().SupportingTexts);
203226
model.addAttribute(ATTR_TITLE, "Return from DocuSign");
204227
model.addAttribute(ATTR_EVENT, event);
@@ -221,4 +244,4 @@ private String getLoginPath(AuthType authTypeSelected) {
221244
}
222245
return loginPath;
223246
}
224-
}
247+
}

Diff for: src/main/java/com/docusign/core/model/Session.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
import java.util.UUID;
1313

1414
@Component
15-
@Scope(value = WebApplicationContext.SCOPE_SESSION,
16-
proxyMode = ScopedProxyMode.TARGET_CLASS)
15+
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
1716
@Data
1817
public class Session implements Serializable {
1918
private static final long serialVersionUID = 2695379118371574037L;
@@ -75,4 +74,6 @@ public class Session implements Serializable {
7574
private String instanceId;
7675

7776
private Boolean isWorkflowPublished = false;
77+
78+
private Boolean isPKCEWorking = true;
7879
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.docusign.core.security;
2+
3+
import org.springframework.security.core.AuthenticationException;
4+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
5+
import javax.servlet.ServletException;
6+
import javax.servlet.http.HttpServletRequest;
7+
import javax.servlet.http.HttpServletResponse;
8+
import java.io.IOException;
9+
10+
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
11+
12+
@Override
13+
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
14+
AuthenticationException exception) throws IOException, ServletException {
15+
String code = request.getParameter("code");
16+
String state = request.getParameter("state");
17+
18+
if (code != null) {
19+
response.sendRedirect("/pkce?code=" + code + "&state=" + state);
20+
} else {
21+
response.sendRedirect("/login?error=true");
22+
}
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package com.docusign.core.security;
2+
3+
import com.docusign.esign.client.auth.OAuth;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import org.springframework.security.core.GrantedAuthority;
6+
import org.springframework.security.core.authority.AuthorityUtils;
7+
import org.springframework.security.oauth2.core.user.OAuth2User;
8+
9+
import java.util.*;
10+
11+
public class JWTOAuth2User implements OAuth2User {
12+
private List <GrantedAuthority> authorities;
13+
14+
private Map <String, Object> attributes;
15+
16+
private String sub;
17+
18+
private String name;
19+
20+
private String givenName;
21+
22+
private String familyName;
23+
24+
private OAuth.OAuthToken accessToken;
25+
26+
private String email;
27+
28+
private List <Map <String, Object>> accounts;
29+
30+
private String created;
31+
32+
@Override
33+
public Collection <? extends GrantedAuthority> getAuthorities() {
34+
return this.authorities;
35+
}
36+
37+
public void setAuthorities(List < String > scopes) {
38+
String authoritiesString = "ROLE_USER";
39+
for (String scope: scopes) {
40+
authoritiesString += ",SCOPE_" + scope;
41+
}
42+
authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authoritiesString);
43+
}
44+
45+
@Override
46+
public Map <String, Object> getAttributes() {
47+
if (this.attributes == null) {
48+
this.attributes = new HashMap <> ();
49+
this.attributes.put("sub", this.getSub());
50+
this.attributes.put("name", this.getName());
51+
this.attributes.put("given_name", this.getGivenName());
52+
this.attributes.put("family_name", this.getFamilyName());
53+
this.attributes.put("created", this.getCreated());
54+
this.attributes.put("email", this.getEmail());
55+
this.attributes.put("accounts", this.getAccounts());
56+
this.attributes.put("access_token", this.getAccessToken());
57+
}
58+
return attributes;
59+
}
60+
61+
public String getSub() {
62+
return this.sub;
63+
}
64+
65+
public void setSub(String sub) {
66+
this.sub = sub;
67+
}
68+
69+
@Override
70+
public String getName() {
71+
return this.name;
72+
}
73+
74+
public void setName(String name) {
75+
this.name = name;
76+
}
77+
78+
public String getGivenName() {
79+
return this.givenName;
80+
}
81+
82+
public void setGivenName(String givenName) {
83+
this.givenName = givenName;
84+
}
85+
86+
public OAuth.OAuthToken getAccessToken() {
87+
return this.accessToken;
88+
}
89+
90+
public void setAccessToken(OAuth.OAuthToken accessToken) {
91+
this.accessToken = accessToken;
92+
}
93+
94+
public String getFamilyName() {
95+
return this.familyName;
96+
}
97+
98+
public void setFamilyName(String familyName) {
99+
this.familyName = familyName;
100+
}
101+
102+
public String getCreated() {
103+
return this.created;
104+
}
105+
106+
public void setCreated(String created) {
107+
this.created = created;
108+
}
109+
110+
public String getEmail() {
111+
return this.email;
112+
}
113+
114+
public void setEmail(String email) {
115+
this.email = email;
116+
}
117+
118+
public List <Map <String, Object>> getAccounts() {
119+
return this.accounts;
120+
}
121+
122+
public void setAccounts(List <OAuth.Account> accounts) {
123+
this.accounts = new ArrayList <> ();
124+
for (OAuth.Account account: accounts) {
125+
ObjectMapper mapObject = new ObjectMapper();
126+
Map <String, Object> mapObj = mapObject.convertValue(account, Map.class);
127+
this.accounts.add(mapObj);
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)