diff --git a/pom.xml b/pom.xml
index fba2dad..6b79bb1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,6 +59,11 @@
commons-lang3
3.4
+
+ commons-codec
+ commons-codec
+ 1.16.0
+
diff --git a/src/main/java/com/ysoft/geecon/OAuthResource.java b/src/main/java/com/ysoft/geecon/OAuthResource.java
index 812de81..6695cd5 100644
--- a/src/main/java/com/ysoft/geecon/OAuthResource.java
+++ b/src/main/java/com/ysoft/geecon/OAuthResource.java
@@ -103,6 +103,9 @@ public class OAuthResource {
validateClient(params);
var session = sessionsRepo.redeemAuthorizationCode(params.getCode())
.orElseThrow(() -> new OAuthException("Invalid code"));
+ if (!session.validateCodeChallenge(params.getCodeVerifier())) {
+ throw new OAuthException("Invalid code verifier");
+ }
String idToken = null;
diff --git a/src/main/java/com/ysoft/geecon/dto/AuthParams.java b/src/main/java/com/ysoft/geecon/dto/AuthParams.java
index bcc9f74..3c1c630 100644
--- a/src/main/java/com/ysoft/geecon/dto/AuthParams.java
+++ b/src/main/java/com/ysoft/geecon/dto/AuthParams.java
@@ -24,6 +24,12 @@ public class AuthParams {
String scope;
@RestQuery("state")
String state;
+ @RestQuery("code_challenge_method")
+ String codeChallengeMethod;
+ @RestQuery("code_challenge")
+ String codeChallenge;
+ @RestQuery("nonce")
+ String nonce;
public String getLoginHint() {
return loginHint;
@@ -76,4 +82,28 @@ public class AuthParams {
public void setState(String state) {
this.state = state;
}
+
+ public String getCodeChallengeMethod() {
+ return codeChallengeMethod;
+ }
+
+ public void setCodeChallengeMethod(String codeChallengeMethod) {
+ this.codeChallengeMethod = codeChallengeMethod;
+ }
+
+ public String getCodeChallenge() {
+ return codeChallenge;
+ }
+
+ public void setCodeChallenge(String codeChallenge) {
+ this.codeChallenge = codeChallenge;
+ }
+
+ public String getNonce() {
+ return nonce;
+ }
+
+ public void setNonce(String nonce) {
+ this.nonce = nonce;
+ }
}
diff --git a/src/main/java/com/ysoft/geecon/dto/AuthorizationSession.java b/src/main/java/com/ysoft/geecon/dto/AuthorizationSession.java
index 5b55f85..d9a28fa 100644
--- a/src/main/java/com/ysoft/geecon/dto/AuthorizationSession.java
+++ b/src/main/java/com/ysoft/geecon/dto/AuthorizationSession.java
@@ -36,4 +36,15 @@ public record AuthorizationSession(AuthParams params,
public String scope() {
return acceptedScopes == null ? null : String.join(" ", acceptedScopes);
}
+
+ public boolean validateCodeChallenge(String codeVerifier) {
+ if (params.codeChallengeMethod == null) {
+ return true;
+ }
+ if (codeVerifier == null) {
+ return false;
+ }
+ return Pkce.validate(params.codeChallengeMethod, params.codeChallenge, codeVerifier);
+ }
+
}
diff --git a/src/main/java/com/ysoft/geecon/dto/Pkce.java b/src/main/java/com/ysoft/geecon/dto/Pkce.java
new file mode 100644
index 0000000..a1946d1
--- /dev/null
+++ b/src/main/java/com/ysoft/geecon/dto/Pkce.java
@@ -0,0 +1,18 @@
+package com.ysoft.geecon.dto;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+
+public class Pkce {
+ static boolean validate(String challengeMethod, String codeChallenge, String codeVerifier) {
+ return switch (challengeMethod) {
+ case "plain" -> codeVerifier.equals(codeChallenge);
+ case "S256" -> codeChallenge.equals(s256(codeVerifier));
+ default -> false;
+ };
+ }
+
+ static String s256(String codeVerifier) {
+ return Base64.encodeBase64URLSafeString(DigestUtils.sha256(codeVerifier));
+ }
+}
diff --git a/src/main/java/com/ysoft/geecon/dto/TokenParams.java b/src/main/java/com/ysoft/geecon/dto/TokenParams.java
index a1a2e04..0b5f8b6 100644
--- a/src/main/java/com/ysoft/geecon/dto/TokenParams.java
+++ b/src/main/java/com/ysoft/geecon/dto/TokenParams.java
@@ -1,10 +1,6 @@
package com.ysoft.geecon.dto;
import jakarta.ws.rs.FormParam;
-import org.jboss.resteasy.reactive.RestQuery;
-
-import java.util.Arrays;
-import java.util.List;
public class TokenParams {
@@ -23,6 +19,9 @@ public class TokenParams {
@FormParam("code")
private String code;
+ @FormParam("code_verifier")
+ private String codeVerifier;
+
public String getGrantType() {
return grantType;
}
@@ -62,4 +61,12 @@ public class TokenParams {
public void setCode(String code) {
this.code = code;
}
+
+ public String getCodeVerifier() {
+ return codeVerifier;
+ }
+
+ public void setCodeVerifier(String codeVerifier) {
+ this.codeVerifier = codeVerifier;
+ }
}
diff --git a/src/main/java/com/ysoft/geecon/error/OAuthException.java b/src/main/java/com/ysoft/geecon/error/OAuthException.java
index 7e17808..7e5779d 100644
--- a/src/main/java/com/ysoft/geecon/error/OAuthException.java
+++ b/src/main/java/com/ysoft/geecon/error/OAuthException.java
@@ -1,6 +1,7 @@
package com.ysoft.geecon.error;
public class OAuthException extends RuntimeException {
+ // https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-09.html#name-error-response-2
public OAuthException(String message) {
super(message);
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index e69de29..d6c772b 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+quarkus.http.cors=true
+quarkus.http.cors.origins=/.*/
\ No newline at end of file
diff --git a/src/test/java/com/ysoft/geecon/dto/PkceTest.java b/src/test/java/com/ysoft/geecon/dto/PkceTest.java
new file mode 100644
index 0000000..f6bed2e
--- /dev/null
+++ b/src/test/java/com/ysoft/geecon/dto/PkceTest.java
@@ -0,0 +1,13 @@
+package com.ysoft.geecon.dto;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class PkceTest {
+
+ @Test
+ void validate() {
+ assertTrue(Pkce.validate("S256", "W2ap_IuB0HJkMIkuxaXV2l8_Gx5mVmMStG_HQrAnQxA", "7AmAEXcl2Km9LQMwtUhif7GQ97HZy9RT72KZBwmxBRI"));
+ }
+}
\ No newline at end of file