feat: make codeChallenge dynamic for custom OAuth provider (#4924)

This commit is contained in:
DacongDA
2026-01-28 17:56:28 +08:00
committed by GitHub
parent 33298e44d4
commit a06d003589
8 changed files with 95 additions and 14 deletions

View File

@@ -739,6 +739,7 @@ func (c *ApiController) Login() {
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
// OAuth
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
idpInfo.CodeVerifier = authForm.CodeVerifier
var idProvider idp.IdProvider
idProvider, err = idp.GetIdProvider(idpInfo, authForm.RedirectUri)
if err != nil {

View File

@@ -46,6 +46,7 @@ type AuthForm struct {
State string `json:"state"`
RedirectUri string `json:"redirectUri"`
Method string `json:"method"`
CodeVerifier string `json:"codeVerifier"`
EmailCode string `json:"emailCode"`
PhoneCode string `json:"phoneCode"`

View File

@@ -31,11 +31,12 @@ type CustomIdProvider struct {
Client *http.Client
Config *oauth2.Config
UserInfoURL string
TokenURL string
AuthURL string
UserMapping map[string]string
Scopes []string
UserInfoURL string
TokenURL string
AuthURL string
UserMapping map[string]string
Scopes []string
CodeVerifier string
}
func NewCustomIdProvider(idpInfo *ProviderInfo, redirectUrl string) *CustomIdProvider {
@@ -53,6 +54,7 @@ func NewCustomIdProvider(idpInfo *ProviderInfo, redirectUrl string) *CustomIdPro
idp.UserInfoURL = idpInfo.UserInfoURL
idp.UserMapping = idpInfo.UserMapping
idp.CodeVerifier = idpInfo.CodeVerifier
return idp
}
@@ -62,7 +64,11 @@ func (idp *CustomIdProvider) SetHttpClient(client *http.Client) {
func (idp *CustomIdProvider) GetToken(code string) (*oauth2.Token, error) {
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, idp.Client)
return idp.Config.Exchange(ctx, code)
var oauth2Opts []oauth2.AuthCodeOption
if idp.CodeVerifier != "" {
oauth2Opts = append(oauth2Opts, oauth2.VerifierOption(idp.CodeVerifier))
}
return idp.Config.Exchange(ctx, code, oauth2Opts...)
}
func getNestedValue(data map[string]interface{}, path string) (interface{}, error) {

View File

@@ -87,8 +87,9 @@ import (
)
type GothIdProvider struct {
Provider goth.Provider
Session goth.Session
Provider goth.Provider
Session goth.Session
CodeVerifier string
}
func NewGothIdProvider(providerType string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, redirectUrl string, hostUrl string) (*GothIdProvider, error) {
@@ -448,7 +449,13 @@ func (idp *GothIdProvider) GetToken(code string) (*oauth2.Token, error) {
value = url.Values{}
value.Add("code", code)
if idp.Provider.Name() == "twitterv2" || idp.Provider.Name() == "fitbit" {
value.Add("oauth_verifier", "casdoor-verifier")
// Use dynamic code verifier if provided, otherwise fall back to static one
verifier := idp.CodeVerifier
if verifier == "" {
verifier = "casdoor-verifier"
}
// RFC 7636 PKCE uses 'code_verifier' parameter
value.Add("code_verifier", verifier)
}
}
accessToken, err := idp.Session.Authorize(idp.Provider, value)

View File

@@ -45,6 +45,7 @@ type ProviderInfo struct {
HostUrl string
RedirectUrl string
DisableSsl bool
CodeVerifier string
TokenURL string
AuthURL string
@@ -128,7 +129,9 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
case "Web3Onboard":
return NewWeb3OnboardIdProvider(), nil
case "Twitter":
return NewTwitterIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
provider := NewTwitterIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
provider.CodeVerifier = idpInfo.CodeVerifier
return provider, nil
case "Telegram":
return NewTelegramIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
default:

View File

@@ -28,8 +28,9 @@ import (
)
type TwitterIdProvider struct {
Client *http.Client
Config *oauth2.Config
Client *http.Client
Config *oauth2.Config
CodeVerifier string
}
func NewTwitterIdProvider(clientId string, clientSecret string, redirectUrl string) *TwitterIdProvider {
@@ -84,7 +85,12 @@ func (idp *TwitterIdProvider) GetToken(code string) (*oauth2.Token, error) {
params := url.Values{}
// params.Add("client_id", idp.Config.ClientID)
params.Add("redirect_uri", idp.Config.RedirectURL)
params.Add("code_verifier", "casdoor-verifier")
// Use dynamic code verifier if provided, otherwise fall back to static one
verifier := idp.CodeVerifier
if verifier == "" {
verifier = "casdoor-verifier"
}
params.Add("code_verifier", verifier)
params.Add("code", code)
params.Add("grant_type", "authorization_code")
req, err := http.NewRequest("POST", "https://api.twitter.com/2/oauth2/token", strings.NewReader(params.Encode()))

View File

@@ -17,6 +17,7 @@ import {Spin} from "antd";
import {withRouter} from "react-router-dom";
import * as AuthBackend from "./AuthBackend";
import * as Util from "./Util";
import * as Provider from "./Provider";
import {authConfig} from "./Auth";
import * as Setting from "../Setting";
import i18next from "i18next";
@@ -146,6 +147,9 @@ class AuthCallback extends React.Component {
const redirectUri = `${window.location.origin}/callback`;
// Retrieve the code verifier for PKCE if it exists
const codeVerifier = Provider.getCodeVerifier(params.get("state"));
const body = {
type: this.getResponseType(),
application: applicationName,
@@ -156,8 +160,14 @@ class AuthCallback extends React.Component {
state: applicationName,
redirectUri: redirectUri,
method: method,
codeVerifier: codeVerifier, // Include PKCE code verifier
};
// Clean up the stored code verifier after using it
if (codeVerifier) {
Provider.clearCodeVerifier(params.get("state"));
}
if (this.getResponseType() === "cas") {
// user is using casdoor as cas sso server, and wants the ticket to be acquired
AuthBackend.loginCas(body, {"service": casService}).then((res) => {

View File

@@ -14,9 +14,52 @@
import React from "react";
import {Tooltip} from "antd";
import CryptoJS from "crypto-js";
import * as Util from "./Util";
import * as Setting from "../Setting";
// PKCE helper functions
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
function base64UrlEncode(buffer) {
const base64 = btoa(String.fromCharCode.apply(null, buffer));
return base64
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function generateCodeChallenge(verifier) {
// Convert verifier to UTF-8 bytes and compute SHA-256 hash
const hash = CryptoJS.SHA256(CryptoJS.enc.Utf8.parse(verifier));
const base64Hash = CryptoJS.enc.Base64.stringify(hash);
return base64Hash
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function storeCodeVerifier(state, verifier) {
localStorage.setItem("pkce_verifier", `${state}#${verifier}`);
}
export function getCodeVerifier(state) {
const verifierStore = localStorage.getItem("pkce_verifier");
const [storedState, verifier] = verifierStore ? verifierStore.split("#") : [null, null];
if (storedState !== state) {
return null;
}
return verifier;
}
export function clearCodeVerifier(state) {
localStorage.removeItem("pkce_verifier");
}
const authInfo = {
Google: {
scope: "profile+email",
@@ -402,7 +445,11 @@ export function getAuthUrl(application, provider, method, code) {
applicationName = `${application.name}-org-${application.organization}`;
}
const state = Util.getStateFromQueryParams(applicationName, provider.name, method, isShortState);
const codeChallenge = "P3S-a7dr8bgM4bF6vOyiKkKETDl16rcAzao9F8UIL1Y"; // SHA256(Base64-URL-encode("casdoor-verifier"))
// Generate PKCE code verifier and challenge dynamically
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
storeCodeVerifier(state, codeVerifier);
if (provider.type === "AzureAD") {
if (provider.domain !== "") {