Skip to content

Commit

Permalink
feat: support CAPTCHA action via Casdoor (#71)
Browse files Browse the repository at this point in the history
* feat: support CAPTCHA action via Casdoor

* update: update google/uuid to 1.6.0

* feat: add apache header

* fix: go test error
  • Loading branch information
love98ooo authored Aug 27, 2024
1 parent 1671f43 commit a9b1f4e
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 5 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/go-git/go-git/v5 v5.9.0
github.com/go-mysql-org/go-mysql v1.7.0
github.com/go-sql-driver/mysql v1.6.0
github.com/google/uuid v1.6.0
github.com/hsluoyz/modsecurity-go v0.0.7
github.com/lib/pq v1.10.2
github.com/likexian/whois v1.15.1
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,9 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
Expand Down
2 changes: 2 additions & 0 deletions rule/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ func CheckRules(ruleIds []string, r *http.Request) (string, string, error) {
return action, reason, nil
} else if action == "Allow" {
return action, reason, nil
} else if action == "Captcha" {
return action, reason, nil
} else {
return "", "", fmt.Errorf("unknown rule action: %s for rule: %s", action, rule.GetId())
}
Expand Down
139 changes: 139 additions & 0 deletions service/captcha.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright 2023 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package service

import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"time"

"github.com/casbin/caswaf/conf"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"github.com/google/uuid"
)

type verifyResponse struct {
Status string `json:"status"`
Msg string `json:"msg"`
Sub string `json:"sub"`
Name string `json:"name"`
Data bool `json:"data"`
Data2 interface{} `json:"data2"`
}

var verifiedSession = make(map[string]time.Time)

func redirectToCaptcha(w http.ResponseWriter, r *http.Request) {
scheme := getScheme(r)
callbackUrl := fmt.Sprintf("%s://%s/caswaf-captcha-verify", scheme, r.Host)
captchaUri := fmt.Sprintf(
"%s/captcha?client_id=%s&redirect_uri=%s&state=%s",
getCasdoorEndpoint(),
conf.GetConfigString("clientId"),
callbackUrl,
conf.GetConfigString("casdoorApplication"),
)
http.Redirect(w, r, captchaUri, http.StatusFound)
}

func handleCaptchaCallback(w http.ResponseWriter, r *http.Request) {
host := r.Host

code := r.URL.Query().Get("code")
typeStr := r.URL.Query().Get("type")
secret := r.URL.Query().Get("secret")
applicationId := r.URL.Query().Get("applicationId")
if code == "" || typeStr == "" || secret == "" || applicationId == "" {
redirectToCaptcha(w, r)
}

var b bytes.Buffer
writer := multipart.NewWriter(&b)
_ = writer.WriteField("captchaToken", code)
_ = writer.WriteField("clientSecret", secret)
_ = writer.WriteField("applicationId", applicationId)
_ = writer.WriteField("captchaType", typeStr)
_ = writer.Close()
verifyURL := casdoorsdk.GetUrl("verify-captcha", nil)
req, err := http.NewRequest("POST", verifyURL, &b)
if err != nil {
redirectToCaptcha(w, r)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
redirectToCaptcha(w, r)
}
// read response
body, err := io.ReadAll(resp.Body)
if err != nil {
redirectToCaptcha(w, r)
}
// parse response
var vr verifyResponse
err = json.Unmarshal(body, &vr)
if err != nil {
redirectToCaptcha(w, r)
}
if vr.Status != "ok" || !vr.Data {
redirectToCaptcha(w, r)
}
// set verified session
uuidStr := uuid.NewString()
verifiedSession[uuidStr] = time.Now()
cookie := &http.Cookie{
Name: "casdoor_captcha_token",
Value: uuidStr,
Path: "/",
Domain: host,
Expires: time.Now().Add(30 * time.Minute),
RawExpires: "",
MaxAge: 0,
Secure: false,
HttpOnly: false,
SameSite: 0,
Raw: "",
Unparsed: nil,
}

scheme := getScheme(r)
http.SetCookie(w, cookie)
http.Redirect(w, r, scheme+"://"+host, http.StatusFound)
return
}

func isVerifiedSession(r *http.Request) bool {
cookie, err := r.Cookie("casdoor_captcha_token")
if err != nil {
return false
}
token := cookie.Value
if token == "" {
return false
}
t, ok := verifiedSession[token]
if ok {
if time.Now().Sub(t) < 30*time.Minute {
return true
}
delete(verifiedSession, token)
}
return false
}
5 changes: 1 addition & 4 deletions service/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ func getSigninUrl(casdoorClient *casdoorsdk.Client, callbackUrl string, original
}

func redirectToCasdoor(casdoorClient *casdoorsdk.Client, w http.ResponseWriter, r *http.Request) {
scheme := r.URL.Scheme
if scheme == "" {
scheme = "http"
}
scheme := getScheme(r)

callbackUrl := fmt.Sprintf("%s://%s/caswaf-handler", scheme, r.Host)
originalPath := r.RequestURI
Expand Down
11 changes: 11 additions & 0 deletions service/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
case "Drop":
responseError(w, "Dropped by CasWAF: %s", reason)
w.WriteHeader(http.StatusBadRequest)
case "Captcha":
ok := isVerifiedSession(r)
if ok {
w.WriteHeader(http.StatusOK)
nextHandle(w, r)
return
}
w.Header().Set("Set-Cookie", "casdoor_captcha_token=; Path=/; Max-Age=-1")
redirectToCaptcha(w, r)
return
default:
responseError(w, "Error in CasWAF: %s", reason)
w.WriteHeader(http.StatusInternalServerError)
Expand Down Expand Up @@ -251,6 +261,7 @@ func nextHandle(w http.ResponseWriter, r *http.Request) {
func Start() {
http.HandleFunc("/", handleRequest)
http.HandleFunc("/caswaf-handler", handleAuthCallback)
http.HandleFunc("/caswaf-captcha-verify", handleCaptchaCallback)

gatewayEnabled, err := beego.AppConfig.Bool("gatewayEnabled")
if err != nil {
Expand Down
17 changes: 17 additions & 0 deletions service/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"strings"

"github.com/beego/beego"
"github.com/casbin/caswaf/conf"
"github.com/casbin/caswaf/object"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
Expand Down Expand Up @@ -123,3 +124,19 @@ func getCasdoorClientFromSite(site *object.Site) (*casdoorsdk.Client, error) {
res := casdoorsdk.NewClient(casdoorEndpoint, clientId, clientSecret, certificate, site.ApplicationObj.Organization, site.CasdoorApplication)
return res, nil
}

func getScheme(r *http.Request) string {
scheme := r.URL.Scheme
if scheme == "" {
scheme = "http"
}
return scheme
}

func getCasdoorEndpoint() string {
endpoint := conf.GetConfigString("casdoorEndpoint")
if endpoint == "http://localhost:8000" {
endpoint = "http://localhost:7001"
}
return endpoint
}
1 change: 1 addition & 0 deletions web/src/RuleEditPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ class RuleEditPage extends React.Component {
// {value: "redirect", text: "Redirect"},
{value: "Block", text: i18next.t("rule:Block")},
// {value: "drop", text: "Drop"},
{value: "Captcha", text: i18next.t("rule:Captcha")},
].map((item, index) => <Option key={index} value={item.value}>{item.text}</Option>)
}
</Select>
Expand Down

0 comments on commit a9b1f4e

Please sign in to comment.