Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
* <p>user web application pages</p>
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Call<AccessToken> 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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down Expand Up @@ -98,7 +99,8 @@ public IdToken idToken(String id_token) {
}
}

public String getLineWebLoginUrl(String state, String nonce, List<String> scopes) {
public String getLineWebLoginUrl(
String state, String nonce, String codeChallenge, String codeChallengeMethod, List<String> scopes) {
final String encodedCallbackUrl;
final String scope = String.join("%20", scopes);

Expand All @@ -113,7 +115,9 @@ public String getLineWebLoginUrl(String state, String nonce, List<String> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.linecorp.sample.login.infra.pkce;


/**
* Code Challenge Method. <br></br>
* for more details, please refer to: <a href="https://tools.ietf.org/html/rfc7636#section-4.3">RFC 7636: Proof Key for Code Exchange - Section 4.3</a>
*/
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;
}
}
83 changes: 83 additions & 0 deletions src/main/java/com/linecorp/sample/login/infra/pkce/PKCECode.java
Original file line number Diff line number Diff line change
@@ -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. <br></br>
* for more details, please refer to: <a href="https://oauth.net/2/pkce/">RFC 7636: Proof Key for Code Exchange</a>
*/
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 + '\'' +
'}';
}
}