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 + '\'' +
+ '}';
+ }
+}