diff --git a/src/main/java/com/linecorp/sample/login/application/controller/WebController.java b/src/main/java/com/linecorp/sample/login/application/controller/WebController.java index 648797a..3356e60 100755 --- a/src/main/java/com/linecorp/sample/login/application/controller/WebController.java +++ b/src/main/java/com/linecorp/sample/login/application/controller/WebController.java @@ -29,6 +29,8 @@ import com.linecorp.sample.login.infra.line.api.v2.response.AccessToken; import com.linecorp.sample.login.infra.line.api.v2.response.IdToken; import com.linecorp.sample.login.infra.utils.CommonUtils; +import com.linecorp.sample.login.infra.pkce.CodeChallengeMethod; +import com.linecorp.sample.login.infra.pkce.PKCECode; /** *

user web application pages

@@ -40,6 +42,7 @@ public class WebController { static final String ACCESS_TOKEN = "accessToken"; private static final Logger logger = Logger.getLogger(WebController.class); private static final String NONCE = "nonce"; + private static final String LINE_WEB_LOGIN_CODE_VERIFIER = "lineWebLoginCodeVerifier"; @Autowired private LineAPIService lineAPIService; @@ -62,7 +65,16 @@ public String goToAuthPage(HttpSession httpSession){ final String nonce = CommonUtils.getToken(); httpSession.setAttribute(LINE_WEB_LOGIN_STATE, state); httpSession.setAttribute(NONCE, nonce); - final String url = lineAPIService.getLineWebLoginUrl(state, nonce, Arrays.asList("openid", "profile")); + + // generate PKCE code + PKCECode pkce = PKCECode.newCode(); + final String codeVerifier = pkce.getVerifier(); + final String codeChallenge = pkce.getChallenge(); + httpSession.setAttribute(LINE_WEB_LOGIN_CODE_VERIFIER, codeVerifier); + final String codeChallengeMethod = CodeChallengeMethod.S256.getValue(); + + final String url = lineAPIService.getLineWebLoginUrl( + state, nonce, codeChallenge, codeChallengeMethod, Arrays.asList("openid", "profile")); return "redirect:" + url; } @@ -98,7 +110,9 @@ public String auth( } httpSession.removeAttribute(LINE_WEB_LOGIN_STATE); - AccessToken token = lineAPIService.accessToken(code); + String codeVerifier = httpSession.getAttribute(LINE_WEB_LOGIN_CODE_VERIFIER).toString(); + logger.debug("parameter codeVerifier : " + codeVerifier); + AccessToken token = lineAPIService.accessToken(code, codeVerifier); if (logger.isDebugEnabled()) { logger.debug("scope : " + token.scope); logger.debug("access_token : " + token.access_token); diff --git a/src/main/java/com/linecorp/sample/login/infra/line/api/v2/LineAPI.java b/src/main/java/com/linecorp/sample/login/infra/line/api/v2/LineAPI.java index c190529..4170626 100755 --- a/src/main/java/com/linecorp/sample/login/infra/line/api/v2/LineAPI.java +++ b/src/main/java/com/linecorp/sample/login/infra/line/api/v2/LineAPI.java @@ -39,6 +39,7 @@ Call accessToken( @Field("client_id") String client_id, @Field("client_secret") String client_secret, @Field("redirect_uri") String callback_url, + @Field("code_verifier") String code_verifier, @Field("code") String code); @Headers("Content-Type: application/x-www-form-urlencoded") diff --git a/src/main/java/com/linecorp/sample/login/infra/line/api/v2/LineAPIService.java b/src/main/java/com/linecorp/sample/login/infra/line/api/v2/LineAPIService.java index 900719a..73a95f2 100755 --- a/src/main/java/com/linecorp/sample/login/infra/line/api/v2/LineAPIService.java +++ b/src/main/java/com/linecorp/sample/login/infra/line/api/v2/LineAPIService.java @@ -52,12 +52,13 @@ public class LineAPIService { @Value("${linecorp.platform.channel.callbackUrl}") private String callbackUrl; - public AccessToken accessToken(String code) { + public AccessToken accessToken(String code, String codeVerifier) { return getClient(t -> t.accessToken( GRANT_TYPE_AUTHORIZATION_CODE, channelId, channelSecret, callbackUrl, + codeVerifier, code)); } @@ -98,7 +99,8 @@ public IdToken idToken(String id_token) { } } - public String getLineWebLoginUrl(String state, String nonce, List scopes) { + public String getLineWebLoginUrl( + String state, String nonce, String codeChallenge, String codeChallengeMethod, List scopes) { final String encodedCallbackUrl; final String scope = String.join("%20", scopes); @@ -113,7 +115,9 @@ public String getLineWebLoginUrl(String state, String nonce, List scopes + "&redirect_uri=" + encodedCallbackUrl + "&state=" + state + "&scope=" + scope - + "&nonce=" + nonce; + + "&nonce=" + nonce + + "&code_challenge=" + codeChallenge + + "&code_challenge_method=" + codeChallengeMethod; } public boolean verifyIdToken(String id_token, String nonce) { diff --git a/src/main/java/com/linecorp/sample/login/infra/pkce/CodeChallengeMethod.java b/src/main/java/com/linecorp/sample/login/infra/pkce/CodeChallengeMethod.java new file mode 100644 index 0000000..ffded20 --- /dev/null +++ b/src/main/java/com/linecorp/sample/login/infra/pkce/CodeChallengeMethod.java @@ -0,0 +1,21 @@ +package com.linecorp.sample.login.infra.pkce; + + +/** + * Code Challenge Method.

+ * for more details, please refer to: RFC 7636: Proof Key for Code Exchange - Section 4.3 + */ +public enum CodeChallengeMethod { + PLAIN("plain"), // not used + S256("S256"); // always use S256 in LINE SDK + + private final String value; + + CodeChallengeMethod(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/com/linecorp/sample/login/infra/pkce/PKCECode.java b/src/main/java/com/linecorp/sample/login/infra/pkce/PKCECode.java new file mode 100644 index 0000000..e939f5b --- /dev/null +++ b/src/main/java/com/linecorp/sample/login/infra/pkce/PKCECode.java @@ -0,0 +1,83 @@ +package com.linecorp.sample.login.infra.pkce; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +/** + * Proof Key for Code Exchange.

+ * for more details, please refer to: RFC 7636: Proof Key for Code Exchange + */ +public class PKCECode { + private static final int LENGTH_VERIFIER = 64; + + private final String verifier; + private final String challenge; + + private PKCECode(final String verifier) { + this.verifier = verifier; + challenge = generateChallenge(verifier); + } + + public static PKCECode newCode() { + final String verifier = generateVerifier(); + return new PKCECode(verifier); + } + + private static String generateVerifier() { + byte[] bytes = new byte[LENGTH_VERIFIER]; + (new SecureRandom()).nextBytes(bytes); + String token = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + return token; + } + + private static String generateChallenge(final String verifier) { + try { + final MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(verifier.getBytes()); + final byte[] hashBytes = md.digest(); + final String toBe = Base64.getUrlEncoder().withoutPadding().encodeToString(hashBytes); + return toBe; + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(e); + } + } + + public String getVerifier() { + return verifier; + } + + public String getChallenge() { + return challenge; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { return true; } + if (!(o instanceof PKCECode)) { return false; } + + final PKCECode pkceCode = (PKCECode) o; + + if (!verifier.equals(pkceCode.verifier)) { return false; } + return challenge.equals(pkceCode.challenge); + } + + @Override + public int hashCode() { + int result = verifier.hashCode(); + result = 31 * result + challenge.hashCode(); + return result; + } + + @Override + public String toString() { + return "PKCECode{" + + "verifier='" + verifier + '\'' + + ", challenge='" + challenge + '\'' + + '}'; + } +}