forked from casdoor/casdoor
feat: make codeChallenge dynamic for custom OAuth provider (#4924)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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) {
|
||||
|
||||
13
idp/goth.go
13
idp/goth.go
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 !== "") {
|
||||
|
||||
Reference in New Issue
Block a user