Skip to content

Commit

Permalink
Updating to support PKCE for Authorization Code Grant
Browse files Browse the repository at this point in the history
  • Loading branch information
InbarGazit committed Oct 31, 2024
1 parent 6cf09c9 commit ce2d005
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 116 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ For a list of code examples that use the Web Forms API, see the [How-to guides o
### Prerequisites
**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.

1. A free [Docusign developer account](https://www.docusign.com/developers/sandbox); create one if you don't already have one.
1. A free [Docusign developer account](https://go.docusign.com/o/sandbox/); create one if you don't already have one.
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.

This [video](https://www.youtube.com/watch?v=eiRI4fe5HgM) demonstrates how to obtain an integration key.
Expand Down
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
<rooms.version>1.4.3</rooms.version>
<click.version>1.5.0</click.version>
<monitor.version>1.4.0</monitor.version>
<admin.version>2.0.0-RC1</admin.version>
<webforms.version>1.0.2-RC12</webforms.version>
<maestro.version>2.0.0-RC1</maestro.version>
<admin.version>2.0.0-RC2</admin.version>
<webforms.version>2.0.0-RC1</webforms.version>
<maestro.version>2.0.0</maestro.version>
<swagger-core-version>2.2.22</swagger-core-version>
<jackson-version>2.17.2</jackson-version>
<jersey2.version>3.1.8</jersey2.version>
Expand Down
16 changes: 13 additions & 3 deletions src/main/java/com/docusign/DSConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ public class DSConfiguration {
@Value("${spring.security.oauth2.client.registration.jwt.client-id}")
private String userId;

@Value("${spring.security.oauth2.client.registration.acg.client-secret}")
private String secretUserId;

@Value("${spring.security.oauth2.client.provider.acg.token-uri}")
private String tokenEndpoint;

@Value("${spring.security.oauth2.client.provider.acg.authorization-uri}")
private String authorizationEndpoint;

@Value("${jwt.grant.sso.redirect-url}")
private String jwtRedirectURL;

Expand Down Expand Up @@ -158,7 +167,8 @@ public ManifestStructure getCodeExamplesText() {
}

try {
codeExamplesText = new ObjectMapper().readValue(loadFileData(codeExamplesManifest), ManifestStructure.class);
codeExamplesText = new ObjectMapper().readValue(loadFileData(codeExamplesManifest),
ManifestStructure.class);
} catch (Exception e) {
e.printStackTrace();
}
Expand All @@ -172,8 +182,8 @@ public String loadFileData(String linkToManifestFile) throws Exception {
httpConnection.setRequestMethod(HttpMethod.GET);

httpConnection.setRequestProperty(
HttpHeaders.CONTENT_TYPE,
String.valueOf(MediaType.APPLICATION_JSON));
HttpHeaders.CONTENT_TYPE,
String.valueOf(MediaType.APPLICATION_JSON));

int responseCode = httpConnection.getResponseCode();

Expand Down
12 changes: 6 additions & 6 deletions src/main/java/com/docusign/WebSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;

import com.docusign.core.security.CustomAuthenticationFailureHandler;

@EnableWebSecurity
public class WebSecurityConfig {

Expand All @@ -28,24 +30,22 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
try {
authorize
.antMatchers("/", "/error**", "/assets/**", "/ds/mustAuthenticate**",
"/ds/authenticate**", "/ds/selectApi**", "/con001")
"/ds/authenticate**", "/ds/selectApi**", "/con001", "/pkce")
.permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/ds/mustAuthenticate")
);
new LoginUrlAuthenticationEntryPoint("/ds/mustAuthenticate"));
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.requestCache().requestCache(requestCache()).and()
.oauth2Login(Customizer.withDefaults())
.oauth2Login(login -> login.failureHandler(new CustomAuthenticationFailureHandler()))
.oauth2Client(Customizer.withDefaults())
.logout(logout -> logout
.logoutSuccessUrl("/")
)
.logoutSuccessUrl("/"))
.csrf().disable();

return http.build();
Expand Down
51 changes: 37 additions & 14 deletions src/main/java/com/docusign/core/controller/IndexController.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.docusign.core.model.AuthType;
import com.docusign.core.model.Session;
import com.docusign.core.model.User;
import com.docusign.core.security.acg.ACGAuthenticationMethod;
import com.docusign.core.security.jwt.JWTAuthenticationMethod;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -94,7 +95,7 @@ public String index(ModelMap model, HttpServletResponse response) throws Excepti
}

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

@GetMapping(path = "/ds/mustAuthenticate")
public ModelAndView mustAuthenticateController(ModelMap model, HttpServletRequest req, HttpServletResponse resp) throws IOException {
public ModelAndView mustAuthenticateController(ModelMap model, HttpServletRequest req, HttpServletResponse resp)
throws IOException {
model.addAttribute(LAUNCHER_TEXTS, config.getCodeExamplesText().SupportingTexts);
model.addAttribute(ATTR_TITLE, config.getCodeExamplesText().SupportingTexts.LoginPage.LoginButton);

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

boolean isRedirectToMonitor = redirectURL.toLowerCase().contains("/m") && !redirectURL.toLowerCase().contains("/mae");
boolean isRedirectToMonitor = redirectURL.toLowerCase().contains("/m") &&
!redirectURL.toLowerCase().contains("/mae");
if (session.isRefreshToken() || config.getQuickstart().equals("true")) {
config.setQuickstart("false");

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

@GetMapping("/pkce")
public RedirectView pkce(String code, String state, HttpServletRequest req, HttpServletResponse resp)
throws Exception {
String redirectURL = getRedirectURLForJWTAuthentication(req, resp);
RedirectView redirect;
try {
redirect = new ACGAuthenticationMethod().exchangeCodeForToken(code, config, session, redirectURL);
} catch (Exception e) {
redirect = getRedirectView(AuthType.AGC);
this.session.setIsPKCEWorking(false);
}

return redirect;
}

@PostMapping("/ds/authenticate")
public RedirectView authenticate(ModelMap model, @RequestBody MultiValueMap<String, String> formParams, HttpServletRequest req, HttpServletResponse resp) throws IOException {
public RedirectView authenticate(ModelMap model, @RequestBody MultiValueMap <String, String> formParams,
HttpServletRequest req, HttpServletResponse resp) throws Exception {
if (!formParams.containsKey("selectAuthType")) {
model.addAttribute("message", "Select option with selectAuthType name must be provided.");
return new RedirectView("pages/error");
}

String redirectURL = getRedirectURLForJWTAuthentication(req, resp);

List<String> selectAuthTypeObject = formParams.get("selectAuthType");
List <String> selectAuthTypeObject = formParams.get("selectAuthType");
AuthType authTypeSelected = AuthType.valueOf(selectAuthTypeObject.get(0));

if (authTypeSelected.equals(AuthType.JWT)) {
this.session.setAuthTypeSelected(AuthType.JWT);
return new JWTAuthenticationMethod().loginUsingJWT(config, session, redirectURL);
} else {
this.session.setAuthTypeSelected(AuthType.AGC);
return getRedirectView(authTypeSelected);
if (this.session.getIsPKCEWorking()) {
return new ACGAuthenticationMethod().initiateAuthorization(config);
} else {
return getRedirectView(authTypeSelected);
}
}
}

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

String[] examplesCodes = new String[]{
ApiIndex.CLICK.getExamplesPathCode(),
String[] examplesCodes = new String[] {
ApiIndex.CLICK.getExamplesPathCode(),
ApiIndex.ESIGNATURE.getExamplesPathCode(),
ApiIndex.MONITOR.getExamplesPathCode(),
ApiIndex.ADMIN.getExamplesPathCode(),
Expand All @@ -185,10 +208,10 @@ private String getRedirectURLForJWTAuthentication(HttpServletRequest req, HttpSe
Integer indexOfExampleCodeInRedirect = StringUtils.indexOfAny(savedRequest.getRedirectUrl(), examplesCodes);

if (indexOfExampleCodeInRedirect != -1) {
Boolean hasNumbers = savedRequest.getRedirectUrl().substring(indexOfExampleCodeInRedirect).matches(".*\\d.*");
Boolean hasNumbers = savedRequest.getRedirectUrl().substring(indexOfExampleCodeInRedirect)
.matches(".*\\d.*");

return "GET".equals(savedRequest.getMethod()) && hasNumbers ?
savedRequest.getRedirectUrl() : "/";
return "GET".equals(savedRequest.getMethod()) && hasNumbers ? savedRequest.getRedirectUrl() : "/";
}
}

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

@GetMapping(path = "/ds-return")
public String returnController(@RequestParam(value = ATTR_STATE, required = false) String state,
@RequestParam(value = ATTR_EVENT, required = false) String event,
@RequestParam(required = false) String envelopeId, ModelMap model) {
@RequestParam(value = ATTR_EVENT, required = false) String event,
@RequestParam(required = false) String envelopeId, ModelMap model) {
model.addAttribute(LAUNCHER_TEXTS, config.getCodeExamplesText().SupportingTexts);
model.addAttribute(ATTR_TITLE, "Return from DocuSign");
model.addAttribute(ATTR_EVENT, event);
Expand All @@ -221,4 +244,4 @@ private String getLoginPath(AuthType authTypeSelected) {
}
return loginPath;
}
}
}
5 changes: 3 additions & 2 deletions src/main/java/com/docusign/core/model/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
import java.util.UUID;

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.TARGET_CLASS)
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
@Data
public class Session implements Serializable {
private static final long serialVersionUID = 2695379118371574037L;
Expand Down Expand Up @@ -75,4 +74,6 @@ public class Session implements Serializable {
private String instanceId;

private Boolean isWorkflowPublished = false;

private Boolean isPKCEWorking = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.docusign.core.security;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String code = request.getParameter("code");
String state = request.getParameter("state");

if (code != null) {
response.sendRedirect("/pkce?code=" + code + "&state=" + state);
} else {
response.sendRedirect("/login?error=true");
}
}
}
130 changes: 130 additions & 0 deletions src/main/java/com/docusign/core/security/JWTOAuth2User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package com.docusign.core.security;

import com.docusign.esign.client.auth.OAuth;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.*;

public class JWTOAuth2User implements OAuth2User {
private List <GrantedAuthority> authorities;

private Map <String, Object> attributes;

private String sub;

private String name;

private String givenName;

private String familyName;

private OAuth.OAuthToken accessToken;

private String email;

private List <Map <String, Object>> accounts;

private String created;

@Override
public Collection <? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

public void setAuthorities(List < String > scopes) {
String authoritiesString = "ROLE_USER";
for (String scope: scopes) {
authoritiesString += ",SCOPE_" + scope;
}
authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authoritiesString);
}

@Override
public Map <String, Object> getAttributes() {
if (this.attributes == null) {
this.attributes = new HashMap <> ();
this.attributes.put("sub", this.getSub());
this.attributes.put("name", this.getName());
this.attributes.put("given_name", this.getGivenName());
this.attributes.put("family_name", this.getFamilyName());
this.attributes.put("created", this.getCreated());
this.attributes.put("email", this.getEmail());
this.attributes.put("accounts", this.getAccounts());
this.attributes.put("access_token", this.getAccessToken());
}
return attributes;
}

public String getSub() {
return this.sub;
}

public void setSub(String sub) {
this.sub = sub;
}

@Override
public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public String getGivenName() {
return this.givenName;
}

public void setGivenName(String givenName) {
this.givenName = givenName;
}

public OAuth.OAuthToken getAccessToken() {
return this.accessToken;
}

public void setAccessToken(OAuth.OAuthToken accessToken) {
this.accessToken = accessToken;
}

public String getFamilyName() {
return this.familyName;
}

public void setFamilyName(String familyName) {
this.familyName = familyName;
}

public String getCreated() {
return this.created;
}

public void setCreated(String created) {
this.created = created;
}

public String getEmail() {
return this.email;
}

public void setEmail(String email) {
this.email = email;
}

public List <Map <String, Object>> getAccounts() {
return this.accounts;
}

public void setAccounts(List <OAuth.Account> accounts) {
this.accounts = new ArrayList <> ();
for (OAuth.Account account: accounts) {
ObjectMapper mapObject = new ObjectMapper();
Map <String, Object> mapObj = mapObject.convertValue(account, Map.class);
this.accounts.add(mapObj);
}
}
}
Loading

0 comments on commit ce2d005

Please sign in to comment.