mirror of
https://github.com/casdoor/casdoor.git
synced 2025-08-24 06:02:34 +08:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b97ae72179 | ||
![]() |
9190db1099 | ||
![]() |
1173f75794 | ||
![]() |
086859d1ce | ||
![]() |
9afaf5d695 | ||
![]() |
521f90a603 | ||
![]() |
4260efcfd0 | ||
![]() |
d772b0b7a8 | ||
![]() |
702b390da1 | ||
![]() |
b15b3b9335 | ||
![]() |
f8f864c5b9 | ||
![]() |
90e790f83c | ||
![]() |
58413246f3 | ||
![]() |
8f307dd907 | ||
![]() |
fe42b5e0ba | ||
![]() |
383bf44391 | ||
![]() |
36f5de3203 | ||
![]() |
eae69c41d7 | ||
![]() |
91057f54f3 | ||
![]() |
daa7b79915 | ||
![]() |
d3a5539dae | ||
![]() |
7d1c614452 | ||
![]() |
e2eafa909b | ||
![]() |
56bcef0592 | ||
![]() |
0860cbf343 |
@@ -47,6 +47,7 @@ p, *, *, GET, /api/get-app-login, *, *
|
||||
p, *, *, POST, /api/logout, *, *
|
||||
p, *, *, GET, /api/logout, *, *
|
||||
p, *, *, POST, /api/callback, *, *
|
||||
p, *, *, POST, /api/device-auth, *, *
|
||||
p, *, *, GET, /api/get-account, *, *
|
||||
p, *, *, GET, /api/userinfo, *, *
|
||||
p, *, *, GET, /api/user, *, *
|
||||
|
@@ -31,7 +31,7 @@ radiusServerPort = 1812
|
||||
radiusDefaultOrganization = "built-in"
|
||||
radiusSecret = "secret"
|
||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
initDataNewOnly = false
|
||||
initDataFile = "./init_data.json"
|
||||
frontendBaseDir = "../cc_0"
|
@@ -115,7 +115,7 @@ func TestGetConfigLogs(t *testing.T) {
|
||||
description string
|
||||
expected string
|
||||
}{
|
||||
{"Default log config", `{"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}`},
|
||||
{"Default log config", `{"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}`},
|
||||
}
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
|
@@ -32,6 +32,7 @@ const (
|
||||
ResponseTypeIdToken = "id_token"
|
||||
ResponseTypeSaml = "saml"
|
||||
ResponseTypeCas = "cas"
|
||||
ResponseTypeDevice = "device"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
|
@@ -25,6 +25,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/captcha"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
@@ -146,7 +147,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
c.ResponseError(c.T("auth:Challenge method should be S256"))
|
||||
return
|
||||
}
|
||||
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
code, err := object.GetOAuthCode(userId, clientId, form.Provider, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
@@ -169,6 +170,32 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
|
||||
resp.Data2 = user.NeedUpdatePassword
|
||||
}
|
||||
} else if form.Type == ResponseTypeDevice {
|
||||
authCache, ok := object.DeviceAuthMap.LoadAndDelete(form.UserCode)
|
||||
if !ok {
|
||||
c.ResponseError(c.T("auth:UserCode Expired"))
|
||||
return
|
||||
}
|
||||
|
||||
authCacheCast := authCache.(object.DeviceAuthCache)
|
||||
if authCacheCast.RequestAt.Add(time.Second * 120).Before(time.Now()) {
|
||||
c.ResponseError(c.T("auth:UserCode Expired"))
|
||||
return
|
||||
}
|
||||
|
||||
deviceAuthCacheDeviceCode, ok := object.DeviceAuthMap.Load(authCacheCast.UserName)
|
||||
if !ok {
|
||||
c.ResponseError(c.T("auth:DeviceCode Invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
deviceAuthCacheDeviceCodeCast := deviceAuthCacheDeviceCode.(object.DeviceAuthCache)
|
||||
deviceAuthCacheDeviceCodeCast.UserName = user.Name
|
||||
deviceAuthCacheDeviceCodeCast.UserSignIn = true
|
||||
|
||||
object.DeviceAuthMap.Store(authCacheCast.UserName, deviceAuthCacheDeviceCodeCast)
|
||||
|
||||
resp = &Response{Status: "ok", Msg: "", Data: userId, Data2: user.NeedUpdatePassword}
|
||||
} else if form.Type == ResponseTypeSaml { // saml flow
|
||||
res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host)
|
||||
if err != nil {
|
||||
@@ -242,6 +269,7 @@ func (c *ApiController) GetApplicationLogin() {
|
||||
state := c.Input().Get("state")
|
||||
id := c.Input().Get("id")
|
||||
loginType := c.Input().Get("type")
|
||||
userCode := c.Input().Get("userCode")
|
||||
|
||||
var application *object.Application
|
||||
var msg string
|
||||
@@ -268,6 +296,19 @@ func (c *ApiController) GetApplicationLogin() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else if loginType == "device" {
|
||||
deviceAuthCache, ok := object.DeviceAuthMap.Load(userCode)
|
||||
if !ok {
|
||||
c.ResponseError(c.T("auth:UserCode Invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache)
|
||||
application, err = object.GetApplication(deviceAuthCacheCast.ApplicationId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
@@ -1215,3 +1256,75 @@ func (c *ApiController) Callback() {
|
||||
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", code, state)
|
||||
c.Ctx.Redirect(http.StatusFound, frontendCallbackUrl)
|
||||
}
|
||||
|
||||
// DeviceAuth
|
||||
// @Title DeviceAuth
|
||||
// @Tag Device Authorization Endpoint
|
||||
// @Description Endpoint for the device authorization flow
|
||||
// @router /device-auth [post]
|
||||
// @Success 200 {object} object.DeviceAuthResponse The Response object
|
||||
func (c *ApiController) DeviceAuth() {
|
||||
clientId := c.Input().Get("client_id")
|
||||
scope := c.Input().Get("scope")
|
||||
application, err := object.GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
c.Data["json"] = object.TokenError{
|
||||
Error: err.Error(),
|
||||
ErrorDescription: err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.Data["json"] = object.TokenError{
|
||||
Error: c.T("token:Invalid client_id"),
|
||||
ErrorDescription: c.T("token:Invalid client_id"),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
deviceCode := util.GenerateId()
|
||||
userCode := util.GetRandomName()
|
||||
|
||||
generateTime := 0
|
||||
for {
|
||||
if generateTime > 5 {
|
||||
c.Data["json"] = object.TokenError{
|
||||
Error: "userCode gen",
|
||||
ErrorDescription: c.T("token:Invalid client_id"),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
_, ok := object.DeviceAuthMap.Load(userCode)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
generateTime++
|
||||
}
|
||||
|
||||
deviceAuthCache := object.DeviceAuthCache{
|
||||
UserSignIn: false,
|
||||
UserName: "",
|
||||
Scope: scope,
|
||||
ApplicationId: application.GetId(),
|
||||
RequestAt: time.Now(),
|
||||
}
|
||||
|
||||
userAuthCache := object.DeviceAuthCache{
|
||||
UserSignIn: false,
|
||||
UserName: deviceCode,
|
||||
Scope: scope,
|
||||
ApplicationId: application.GetId(),
|
||||
RequestAt: time.Now(),
|
||||
}
|
||||
|
||||
object.DeviceAuthMap.Store(deviceCode, deviceAuthCache)
|
||||
object.DeviceAuthMap.Store(userCode, userAuthCache)
|
||||
|
||||
c.Data["json"] = object.GetDeviceAuthResponse(deviceCode, userCode, c.Ctx.Request.Host)
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -170,12 +171,13 @@ func (c *ApiController) GetOAuthToken() {
|
||||
tag := c.Input().Get("tag")
|
||||
avatar := c.Input().Get("avatar")
|
||||
refreshToken := c.Input().Get("refresh_token")
|
||||
deviceCode := c.Input().Get("device_code")
|
||||
|
||||
if clientId == "" && clientSecret == "" {
|
||||
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
|
||||
}
|
||||
|
||||
if len(c.Ctx.Input.RequestBody) != 0 {
|
||||
if len(c.Ctx.Input.RequestBody) != 0 && grantType != "urn:ietf:params:oauth:grant-type:device_code" {
|
||||
// If clientId is empty, try to read data from RequestBody
|
||||
var tokenRequest TokenRequest
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest)
|
||||
@@ -219,6 +221,46 @@ func (c *ApiController) GetOAuthToken() {
|
||||
}
|
||||
}
|
||||
|
||||
if deviceCode != "" {
|
||||
deviceAuthCache, ok := object.DeviceAuthMap.Load(deviceCode)
|
||||
if !ok {
|
||||
c.Data["json"] = &object.TokenError{
|
||||
Error: "expired_token",
|
||||
ErrorDescription: "token is expired",
|
||||
}
|
||||
c.SetTokenErrorHttpStatus()
|
||||
c.ServeJSON()
|
||||
c.SetTokenErrorHttpStatus()
|
||||
return
|
||||
}
|
||||
|
||||
deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache)
|
||||
if !deviceAuthCacheCast.UserSignIn {
|
||||
c.Data["json"] = &object.TokenError{
|
||||
Error: "authorization_pending",
|
||||
ErrorDescription: "authorization pending",
|
||||
}
|
||||
c.SetTokenErrorHttpStatus()
|
||||
c.ServeJSON()
|
||||
c.SetTokenErrorHttpStatus()
|
||||
return
|
||||
}
|
||||
|
||||
if deviceAuthCacheCast.RequestAt.Add(time.Second * 120).Before(time.Now()) {
|
||||
c.Data["json"] = &object.TokenError{
|
||||
Error: "expired_token",
|
||||
ErrorDescription: "token is expired",
|
||||
}
|
||||
c.SetTokenErrorHttpStatus()
|
||||
c.ServeJSON()
|
||||
c.SetTokenErrorHttpStatus()
|
||||
return
|
||||
}
|
||||
object.DeviceAuthMap.Delete(deviceCode)
|
||||
|
||||
username = deviceAuthCacheCast.UserName
|
||||
}
|
||||
|
||||
host := c.Ctx.Request.Host
|
||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
|
@@ -34,6 +34,8 @@ func GetCredManager(passwordType string) CredManager {
|
||||
return NewPbkdf2SaltCredManager()
|
||||
} else if passwordType == "argon2id" {
|
||||
return NewArgon2idCredManager()
|
||||
} else if passwordType == "pbkdf2-django" {
|
||||
return NewPbkdf2DjangoCredManager()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
71
cred/pbkdf2_django.go
Normal file
71
cred/pbkdf2_django.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2025 The Casdoor 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 cred
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// password type: pbkdf2-django
|
||||
|
||||
type Pbkdf2DjangoCredManager struct{}
|
||||
|
||||
func NewPbkdf2DjangoCredManager() *Pbkdf2DjangoCredManager {
|
||||
cm := &Pbkdf2DjangoCredManager{}
|
||||
return cm
|
||||
}
|
||||
|
||||
func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
iterations := 260000
|
||||
salt := userSalt
|
||||
if salt == "" {
|
||||
salt = organizationSalt
|
||||
}
|
||||
|
||||
saltBytes := []byte(salt)
|
||||
passwordBytes := []byte(password)
|
||||
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iterations, sha256.Size, sha256.New)
|
||||
hashBase64 := base64.StdEncoding.EncodeToString(computedHash)
|
||||
return "pbkdf2_sha256$" + strconv.Itoa(iterations) + "$" + salt + "$" + hashBase64
|
||||
}
|
||||
|
||||
func (m *Pbkdf2DjangoCredManager) IsPasswordCorrect(password string, passwordHash string, userSalt string, organizationSalt string) bool {
|
||||
parts := strings.Split(passwordHash, "$")
|
||||
if len(parts) != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
algorithm, iterations, salt, hash := parts[0], parts[1], parts[2], parts[3]
|
||||
if algorithm != "pbkdf2_sha256" {
|
||||
return false
|
||||
}
|
||||
|
||||
iter, err := strconv.Atoi(iterations)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
saltBytes := []byte(salt)
|
||||
passwordBytes := []byte(password)
|
||||
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iter, sha256.Size, sha256.New)
|
||||
computedHashBase64 := base64.StdEncoding.EncodeToString(computedHash)
|
||||
|
||||
return computedHashBase64 == hash
|
||||
}
|
@@ -15,6 +15,8 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -27,13 +29,21 @@ type HttpEmailProvider struct {
|
||||
endpoint string
|
||||
method string
|
||||
httpHeaders map[string]string
|
||||
bodyMapping map[string]string
|
||||
contentType string
|
||||
}
|
||||
|
||||
func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string]string) *HttpEmailProvider {
|
||||
func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) *HttpEmailProvider {
|
||||
if contentType == "" {
|
||||
contentType = "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
client := &HttpEmailProvider{
|
||||
endpoint: endpoint,
|
||||
method: method,
|
||||
httpHeaders: httpHeaders,
|
||||
bodyMapping: bodyMapping,
|
||||
contentType: contentType,
|
||||
}
|
||||
return client
|
||||
}
|
||||
@@ -41,18 +51,52 @@ func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string
|
||||
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
fromNameField := "fromName"
|
||||
toAddressField := "toAddress"
|
||||
subjectField := "subject"
|
||||
contentField := "content"
|
||||
|
||||
for k, v := range c.bodyMapping {
|
||||
switch k {
|
||||
case "fromName":
|
||||
fromNameField = v
|
||||
case "toAddress":
|
||||
toAddressField = v
|
||||
case "subject":
|
||||
subjectField = v
|
||||
case "content":
|
||||
contentField = v
|
||||
}
|
||||
}
|
||||
|
||||
if c.method == "POST" || c.method == "PUT" || c.method == "DELETE" {
|
||||
formValues := url.Values{}
|
||||
formValues.Set("fromName", fromName)
|
||||
formValues.Set("toAddress", toAddress)
|
||||
formValues.Set("subject", subject)
|
||||
formValues.Set("content", content)
|
||||
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
||||
bodyMap := make(map[string]string)
|
||||
bodyMap[fromNameField] = fromName
|
||||
bodyMap[toAddressField] = toAddress
|
||||
bodyMap[subjectField] = subject
|
||||
bodyMap[contentField] = content
|
||||
|
||||
var fromValueBytes []byte
|
||||
if c.contentType == "application/json" {
|
||||
fromValueBytes, err = json.Marshal(bodyMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err = http.NewRequest(c.method, c.endpoint, bytes.NewBuffer(fromValueBytes))
|
||||
} else {
|
||||
formValues := url.Values{}
|
||||
for k, v := range bodyMap {
|
||||
formValues.Add(k, v)
|
||||
}
|
||||
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", c.contentType)
|
||||
} else if c.method == "GET" {
|
||||
req, err = http.NewRequest(c.method, c.endpoint, nil)
|
||||
if err != nil {
|
||||
@@ -60,10 +104,10 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("fromName", fromName)
|
||||
q.Add("toAddress", toAddress)
|
||||
q.Add("subject", subject)
|
||||
q.Add("content", content)
|
||||
q.Add(fromNameField, fromName)
|
||||
q.Add(toAddressField, toAddress)
|
||||
q.Add(subjectField, subject)
|
||||
q.Add(contentField, content)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
} else {
|
||||
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method)
|
||||
|
@@ -18,11 +18,11 @@ type EmailProvider interface {
|
||||
Send(fromAddress string, fromName, toAddress string, subject string, content string) error
|
||||
}
|
||||
|
||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string) EmailProvider {
|
||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) EmailProvider {
|
||||
if typ == "Azure ACS" {
|
||||
return NewAzureACSEmailProvider(clientSecret, host)
|
||||
} else if typ == "Custom HTTP Email" {
|
||||
return NewHttpEmailProvider(endpoint, method, httpHeaders)
|
||||
return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
|
||||
} else if typ == "SendGrid" {
|
||||
return NewSendgridEmailProvider(clientSecret, host, endpoint)
|
||||
} else {
|
||||
|
@@ -70,6 +70,7 @@ type AuthForm struct {
|
||||
|
||||
FaceId []float64 `json:"faceId"`
|
||||
FaceIdImage []string `json:"faceIdImage"`
|
||||
UserCode string `json:"userCode"`
|
||||
}
|
||||
|
||||
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {
|
||||
|
4
go.mod
4
go.mod
@@ -16,7 +16,7 @@ require (
|
||||
github.com/casdoor/go-sms-sender v0.25.0
|
||||
github.com/casdoor/gomail/v2 v2.1.0
|
||||
github.com/casdoor/ldapserver v1.2.0
|
||||
github.com/casdoor/notify v1.0.0
|
||||
github.com/casdoor/notify v1.0.1
|
||||
github.com/casdoor/oss v1.8.0
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||
@@ -101,7 +101,7 @@ require (
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
|
||||
github.com/aliyun/credentials-go v1.3.10 // indirect
|
||||
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.6.1 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.156 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19 // indirect
|
||||
|
8
go.sum
8
go.sum
@@ -188,8 +188,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/atc0005/go-teams-notify/v2 v2.6.1 h1:t22ybzQuaQs4UJe4ceF5VYGsPhs6ir3nZOId/FBy6Go=
|
||||
github.com/atc0005/go-teams-notify/v2 v2.6.1/go.mod h1:xo6GejLDHn3tWBA181F8LrllIL0xC1uRsRxq7YNXaaY=
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0 h1:nbDeHy89NjYlF/PEfLVF6lsserY9O5SnN1iOIw3AxXw=
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0/go.mod h1:WSv9moolRsBcpZbwEf6gZxj7h0uJlJskJq5zkEWKO8Y=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.45.5 h1:bxilnhv9FngUgdPNJmOIv2bk+2sP0dpqX3e4olhWcGM=
|
||||
@@ -238,8 +238,8 @@ github.com/casdoor/gomail/v2 v2.1.0 h1:ua97E3CARnF1Ik8ga/Drz9uGZfaElXJumFexiErWU
|
||||
github.com/casdoor/gomail/v2 v2.1.0/go.mod h1:GFzOD9RhY0nODiiPaQiOa6DfoKtmO9aTesu5qrp26OI=
|
||||
github.com/casdoor/ldapserver v1.2.0 h1:HdSYe+ULU6z9K+2BqgTrJKQRR4//ERAXB64ttOun6Ow=
|
||||
github.com/casdoor/ldapserver v1.2.0/go.mod h1:VwYU2vqQ2pA8sa00PRekH71R2XmgfzMKhmp1XrrDu2s=
|
||||
github.com/casdoor/notify v1.0.0 h1:oldsaaQFPrlufm/OA314z8DwFVE1Tc9Gt1z4ptRHhXw=
|
||||
github.com/casdoor/notify v1.0.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
|
||||
github.com/casdoor/notify v1.0.1 h1:p0kzI7OBlvLbL7zWeKIu31LRcEAygNZGKr5gcFfSIoE=
|
||||
github.com/casdoor/notify v1.0.1/go.mod h1:RUlaFJw87FoM/nbs0iXPP0h+DxKGTaWAIFQV0oZcSQA=
|
||||
github.com/casdoor/oss v1.8.0 h1:uuyKhDIp7ydOtV4lpqhAY23Ban2Ln8La8+QT36CwylM=
|
||||
github.com/casdoor/oss v1.8.0/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U=
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
|
||||
|
@@ -44,6 +44,7 @@ type ProviderInfo struct {
|
||||
AppId string
|
||||
HostUrl string
|
||||
RedirectUrl string
|
||||
DisableSsl bool
|
||||
|
||||
TokenURL string
|
||||
AuthURL string
|
||||
@@ -79,9 +80,9 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
|
||||
return NewLinkedInIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||
case "WeCom":
|
||||
if idpInfo.SubType == "Internal" {
|
||||
return NewWeComInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||
return NewWeComInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.DisableSsl), nil
|
||||
} else if idpInfo.SubType == "Third-party" {
|
||||
return NewWeComIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||
return NewWeComIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.DisableSsl), nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("WeCom provider subType: %s is not supported", idpInfo.SubType)
|
||||
}
|
||||
|
@@ -29,13 +29,16 @@ import (
|
||||
type WeComInternalIdProvider struct {
|
||||
Client *http.Client
|
||||
Config *oauth2.Config
|
||||
|
||||
UseIdAsName bool
|
||||
}
|
||||
|
||||
func NewWeComInternalIdProvider(clientId string, clientSecret string, redirectUrl string) *WeComInternalIdProvider {
|
||||
func NewWeComInternalIdProvider(clientId string, clientSecret string, redirectUrl string, useIdAsName bool) *WeComInternalIdProvider {
|
||||
idp := &WeComInternalIdProvider{}
|
||||
|
||||
config := idp.getConfig(clientId, clientSecret, redirectUrl)
|
||||
idp.Config = config
|
||||
idp.UseIdAsName = useIdAsName
|
||||
|
||||
return idp
|
||||
}
|
||||
@@ -169,5 +172,9 @@ func (idp *WeComInternalIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo,
|
||||
userInfo.Id = userInfo.Username
|
||||
}
|
||||
|
||||
if idp.UseIdAsName {
|
||||
userInfo.Username = userInfo.Id
|
||||
}
|
||||
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
@@ -28,13 +28,16 @@ import (
|
||||
type WeComIdProvider struct {
|
||||
Client *http.Client
|
||||
Config *oauth2.Config
|
||||
|
||||
UseIdAsName bool
|
||||
}
|
||||
|
||||
func NewWeComIdProvider(clientId string, clientSecret string, redirectUrl string) *WeComIdProvider {
|
||||
func NewWeComIdProvider(clientId string, clientSecret string, redirectUrl string, useIdAsName bool) *WeComIdProvider {
|
||||
idp := &WeComIdProvider{}
|
||||
|
||||
config := idp.getConfig(clientId, clientSecret, redirectUrl)
|
||||
idp.Config = config
|
||||
idp.UseIdAsName = useIdAsName
|
||||
|
||||
return idp
|
||||
}
|
||||
@@ -183,6 +186,10 @@ func (idp *WeComIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
|
||||
DisplayName: wecomUserInfo.UserInfo.Name,
|
||||
AvatarUrl: wecomUserInfo.UserInfo.Avatar,
|
||||
}
|
||||
|
||||
if idp.UseIdAsName {
|
||||
userInfo.Username = userInfo.Id
|
||||
}
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
|
19
main.go
19
main.go
@@ -15,6 +15,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego"
|
||||
@@ -77,10 +78,26 @@ func main() {
|
||||
beego.BConfig.WebConfig.Session.SessionGCMaxLifetime = 3600 * 24 * 30
|
||||
// beego.BConfig.WebConfig.Session.SessionCookieSameSite = http.SameSiteNoneMode
|
||||
|
||||
err := logs.SetLogger(logs.AdapterFile, conf.GetConfigString("logConfig"))
|
||||
var logAdapter string
|
||||
logConfigMap := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(conf.GetConfigString("logConfig")), &logConfigMap)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, ok := logConfigMap["adapter"]
|
||||
if !ok {
|
||||
logAdapter = "file"
|
||||
} else {
|
||||
logAdapter = logConfigMap["adapter"].(string)
|
||||
}
|
||||
if logAdapter == "console" {
|
||||
logs.Reset()
|
||||
}
|
||||
err = logs.SetLogger(logAdapter, conf.GetConfigString("logConfig"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
port := beego.AppConfig.DefaultInt("httpport", 8000)
|
||||
// logs.SetLevel(logs.LevelInformational)
|
||||
logs.SetLogFuncCall(false)
|
||||
|
@@ -85,7 +85,7 @@ type Application struct {
|
||||
EnableWebAuthn bool `json:"enableWebAuthn"`
|
||||
EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
|
||||
OrgChoiceMode string `json:"orgChoiceMode"`
|
||||
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
|
||||
SamlReplyUrl string `xorm:"varchar(500)" json:"samlReplyUrl"`
|
||||
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
|
||||
SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"`
|
||||
SignupItems []*SignupItem `xorm:"varchar(3000)" json:"signupItems"`
|
||||
|
@@ -63,7 +63,11 @@ func GetCertCount(owner, field, value string) (int64, error) {
|
||||
|
||||
func GetCerts(owner string) ([]*Cert, error) {
|
||||
certs := []*Cert{}
|
||||
err := ormer.Engine.Where("owner = ? or owner = ? ", "admin", owner).Desc("created_time").Find(&certs, &Cert{})
|
||||
db := ormer.Engine.NewSession()
|
||||
if owner != "" {
|
||||
db = db.Where("owner = ? or owner = ? ", "admin", owner)
|
||||
}
|
||||
err := db.Desc("created_time").Find(&certs, &Cert{})
|
||||
if err != nil {
|
||||
return certs, err
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ func TestSmtpServer(provider *Provider) error {
|
||||
}
|
||||
|
||||
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
||||
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders)
|
||||
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl)
|
||||
|
||||
fromAddress := provider.ClientId2
|
||||
if fromAddress == "" {
|
||||
|
@@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@@ -210,6 +211,12 @@ func DeleteGroup(group *Group) (bool, error) {
|
||||
}
|
||||
|
||||
func checkGroupName(name string) error {
|
||||
if name == "" {
|
||||
return errors.New("group name can't be empty")
|
||||
}
|
||||
if strings.Contains(name, "/") {
|
||||
return errors.New("group name can't contain \"/\"")
|
||||
}
|
||||
exist, err := ormer.Engine.Exist(&Organization{Owner: "admin", Name: name})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -30,6 +30,7 @@ type OidcDiscovery struct {
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
UserinfoEndpoint string `json:"userinfo_endpoint"`
|
||||
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
|
||||
JwksUri string `json:"jwks_uri"`
|
||||
IntrospectionEndpoint string `json:"introspection_endpoint"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||
@@ -119,6 +120,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
|
||||
AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", originFrontend),
|
||||
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", originBackend),
|
||||
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
|
||||
DeviceAuthorizationEndpoint: fmt.Sprintf("%s/api/device-auth", originBackend),
|
||||
JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend),
|
||||
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", originBackend),
|
||||
ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"},
|
||||
@@ -138,7 +140,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
|
||||
|
||||
func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
|
||||
jwks := jose.JSONWebKeySet{}
|
||||
certs, err := GetCerts("admin")
|
||||
certs, err := GetCerts("")
|
||||
if err != nil {
|
||||
return jwks, err
|
||||
}
|
||||
@@ -213,3 +215,14 @@ func GetWebFinger(resource string, rels []string, host string) (WebFinger, error
|
||||
|
||||
return wf, nil
|
||||
}
|
||||
|
||||
func GetDeviceAuthResponse(deviceCode string, userCode string, host string) DeviceAuthResponse {
|
||||
originFrontend, _ := getOriginFromHost(host)
|
||||
|
||||
return DeviceAuthResponse{
|
||||
DeviceCode: deviceCode,
|
||||
UserCode: userCode,
|
||||
VerificationUri: fmt.Sprintf("%s/login/oauth/device/%s", originFrontend, userCode),
|
||||
ExpiresIn: 120,
|
||||
}
|
||||
}
|
||||
|
@@ -179,7 +179,7 @@ func NewAdapterFromDb(driverName string, dataSourceName string, dbName string, d
|
||||
|
||||
func refineDataSourceNameForPostgres(dataSourceName string) string {
|
||||
reg := regexp.MustCompile(`dbname=[^ ]+\s*`)
|
||||
return reg.ReplaceAllString(dataSourceName, "")
|
||||
return reg.ReplaceAllString(dataSourceName, "dbname=postgres")
|
||||
}
|
||||
|
||||
func createDatabaseForPostgres(driverName string, dataSourceName string, dbName string) error {
|
||||
@@ -190,7 +190,7 @@ func createDatabaseForPostgres(driverName string, dataSourceName string, dbName
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName))
|
||||
_, err = db.Exec(fmt.Sprintf("CREATE DATABASE \"%s\";", dbName))
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "already exists") {
|
||||
return err
|
||||
|
@@ -475,6 +475,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
|
||||
AuthURL: provider.CustomAuthUrl,
|
||||
UserInfoURL: provider.CustomUserInfoUrl,
|
||||
UserMapping: provider.UserMapping,
|
||||
DisableSsl: provider.DisableSsl,
|
||||
}
|
||||
|
||||
if provider.Type == "WeChat" {
|
||||
|
@@ -263,6 +263,27 @@ func addWebhookRecord(webhook *Webhook, record *casvisorsdk.Record, statusCode i
|
||||
return err
|
||||
}
|
||||
|
||||
func filterRecordObject(object string, objectFields []string) string {
|
||||
var rawObject map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(object), &rawObject)
|
||||
|
||||
if rawObject == nil {
|
||||
return object
|
||||
}
|
||||
|
||||
filteredObject := make(map[string]interface{})
|
||||
|
||||
for _, field := range objectFields {
|
||||
fieldValue, ok := rawObject[field]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
filteredObject[field] = fieldValue
|
||||
}
|
||||
|
||||
return util.StructToJson(filteredObject)
|
||||
}
|
||||
|
||||
func SendWebhooks(record *casvisorsdk.Record) error {
|
||||
webhooks, err := getWebhooksByOrganization("")
|
||||
if err != nil {
|
||||
@@ -271,7 +292,14 @@ func SendWebhooks(record *casvisorsdk.Record) error {
|
||||
|
||||
errs := []error{}
|
||||
webhooks = getFilteredWebhooks(webhooks, record.Organization, record.Action)
|
||||
|
||||
record2 := *record
|
||||
for _, webhook := range webhooks {
|
||||
|
||||
if len(webhook.ObjectFields) != 0 && webhook.ObjectFields[0] != "All" {
|
||||
record2.Object = filterRecordObject(record.Object, webhook.ObjectFields)
|
||||
}
|
||||
|
||||
var user *User
|
||||
if webhook.IsUserExtended {
|
||||
user, err = getUser(record.Organization, record.User)
|
||||
@@ -287,12 +315,12 @@ func SendWebhooks(record *casvisorsdk.Record) error {
|
||||
}
|
||||
}
|
||||
|
||||
statusCode, respBody, err := sendWebhook(webhook, record, user)
|
||||
statusCode, respBody, err := sendWebhook(webhook, &record2, user)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
err = addWebhookRecord(webhook, record, statusCode, respBody, err)
|
||||
err = addWebhookRecord(webhook, &record2, statusCode, respBody, err)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
@@ -31,7 +31,8 @@ type Claims struct {
|
||||
Tag string `json:"tag"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
// the `azp` (Authorized Party) claim. Optional. See https://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||
Azp string `json:"azp,omitempty"`
|
||||
Azp string `json:"azp,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -46,6 +47,17 @@ type UserShort struct {
|
||||
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||
}
|
||||
|
||||
type UserStandard struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"preferred_username,omitempty"`
|
||||
|
||||
Id string `xorm:"varchar(100) index" json:"id"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"name,omitempty"`
|
||||
Avatar string `xorm:"varchar(500)" json:"picture,omitempty"`
|
||||
Email string `xorm:"varchar(100) index" json:"email,omitempty"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone,omitempty"`
|
||||
}
|
||||
|
||||
type UserWithoutThirdIdp struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
@@ -140,6 +152,7 @@ type ClaimsShort struct {
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Azp string `json:"azp,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -159,6 +172,7 @@ type ClaimsWithoutThirdIdp struct {
|
||||
Tag string `json:"tag"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Azp string `json:"azp,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -176,6 +190,20 @@ func getShortUser(user *User) *UserShort {
|
||||
return res
|
||||
}
|
||||
|
||||
func getStandardUser(user *User) *UserStandard {
|
||||
res := &UserStandard{
|
||||
Owner: user.Owner,
|
||||
Name: user.Name,
|
||||
|
||||
Id: user.Id,
|
||||
DisplayName: user.DisplayName,
|
||||
Avatar: user.Avatar,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
|
||||
res := &UserWithoutThirdIdp{
|
||||
Owner: user.Owner,
|
||||
@@ -274,6 +302,7 @@ func getShortClaims(claims Claims) ClaimsShort {
|
||||
Scope: claims.Scope,
|
||||
RegisteredClaims: claims.RegisteredClaims,
|
||||
Azp: claims.Azp,
|
||||
Provider: claims.Provider,
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -287,6 +316,7 @@ func getClaimsWithoutThirdIdp(claims Claims) ClaimsWithoutThirdIdp {
|
||||
Scope: claims.Scope,
|
||||
RegisteredClaims: claims.RegisteredClaims,
|
||||
Azp: claims.Azp,
|
||||
Provider: claims.Provider,
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -308,6 +338,7 @@ func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
|
||||
res["tag"] = claims.Tag
|
||||
res["scope"] = claims.Scope
|
||||
res["azp"] = claims.Azp
|
||||
res["provider"] = claims.Provider
|
||||
|
||||
for _, field := range tokenField {
|
||||
userField := userValue.FieldByName(field)
|
||||
@@ -342,7 +373,7 @@ func refineUser(user *User) *User {
|
||||
return user
|
||||
}
|
||||
|
||||
func generateJwtToken(application *Application, user *User, nonce string, scope string, host string) (string, string, string, error) {
|
||||
func generateJwtToken(application *Application, user *User, provider string, nonce string, scope string, host string) (string, string, string, error) {
|
||||
nowTime := time.Now()
|
||||
expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour)
|
||||
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
|
||||
@@ -362,9 +393,10 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
|
||||
TokenType: "access-token",
|
||||
Nonce: nonce,
|
||||
// FIXME: A workaround for custom claim by reusing `tag` in user info
|
||||
Tag: user.Tag,
|
||||
Scope: scope,
|
||||
Azp: application.ClientId,
|
||||
Tag: user.Tag,
|
||||
Scope: scope,
|
||||
Azp: application.ClientId,
|
||||
Provider: provider,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: originBackend,
|
||||
Subject: user.Id,
|
||||
|
@@ -18,6 +18,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
@@ -37,6 +38,8 @@ const (
|
||||
EndpointError = "endpoint_error"
|
||||
)
|
||||
|
||||
var DeviceAuthMap = sync.Map{}
|
||||
|
||||
type Code struct {
|
||||
Message string `xorm:"varchar(100)" json:"message"`
|
||||
Code string `xorm:"varchar(100)" json:"code"`
|
||||
@@ -71,6 +74,22 @@ type IntrospectionResponse struct {
|
||||
Jti string `json:"jti,omitempty"`
|
||||
}
|
||||
|
||||
type DeviceAuthCache struct {
|
||||
UserSignIn bool
|
||||
UserName string
|
||||
ApplicationId string
|
||||
Scope string
|
||||
RequestAt time.Time
|
||||
}
|
||||
|
||||
type DeviceAuthResponse struct {
|
||||
DeviceCode string `json:"device_code"`
|
||||
UserCode string `json:"user_code"`
|
||||
VerificationUri string `json:"verification_uri"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Interval int `json:"interval"`
|
||||
}
|
||||
|
||||
func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) {
|
||||
token, err := GetTokenByAccessToken(accessToken)
|
||||
if err != nil {
|
||||
@@ -117,7 +136,7 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
|
||||
return "", application, nil
|
||||
}
|
||||
|
||||
func GetOAuthCode(userId string, clientId string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string, lang string) (*Code, error) {
|
||||
func GetOAuthCode(userId string, clientId string, provider string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string, lang string) (*Code, error) {
|
||||
user, err := GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -152,7 +171,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, provider, nonce, scope, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -222,6 +241,8 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
||||
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
|
||||
case "token", "id_token": // Implicit Grant
|
||||
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
|
||||
case "urn:ietf:params:oauth:grant-type:device_code":
|
||||
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
|
||||
case "refresh_token":
|
||||
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
|
||||
if err != nil {
|
||||
@@ -358,7 +379,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
|
||||
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", "", scope, host)
|
||||
if err != nil {
|
||||
return &TokenError{
|
||||
Error: EndpointError,
|
||||
@@ -537,7 +558,7 @@ func GetPasswordToken(application *Application, username string, password string
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", scope, host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
@@ -583,7 +604,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
|
||||
Type: "application",
|
||||
}
|
||||
|
||||
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", scope, host)
|
||||
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", "", scope, host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
@@ -647,7 +668,7 @@ func GetTokenByUser(application *Application, user *User, scope string, nonce st
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", nonce, scope, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -754,7 +775,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", host)
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
|
@@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
type ClaimsStandard struct {
|
||||
*UserShort
|
||||
*UserStandard
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
PhoneNumber string `json:"phone_number,omitempty"`
|
||||
PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
|
||||
@@ -33,6 +33,7 @@ type ClaimsStandard struct {
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Address OIDCAddress `json:"address,omitempty"`
|
||||
Azp string `json:"azp,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
@@ -47,13 +48,14 @@ func getStreetAddress(user *User) string {
|
||||
|
||||
func getStandardClaims(claims Claims) ClaimsStandard {
|
||||
res := ClaimsStandard{
|
||||
UserShort: getShortUser(claims.User),
|
||||
UserStandard: getStandardUser(claims.User),
|
||||
EmailVerified: claims.User.EmailVerified,
|
||||
TokenType: claims.TokenType,
|
||||
Nonce: claims.Nonce,
|
||||
Scope: claims.Scope,
|
||||
RegisteredClaims: claims.RegisteredClaims,
|
||||
Azp: claims.Azp,
|
||||
Provider: claims.Provider,
|
||||
}
|
||||
|
||||
res.Phone = ""
|
||||
|
@@ -837,6 +837,10 @@ func AddUser(user *User) (bool, error) {
|
||||
return false, fmt.Errorf("the user's owner and name should not be empty")
|
||||
}
|
||||
|
||||
if CheckUsernameWithEmail(user.Name, "en") != "" {
|
||||
user.Name = util.GetRandomName()
|
||||
}
|
||||
|
||||
organization, err := GetOrganizationByUser(user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@@ -39,6 +39,7 @@ type Webhook struct {
|
||||
Headers []*Header `xorm:"mediumtext" json:"headers"`
|
||||
Events []string `xorm:"varchar(1000)" json:"events"`
|
||||
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
||||
ObjectFields []string `xorm:"varchar(1000)" json:"objectFields"`
|
||||
IsUserExtended bool `json:"isUserExtended"`
|
||||
SingleOrgOnly bool `json:"singleOrgOnly"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
|
@@ -66,6 +66,7 @@ func initAPI() {
|
||||
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
|
||||
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")
|
||||
beego.Router("/api/callback", &controllers.ApiController{}, "POST:Callback")
|
||||
beego.Router("/api/device-auth", &controllers.ApiController{}, "POST:DeviceAuth")
|
||||
|
||||
beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
|
||||
beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")
|
||||
|
@@ -89,7 +89,7 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
|
||||
code, err := object.GetOAuthCode(userId, clientId, "", responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if code.Message != "" {
|
||||
|
@@ -53,6 +53,14 @@ const template = `<style>
|
||||
background-color: #333333;
|
||||
box-shadow: 0 0 30px 20px rgba(255, 255, 255, 0.20);
|
||||
}
|
||||
.forget-content {
|
||||
padding: 10px 100px 20px;
|
||||
margin: 30px auto;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 7px;
|
||||
background-color: rgb(255 255 255);
|
||||
box-shadow: 0 0 20px rgb(0 0 0 / 20%);
|
||||
}
|
||||
</style>`;
|
||||
|
||||
const previewGrid = Setting.isMobile() ? 22 : 11;
|
||||
@@ -446,6 +454,7 @@ class ApplicationEditPage extends React.Component {
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} disabled={this.state.application.tokenFormat !== "JWT-Custom"} mode="tags" showSearch style={{width: "100%"}} value={this.state.application.tokenFields} onChange={(value => {this.updateApplicationField("tokenFields", value);})}>
|
||||
<Option key={"provider"} value={"provider"}>{"Provider"}</Option>)
|
||||
{
|
||||
Setting.getUserCommonFields().map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
@@ -718,6 +727,7 @@ class ApplicationEditPage extends React.Component {
|
||||
{id: "token", name: "Token"},
|
||||
{id: "id_token", name: "ID Token"},
|
||||
{id: "refresh_token", name: "Refresh Token"},
|
||||
{id: "urn:ietf:params:oauth:grant-type:device_code", name: "Device Code"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
|
@@ -119,6 +119,7 @@ class EntryPage extends React.Component {
|
||||
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||
<Route exact path="/login/oauth/device/:userCode" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"device"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"saml"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||
<Route exact path="/forget" render={(props) => <SelfForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||
<Route exact path="/forget/:applicationName" render={(props) => <ForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||
|
@@ -276,7 +276,7 @@ class OrganizationEditPage extends React.Component {
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField("passwordType", value);})}
|
||||
options={["plain", "salt", "sha512-salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id"].map(item => Setting.getOption(item, item))}
|
||||
options={["plain", "salt", "sha512-salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id", "pbkdf2-django"].map(item => Setting.getOption(item, item))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@@ -42,6 +42,13 @@ const defaultUserMapping = {
|
||||
avatarUrl: "avatarUrl",
|
||||
};
|
||||
|
||||
const defaultEmailMapping = {
|
||||
fromName: "fromName",
|
||||
toAddress: "toAddress",
|
||||
subject: "subject",
|
||||
content: "content",
|
||||
};
|
||||
|
||||
class ProviderEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -72,7 +79,16 @@ class ProviderEditPage extends React.Component {
|
||||
|
||||
if (res.status === "ok") {
|
||||
const provider = res.data;
|
||||
provider.userMapping = provider.userMapping || defaultUserMapping;
|
||||
if (provider.type === "Custom HTTP Email") {
|
||||
if (!provider.userMapping) {
|
||||
provider.userMapping = provider.userMapping || defaultEmailMapping;
|
||||
}
|
||||
if (!provider.userMapping?.fromName) {
|
||||
provider.userMapping = defaultEmailMapping;
|
||||
}
|
||||
} else {
|
||||
provider.userMapping = provider.userMapping || defaultUserMapping;
|
||||
}
|
||||
this.setState({
|
||||
provider: provider,
|
||||
});
|
||||
@@ -146,9 +162,16 @@ class ProviderEditPage extends React.Component {
|
||||
const requiredKeys = ["id", "username", "displayName"];
|
||||
const provider = this.state.provider;
|
||||
|
||||
if (value === "" && requiredKeys.includes(key)) {
|
||||
Setting.showMessage("error", i18next.t("provider:This field is required"));
|
||||
return;
|
||||
if (provider.type === "Custom HTTP Email") {
|
||||
if (value === "") {
|
||||
Setting.showMessage("error", i18next.t("provider:This field is required"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (value === "" && requiredKeys.includes(key)) {
|
||||
Setting.showMessage("error", i18next.t("provider:This field is required"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
provider.userMapping[key] = value;
|
||||
@@ -184,6 +207,30 @@ class ProviderEditPage extends React.Component {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmailMappingInput() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{Setting.getLabel(i18next.t("provider:From name"), i18next.t("provider:From name - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.fromName} onChange={e => {
|
||||
this.updateUserMappingField("fromName", e.target.value);
|
||||
}} />
|
||||
{Setting.getLabel(i18next.t("provider:From address"), i18next.t("provider:From address - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.toAddress} onChange={e => {
|
||||
this.updateUserMappingField("toAddress", e.target.value);
|
||||
}} />
|
||||
{Setting.getLabel(i18next.t("provider:Subject"), i18next.t("provider:Subject - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.subject} onChange={e => {
|
||||
this.updateUserMappingField("subject", e.target.value);
|
||||
}} />
|
||||
{Setting.getLabel(i18next.t("provider:Email content"), i18next.t("provider:Email content - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.content} onChange={e => {
|
||||
this.updateUserMappingField("content", e.target.value);
|
||||
}} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
getClientIdLabel(provider) {
|
||||
switch (provider.category) {
|
||||
case "OAuth":
|
||||
@@ -645,23 +692,35 @@ class ProviderEditPage extends React.Component {
|
||||
</Row>
|
||||
{
|
||||
this.state.provider.type !== "WeCom" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => {
|
||||
this.updateProviderField("method", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{id: "Normal", name: i18next.t("provider:Normal")},
|
||||
{id: "Silent", name: i18next.t("provider:Silent")},
|
||||
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>)
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => {
|
||||
this.updateProviderField("method", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{id: "Normal", name: i18next.t("provider:Normal")},
|
||||
{id: "Silent", name: i18next.t("provider:Silent")},
|
||||
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Use id as name"), i18next.t("provider:Use id as name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
|
||||
this.updateProviderField("disableSsl", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>)
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
@@ -1097,33 +1156,66 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => {
|
||||
this.updateProviderField("method", value);
|
||||
}}>
|
||||
{
|
||||
!["Custom HTTP Email"].includes(this.state.provider.type) ? null : (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => {
|
||||
this.updateProviderField("method", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{id: "GET", name: "GET"},
|
||||
{id: "POST", name: "POST"},
|
||||
{id: "PUT", name: "PUT"},
|
||||
{id: "DELETE", name: "DELETE"},
|
||||
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
[
|
||||
{id: "GET", name: "GET"},
|
||||
{id: "POST", name: "POST"},
|
||||
{id: "PUT", name: "PUT"},
|
||||
{id: "DELETE", name: "DELETE"},
|
||||
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||
this.state.provider.method !== "GET" ? (<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("webhook:Content type"), i18next.t("webhook:Content type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.issuerUrl === "" ? "application/x-www-form-urlencoded" : this.state.provider.issuerUrl} onChange={value => {
|
||||
this.updateProviderField("issuerUrl", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{id: "application/json", name: "application/json"},
|
||||
{id: "application/x-www-form-urlencoded", name: "application/x-www-form-urlencoded"},
|
||||
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>) : null
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:HTTP header"), i18next.t("provider:HTTP header - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<HttpHeaderTable httpHeaders={this.state.provider.httpHeaders} onUpdateTable={(value) => {this.updateProviderField("httpHeaders", value);}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:HTTP header"), i18next.t("provider:HTTP header - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<HttpHeaderTable httpHeaders={this.state.provider.httpHeaders} onUpdateTable={(value) => {this.updateProviderField("httpHeaders", value);}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{this.state.provider.method !== "GET" ? <Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:HTTP body mapping"), i18next.t("provider:HTTP body mapping - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
{this.renderEmailMappingInput()}
|
||||
</Col>
|
||||
</Row> : null}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :
|
||||
|
@@ -1616,7 +1616,7 @@ export function isDarkTheme(themeAlgorithm) {
|
||||
|
||||
function getPreferredMfaProp(mfaProps) {
|
||||
for (const i in mfaProps) {
|
||||
if (mfaProps[i].isPreffered) {
|
||||
if (mfaProps[i].isPreferred) {
|
||||
return mfaProps[i];
|
||||
}
|
||||
}
|
||||
|
@@ -144,6 +144,9 @@ class WebhookEditPage extends React.Component {
|
||||
if (["port"].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
if (key === "objectFields") {
|
||||
value = value.includes("All") ? ["All"] : value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -294,6 +297,19 @@ class WebhookEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("webhook:Object fields"), i18next.t("webhook:Object fields - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" showSearch style={{width: "100%"}} value={this.state.webhook.objectFields} onChange={(value => {this.updateWebhookField("objectFields", value);})}>
|
||||
<Option key="All" value="All">{i18next.t("general:All")}</Option>
|
||||
{
|
||||
["owner", "name", "createdTime", "updatedTime", "deletedTime", "id", "displayName"].map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("webhook:Is user extended"), i18next.t("webhook:Is user extended - Tooltip"))} :
|
||||
|
@@ -61,7 +61,14 @@ export function oAuthParamsToQuery(oAuthParams) {
|
||||
}
|
||||
|
||||
export function getApplicationLogin(params) {
|
||||
const queryParams = (params?.type === "cas") ? casLoginParamsToQuery(params) : oAuthParamsToQuery(params);
|
||||
let queryParams = "";
|
||||
if (params?.type === "cas") {
|
||||
queryParams = casLoginParamsToQuery(params);
|
||||
} else if (params?.type === "device") {
|
||||
queryParams = `?userCode=${params.userCode}&type=device`;
|
||||
} else {
|
||||
queryParams = oAuthParamsToQuery(params);
|
||||
}
|
||||
return fetch(`${authConfig.serverUrl}/api/get-app-login${queryParams}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
|
@@ -471,6 +471,8 @@ class ForgetPage extends React.Component {
|
||||
<React.Fragment>
|
||||
<CustomGithubCorner />
|
||||
<div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}>
|
||||
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
|
||||
{Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
|
||||
<Button type="text"
|
||||
style={{position: "relative", left: Setting.isMobile() ? "10px" : "-90px", top: 0}}
|
||||
icon={<ArrowLeftOutlined style={{fontSize: "24px"}} />}
|
||||
|
@@ -65,6 +65,8 @@ class LoginPage extends React.Component {
|
||||
orgChoiceMode: new URLSearchParams(props.location?.search).get("orgChoiceMode") ?? null,
|
||||
userLang: null,
|
||||
loginLoading: false,
|
||||
userCode: props.userCode ?? (props.match?.params?.userCode ?? null),
|
||||
userCodeStatus: "",
|
||||
};
|
||||
|
||||
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
|
||||
@@ -81,7 +83,7 @@ class LoginPage extends React.Component {
|
||||
if (this.getApplicationObj() === undefined) {
|
||||
if (this.state.type === "login" || this.state.type === "saml") {
|
||||
this.getApplication();
|
||||
} else if (this.state.type === "code" || this.state.type === "cas") {
|
||||
} else if (this.state.type === "code" || this.state.type === "cas" || this.state.type === "device") {
|
||||
this.getApplicationLogin();
|
||||
} else {
|
||||
Setting.showMessage("error", `Unknown authentication type: ${this.state.type}`);
|
||||
@@ -155,13 +157,25 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
|
||||
getApplicationLogin() {
|
||||
const loginParams = (this.state.type === "cas") ? Util.getCasLoginParameters("admin", this.state.applicationName) : Util.getOAuthGetParameters();
|
||||
let loginParams;
|
||||
if (this.state.type === "cas") {
|
||||
loginParams = Util.getCasLoginParameters("admin", this.state.applicationName);
|
||||
} else if (this.state.type === "device") {
|
||||
loginParams = {userCode: this.state.userCode, type: this.state.type};
|
||||
} else {
|
||||
loginParams = Util.getOAuthGetParameters();
|
||||
}
|
||||
AuthBackend.getApplicationLogin(loginParams)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const application = res.data;
|
||||
this.onUpdateApplication(application);
|
||||
} else {
|
||||
if (this.state.type === "device") {
|
||||
this.setState({
|
||||
userCodeStatus: "expired",
|
||||
});
|
||||
}
|
||||
this.onUpdateApplication(null);
|
||||
this.setState({
|
||||
msg: res.msg,
|
||||
@@ -266,6 +280,9 @@ class LoginPage extends React.Component {
|
||||
|
||||
onUpdateApplication(application) {
|
||||
this.props.onUpdateApplication(application);
|
||||
if (application === null) {
|
||||
return;
|
||||
}
|
||||
for (const idx in application.providers) {
|
||||
const provider = application.providers[idx];
|
||||
if (provider.provider?.category === "Face ID") {
|
||||
@@ -296,6 +313,9 @@ class LoginPage extends React.Component {
|
||||
const oAuthParams = Util.getOAuthGetParameters();
|
||||
|
||||
values["type"] = oAuthParams?.responseType ?? this.state.type;
|
||||
if (this.state.userCode) {
|
||||
values["userCode"] = this.state.userCode;
|
||||
}
|
||||
|
||||
if (oAuthParams?.samlRequest) {
|
||||
values["samlRequest"] = oAuthParams.samlRequest;
|
||||
@@ -391,6 +411,9 @@ class LoginPage extends React.Component {
|
||||
}).then(res => res.json())
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
this.setState({
|
||||
loginLoading: false,
|
||||
});
|
||||
Setting.showMessage("error", res.msg);
|
||||
return;
|
||||
}
|
||||
@@ -455,6 +478,7 @@ class LoginPage extends React.Component {
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({loginLoading: false});
|
||||
});
|
||||
} else {
|
||||
@@ -475,6 +499,11 @@ class LoginPage extends React.Component {
|
||||
this.props.onLoginSuccess();
|
||||
} else if (responseType === "code") {
|
||||
this.postCodeLoginAction(res);
|
||||
} else if (responseType === "device") {
|
||||
Setting.showMessage("success", "Successful login");
|
||||
this.setState({
|
||||
userCodeStatus: "success",
|
||||
});
|
||||
} else if (responseType === "token" || responseType === "id_token") {
|
||||
if (res.data2) {
|
||||
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
|
||||
@@ -511,6 +540,7 @@ class LoginPage extends React.Component {
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({loginLoading: false});
|
||||
});
|
||||
}
|
||||
@@ -720,7 +750,7 @@ class LoginPage extends React.Component {
|
||||
values["FaceIdImage"] = FaceIdImage;
|
||||
this.login(values);
|
||||
this.setState({openFaceRecognitionModal: false});
|
||||
}} onCancel={() => this.setState({openFaceRecognitionModal: false})} /></Suspense> :
|
||||
}} onCancel={() => this.setState({openFaceRecognitionModal: false, loginLoading: false})} /></Suspense> :
|
||||
<Suspense fallback={null}>
|
||||
<FaceRecognitionModal
|
||||
visible={this.state.openFaceRecognitionModal}
|
||||
@@ -731,7 +761,7 @@ class LoginPage extends React.Component {
|
||||
this.login(values);
|
||||
this.setState({openFaceRecognitionModal: false});
|
||||
}}
|
||||
onCancel={() => this.setState({openFaceRecognitionModal: false})}
|
||||
onCancel={() => this.setState({openFaceRecognitionModal: false, loginLoading: false})}
|
||||
/>
|
||||
</Suspense>
|
||||
:
|
||||
@@ -821,6 +851,16 @@ class LoginPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.userCode && this.state.userCodeStatus === "success") {
|
||||
return (
|
||||
<Result
|
||||
status="success"
|
||||
title={i18next.t("application:Logged in successfully")}
|
||||
>
|
||||
</Result>
|
||||
);
|
||||
}
|
||||
|
||||
const showForm = Setting.isPasswordEnabled(application) || Setting.isCodeSigninEnabled(application) || Setting.isWebAuthnEnabled(application) || Setting.isLdapEnabled(application) || Setting.isFaceIdEnabled(application);
|
||||
if (showForm) {
|
||||
let loginWidth = 320;
|
||||
@@ -981,6 +1021,10 @@ class LoginPage extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.state.userCode && this.state.userCodeStatus === "success") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{fontSize: 16, textAlign: "left"}}>
|
||||
@@ -1065,6 +1109,12 @@ class LoginPage extends React.Component {
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}${error}`);
|
||||
});
|
||||
}).catch(error => {
|
||||
Setting.showMessage("error", `${error}`);
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
loginLoading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1257,6 +1307,15 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.userCodeStatus === "expired") {
|
||||
return <Result
|
||||
style={{width: "100%"}}
|
||||
status="error"
|
||||
title={`Code ${i18next.t("subscription:Expired")}`}
|
||||
>
|
||||
</Result>;
|
||||
}
|
||||
|
||||
const application = this.getApplicationObj();
|
||||
if (application === undefined) {
|
||||
return null;
|
||||
|
@@ -17,6 +17,7 @@ import React, {useState} from "react";
|
||||
import {Button, Modal, Progress, Space, Spin, message} from "antd";
|
||||
import i18next from "i18next";
|
||||
import Dragger from "antd/es/upload/Dragger";
|
||||
import * as Setting from "../../Setting";
|
||||
|
||||
const FaceRecognitionModal = (props) => {
|
||||
const {visible, onOk, onCancel, withImage} = props;
|
||||
@@ -35,9 +36,8 @@ const FaceRecognitionModal = (props) => {
|
||||
|
||||
React.useEffect(() => {
|
||||
const loadModels = async() => {
|
||||
// const MODEL_URL = process.env.PUBLIC_URL + "/models";
|
||||
// const MODEL_URL = "https://justadudewhohacks.github.io/face-api.js/models";
|
||||
const MODEL_URL = "https://cdn.casdoor.com/casdoor/models";
|
||||
const MODEL_URL = `${Setting.StaticBaseUrl}/casdoor/models`;
|
||||
|
||||
Promise.all([
|
||||
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
|
||||
|
Reference in New Issue
Block a user