mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-15 14:43:50 +08:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
7b78e60265 | |||
7464f9a8ad | |||
d3a7a062d3 | |||
67a0264411 | |||
a6a055cc83 | |||
a89a7f9eb7 | |||
287f60353c | |||
530330bd66 | |||
70a1428972 | |||
1d183decea | |||
b92d03e2bb | |||
9877174780 | |||
b178be9aef | |||
7236cca8cf | |||
15daf5dbfe | |||
0b546bba5e | |||
938cdbccf4 | |||
801302c6e7 | |||
91602d2b21 | |||
86b3a078ef |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -70,7 +70,7 @@ jobs:
|
||||
|
||||
- name: Fetch Previous version
|
||||
id: get-previous-tag
|
||||
uses: actions-ecosystem/action-get-latest-tag@v1
|
||||
uses: actions-ecosystem/action-get-latest-tag@v1.6.0
|
||||
|
||||
- name: Release
|
||||
run: yarn global add semantic-release@17.4.4 && semantic-release
|
||||
@ -79,7 +79,7 @@ jobs:
|
||||
|
||||
- name: Fetch Current version
|
||||
id: get-current-tag
|
||||
uses: actions-ecosystem/action-get-latest-tag@v1
|
||||
uses: actions-ecosystem/action-get-latest-tag@v1.6.0
|
||||
|
||||
- name: Decide Should_Push Or Not
|
||||
id: should_push
|
||||
@ -101,7 +101,7 @@ jobs:
|
||||
echo ::set-output name=push::'false'
|
||||
|
||||
fi
|
||||
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' &&steps.should_push.outputs.push=='true'
|
||||
@ -109,14 +109,14 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
|
||||
|
||||
- name: Push to Docker Hub
|
||||
uses: docker/build-push-action@v2
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
|
||||
with:
|
||||
push: true
|
||||
tags: casbin/casdoor:${{steps.get-current-tag.outputs.tag }},casbin/casdoor:latest
|
||||
|
||||
|
||||
- name: Push All In One Version to Docker Hub
|
||||
uses: docker/build-push-action@v2
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
|
||||
|
4
.github/workflows/sync.yml
vendored
4
.github/workflows/sync.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@1.2.0
|
||||
uses: crowdin/github-action@1.4.8
|
||||
with:
|
||||
upload_translations: true
|
||||
|
||||
@ -32,4 +32,4 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: '463556'
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
@ -1,8 +1,7 @@
|
||||
FROM golang:1.17.5 AS BACK
|
||||
WORKDIR /go/src/casdoor
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build -ldflags="-w -s" -o server . \
|
||||
&& apt update && apt install wait-for-it && chmod +x /usr/bin/wait-for-it
|
||||
RUN ./build.sh && apt update && apt install wait-for-it && chmod +x /usr/bin/wait-for-it
|
||||
|
||||
FROM node:16.13.0 AS FRONT
|
||||
WORKDIR /web
|
||||
|
@ -101,6 +101,7 @@ p, *, *, GET, /.well-known/openid-configuration, *, *
|
||||
p, *, *, *, /.well-known/jwks, *, *
|
||||
p, *, *, GET, /api/get-saml-login, *, *
|
||||
p, *, *, POST, /api/acs, *, *
|
||||
p, *, *, GET, /api/saml/metadata, *, *
|
||||
p, *, *, *, /cas, *, *
|
||||
`
|
||||
|
||||
|
11
build.sh
Executable file
11
build.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
#try to connect to google to determine whether user need to use proxy
|
||||
curl www.google.com -o /dev/null --connect-timeout 5
|
||||
if [ $? == 0 ]
|
||||
then
|
||||
echo "connect to google.com successed"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .
|
||||
else
|
||||
echo "connect to google.com failed"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build -ldflags="-w -s" -o server .
|
||||
fi
|
@ -29,6 +29,7 @@ const (
|
||||
ResponseTypeCode = "code"
|
||||
ResponseTypeToken = "token"
|
||||
ResponseTypeIdToken = "id_token"
|
||||
ResponseTypeSaml = "saml"
|
||||
ResponseTypeCas = "cas"
|
||||
)
|
||||
|
||||
@ -61,6 +62,7 @@ type RequestForm struct {
|
||||
AutoSignin bool `json:"autoSignin"`
|
||||
|
||||
RelayState string `json:"relayState"`
|
||||
SamlRequest string `json:"samlRequest"`
|
||||
SamlResponse string `json:"samlResponse"`
|
||||
}
|
||||
|
||||
|
@ -83,6 +83,13 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
resp = tokenToResponse(token)
|
||||
}
|
||||
|
||||
} else if form.Type == ResponseTypeSaml { // saml flow
|
||||
res, redirectUrl, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
resp = &Response{Status: "ok", Msg: "", Data: res, Data2: redirectUrl}
|
||||
} else if form.Type == ResponseTypeCas {
|
||||
//not oauth but CAS SSO protocol
|
||||
service := c.Input().Get("service")
|
||||
@ -276,7 +283,7 @@ func (c *ApiController) Login() {
|
||||
clientSecret = provider.ClientSecret2
|
||||
}
|
||||
|
||||
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri, provider.Domain)
|
||||
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri, provider.Domain, provider.CustomAuthUrl, provider.CustomTokenUrl, provider.CustomUserInfoUrl)
|
||||
if idProvider == nil {
|
||||
c.ResponseError(fmt.Sprintf("The provider type: %s is not supported", provider.Type))
|
||||
return
|
||||
|
@ -15,6 +15,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -42,7 +43,7 @@ func (c *RootController) CasValidate() {
|
||||
c.Ctx.Output.Body([]byte("no\n"))
|
||||
return
|
||||
}
|
||||
if ok, response, issuedService := object.GetCasTokenByTicket(ticket); ok {
|
||||
if ok, response, issuedService, _ := object.GetCasTokenByTicket(ticket); ok {
|
||||
//check whether service is the one for which we previously issued token
|
||||
if issuedService == service {
|
||||
c.Ctx.Output.Body([]byte(fmt.Sprintf("yes\n%s\n", response.User)))
|
||||
@ -54,7 +55,25 @@ func (c *RootController) CasValidate() {
|
||||
c.Ctx.Output.Body([]byte("no\n"))
|
||||
}
|
||||
|
||||
func (c *RootController) CasServiceAndProxyValidate() {
|
||||
func (c *RootController) CasServiceValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
format := c.Input().Get("format")
|
||||
if !strings.HasPrefix(ticket, "ST") {
|
||||
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
|
||||
}
|
||||
c.CasP3ServiceAndProxyValidate()
|
||||
}
|
||||
|
||||
func (c *RootController) CasProxyValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
format := c.Input().Get("format")
|
||||
if !strings.HasPrefix(ticket, "PT") {
|
||||
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
|
||||
}
|
||||
c.CasP3ServiceAndProxyValidate()
|
||||
}
|
||||
|
||||
func (c *RootController) CasP3ServiceAndProxyValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
format := c.Input().Get("format")
|
||||
service := c.Input().Get("service")
|
||||
@ -69,10 +88,9 @@ func (c *RootController) CasServiceAndProxyValidate() {
|
||||
c.sendCasAuthenticationResponseErr(InvalidRequest, "service and ticket must exist", format)
|
||||
return
|
||||
}
|
||||
|
||||
ok, response, issuedService, userId := object.GetCasTokenByTicket(ticket)
|
||||
//find the token
|
||||
if ok, response, issuedService := object.GetCasTokenByTicket(ticket); ok {
|
||||
|
||||
if ok {
|
||||
//check whether service is the one for which we previously issued token
|
||||
if strings.HasPrefix(service, issuedService) {
|
||||
serviceResponse.Success = response
|
||||
@ -89,7 +107,7 @@ func (c *RootController) CasServiceAndProxyValidate() {
|
||||
|
||||
if pgtUrl != "" && serviceResponse.Failure == nil {
|
||||
//that means we are in proxy web flow
|
||||
pgt := object.StoreCasTokenForPgt(serviceResponse.Success, service)
|
||||
pgt := object.StoreCasTokenForPgt(serviceResponse.Success, service, userId)
|
||||
pgtiou := serviceResponse.Success.ProxyGrantingTicket
|
||||
//todo: check whether it is https
|
||||
pgtUrlObj, err := url.Parse(pgtUrl)
|
||||
@ -139,7 +157,7 @@ func (c *RootController) CasProxy() {
|
||||
return
|
||||
}
|
||||
|
||||
ok, authenticationSuccess, issuedService := object.GetCasTokenByPgt(pgt)
|
||||
ok, authenticationSuccess, issuedService, userId := object.GetCasTokenByPgt(pgt)
|
||||
if !ok {
|
||||
c.sendCasProxyResponseErr(UnauthorizedService, "service not authorized", format)
|
||||
return
|
||||
@ -150,7 +168,7 @@ func (c *RootController) CasProxy() {
|
||||
newAuthenticationSuccess.Proxies = &object.CasProxies{}
|
||||
}
|
||||
newAuthenticationSuccess.Proxies.Proxies = append(newAuthenticationSuccess.Proxies.Proxies, issuedService)
|
||||
proxyTicket := object.StoreCasTokenForProxyTicket(&newAuthenticationSuccess, targetService)
|
||||
proxyTicket := object.StoreCasTokenForProxyTicket(&newAuthenticationSuccess, targetService, userId)
|
||||
|
||||
serviceResponse := object.CasServiceResponse{
|
||||
Xmlns: "http://www.yale.edu/tp/cas",
|
||||
@ -168,6 +186,55 @@ func (c *RootController) CasProxy() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *RootController) SamlValidate() {
|
||||
c.Ctx.Output.Header("Content-Type", "text/xml; charset=utf-8")
|
||||
target := c.Input().Get("TARGET")
|
||||
body := c.Ctx.Input.RequestBody
|
||||
envelopRequest := struct {
|
||||
XMLName xml.Name `xml:"Envelope"`
|
||||
Body struct {
|
||||
XMLName xml.Name `xml:"Body"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
}{}
|
||||
|
||||
err := xml.Unmarshal(body, &envelopRequest)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response, service, err := object.GetValidationBySaml(envelopRequest.Body.Content, c.Ctx.Request.Host)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(target, service) {
|
||||
c.ResponseError(fmt.Sprintf("service %s and %s do not match", target, service))
|
||||
return
|
||||
}
|
||||
|
||||
envelopReponse := struct {
|
||||
XMLName xml.Name `xml:"SOAP-ENV:Envelope"`
|
||||
Xmlns string `xml:"xmlns:SOAP-ENV"`
|
||||
Body struct {
|
||||
XMLName xml.Name `xml:"SOAP-ENV:Body"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
}{}
|
||||
envelopReponse.Xmlns = "http://schemas.xmlsoap.org/soap/envelope/"
|
||||
envelopReponse.Body.Content = response
|
||||
|
||||
data, err := xml.Marshal(envelopReponse)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Ctx.Output.Body([]byte(data))
|
||||
}
|
||||
|
||||
func (c *RootController) sendCasProxyResponseErr(code, msg, format string) {
|
||||
serviceResponse := object.CasServiceResponse{
|
||||
Xmlns: "http://www.yale.edu/tp/cas",
|
||||
|
33
controllers/saml.go
Normal file
33
controllers/saml.go
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2022 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 controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
func (c *ApiController) GetSamlMeta() {
|
||||
host := c.Ctx.Request.Host
|
||||
paramApp := c.Input().Get("application")
|
||||
application := object.GetApplication(paramApp)
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf("err: application %s not found", paramApp))
|
||||
}
|
||||
metadata, _ := object.GetSamlMeta(application, host)
|
||||
c.Data["xml"] = metadata
|
||||
c.ServeXML()
|
||||
}
|
@ -16,7 +16,6 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/astaxie/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -114,3 +113,18 @@ func (c *ApiController) DeleteSyncer() {
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSyncer(&syncer))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title RunSyncer
|
||||
// @Tag Syncer API
|
||||
// @Description run syncer
|
||||
// @Param body body object.Syncer true "The details of the syncer"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /run-syncer [get]
|
||||
func (c *ApiController) RunSyncer() {
|
||||
id := c.Input().Get("id")
|
||||
syncer := object.GetSyncer(id)
|
||||
|
||||
object.RunSyncer(syncer)
|
||||
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
@ -175,6 +175,8 @@ func (c *ApiController) GetOAuthToken() {
|
||||
scope := c.Input().Get("scope")
|
||||
username := c.Input().Get("username")
|
||||
password := c.Input().Get("password")
|
||||
tag := c.Input().Get("tag")
|
||||
avatar := c.Input().Get("avatar")
|
||||
|
||||
if clientId == "" && clientSecret == "" {
|
||||
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
|
||||
@ -191,11 +193,13 @@ func (c *ApiController) GetOAuthToken() {
|
||||
scope = tokenRequest.Scope
|
||||
username = tokenRequest.Username
|
||||
password = tokenRequest.Password
|
||||
tag = tokenRequest.Tag
|
||||
avatar = tokenRequest.Avatar
|
||||
}
|
||||
}
|
||||
host := c.Ctx.Request.Host
|
||||
|
||||
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host)
|
||||
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host, tag, avatar)
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
|
@ -23,4 +23,6 @@ type TokenRequest struct {
|
||||
Scope string `json:"scope"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Tag string `json:"tag"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
@ -87,6 +87,17 @@ func (c *ApiController) GetUser() {
|
||||
id := c.Input().Get("id")
|
||||
owner := c.Input().Get("owner")
|
||||
email := c.Input().Get("email")
|
||||
userOwner, _ := util.GetOwnerAndNameFromId(id)
|
||||
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", userOwner))
|
||||
|
||||
if !organization.IsProfilePublic {
|
||||
requestUserId := c.GetSessionUsername()
|
||||
hasPermission, err := object.CheckUserPermission(requestUserId, id, false)
|
||||
if !hasPermission {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var user *object.User
|
||||
if email == "" {
|
||||
@ -111,6 +122,10 @@ func (c *ApiController) UpdateUser() {
|
||||
id := c.Input().Get("id")
|
||||
columnsStr := c.Input().Get("columns")
|
||||
|
||||
if id == "" {
|
||||
id = c.GetSessionUsername()
|
||||
}
|
||||
|
||||
var user object.User
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
|
||||
if err != nil {
|
||||
@ -229,39 +244,15 @@ func (c *ApiController) SetPassword() {
|
||||
newPassword := c.Ctx.Request.Form.Get("newPassword")
|
||||
|
||||
requestUserId := c.GetSessionUsername()
|
||||
if requestUserId == "" {
|
||||
c.ResponseError("Please login first")
|
||||
return
|
||||
}
|
||||
|
||||
userId := fmt.Sprintf("%s/%s", userOwner, userName)
|
||||
targetUser := object.GetUser(userId)
|
||||
if targetUser == nil {
|
||||
c.ResponseError(fmt.Sprintf("The user: %s doesn't exist", userId))
|
||||
|
||||
hasPermission, err := object.CheckUserPermission(requestUserId, userId, true)
|
||||
if !hasPermission {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission := false
|
||||
if strings.HasPrefix(requestUserId, "app/") {
|
||||
hasPermission = true
|
||||
} else {
|
||||
requestUser := object.GetUser(requestUserId)
|
||||
if requestUser == nil {
|
||||
c.ResponseError("Session outdated. Please login again.")
|
||||
return
|
||||
}
|
||||
if requestUser.IsGlobalAdmin {
|
||||
hasPermission = true
|
||||
} else if requestUserId == userId {
|
||||
hasPermission = true
|
||||
} else if targetUser.Owner == requestUser.Owner && requestUser.IsAdmin {
|
||||
hasPermission = true
|
||||
}
|
||||
}
|
||||
if !hasPermission {
|
||||
c.ResponseError("You don't have the permission to do this.")
|
||||
return
|
||||
}
|
||||
targetUser := object.GetUser(userId)
|
||||
|
||||
if oldPassword != "" {
|
||||
msg := object.CheckPassword(targetUser, oldPassword)
|
||||
|
@ -28,6 +28,8 @@ func GetCredManager(passwordType string) CredManager {
|
||||
return NewMd5UserSaltCredManager()
|
||||
} else if passwordType == "bcrypt" {
|
||||
return NewBcryptCredManager()
|
||||
} else if passwordType == "pbkdf2-salt" {
|
||||
return NewPbkdf2SaltCredManager()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -32,8 +32,8 @@ func getMd5HexDigest(s string) string {
|
||||
return res
|
||||
}
|
||||
|
||||
func NewMd5UserSaltCredManager() *Sha256SaltCredManager {
|
||||
cm := &Sha256SaltCredManager{}
|
||||
func NewMd5UserSaltCredManager() *Md5UserSaltCredManager {
|
||||
cm := &Md5UserSaltCredManager{}
|
||||
return cm
|
||||
}
|
||||
|
||||
|
39
cred/pbkdf2-salt.go
Normal file
39
cred/pbkdf2-salt.go
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright 2022 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"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
type Pbkdf2SaltCredManager struct{}
|
||||
|
||||
func NewPbkdf2SaltCredManager() *Pbkdf2SaltCredManager {
|
||||
cm := &Pbkdf2SaltCredManager{}
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *Pbkdf2SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
// https://www.keycloak.org/docs/latest/server_admin/index.html#password-database-compromised
|
||||
decodedSalt, _ := base64.StdEncoding.DecodeString(userSalt)
|
||||
res := pbkdf2.Key([]byte(password), decodedSalt, 27500, 64, sha256.New)
|
||||
return base64.StdEncoding.EncodeToString(res)
|
||||
}
|
||||
|
||||
func (cm *Pbkdf2SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
|
||||
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
|
||||
}
|
5
go.mod
5
go.mod
@ -3,10 +3,12 @@ module github.com/casdoor/casdoor
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible // indirect
|
||||
github.com/astaxie/beego v1.12.3
|
||||
github.com/aws/aws-sdk-go v1.37.30
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/casbin/casbin/v2 v2.30.1
|
||||
github.com/casbin/xorm-adapter/v2 v2.5.1
|
||||
github.com/casdoor/go-sms-sender v0.2.0
|
||||
@ -19,12 +21,15 @@ require (
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0
|
||||
github.com/google/uuid v1.2.0
|
||||
github.com/jinzhu/configor v1.2.1 // indirect
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
github.com/lestrrat-go/jwx v0.9.0
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/qiangmzsx/string-adapter/v2 v2.1.0
|
||||
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/russellhaering/gosaml2 v0.6.0
|
||||
github.com/russellhaering/goxmldsig v1.1.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tealeg/xlsx v1.0.5
|
||||
|
8
go.sum
8
go.sum
@ -44,6 +44,8 @@ github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8L
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b h1:EgJ6N2S0h1WfFIjU5/VVHWbMSVYXAluop97Qxpr/lfQ=
|
||||
github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b/go.mod h1:3SAoF0F5EbcOuBD5WT9nYkbIJieBS84cUQXADbXeBsU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@ -235,6 +237,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
@ -254,6 +258,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c h1:UPJygtyk491bJJ/DnRJFuzcq9Dl9NSeFrJ7VdiRzMxc=
|
||||
github.com/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c/go.mod h1:KEgVcb43+f5KFUH/x6Vd3NROG0AIL2CuKMrIqYsmx6E=
|
||||
github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0=
|
||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911 h1:erppMjjp69Rertg1zlgRbLJH1u+eCmRPxKjMZ5I8/Ro=
|
||||
@ -274,6 +280,8 @@ github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c h1:3wkDRdxK92dF+c1ke
|
||||
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
|
||||
|
@ -131,6 +131,7 @@ func (idp *CasdoorIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
109
idp/custom.go
Normal file
109
idp/custom.go
Normal file
@ -0,0 +1,109 @@
|
||||
// Copyright 2022 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 idp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
_ "net/url"
|
||||
_ "time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type CustomIdProvider struct {
|
||||
Client *http.Client
|
||||
Config *oauth2.Config
|
||||
UserInfoUrl string
|
||||
}
|
||||
|
||||
func NewCustomIdProvider(clientId string, clientSecret string, redirectUrl string, authUrl string, tokenUrl string, userInfoUrl string) *CustomIdProvider {
|
||||
idp := &CustomIdProvider{}
|
||||
idp.UserInfoUrl = userInfoUrl
|
||||
|
||||
var config = &oauth2.Config{
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: redirectUrl,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: authUrl,
|
||||
TokenURL: tokenUrl,
|
||||
},
|
||||
}
|
||||
idp.Config = config
|
||||
|
||||
return idp
|
||||
}
|
||||
|
||||
func (idp *CustomIdProvider) SetHttpClient(client *http.Client) {
|
||||
idp.Client = 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)
|
||||
}
|
||||
|
||||
type CustomUserInfo struct {
|
||||
Id string `json:"sub"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"preferred_username"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"picture"`
|
||||
Status string `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func (idp *CustomIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||
ctUserinfo := &CustomUserInfo{}
|
||||
accessToken := token.AccessToken
|
||||
request, err := http.NewRequest("GET", idp.UserInfoUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//add accessToken to request header
|
||||
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||
resp, err := idp.Client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, ctUserinfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ctUserinfo.Status != "" {
|
||||
return nil, fmt.Errorf("err: %s", ctUserinfo.Msg)
|
||||
}
|
||||
|
||||
userInfo := &UserInfo{
|
||||
Id: ctUserinfo.Id,
|
||||
Username: ctUserinfo.Name,
|
||||
DisplayName: ctUserinfo.DisplayName,
|
||||
Email: ctUserinfo.Email,
|
||||
AvatarUrl: ctUserinfo.AvatarUrl,
|
||||
}
|
||||
return userInfo, nil
|
||||
}
|
@ -143,6 +143,7 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
@ -169,8 +169,11 @@ func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||
req.Header.Set("Authorization", "Bearer "+token.AccessToken)
|
||||
|
||||
resp, err := idp.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err = ioutil.ReadAll(resp.Body)
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ type IdProvider interface {
|
||||
GetUserInfo(token *oauth2.Token) (*UserInfo, error)
|
||||
}
|
||||
|
||||
func GetIdProvider(typ string, subType string, clientId string, clientSecret string, appId string, redirectUrl string, hostUrl string) IdProvider {
|
||||
func GetIdProvider(typ string, subType string, clientId string, clientSecret string, appId string, redirectUrl string, hostUrl string, authUrl string, tokenUrl string, userInfoUrl string) IdProvider {
|
||||
if typ == "GitHub" {
|
||||
return NewGithubIdProvider(clientId, clientSecret, redirectUrl)
|
||||
} else if typ == "Google" {
|
||||
@ -72,6 +72,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
|
||||
return NewBaiduIdProvider(clientId, clientSecret, redirectUrl)
|
||||
} else if typ == "Alipay" {
|
||||
return NewAlipayIdProvider(clientId, clientSecret, redirectUrl)
|
||||
} else if typ == "Custom" {
|
||||
return NewCustomIdProvider(clientId, clientSecret, redirectUrl, authUrl, tokenUrl, userInfoUrl)
|
||||
} else if typ == "Infoflow" {
|
||||
if subType == "Internal" {
|
||||
return NewInfoflowInternalIdProvider(clientId, clientSecret, appId, redirectUrl)
|
||||
|
82
idp/wechat_miniprogram.go
Normal file
82
idp/wechat_miniprogram.go
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2022 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 idp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type WeChatMiniProgramIdProvider struct {
|
||||
Client *http.Client
|
||||
Config *oauth2.Config
|
||||
}
|
||||
|
||||
func NewWeChatMiniProgramIdProvider(clientId string, clientSecret string) *WeChatMiniProgramIdProvider {
|
||||
idp := &WeChatMiniProgramIdProvider{}
|
||||
|
||||
config := idp.getConfig(clientId, clientSecret)
|
||||
idp.Config = config
|
||||
idp.Client = &http.Client{}
|
||||
return idp
|
||||
}
|
||||
|
||||
func (idp *WeChatMiniProgramIdProvider) SetHttpClient(client *http.Client) {
|
||||
idp.Client = client
|
||||
}
|
||||
|
||||
func (idp *WeChatMiniProgramIdProvider) getConfig(clientId string, clientSecret string) *oauth2.Config {
|
||||
var config = &oauth2.Config{
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
type WeChatMiniProgramSessionResponse struct {
|
||||
Openid string `json:"openid"`
|
||||
SessionKey string `json:"session_key"`
|
||||
Unionid string `json:"unionid"`
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
}
|
||||
|
||||
func (idp *WeChatMiniProgramIdProvider) GetSessionByCode(code string) (*WeChatMiniProgramSessionResponse, error) {
|
||||
sessionUri := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", idp.Config.ClientID, idp.Config.ClientSecret, code)
|
||||
sessionResponse, err := idp.Client.Get(sessionUri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer sessionResponse.Body.Close()
|
||||
data, err := ioutil.ReadAll(sessionResponse.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var session WeChatMiniProgramSessionResponse
|
||||
err = json.Unmarshal(data, &session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if session.Errcode != 0 {
|
||||
return nil, fmt.Errorf("err: %s", session.Errmsg)
|
||||
}
|
||||
return &session, nil
|
||||
|
||||
}
|
@ -300,7 +300,6 @@ func (application *Application) GetId() string {
|
||||
func CheckRedirectUriValid(application *Application, redirectUri string) bool {
|
||||
var validUri = false
|
||||
for _, tmpUri := range application.RedirectUris {
|
||||
fmt.Println(tmpUri, redirectUri)
|
||||
if strings.Contains(redirectUri, tmpUri) {
|
||||
validUri = true
|
||||
break
|
||||
|
@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/cred"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -195,3 +196,37 @@ func CheckUserPassword(organization string, username string, password string) (*
|
||||
func filterField(field string) bool {
|
||||
return reFieldWhiteList.MatchString(field)
|
||||
}
|
||||
|
||||
func CheckUserPermission(requestUserId, userId string, strict bool) (bool, error) {
|
||||
if requestUserId == "" {
|
||||
return false, fmt.Errorf("please login first")
|
||||
}
|
||||
|
||||
targetUser := GetUser(userId)
|
||||
if targetUser == nil {
|
||||
return false, fmt.Errorf("the user: %s doesn't exist", userId)
|
||||
}
|
||||
|
||||
hasPermission := false
|
||||
if strings.HasPrefix(requestUserId, "app/") {
|
||||
hasPermission = true
|
||||
} else {
|
||||
requestUser := GetUser(requestUserId)
|
||||
if requestUser == nil {
|
||||
return false, fmt.Errorf("session outdated, please login again")
|
||||
}
|
||||
if requestUser.IsGlobalAdmin {
|
||||
hasPermission = true
|
||||
} else if requestUserId == userId {
|
||||
hasPermission = true
|
||||
} else if targetUser.Owner == requestUser.Owner {
|
||||
if strict {
|
||||
hasPermission = requestUser.IsAdmin
|
||||
} else {
|
||||
hasPermission = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasPermission, fmt.Errorf("you don't have the permission to do this")
|
||||
}
|
@ -35,6 +35,7 @@ type Organization struct {
|
||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
|
||||
EnableSoftDeletion bool `json:"enableSoftDeletion"`
|
||||
IsProfilePublic bool `json:"isProfilePublic"`
|
||||
}
|
||||
|
||||
func GetOrganizationCount(owner, field, value string) int {
|
||||
|
@ -27,16 +27,21 @@ type Provider struct {
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Category string `xorm:"varchar(100)" json:"category"`
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
SubType string `xorm:"varchar(100)" json:"subType"`
|
||||
Method string `xorm:"varchar(100)" json:"method"`
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"`
|
||||
ClientId2 string `xorm:"varchar(100)" json:"clientId2"`
|
||||
ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"`
|
||||
Cert string `xorm:"varchar(100)" json:"cert"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Category string `xorm:"varchar(100)" json:"category"`
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
SubType string `xorm:"varchar(100)" json:"subType"`
|
||||
Method string `xorm:"varchar(100)" json:"method"`
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"`
|
||||
ClientId2 string `xorm:"varchar(100)" json:"clientId2"`
|
||||
ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"`
|
||||
Cert string `xorm:"varchar(100)" json:"cert"`
|
||||
CustomAuthUrl string `xorm:"varchar(200)" json:"customAuthUrl"`
|
||||
CustomScope string `xorm:"varchar(200)" json:"customScope"`
|
||||
CustomTokenUrl string `xorm:"varchar(200)" json:"customTokenUrl"`
|
||||
CustomUserInfoUrl string `xorm:"varchar(200)" json:"customUserInfoUrl"`
|
||||
CustomLogo string `xorm:"varchar(200)" json:"customLogo"`
|
||||
|
||||
Host string `xorm:"varchar(100)" json:"host"`
|
||||
Port int `json:"port"`
|
||||
@ -151,6 +156,16 @@ func GetDefaultHumanCheckProvider() *Provider {
|
||||
return &provider
|
||||
}
|
||||
|
||||
func GetWechatMiniProgramProvider(application *Application) *Provider {
|
||||
providers := application.Providers
|
||||
for _, provider := range providers {
|
||||
if provider.Provider.Type == "WeChatMiniProgram" {
|
||||
return provider.Provider
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateProvider(id string, provider *Provider) bool {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
if getProvider(owner, name) == nil {
|
||||
|
@ -23,7 +23,7 @@ import (
|
||||
|
||||
type Resource struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(200) notnull pk" json:"name"`
|
||||
Name string `xorm:"varchar(250) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
@ -31,7 +31,7 @@ type Resource struct {
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
Parent string `xorm:"varchar(100)" json:"parent"`
|
||||
FileName string `xorm:"varchar(100)" json:"fileName"`
|
||||
FileName string `xorm:"varchar(1000)" json:"fileName"`
|
||||
FileType string `xorm:"varchar(100)" json:"fileType"`
|
||||
FileFormat string `xorm:"varchar(100)" json:"fileFormat"`
|
||||
FileSize int `json:"fileSize"`
|
||||
|
352
object/saml_idp.go
Normal file
352
object/saml_idp.go
Normal file
@ -0,0 +1,352 @@
|
||||
// Copyright 2022 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 object
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/flate"
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/RobotsAndPencils/go-saml"
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/beevik/etree"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
//returns a saml2 response
|
||||
func NewSamlResponse(user *User, host string, publicKey string, destination string, iss string, redirectUri []string) (*etree.Element, error) {
|
||||
samlResponse := &etree.Element{
|
||||
Space: "samlp",
|
||||
Tag: "Response",
|
||||
}
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
expireTime := time.Now().UTC().Add(time.Hour * 24).Format(time.RFC3339)
|
||||
samlResponse.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol")
|
||||
samlResponse.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")
|
||||
arId := uuid.NewV4()
|
||||
|
||||
samlResponse.CreateAttr("ID", fmt.Sprintf("_%s", arId))
|
||||
samlResponse.CreateAttr("Version", "2.0")
|
||||
samlResponse.CreateAttr("IssueInstant", now)
|
||||
samlResponse.CreateAttr("Destination", destination)
|
||||
samlResponse.CreateAttr("InResponseTo", fmt.Sprintf("Casdoor_%s", arId))
|
||||
samlResponse.CreateElement("saml:Issuer").SetText(host)
|
||||
|
||||
samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "urn:oasis:names:tc:SAML:2.0:status:Success")
|
||||
|
||||
assertion := samlResponse.CreateElement("saml:Assertion")
|
||||
assertion.CreateAttr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
assertion.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
|
||||
assertion.CreateAttr("ID", fmt.Sprintf("_%s", uuid.NewV4()))
|
||||
assertion.CreateAttr("Version", "2.0")
|
||||
assertion.CreateAttr("IssueInstant", now)
|
||||
assertion.CreateElement("saml:Issuer").SetText(host)
|
||||
subject := assertion.CreateElement("saml:Subject")
|
||||
subject.CreateElement("saml:NameID").SetText(user.Email)
|
||||
subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation")
|
||||
subjectConfirmation.CreateAttr("Method", "urn:oasis:names:tc:SAML:2.0:cm:bearer")
|
||||
subjectConfirmationData := subjectConfirmation.CreateElement("saml:SubjectConfirmationData")
|
||||
subjectConfirmationData.CreateAttr("InResponseTo", fmt.Sprintf("_%s", arId))
|
||||
subjectConfirmationData.CreateAttr("Recipient", destination)
|
||||
subjectConfirmationData.CreateAttr("NotOnOrAfter", expireTime)
|
||||
condition := assertion.CreateElement("saml:Conditions")
|
||||
condition.CreateAttr("NotBefore", now)
|
||||
condition.CreateAttr("NotOnOrAfter", expireTime)
|
||||
audience := condition.CreateElement("saml:AudienceRestriction")
|
||||
audience.CreateElement("saml:Audience").SetText(iss)
|
||||
for _, value := range redirectUri {
|
||||
audience.CreateElement("saml:Audience").SetText(value)
|
||||
}
|
||||
authnStatement := assertion.CreateElement("saml:AuthnStatement")
|
||||
authnStatement.CreateAttr("AuthnInstant", now)
|
||||
authnStatement.CreateAttr("SessionIndex", fmt.Sprintf("_%s", uuid.NewV4()))
|
||||
authnStatement.CreateAttr("SessionNotOnOrAfter", expireTime)
|
||||
authnStatement.CreateElement("saml:AuthnContext").CreateElement("saml:AuthnContextClassRef").SetText("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport")
|
||||
|
||||
attributes := assertion.CreateElement("saml:AttributeStatement")
|
||||
email := attributes.CreateElement("saml:Attribute")
|
||||
email.CreateAttr("Name", "Email")
|
||||
email.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
|
||||
email.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.Email)
|
||||
name := attributes.CreateElement("saml:Attribute")
|
||||
name.CreateAttr("Name", "Name")
|
||||
name.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
|
||||
name.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.Name)
|
||||
displayName := attributes.CreateElement("saml:Attribute")
|
||||
displayName.CreateAttr("Name", "DisplayName")
|
||||
displayName.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
|
||||
displayName.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.DisplayName)
|
||||
|
||||
return samlResponse, nil
|
||||
|
||||
}
|
||||
|
||||
type X509Key struct {
|
||||
X509Certificate string
|
||||
PrivateKey string
|
||||
}
|
||||
|
||||
func (x X509Key) GetKeyPair() (privateKey *rsa.PrivateKey, cert []byte, err error) {
|
||||
cert, _ = base64.StdEncoding.DecodeString(x.X509Certificate)
|
||||
privateKey, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(x.PrivateKey))
|
||||
return privateKey, cert, err
|
||||
}
|
||||
|
||||
//SAML METADATA
|
||||
type IdpEntityDescriptor struct {
|
||||
XMLName xml.Name `xml:"EntityDescriptor"`
|
||||
DS string `xml:"xmlns:ds,attr"`
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
MD string `xml:"xmlns:md,attr"`
|
||||
EntityId string `xml:"entityID,attr"`
|
||||
|
||||
IdpSSODescriptor IdpSSODescriptor `xml:"IDPSSODescriptor"`
|
||||
}
|
||||
|
||||
type KeyInfo struct {
|
||||
XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# KeyInfo"`
|
||||
X509Data X509Data `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type X509Data struct {
|
||||
XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# X509Data"`
|
||||
X509Certificate X509Certificate `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type X509Certificate struct {
|
||||
XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# X509Certificate"`
|
||||
Cert string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type KeyDescriptor struct {
|
||||
XMLName xml.Name `xml:"KeyDescriptor"`
|
||||
Use string `xml:"use,attr"`
|
||||
KeyInfo KeyInfo `xml:"KeyInfo"`
|
||||
}
|
||||
|
||||
type IdpSSODescriptor struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"`
|
||||
ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"`
|
||||
SigningKeyDescriptor KeyDescriptor
|
||||
NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
|
||||
SingleSignOnService SingleSignOnService `xml:"SingleSignOnService"`
|
||||
Attribute []Attribute `xml:"Attribute"`
|
||||
}
|
||||
|
||||
type NameIDFormat struct {
|
||||
XMLName xml.Name
|
||||
Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type SingleSignOnService struct {
|
||||
XMLName xml.Name
|
||||
Binding string `xml:"Binding,attr"`
|
||||
Location string `xml:"Location,attr"`
|
||||
}
|
||||
|
||||
type Attribute struct {
|
||||
XMLName xml.Name
|
||||
Name string `xml:"Name,attr"`
|
||||
NameFormat string `xml:"NameFormat,attr"`
|
||||
FriendlyName string `xml:"FriendlyName,attr"`
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
}
|
||||
|
||||
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) {
|
||||
//_, originBackend := getOriginFromHost(host)
|
||||
cert := getCertByApplication(application)
|
||||
block, _ := pem.Decode([]byte(cert.PublicKey))
|
||||
publicKey := base64.StdEncoding.EncodeToString(block.Bytes)
|
||||
|
||||
origin := beego.AppConfig.String("origin")
|
||||
originFrontend, originBackend := getOriginFromHost(host)
|
||||
if origin != "" {
|
||||
originBackend = origin
|
||||
}
|
||||
d := IdpEntityDescriptor{
|
||||
XMLName: xml.Name{
|
||||
Local: "md:EntityDescriptor",
|
||||
},
|
||||
DS: "http://www.w3.org/2000/09/xmldsig#",
|
||||
XMLNS: "urn:oasis:names:tc:SAML:2.0:metadata",
|
||||
MD: "urn:oasis:names:tc:SAML:2.0:metadata",
|
||||
EntityId: originBackend,
|
||||
IdpSSODescriptor: IdpSSODescriptor{
|
||||
SigningKeyDescriptor: KeyDescriptor{
|
||||
Use: "signing",
|
||||
KeyInfo: KeyInfo{
|
||||
X509Data: X509Data{
|
||||
X509Certificate: X509Certificate{
|
||||
Cert: publicKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
NameIDFormats: []NameIDFormat{
|
||||
{Value: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"},
|
||||
{Value: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"},
|
||||
{Value: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"},
|
||||
},
|
||||
Attribute: []Attribute{
|
||||
{Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "Email", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "E-Mail"},
|
||||
{Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "DisplayName", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "displayName"},
|
||||
{Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "Name", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "Name"},
|
||||
},
|
||||
SingleSignOnService: SingleSignOnService{
|
||||
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
||||
Location: fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name),
|
||||
},
|
||||
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
|
||||
},
|
||||
}
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
//GenerateSamlResponse generates a SAML2.0 response
|
||||
//parameter samlRequest is saml request in base64 format
|
||||
func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, error) {
|
||||
//decode samlRequest
|
||||
defated, err := base64.StdEncoding.DecodeString(samlRequest)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
rdr := flate.NewReader(bytes.NewReader(defated))
|
||||
io.Copy(&buffer, rdr)
|
||||
var authnRequest saml.AuthnRequest
|
||||
err = xml.Unmarshal(buffer.Bytes(), &authnRequest)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
//verify samlRequest
|
||||
if valid := CheckRedirectUriValid(application, authnRequest.Issuer.Url); !valid {
|
||||
return "", "", fmt.Errorf("err: invalid issuer url")
|
||||
}
|
||||
|
||||
//get publickey string
|
||||
cert := getCertByApplication(application)
|
||||
block, _ := pem.Decode([]byte(cert.PublicKey))
|
||||
publicKey := base64.StdEncoding.EncodeToString(block.Bytes)
|
||||
|
||||
_, originBackend := getOriginFromHost(host)
|
||||
|
||||
//build signedResponse
|
||||
samlResponse, _ := NewSamlResponse(user, originBackend, publicKey, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, application.RedirectUris)
|
||||
randomKeyStore := &X509Key{
|
||||
PrivateKey: cert.PrivateKey,
|
||||
X509Certificate: publicKey,
|
||||
}
|
||||
ctx := dsig.NewDefaultSigningContext(randomKeyStore)
|
||||
ctx.Hash = crypto.SHA1
|
||||
signedXML, err := ctx.SignEnveloped(samlResponse)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
|
||||
doc := etree.NewDocument()
|
||||
doc.SetRoot(signedXML)
|
||||
xmlStr, err := doc.WriteToString()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
res := base64.StdEncoding.EncodeToString([]byte(xmlStr))
|
||||
return res, authnRequest.AssertionConsumerServiceURL, nil
|
||||
}
|
||||
|
||||
//return a saml1.1 response(not 2.0)
|
||||
func NewSamlResponse11(user *User, requestID string, host string) *etree.Element {
|
||||
samlResponse := &etree.Element{
|
||||
Space: "samlp",
|
||||
Tag: "Response",
|
||||
}
|
||||
//create samlresponse
|
||||
samlResponse.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:1.0:protocol")
|
||||
samlResponse.CreateAttr("MajorVersion", "1")
|
||||
samlResponse.CreateAttr("MinorVersion", "1")
|
||||
|
||||
responseID := uuid.NewV4()
|
||||
samlResponse.CreateAttr("ResponseID", fmt.Sprintf("_%s", responseID))
|
||||
samlResponse.CreateAttr("InResponseTo", requestID)
|
||||
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
expireTime := time.Now().UTC().Add(time.Hour * 24).Format(time.RFC3339)
|
||||
|
||||
samlResponse.CreateAttr("IssueInstant", now)
|
||||
|
||||
samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "samlp:Success")
|
||||
|
||||
//create assertion which is inside the response
|
||||
assertion := samlResponse.CreateElement("saml:Assertion")
|
||||
assertion.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:1.0:assertion")
|
||||
assertion.CreateAttr("MajorVersion", "1")
|
||||
assertion.CreateAttr("MinorVersion", "1")
|
||||
assertion.CreateAttr("AssertionID", uuid.NewV4().String())
|
||||
assertion.CreateAttr("Issuer", host)
|
||||
assertion.CreateAttr("IssueInstant", now)
|
||||
|
||||
condition := assertion.CreateElement("saml:Conditions")
|
||||
condition.CreateAttr("NotBefore", now)
|
||||
condition.CreateAttr("NotOnOrAfter", expireTime)
|
||||
|
||||
//AuthenticationStatement inside assertion
|
||||
authenticationStatement := assertion.CreateElement("saml:AuthenticationStatement")
|
||||
authenticationStatement.CreateAttr("AuthenticationMethod", "urn:oasis:names:tc:SAML:1.0:am:password")
|
||||
authenticationStatement.CreateAttr("AuthenticationInstant", now)
|
||||
|
||||
//subject inside AuthenticationStatement
|
||||
subject := assertion.CreateElement("saml:Subject")
|
||||
//nameIdentifier inside subject
|
||||
nameIdentifier := subject.CreateElement("saml:NameIdentifier")
|
||||
//nameIdentifier.CreateAttr("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
|
||||
nameIdentifier.SetText(user.Name)
|
||||
|
||||
//subjectConfirmation inside subject
|
||||
subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation")
|
||||
subjectConfirmation.CreateElement("saml:ConfirmationMethod").SetText("urn:oasis:names:tc:SAML:1.0:cm:artifact")
|
||||
|
||||
attributeStatement := assertion.CreateElement("saml:AttributeStatement")
|
||||
subjectInAttribute := attributeStatement.CreateElement("saml:Subject")
|
||||
nameIdentifierInAttribute := subjectInAttribute.CreateElement("saml:NameIdentifier")
|
||||
nameIdentifierInAttribute.SetText(user.Name)
|
||||
|
||||
subjectConfirmationInAttribute := subjectInAttribute.CreateElement("saml:SubjectConfirmation")
|
||||
subjectConfirmationInAttribute.CreateElement("saml:ConfirmationMethod").SetText("urn:oasis:names:tc:SAML:1.0:cm:artifact")
|
||||
|
||||
data, _ := json.Marshal(user)
|
||||
tmp := map[string]string{}
|
||||
json.Unmarshal(data, &tmp)
|
||||
|
||||
for k, v := range tmp {
|
||||
if v != "" {
|
||||
attr := attributeStatement.CreateElement("saml:Attribute")
|
||||
attr.CreateAttr("saml:AttributeName", k)
|
||||
attr.CreateAttr("saml:AttributeNamespace", "http://www.ja-sig.org/products/cas/")
|
||||
attr.CreateElement("saml:AttributeValue").SetText(v)
|
||||
}
|
||||
}
|
||||
|
||||
return samlResponse
|
||||
}
|
@ -206,3 +206,8 @@ func (syncer *Syncer) getTable() string {
|
||||
return syncer.Table
|
||||
}
|
||||
}
|
||||
|
||||
func RunSyncer(syncer *Syncer) {
|
||||
syncer.initAdapter()
|
||||
syncer.syncUsers()
|
||||
}
|
||||
|
@ -25,6 +25,11 @@ import (
|
||||
|
||||
type OriginalUser = User
|
||||
|
||||
type Credential struct {
|
||||
Value string `json:"value"`
|
||||
Salt string `json:"salt"`
|
||||
}
|
||||
|
||||
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
|
||||
sql := fmt.Sprintf("select * from %s", syncer.getTable())
|
||||
results, err := syncer.Adapter.Engine.QueryString(sql)
|
||||
|
@ -15,9 +15,11 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@ -99,12 +101,18 @@ func (syncer *Syncer) setUserByKeyValue(user *User, key string, value string) {
|
||||
user.PasswordSalt = value
|
||||
case "DisplayName":
|
||||
user.DisplayName = value
|
||||
case "FirstName":
|
||||
user.FirstName = value
|
||||
case "LastName":
|
||||
user.LastName = value
|
||||
case "Avatar":
|
||||
user.Avatar = syncer.getPartialAvatarUrl(value)
|
||||
case "PermanentAvatar":
|
||||
user.PermanentAvatar = value
|
||||
case "Email":
|
||||
user.Email = value
|
||||
case "EmailVerified":
|
||||
user.EmailVerified = util.ParseBool(value)
|
||||
case "Phone":
|
||||
user.Phone = value
|
||||
case "Location":
|
||||
@ -167,6 +175,32 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
|
||||
for _, tableColumn := range syncer.TableColumns {
|
||||
syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, result[tableColumn.Name])
|
||||
}
|
||||
|
||||
if syncer.Type == "Keycloak" {
|
||||
// query and set password and password salt from credential table
|
||||
sql := fmt.Sprintf("select * from credential where type = 'password' and user_id = '%s'", originalUser.Id)
|
||||
credentialResult, _ := syncer.Adapter.Engine.QueryString(sql)
|
||||
if len(credentialResult) > 0 {
|
||||
credential := Credential{}
|
||||
_ = json.Unmarshal([]byte(credentialResult[0]["SECRET_DATA"]), &credential)
|
||||
originalUser.Password = credential.Value
|
||||
originalUser.PasswordSalt = credential.Salt
|
||||
}
|
||||
// query and set signup application from user group table
|
||||
sql = fmt.Sprintf("select name from keycloak_group where id = " +
|
||||
"(select group_id as gid from user_group_membership where user_id = '%s')", originalUser.Id)
|
||||
groupResult, _ := syncer.Adapter.Engine.QueryString(sql)
|
||||
if len(groupResult) > 0 {
|
||||
originalUser.SignupApplication = groupResult[0]["name"]
|
||||
}
|
||||
// create time
|
||||
i, _ := strconv.ParseInt(originalUser.CreatedTime, 10, 64)
|
||||
tm := time.Unix(i/int64(1000), 0)
|
||||
originalUser.CreatedTime = tm.Format("2006-01-02T15:04:05+08:00")
|
||||
// enable
|
||||
originalUser.IsForbidden = !(result["ENABLED"] == "\x01")
|
||||
}
|
||||
|
||||
users = append(users, originalUser)
|
||||
}
|
||||
return users
|
||||
|
136
object/token.go
136
object/token.go
@ -22,10 +22,15 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"xorm.io/core"
|
||||
)
|
||||
|
||||
const (
|
||||
hourSeconds = 3600
|
||||
)
|
||||
|
||||
type Code struct {
|
||||
Message string `xorm:"varchar(100)" json:"message"`
|
||||
Code string `xorm:"varchar(100)" json:"code"`
|
||||
@ -58,6 +63,7 @@ type TokenWrapper struct {
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type IntrospectionResponse struct {
|
||||
@ -290,7 +296,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeChallenge: challenge,
|
||||
@ -305,24 +311,31 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
|
||||
}
|
||||
}
|
||||
|
||||
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string) *TokenWrapper {
|
||||
|
||||
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string, tag string, avatar string) *TokenWrapper {
|
||||
var errString string
|
||||
application := GetApplicationByClientId(clientId)
|
||||
if application == nil {
|
||||
errString = "error: invalid client_id"
|
||||
return &TokenWrapper{
|
||||
AccessToken: "error: invalid client_id",
|
||||
AccessToken: errString,
|
||||
TokenType: "",
|
||||
ExpiresIn: 0,
|
||||
Scope: "",
|
||||
Error: errString,
|
||||
}
|
||||
}
|
||||
|
||||
//Check if grantType is allowed in the current application
|
||||
if !IsGrantTypeValid(grantType, application.GrantTypes) {
|
||||
|
||||
if !IsGrantTypeValid(grantType, application.GrantTypes) && tag == "" {
|
||||
errString = fmt.Sprintf("error: grant_type: %s is not supported in this application", grantType)
|
||||
return &TokenWrapper{
|
||||
AccessToken: fmt.Sprintf("error: grant_type: %s is not supported in this application", grantType),
|
||||
AccessToken: errString,
|
||||
TokenType: "",
|
||||
ExpiresIn: 0,
|
||||
Scope: "",
|
||||
Error: errString,
|
||||
}
|
||||
}
|
||||
|
||||
@ -337,12 +350,19 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
||||
token, err = GetClientCredentialsToken(application, clientSecret, scope, host)
|
||||
}
|
||||
|
||||
if tag == "wechat_miniprogram" {
|
||||
// Wechat Mini Program
|
||||
token, err = GetWechatMiniProgramToken(application, code, host, username, avatar)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errString = err.Error()
|
||||
return &TokenWrapper{
|
||||
AccessToken: err.Error(),
|
||||
AccessToken: errString,
|
||||
TokenType: "",
|
||||
ExpiresIn: 0,
|
||||
Scope: "",
|
||||
Error: errString,
|
||||
}
|
||||
}
|
||||
|
||||
@ -361,62 +381,75 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
||||
}
|
||||
|
||||
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) *TokenWrapper {
|
||||
var errString string
|
||||
// check parameters
|
||||
if grantType != "refresh_token" {
|
||||
errString = "error: grant_type should be \"refresh_token\""
|
||||
return &TokenWrapper{
|
||||
AccessToken: "error: grant_type should be \"refresh_token\"",
|
||||
AccessToken: errString,
|
||||
TokenType: "",
|
||||
ExpiresIn: 0,
|
||||
Scope: "",
|
||||
Error: errString,
|
||||
}
|
||||
}
|
||||
application := GetApplicationByClientId(clientId)
|
||||
if application == nil {
|
||||
errString = "error: invalid client_id"
|
||||
return &TokenWrapper{
|
||||
AccessToken: "error: invalid client_id",
|
||||
AccessToken: errString,
|
||||
TokenType: "",
|
||||
ExpiresIn: 0,
|
||||
Scope: "",
|
||||
Error: errString,
|
||||
}
|
||||
}
|
||||
if clientSecret != "" && application.ClientSecret != clientSecret {
|
||||
errString = "error: invalid client_secret"
|
||||
return &TokenWrapper{
|
||||
AccessToken: "error: invalid client_secret",
|
||||
AccessToken: errString,
|
||||
TokenType: "",
|
||||
ExpiresIn: 0,
|
||||
Scope: "",
|
||||
Error: errString,
|
||||
}
|
||||
}
|
||||
// check whether the refresh token is valid, and has not expired.
|
||||
token := Token{RefreshToken: refreshToken}
|
||||
existed, err := adapter.Engine.Get(&token)
|
||||
if err != nil || !existed {
|
||||
errString = "error: invalid refresh_token"
|
||||
return &TokenWrapper{
|
||||
AccessToken: "error: invalid refresh_token",
|
||||
AccessToken: errString,
|
||||
TokenType: "",
|
||||
ExpiresIn: 0,
|
||||
Scope: "",
|
||||
Error: errString,
|
||||
}
|
||||
}
|
||||
|
||||
cert := getCertByApplication(application)
|
||||
_, err = ParseJwtToken(refreshToken, cert)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("error: %s", err.Error())
|
||||
return &TokenWrapper{
|
||||
AccessToken: fmt.Sprintf("error: %s", err.Error()),
|
||||
AccessToken: errString,
|
||||
TokenType: "",
|
||||
ExpiresIn: 0,
|
||||
Scope: "",
|
||||
Error: errString,
|
||||
}
|
||||
}
|
||||
// generate a new token
|
||||
user := getUser(application.Organization, token.User)
|
||||
if user.IsForbidden {
|
||||
errString = "error: the user is forbidden to sign in, please contact the administrator"
|
||||
return &TokenWrapper{
|
||||
AccessToken: "error: the user is forbidden to sign in, please contact the administrator",
|
||||
AccessToken: errString,
|
||||
TokenType: "",
|
||||
ExpiresIn: 0,
|
||||
Scope: "",
|
||||
Error: errString,
|
||||
}
|
||||
}
|
||||
newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "", scope, host)
|
||||
@ -434,7 +467,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: newAccessToken,
|
||||
RefreshToken: newRefreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
@ -543,7 +576,7 @@ func GetPasswordToken(application *Application, username string, password string
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
@ -575,7 +608,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
|
||||
User: nullUser.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
@ -600,7 +633,7 @@ func GetTokenByUser(application *Application, user *User, scope string, host str
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
@ -608,3 +641,74 @@ func GetTokenByUser(application *Application, user *User, scope string, host str
|
||||
AddToken(token)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Wechat Mini Program flow
|
||||
func GetWechatMiniProgramToken(application *Application, code string, host string, username string, avatar string) (*Token, error) {
|
||||
mpProvider := GetWechatMiniProgramProvider(application)
|
||||
if mpProvider == nil {
|
||||
return nil, errors.New("error: the application does not support wechat mini program")
|
||||
}
|
||||
provider := GetProvider(util.GetId(mpProvider.Name))
|
||||
mpIdp := idp.NewWeChatMiniProgramIdProvider(provider.ClientId, provider.ClientSecret)
|
||||
session, err := mpIdp.GetSessionByCode(code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openId, unionId := session.Openid, session.Unionid
|
||||
if openId == "" && unionId == "" {
|
||||
return nil, errors.New("err: WeChat's openid and unionid are empty")
|
||||
}
|
||||
user := getUserByWechatId(openId, unionId)
|
||||
if user == nil {
|
||||
if !application.EnableSignUp {
|
||||
return nil, errors.New("err: the application does not allow to sign up new account")
|
||||
}
|
||||
//Add new user
|
||||
var name string
|
||||
if username != "" {
|
||||
name = username
|
||||
} else {
|
||||
name = fmt.Sprintf("wechat-%s", openId)
|
||||
}
|
||||
|
||||
user = &User{
|
||||
Owner: application.Organization,
|
||||
Id: util.GenerateId(),
|
||||
Name: name,
|
||||
Avatar: avatar,
|
||||
SignupApplication: application.Name,
|
||||
WeChat: openId,
|
||||
WeChatUnionId: unionId,
|
||||
Type: "normal-user",
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
IsAdmin: false,
|
||||
IsGlobalAdmin: false,
|
||||
IsForbidden: false,
|
||||
IsDeleted: false,
|
||||
}
|
||||
AddUser(user)
|
||||
}
|
||||
|
||||
accessToken, refreshToken, err := generateJwtToken(application, user, "", "", host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := &Token{
|
||||
Owner: application.Owner,
|
||||
Name: util.GenerateId(),
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: application.Name,
|
||||
Organization: user.Owner,
|
||||
User: user.Name,
|
||||
Code: session.SessionKey, //a trick, because miniprogram does not use the code, so use the code field to save the session_key
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
Scope: "",
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
}
|
||||
AddToken(token)
|
||||
return token, nil
|
||||
}
|
||||
|
@ -15,14 +15,19 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
)
|
||||
|
||||
type CasServiceResponse struct {
|
||||
@ -84,6 +89,7 @@ type CasAnyAttribute struct {
|
||||
type CasAuthenticationSuccessWrapper struct {
|
||||
AuthenticationSuccess *CasAuthenticationSuccess // the token we issued
|
||||
Service string //to which service this token is issued
|
||||
UserId string
|
||||
}
|
||||
|
||||
type CasProxySuccess struct {
|
||||
@ -96,17 +102,32 @@ type CasProxyFailure struct {
|
||||
Message string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type Saml11Request struct {
|
||||
XMLName xml.Name `xml:"Request"`
|
||||
SAMLP string `xml:"samlp,attr"`
|
||||
MajorVersion string `xml:"MajorVersion,attr"`
|
||||
MinorVersion string `xml:"MinorVersion,attr"`
|
||||
RequestID string `xml:"RequestID,attr"`
|
||||
IssueInstant string `xml:"IssueInstance,attr"`
|
||||
AssertionArtifact Saml11AssertionArtifact
|
||||
}
|
||||
type Saml11AssertionArtifact struct {
|
||||
XMLName xml.Name `xml:"AssertionArtifact"`
|
||||
InnerXML string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
//st is short for service ticket
|
||||
var stToServiceResponse sync.Map
|
||||
|
||||
//pgt is short for proxy granting ticket
|
||||
var pgtToServiceResponse sync.Map
|
||||
|
||||
func StoreCasTokenForPgt(token *CasAuthenticationSuccess, service string) string {
|
||||
func StoreCasTokenForPgt(token *CasAuthenticationSuccess, service, userId string) string {
|
||||
pgt := fmt.Sprintf("PGT-%s", util.GenerateId())
|
||||
pgtToServiceResponse.Store(pgt, &CasAuthenticationSuccessWrapper{
|
||||
AuthenticationSuccess: token,
|
||||
Service: service,
|
||||
UserId: userId,
|
||||
})
|
||||
return pgt
|
||||
}
|
||||
@ -115,33 +136,45 @@ func GenerateId() {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func GetCasTokenByPgt(pgt string) (bool, *CasAuthenticationSuccess, string) {
|
||||
/**
|
||||
@ret1: whether a token is found
|
||||
@ret2: token, nil if not found
|
||||
@ret3: the service URL who requested to issue this token
|
||||
@ret4: userIf of user who requested to issue this token
|
||||
*/
|
||||
func GetCasTokenByPgt(pgt string) (bool, *CasAuthenticationSuccess, string, string) {
|
||||
if responseWrapperType, ok := pgtToServiceResponse.LoadAndDelete(pgt); ok {
|
||||
responseWrapperTypeCast := responseWrapperType.(*CasAuthenticationSuccessWrapper)
|
||||
return true, responseWrapperTypeCast.AuthenticationSuccess, responseWrapperTypeCast.Service
|
||||
return true, responseWrapperTypeCast.AuthenticationSuccess, responseWrapperTypeCast.Service, responseWrapperTypeCast.UserId
|
||||
}
|
||||
return false, nil, ""
|
||||
return false, nil, "", ""
|
||||
}
|
||||
|
||||
func GetCasTokenByTicket(ticket string) (bool, *CasAuthenticationSuccess, string) {
|
||||
/**
|
||||
@ret1: whether a token is found
|
||||
@ret2: token, nil if not found
|
||||
@ret3: the service URL who requested to issue this token
|
||||
@ret4: userIf of user who requested to issue this token
|
||||
*/
|
||||
func GetCasTokenByTicket(ticket string) (bool, *CasAuthenticationSuccess, string, string) {
|
||||
if responseWrapperType, ok := stToServiceResponse.LoadAndDelete(ticket); ok {
|
||||
responseWrapperTypeCast := responseWrapperType.(*CasAuthenticationSuccessWrapper)
|
||||
return true, responseWrapperTypeCast.AuthenticationSuccess, responseWrapperTypeCast.Service
|
||||
return true, responseWrapperTypeCast.AuthenticationSuccess, responseWrapperTypeCast.Service, responseWrapperTypeCast.UserId
|
||||
}
|
||||
return false, nil, ""
|
||||
return false, nil, "", ""
|
||||
}
|
||||
|
||||
func StoreCasTokenForProxyTicket(token *CasAuthenticationSuccess, targetService string) string {
|
||||
func StoreCasTokenForProxyTicket(token *CasAuthenticationSuccess, targetService, userId string) string {
|
||||
proxyTicket := fmt.Sprintf("PT-%s", util.GenerateId())
|
||||
stToServiceResponse.Store(proxyTicket, &CasAuthenticationSuccessWrapper{
|
||||
AuthenticationSuccess: token,
|
||||
Service: targetService,
|
||||
UserId: userId,
|
||||
})
|
||||
return proxyTicket
|
||||
}
|
||||
|
||||
func GenerateCasToken(userId string, service string) (string, error) {
|
||||
|
||||
if user := GetUser(userId); user != nil {
|
||||
authenticationSuccess := CasAuthenticationSuccess{
|
||||
User: user.Name,
|
||||
@ -166,11 +199,69 @@ func GenerateCasToken(userId string, service string) (string, error) {
|
||||
stToServiceResponse.Store(st, &CasAuthenticationSuccessWrapper{
|
||||
AuthenticationSuccess: &authenticationSuccess,
|
||||
Service: service,
|
||||
UserId: userId,
|
||||
})
|
||||
return st, nil
|
||||
} else {
|
||||
return "", fmt.Errorf("invalid user Id")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ret1: saml response
|
||||
@ret2: the service URL who requested to issue this token
|
||||
@ret3: error
|
||||
*/
|
||||
func GetValidationBySaml(samlRequest string, host string) (string, string, error) {
|
||||
var request Saml11Request
|
||||
err := xml.Unmarshal([]byte(samlRequest), &request)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
ticket := request.AssertionArtifact.InnerXML
|
||||
if ticket == "" {
|
||||
return "", "", fmt.Errorf("samlp:AssertionArtifact field not found")
|
||||
}
|
||||
|
||||
ok, _, service, userId := GetCasTokenByTicket(ticket)
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("ticket %s found", ticket)
|
||||
}
|
||||
|
||||
user := GetUser(userId)
|
||||
if user == nil {
|
||||
return "", "", fmt.Errorf("user %s found", userId)
|
||||
}
|
||||
application := GetApplicationByUser(user)
|
||||
if application == nil {
|
||||
return "", "", fmt.Errorf("application for user %s found", userId)
|
||||
}
|
||||
|
||||
samlResponse := NewSamlResponse11(user, request.RequestID, host)
|
||||
|
||||
cert := getCertByApplication(application)
|
||||
block, _ := pem.Decode([]byte(cert.PublicKey))
|
||||
publicKey := base64.StdEncoding.EncodeToString(block.Bytes)
|
||||
randomKeyStore := &X509Key{
|
||||
PrivateKey: cert.PrivateKey,
|
||||
X509Certificate: publicKey,
|
||||
}
|
||||
|
||||
ctx := dsig.NewDefaultSigningContext(randomKeyStore)
|
||||
ctx.Hash = crypto.SHA1
|
||||
signedXML, err := ctx.SignEnveloped(samlResponse)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
|
||||
doc := etree.NewDocument()
|
||||
doc.SetRoot(signedXML)
|
||||
xmlStr, err := doc.WriteToString()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("err: %s", err.Error())
|
||||
}
|
||||
return xmlStr, service, nil
|
||||
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,7 @@ type User struct {
|
||||
Avatar string `xorm:"varchar(500)" json:"avatar"`
|
||||
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
|
||||
Email string `xorm:"varchar(100) index" json:"email"`
|
||||
EmailVerified bool `json:"emailVerified"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||
Location string `xorm:"varchar(100)" json:"location"`
|
||||
Address []string `json:"address"`
|
||||
@ -71,27 +72,29 @@ type User struct {
|
||||
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
|
||||
LastSigninIp string `xorm:"varchar(100)" json:"lastSigninIp"`
|
||||
|
||||
Github string `xorm:"varchar(100)" json:"github"`
|
||||
Google string `xorm:"varchar(100)" json:"google"`
|
||||
QQ string `xorm:"qq varchar(100)" json:"qq"`
|
||||
WeChat string `xorm:"wechat varchar(100)" json:"wechat"`
|
||||
Facebook string `xorm:"facebook varchar(100)" json:"facebook"`
|
||||
DingTalk string `xorm:"dingtalk varchar(100)" json:"dingtalk"`
|
||||
Weibo string `xorm:"weibo varchar(100)" json:"weibo"`
|
||||
Gitee string `xorm:"gitee varchar(100)" json:"gitee"`
|
||||
LinkedIn string `xorm:"linkedin varchar(100)" json:"linkedin"`
|
||||
Wecom string `xorm:"wecom varchar(100)" json:"wecom"`
|
||||
Lark string `xorm:"lark varchar(100)" json:"lark"`
|
||||
Gitlab string `xorm:"gitlab varchar(100)" json:"gitlab"`
|
||||
Adfs string `xorm:"adfs varchar(100)" json:"adfs"`
|
||||
Baidu string `xorm:"baidu varchar(100)" json:"baidu"`
|
||||
Alipay string `xorm:"alipay varchar(100)" json:"alipay"`
|
||||
Casdoor string `xorm:"casdoor varchar(100)" json:"casdoor"`
|
||||
Infoflow string `xorm:"infoflow varchar(100)" json:"infoflow"`
|
||||
Apple string `xorm:"apple varchar(100)" json:"apple"`
|
||||
AzureAD string `xorm:"azuread varchar(100)" json:"azuread"`
|
||||
Slack string `xorm:"slack varchar(100)" json:"slack"`
|
||||
Steam string `xorm:"steam varchar(100)" json:"steam"`
|
||||
Github string `xorm:"varchar(100)" json:"github"`
|
||||
Google string `xorm:"varchar(100)" json:"google"`
|
||||
QQ string `xorm:"qq varchar(100)" json:"qq"`
|
||||
WeChat string `xorm:"wechat varchar(100)" json:"wechat"`
|
||||
WeChatUnionId string `xorm:"varchar(100)" json:"unionId"`
|
||||
Facebook string `xorm:"facebook varchar(100)" json:"facebook"`
|
||||
DingTalk string `xorm:"dingtalk varchar(100)" json:"dingtalk"`
|
||||
Weibo string `xorm:"weibo varchar(100)" json:"weibo"`
|
||||
Gitee string `xorm:"gitee varchar(100)" json:"gitee"`
|
||||
LinkedIn string `xorm:"linkedin varchar(100)" json:"linkedin"`
|
||||
Wecom string `xorm:"wecom varchar(100)" json:"wecom"`
|
||||
Lark string `xorm:"lark varchar(100)" json:"lark"`
|
||||
Gitlab string `xorm:"gitlab varchar(100)" json:"gitlab"`
|
||||
Adfs string `xorm:"adfs varchar(100)" json:"adfs"`
|
||||
Baidu string `xorm:"baidu varchar(100)" json:"baidu"`
|
||||
Alipay string `xorm:"alipay varchar(100)" json:"alipay"`
|
||||
Casdoor string `xorm:"casdoor varchar(100)" json:"casdoor"`
|
||||
Infoflow string `xorm:"infoflow varchar(100)" json:"infoflow"`
|
||||
Apple string `xorm:"apple varchar(100)" json:"apple"`
|
||||
AzureAD string `xorm:"azuread varchar(100)" json:"azuread"`
|
||||
Slack string `xorm:"slack varchar(100)" json:"slack"`
|
||||
Steam string `xorm:"steam varchar(100)" json:"steam"`
|
||||
Custom string `xorm:"custom varchar(100)" json:"custom"`
|
||||
|
||||
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
|
||||
Properties map[string]string `json:"properties"`
|
||||
@ -226,6 +229,23 @@ func getUserById(owner string, id string) *User {
|
||||
}
|
||||
}
|
||||
|
||||
func getUserByWechatId(wechatOpenId string, wechatUnionId string) *User {
|
||||
if wechatUnionId == "" {
|
||||
wechatUnionId = wechatOpenId
|
||||
}
|
||||
user := &User{}
|
||||
existed, err := adapter.Engine.Where("wechat = ? OR wechat = ? OR unionid = ?", wechatOpenId, wechatUnionId, wechatUnionId).Get(user)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if existed {
|
||||
return user
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserByEmail(owner string, email string) *User {
|
||||
if owner == "" || email == "" {
|
||||
return nil
|
||||
|
@ -101,7 +101,7 @@ func willLog(subOwner string, subName string, method string, urlPath string, obj
|
||||
}
|
||||
|
||||
func getUrlPath(urlPath string) string {
|
||||
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate")) {
|
||||
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate") || strings.HasSuffix(urlPath, "/p3/serviceValidate") || strings.HasSuffix(urlPath, "/p3/proxyValidate") || strings.HasSuffix(urlPath, "/samlValidate")) {
|
||||
return "/cas"
|
||||
}
|
||||
return urlPath
|
||||
|
@ -54,6 +54,7 @@ func initAPI() {
|
||||
beego.Router("/api/unlink", &controllers.ApiController{}, "POST:Unlink")
|
||||
beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin")
|
||||
beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin")
|
||||
beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta")
|
||||
|
||||
beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
|
||||
beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")
|
||||
@ -144,6 +145,7 @@ func initAPI() {
|
||||
beego.Router("/api/update-syncer", &controllers.ApiController{}, "POST:UpdateSyncer")
|
||||
beego.Router("/api/add-syncer", &controllers.ApiController{}, "POST:AddSyncer")
|
||||
beego.Router("/api/delete-syncer", &controllers.ApiController{}, "POST:DeleteSyncer")
|
||||
beego.Router("/api/run-syncer", &controllers.ApiController{}, "GET:RunSyncer")
|
||||
|
||||
beego.Router("/api/get-certs", &controllers.ApiController{}, "GET:GetCerts")
|
||||
beego.Router("/api/get-cert", &controllers.ApiController{}, "GET:GetCert")
|
||||
@ -172,9 +174,13 @@ func initAPI() {
|
||||
beego.Router("/.well-known/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscovery")
|
||||
beego.Router("/.well-known/jwks", &controllers.RootController{}, "*:GetJwks")
|
||||
|
||||
beego.Router("/cas/:organization/:application/serviceValidate", &controllers.RootController{}, "GET:CasServiceAndProxyValidate")
|
||||
beego.Router("/cas/:organization/:application/proxyValidate", &controllers.RootController{}, "GET:CasServiceAndProxyValidate")
|
||||
beego.Router("/cas/:organization/:application/serviceValidate", &controllers.RootController{}, "GET:CasServiceValidate")
|
||||
beego.Router("/cas/:organization/:application/proxyValidate", &controllers.RootController{}, "GET:CasProxyValidate")
|
||||
beego.Router("/cas/:organization/:application/proxy", &controllers.RootController{}, "GET:CasProxy")
|
||||
beego.Router("/cas/:organization/:application/validate", &controllers.RootController{}, "GET:CasValidate")
|
||||
|
||||
beego.Router("/cas/:organization/:application/p3/serviceValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate")
|
||||
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate")
|
||||
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
|
||||
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ func StaticFilter(ctx *context.Context) {
|
||||
if strings.HasPrefix(urlPath, "/api/") || strings.HasPrefix(urlPath, "/.well-known/") {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate")) {
|
||||
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate") || strings.HasSuffix(urlPath, "/p3/serviceValidate") || strings.HasSuffix(urlPath, "/p3/proxyValidate") || strings.HasSuffix(urlPath, "/samlValidate")) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -2797,11 +2797,11 @@
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"2026.0xc000380de0.false": {
|
||||
"2127.0xc00036c600.false": {
|
||||
"title": "false",
|
||||
"type": "object"
|
||||
},
|
||||
"2060.0xc000380e10.false": {
|
||||
"2161.0xc00036c630.false": {
|
||||
"title": "false",
|
||||
"type": "object"
|
||||
},
|
||||
@ -2818,10 +2818,10 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/2026.0xc000380de0.false"
|
||||
"$ref": "#/definitions/2127.0xc00036c600.false"
|
||||
},
|
||||
"data2": {
|
||||
"$ref": "#/definitions/2060.0xc000380e10.false"
|
||||
"$ref": "#/definitions/2161.0xc00036c630.false"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
@ -2842,10 +2842,10 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/2026.0xc000380de0.false"
|
||||
"$ref": "#/definitions/2127.0xc00036c600.false"
|
||||
},
|
||||
"data2": {
|
||||
"$ref": "#/definitions/2060.0xc000380e10.false"
|
||||
"$ref": "#/definitions/2161.0xc00036c630.false"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
@ -3648,6 +3648,9 @@
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"expires_in": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
@ -3682,6 +3685,9 @@
|
||||
"affiliation": {
|
||||
"type": "string"
|
||||
},
|
||||
"alipay": {
|
||||
"type": "string"
|
||||
},
|
||||
"apple": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -3721,6 +3727,9 @@
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailVerified": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"facebook": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -1831,10 +1831,10 @@ paths:
|
||||
schema:
|
||||
$ref: '#/definitions/object.Userinfo'
|
||||
definitions:
|
||||
2026.0xc000380de0.false:
|
||||
2127.0xc00036c600.false:
|
||||
title: "false"
|
||||
type: object
|
||||
2060.0xc000380e10.false:
|
||||
2161.0xc00036c630.false:
|
||||
title: "false"
|
||||
type: object
|
||||
RequestForm:
|
||||
@ -1848,9 +1848,9 @@ definitions:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/2026.0xc000380de0.false'
|
||||
$ref: '#/definitions/2127.0xc00036c600.false'
|
||||
data2:
|
||||
$ref: '#/definitions/2060.0xc000380e10.false'
|
||||
$ref: '#/definitions/2161.0xc00036c630.false'
|
||||
msg:
|
||||
type: string
|
||||
name:
|
||||
@ -1864,9 +1864,9 @@ definitions:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/2026.0xc000380de0.false'
|
||||
$ref: '#/definitions/2127.0xc00036c600.false'
|
||||
data2:
|
||||
$ref: '#/definitions/2060.0xc000380e10.false'
|
||||
$ref: '#/definitions/2161.0xc00036c630.false'
|
||||
msg:
|
||||
type: string
|
||||
name:
|
||||
@ -2407,6 +2407,8 @@ definitions:
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
error:
|
||||
type: string
|
||||
expires_in:
|
||||
type: integer
|
||||
format: int64
|
||||
@ -2430,6 +2432,8 @@ definitions:
|
||||
type: string
|
||||
affiliation:
|
||||
type: string
|
||||
alipay:
|
||||
type: string
|
||||
apple:
|
||||
type: string
|
||||
avatar:
|
||||
@ -2456,6 +2460,8 @@ definitions:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
emailVerified:
|
||||
type: boolean
|
||||
facebook:
|
||||
type: string
|
||||
firstName:
|
||||
|
@ -52,6 +52,10 @@ func ParseFloat(s string) float64 {
|
||||
}
|
||||
|
||||
func ParseBool(s string) bool {
|
||||
if s == "\x01" {
|
||||
return true
|
||||
}
|
||||
|
||||
i := ParseInt(s)
|
||||
return i != 0
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ preserve_hierarchy: true
|
||||
files: [
|
||||
# JSON translation files
|
||||
{
|
||||
source: '/web/src/locales/en/data.json',
|
||||
translation: '/web/src/locales/%two_letters_code%/data.json',
|
||||
source: '/src/locales/en/data.json',
|
||||
translation: '/src/locales/%two_letters_code%/data.json',
|
||||
},
|
||||
]
|
||||
|
@ -656,6 +656,7 @@ class App extends Component {
|
||||
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />}/>
|
||||
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />}/>
|
||||
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage account={this.state.account} type={"saml"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account)}} />}/>
|
||||
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout clearAccount={() => this.setState({account: null})} {...props} />)} />
|
||||
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage type={"cas"} mode={"signup"} account={this.state.account} {...props} />)}} />
|
||||
<Route exact path="/callback" component={AuthCallback}/>
|
||||
|
@ -61,7 +61,7 @@ class ApplicationEditPage extends React.Component {
|
||||
getApplication() {
|
||||
ApplicationBackend.getApplication("admin", this.state.applicationName)
|
||||
.then((application) => {
|
||||
if (application.grantTypes === null || application.grantTypes.length === 0) {
|
||||
if (application.grantTypes === null || application.grantTypes === undefined || application.grantTypes.length === 0) {
|
||||
application.grantTypes = ["authorization_code"];
|
||||
}
|
||||
this.setState({
|
||||
|
@ -18,4 +18,4 @@ export const GithubRepo = "https://github.com/casdoor/casdoor";
|
||||
export const ForceLanguage = "";
|
||||
export const DefaultLanguage = "en";
|
||||
|
||||
export const EnableExtraPages = false;
|
||||
export const EnableExtraPages = true;
|
||||
|
@ -155,7 +155,7 @@ class OrganizationEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: '100%'}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField('passwordType', value);})}>
|
||||
{
|
||||
['plain', 'salt', 'md5-salt', 'bcrypt']
|
||||
['plain', 'salt', 'md5-salt', 'bcrypt', 'pbkdf2-salt']
|
||||
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
@ -240,6 +240,16 @@ class OrganizationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Is profile public"), i18next.t("organization:Is profile public - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.organization.isProfilePublic} onChange={checked => {
|
||||
this.updateOrganizationField('isProfilePublic', checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}}>
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} :
|
||||
|
@ -39,6 +39,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
tags: [],
|
||||
masterPassword: "",
|
||||
enableSoftDeletion: false,
|
||||
isProfilePublic: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,7 +148,7 @@ class ProductBuyPage extends React.Component {
|
||||
|
||||
return (
|
||||
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={
|
||||
<img style={{marginRight: "10px"}} width={36} height={36} src={Provider.getProviderLogo(provider)} alt={provider.displayName} />
|
||||
<img style={{marginRight: "10px"}} width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
} size={"large"} >
|
||||
{
|
||||
text
|
||||
|
@ -212,6 +212,12 @@ class ProviderEditPage extends React.Component {
|
||||
if (value === "Local File System") {
|
||||
this.updateProviderField('domain', Setting.getFullServerUrl());
|
||||
}
|
||||
if (value === "Custom") {
|
||||
this.updateProviderField('customAuthUrl', 'https://door.casdoor.com/login/oauth/authorize');
|
||||
this.updateProviderField('customScope', 'openid profile email');
|
||||
this.updateProviderField('customTokenUrl', 'https://door.casdoor.com/api/login/oauth/access_token');
|
||||
this.updateProviderField('customUserInfoUrl', 'https://door.casdoor.com/api/userinfo');
|
||||
}
|
||||
})}>
|
||||
{
|
||||
Setting.getProviderTypeOptions(this.state.provider.category).map((providerType, index) => <Option key={index} value={providerType.id}>{providerType.name}</Option>)
|
||||
@ -256,6 +262,79 @@ class ProviderEditPage extends React.Component {
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.provider.type !== "Custom" ? null : (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Auth URL"), i18next.t("provider:Auth URL - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.customAuthUrl} onChange={e => {
|
||||
this.updateProviderField('customAuthUrl', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.customScope} onChange={e => {
|
||||
this.updateProviderField('customScope', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Token URL"), i18next.t("provider:Token URL - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.customTokenUrl} onChange={e => {
|
||||
this.updateProviderField('customTokenUrl', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:UserInfo URL"), i18next.t("provider:UserInfo URL - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.customUserInfoUrl} onChange={e => {
|
||||
this.updateProviderField('customUserInfoUrl', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel( i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={23} >
|
||||
<Input prefix={<LinkOutlined/>} value={this.state.provider.customLogo} onChange={e => {
|
||||
this.updateProviderField('customLogo', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Preview")}:
|
||||
</Col>
|
||||
<Col span={23} >
|
||||
<a target="_blank" rel="noreferrer" href={this.state.provider.customLogo}>
|
||||
<img src={this.state.provider.customLogo} alt={this.state.provider.customLogo} height={90} style={{marginBottom: '20px'}}/>
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{this.getClientIdLabel()}
|
||||
|
@ -32,6 +32,79 @@ export const StaticBaseUrl = "https://cdn.casbin.org";
|
||||
// https://catamphetamine.gitlab.io/country-flag-icons/3x2/index.html
|
||||
export const CountryRegionData = getCountryRegionData();
|
||||
|
||||
export const OtherProviderInfo = {
|
||||
SMS: {
|
||||
"Aliyun SMS": {
|
||||
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/sms",
|
||||
},
|
||||
"Tencent Cloud SMS": {
|
||||
logo: `${StaticBaseUrl}/img/social_tencent_cloud.jpg`,
|
||||
url: "https://cloud.tencent.com/product/sms",
|
||||
},
|
||||
"Volc Engine SMS": {
|
||||
logo: `${StaticBaseUrl}/img/social_volc_engine.jpg`,
|
||||
url: "https://www.volcengine.com/products/cloud-sms",
|
||||
},
|
||||
"Huawei Cloud SMS": {
|
||||
logo: `${StaticBaseUrl}/img/social_huawei.png`,
|
||||
url: "https://www.huaweicloud.com/product/msgsms.html",
|
||||
},
|
||||
},
|
||||
Email: {
|
||||
"Default": {
|
||||
logo: `${StaticBaseUrl}/img/social_default.png`,
|
||||
url: "",
|
||||
},
|
||||
},
|
||||
Storage: {
|
||||
"Local File System": {
|
||||
logo: `${StaticBaseUrl}/img/social_file.png`,
|
||||
url: "",
|
||||
},
|
||||
"AWS S3": {
|
||||
logo: `${StaticBaseUrl}/img/social_aws.png`,
|
||||
url: "https://aws.amazon.com/s3",
|
||||
},
|
||||
"Aliyun OSS": {
|
||||
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/oss",
|
||||
},
|
||||
"Tencent Cloud COS": {
|
||||
logo: `${StaticBaseUrl}/img/social_tencent_cloud.jpg`,
|
||||
url: "https://cloud.tencent.com/product/cos",
|
||||
},
|
||||
},
|
||||
SAML: {
|
||||
"Aliyun IDaaS": {
|
||||
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/idaas"
|
||||
},
|
||||
"Keycloak": {
|
||||
logo: `${StaticBaseUrl}/img/social_keycloak.png`,
|
||||
url: "https://www.keycloak.org/"
|
||||
},
|
||||
},
|
||||
Payment: {
|
||||
"Alipay": {
|
||||
logo: `${StaticBaseUrl}/img/payment_alipay.png`,
|
||||
url: "https://www.alipay.com/"
|
||||
},
|
||||
"WeChat Pay": {
|
||||
logo: `${StaticBaseUrl}/img/payment_wechat_pay.png`,
|
||||
url: "https://pay.weixin.qq.com/"
|
||||
},
|
||||
"PayPal": {
|
||||
logo: `${StaticBaseUrl}/img/payment_paypal.png`,
|
||||
url: "https://www.paypal.com/"
|
||||
},
|
||||
"GC": {
|
||||
logo: `${StaticBaseUrl}/img/payment_gc.png`,
|
||||
url: "https://gc.org"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getCountryRegionData() {
|
||||
let language = i18next.language;
|
||||
if (language === null || language === "null") {
|
||||
@ -76,6 +149,10 @@ export function isProviderVisible(providerItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (providerItem.provider.type === "WeChatMiniProgram"){
|
||||
return false
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -376,9 +453,20 @@ export function getClickable(text) {
|
||||
)
|
||||
}
|
||||
|
||||
export function getProviderLogoURL(provider) {
|
||||
if (provider.category === "OAuth") {
|
||||
if (provider.type === "Custom") {
|
||||
return provider.customLogo;
|
||||
}
|
||||
return `${StaticBaseUrl}/img/social_${provider.type.toLowerCase()}.png`;
|
||||
} else {
|
||||
return OtherProviderInfo[provider.category][provider.type].logo;
|
||||
}
|
||||
}
|
||||
|
||||
export function getProviderLogo(provider) {
|
||||
const idp = provider.type.toLowerCase().trim().split(' ')[0];
|
||||
const url = `${StaticBaseUrl}/img/social_${idp}.png`;
|
||||
const url = getProviderLogoURL(provider);
|
||||
return (
|
||||
<img width={30} height={30} src={url} alt={idp} />
|
||||
)
|
||||
@ -392,6 +480,7 @@ export function getProviderTypeOptions(category) {
|
||||
{id: 'GitHub', name: 'GitHub'},
|
||||
{id: 'QQ', name: 'QQ'},
|
||||
{id: 'WeChat', name: 'WeChat'},
|
||||
{id: 'WeChatMiniProgram', name: 'WeChat Mini Program'},
|
||||
{id: 'Facebook', name: 'Facebook'},
|
||||
{id: 'DingTalk', name: 'DingTalk'},
|
||||
{id: 'Weibo', name: 'Weibo'},
|
||||
@ -409,6 +498,7 @@ export function getProviderTypeOptions(category) {
|
||||
{id: 'AzureAD', name: 'AzureAD'},
|
||||
{id: 'Slack', name: 'Slack'},
|
||||
{id: 'Steam', name: 'Steam'},
|
||||
{id: 'Custom', name: 'Custom'},
|
||||
]
|
||||
);
|
||||
} else if (category === "Email") {
|
||||
@ -654,3 +744,94 @@ export function getFromLink() {
|
||||
}
|
||||
return from;
|
||||
}
|
||||
|
||||
export function getSyncerTableColumns(syncer) {
|
||||
switch (syncer.type) {
|
||||
case "Keycloak":
|
||||
return [
|
||||
{
|
||||
"name":"ID",
|
||||
"type":"string",
|
||||
"casdoorName":"Id",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"USERNAME",
|
||||
"type":"string",
|
||||
"casdoorName":"Name",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"USERNAME",
|
||||
"type":"string",
|
||||
"casdoorName":"DisplayName",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"EMAIL",
|
||||
"type":"string",
|
||||
"casdoorName":"Email",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"EMAIL_VERIFIED",
|
||||
"type":"boolean",
|
||||
"casdoorName":"EmailVerified",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"FIRST_NAME",
|
||||
"type":"string",
|
||||
"casdoorName":"FirstName",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"LAST_NAME",
|
||||
"type":"string",
|
||||
"casdoorName":"LastName",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"CREATED_TIMESTAMP",
|
||||
"type":"string",
|
||||
"casdoorName":"CreatedTime",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"ENABLED",
|
||||
"type":"boolean",
|
||||
"casdoorName":"IsForbidden",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
@ -117,9 +117,13 @@ class SyncerEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: '100%'}} value={this.state.syncer.type} onChange={(value => {this.updateSyncerField('type', value);})}>
|
||||
<Select virtual={false} style={{width: '100%'}} value={this.state.syncer.type} onChange={(value => {
|
||||
this.updateSyncerField('type', value);
|
||||
this.state.syncer["tableColumns"] = Setting.getSyncerTableColumns(this.state.syncer);
|
||||
this.state.syncer.table = value === "Keycloak" ? "user_entity" : this.state.syncer.table;
|
||||
})}>
|
||||
{
|
||||
['Database', 'LDAP']
|
||||
['Database', 'LDAP', 'Keycloak']
|
||||
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
@ -198,7 +202,8 @@ class SyncerEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.syncer.table} onChange={e => {
|
||||
<Input value={this.state.syncer.table}
|
||||
disabled={this.state.syncer.type === "Keycloak"} onChange={e => {
|
||||
this.updateSyncerField('table', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
|
@ -73,6 +73,20 @@ class SyncerListPage extends BaseListPage {
|
||||
});
|
||||
}
|
||||
|
||||
runSyncer(i) {
|
||||
this.setState({loading: true});
|
||||
SyncerBackend.runSyncer("admin", this.state.data[i].name)
|
||||
.then((res) => {
|
||||
this.setState({loading: false});
|
||||
Setting.showMessage("success", `Syncer sync users successfully`);
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
this.setState({loading: false});
|
||||
Setting.showMessage("error", `Syncer failed to sync users: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(syncers) {
|
||||
const columns = [
|
||||
{
|
||||
@ -205,12 +219,13 @@ class SyncerListPage extends BaseListPage {
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: '',
|
||||
key: 'op',
|
||||
width: '170px',
|
||||
width: '240px',
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/syncers/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.runSyncer(index)}>{i18next.t("general:Sync")}</Button>
|
||||
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} onClick={() => this.props.history.push(`/syncers/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Popconfirm
|
||||
title={`Sure to delete syncer: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteSyncer(index)}
|
||||
|
@ -98,9 +98,9 @@ class SyncerTableColumnTable extends React.Component {
|
||||
return (
|
||||
<Select virtual={false} style={{width: '100%'}} value={text} onChange={(value => {this.updateField(table, index, 'casdoorName', value);})}>
|
||||
{
|
||||
['Name', 'CreatedTime', 'UpdatedTime', 'Id', 'Type', 'Password', 'PasswordSalt', 'DisplayName', 'Avatar', 'PermanentAvatar', 'Email', 'Phone',
|
||||
'Location', 'Address', 'Affiliation', 'Title', 'IdCardType', 'IdCard', 'Homepage', 'Bio', 'Tag', 'Region', 'Language', 'Gender', 'Birthday',
|
||||
'Education', 'Score', 'Ranking', 'IsDefaultAvatar', 'IsOnline', 'IsAdmin', 'IsGlobalAdmin', 'IsForbidden', 'IsDeleted', 'CreatedIp']
|
||||
['Name', 'CreatedTime', 'UpdatedTime', 'Id', 'Type', 'Password', 'PasswordSalt', 'DisplayName', 'FirstName', 'LastName', 'Avatar', 'PermanentAvatar',
|
||||
'Email', 'EmailVerified', 'Phone', 'Location', 'Address', 'Affiliation', 'Title', 'IdCardType', 'IdCard', 'Homepage', 'Bio', 'Tag', 'Region',
|
||||
'Language', 'Gender', 'Birthday', 'Education', 'Score', 'Ranking', 'IsDefaultAvatar', 'IsOnline', 'IsAdmin', 'IsGlobalAdmin', 'IsForbidden', 'IsDeleted', 'CreatedIp']
|
||||
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
|
@ -135,7 +135,7 @@ class TokenEditPage extends React.Component {
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.token.expiresIn} onChange={e => {
|
||||
this.updateTokenField('expiresIn', e.target.value);
|
||||
this.updateTokenField('expiresIn', parseInt(e.target.value));
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd';
|
||||
import {Button, Card, Col, Input, Result, Row, Select, Spin, Switch} from 'antd';
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
@ -47,6 +47,7 @@ class UserEditPage extends React.Component {
|
||||
organizations: [],
|
||||
applications: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
loading: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -59,9 +60,14 @@ class UserEditPage extends React.Component {
|
||||
|
||||
getUser() {
|
||||
UserBackend.getUser(this.state.organizationName, this.state.userName)
|
||||
.then((user) => {
|
||||
.then((data) => {
|
||||
if (data.status === null || data.status !== "error") {
|
||||
this.setState({
|
||||
user: data,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
user: user,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -423,6 +429,8 @@ class UserEditPage extends React.Component {
|
||||
)
|
||||
}
|
||||
</Card>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@ -469,13 +477,24 @@ class UserEditPage extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.user !== null ? this.renderUser() : null
|
||||
this.state.loading ? <Spin loading={this.state.loading} size="large" /> : (
|
||||
this.state.user !== null ? this.renderUser() :
|
||||
<Result
|
||||
status="404"
|
||||
title="404 NOT FOUND"
|
||||
subTitle={i18next.t("general:Sorry, the user you visited does not exist or you are not authorized to access this user.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.user === null ? null :
|
||||
<div style={{marginTop: '20px', marginLeft: '40px'}}>
|
||||
<Button size="large" onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
}
|
||||
<div style={{marginTop: '20px', marginLeft: '40px'}}>
|
||||
<Button size="large" onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -49,6 +49,10 @@ class AuthCallback extends React.Component {
|
||||
const realRedirectUri = innerParams.get("redirect_uri");
|
||||
// Casdoor's own login page, so "code" is not necessary
|
||||
if (realRedirectUri === null) {
|
||||
const samlRequest = innerParams.get("SAMLRequest");
|
||||
if (samlRequest !== null && samlRequest !== undefined && samlRequest !== "") {
|
||||
return "saml"
|
||||
}
|
||||
return "login";
|
||||
}
|
||||
|
||||
@ -92,6 +96,7 @@ class AuthCallback extends React.Component {
|
||||
const applicationName = innerParams.get("application");
|
||||
const providerName = innerParams.get("provider");
|
||||
const method = innerParams.get("method");
|
||||
const samlRequest = innerParams.get("SAMLRequest");
|
||||
|
||||
let redirectUri = `${window.location.origin}/callback`;
|
||||
|
||||
@ -100,6 +105,7 @@ class AuthCallback extends React.Component {
|
||||
application: applicationName,
|
||||
provider: providerName,
|
||||
code: code,
|
||||
samlRequest: samlRequest,
|
||||
// state: innerParams.get("state"),
|
||||
state: applicationName,
|
||||
redirectUri: redirectUri,
|
||||
@ -127,6 +133,10 @@ class AuthCallback extends React.Component {
|
||||
} else if (responseType === "link") {
|
||||
const from = innerParams.get("from");
|
||||
Setting.goToLinkSoft(this, from);
|
||||
} else if (responseType === "saml") {
|
||||
const SAMLResponse = res.data;
|
||||
const redirectUri = res.data2;
|
||||
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
|
@ -53,6 +53,7 @@ class LoginPage extends React.Component {
|
||||
classes: props,
|
||||
type: props.type,
|
||||
applicationName: props.applicationName !== undefined ? props.applicationName : (props.match === undefined ? null : props.match.params.applicationName),
|
||||
owner : props.owner !== undefined ? props.owner : (props.match === undefined ? null : props.match.params.owner),
|
||||
application: null,
|
||||
mode: props.mode !== undefined ? props.mode : (props.match === undefined ? null : props.match.params.mode), // "signup" or "signin"
|
||||
isCodeSignin: false,
|
||||
@ -61,7 +62,6 @@ class LoginPage extends React.Component {
|
||||
validEmailOrPhone: false,
|
||||
validEmail: false,
|
||||
validPhone: false,
|
||||
owner: null,
|
||||
};
|
||||
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
|
||||
this.state.owner = props.match?.params.owner
|
||||
@ -74,6 +74,8 @@ class LoginPage extends React.Component {
|
||||
this.getApplication();
|
||||
} else if (this.state.type === "code") {
|
||||
this.getApplicationLogin();
|
||||
} else if (this.state.type === "saml"){
|
||||
this.getSamlApplication();
|
||||
} else {
|
||||
Util.showMessage("error", `Unknown authentication type: ${this.state.type}`);
|
||||
}
|
||||
@ -110,6 +112,19 @@ class LoginPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getSamlApplication(){
|
||||
if (this.state.applicationName === null){
|
||||
return;
|
||||
}
|
||||
ApplicationBackend.getApplication(this.state.owner, this.state.applicationName)
|
||||
.then((application) => {
|
||||
this.setState({
|
||||
application: application,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getApplicationObj() {
|
||||
if (this.props.application !== undefined) {
|
||||
return this.props.application;
|
||||
@ -157,7 +172,16 @@ class LoginPage extends React.Component {
|
||||
values["type"] = this.state.type;
|
||||
}
|
||||
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
|
||||
|
||||
if (oAuthParams !== null){
|
||||
values["samlRequest"] = oAuthParams.samlRequest;
|
||||
}
|
||||
|
||||
|
||||
if (values["samlRequest"] != null && values["samlRequest"] !== "") {
|
||||
values["type"] = "saml";
|
||||
}
|
||||
|
||||
AuthBackend.login(values, oAuthParams)
|
||||
.then((res) => {
|
||||
if (res.status === 'ok') {
|
||||
@ -198,6 +222,10 @@ class LoginPage extends React.Component {
|
||||
} else if (responseType === "token" || responseType === "id_token") {
|
||||
const accessToken = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}#${responseType}=${accessToken}?state=${oAuthParams.state}&token_type=bearer`);
|
||||
} else if (responseType === "saml") {
|
||||
const SAMLResponse = res.data;
|
||||
const redirectUri = res.data2;
|
||||
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||
}
|
||||
} else {
|
||||
Util.showMessage("error", `Failed to log in: ${res.msg}`);
|
||||
@ -277,13 +305,13 @@ class LoginPage extends React.Component {
|
||||
if (provider.category === "OAuth") {
|
||||
return (
|
||||
<a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "signup")}>
|
||||
<img width={width} height={width} src={Provider.getProviderLogo(provider)} alt={provider.displayName} style={{margin: margin}} />
|
||||
<img width={width} height={width} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
|
||||
</a>
|
||||
)
|
||||
} else if (provider.category === "SAML") {
|
||||
return (
|
||||
<a key={provider.displayName} onClick={this.getSamlUrl.bind(this, provider)}>
|
||||
<img width={width} height={width} src={Provider.getProviderLogo(provider)} alt={provider.displayName} style={{margin: margin}} />
|
||||
<img width={width} height={width} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
@ -36,6 +36,9 @@ const authInfo = {
|
||||
mpScope: "snsapi_userinfo",
|
||||
mpEndpoint: "https://open.weixin.qq.com/connect/oauth2/authorize"
|
||||
},
|
||||
WeChatMiniProgram: {
|
||||
endpoint: "https://mp.weixin.qq.com/",
|
||||
},
|
||||
Facebook: {
|
||||
scope: "email,public_profile",
|
||||
endpoint: "https://www.facebook.com/dialog/oauth",
|
||||
@ -104,89 +107,11 @@ const authInfo = {
|
||||
Steam: {
|
||||
endpoint: "https://steamcommunity.com/openid/login",
|
||||
},
|
||||
};
|
||||
|
||||
const otherProviderInfo = {
|
||||
SMS: {
|
||||
"Aliyun SMS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/sms",
|
||||
},
|
||||
"Tencent Cloud SMS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_tencent_cloud.jpg`,
|
||||
url: "https://cloud.tencent.com/product/sms",
|
||||
},
|
||||
"Volc Engine SMS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_volc_engine.jpg`,
|
||||
url: "https://www.volcengine.com/products/cloud-sms",
|
||||
},
|
||||
"Huawei Cloud SMS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_huawei.png`,
|
||||
url: "https://www.huaweicloud.com/product/msgsms.html",
|
||||
},
|
||||
},
|
||||
Email: {
|
||||
"Default": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_default.png`,
|
||||
url: "",
|
||||
},
|
||||
},
|
||||
Storage: {
|
||||
"Local File System": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_file.png`,
|
||||
url: "",
|
||||
},
|
||||
"AWS S3": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_aws.png`,
|
||||
url: "https://aws.amazon.com/s3",
|
||||
},
|
||||
"Aliyun OSS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/oss",
|
||||
},
|
||||
"Tencent Cloud COS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_tencent_cloud.jpg`,
|
||||
url: "https://cloud.tencent.com/product/cos",
|
||||
},
|
||||
},
|
||||
SAML: {
|
||||
"Aliyun IDaaS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/idaas"
|
||||
},
|
||||
"Keycloak": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_keycloak.png`,
|
||||
url: "https://www.keycloak.org/"
|
||||
},
|
||||
},
|
||||
Payment: {
|
||||
"Alipay": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/payment_alipay.png`,
|
||||
url: "https://www.alipay.com/"
|
||||
},
|
||||
"WeChat Pay": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/payment_wechat_pay.png`,
|
||||
url: "https://pay.weixin.qq.com/"
|
||||
},
|
||||
"PayPal": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/payment_paypal.png`,
|
||||
url: "https://www.paypal.com/"
|
||||
},
|
||||
"GC": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/payment_gc.png`,
|
||||
url: "https://gc.org"
|
||||
},
|
||||
Custom: {
|
||||
endpoint: "https://example.com/",
|
||||
},
|
||||
};
|
||||
|
||||
export function getProviderLogo(provider) {
|
||||
if (provider.category === "OAuth") {
|
||||
return `${Setting.StaticBaseUrl}/img/social_${provider.type.toLowerCase()}.png`;
|
||||
} else {
|
||||
return otherProviderInfo[provider.category][provider.type].logo;
|
||||
}
|
||||
}
|
||||
|
||||
export function getProviderUrl(provider) {
|
||||
if (provider.category === "OAuth") {
|
||||
const endpoint = authInfo[provider.type].endpoint;
|
||||
@ -201,7 +126,7 @@ export function getProviderUrl(provider) {
|
||||
|
||||
return `${urlObj.protocol}//${host}`;
|
||||
} else {
|
||||
return otherProviderInfo[provider.category][provider.type].url;
|
||||
return Setting.OtherProviderInfo[provider.category][provider.type].url;
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,14 +140,14 @@ export function getProviderLogoWidget(provider) {
|
||||
return (
|
||||
<Tooltip title={provider.type}>
|
||||
<a target="_blank" rel="noreferrer" href={getProviderUrl(provider)}>
|
||||
<img width={36} height={36} src={getProviderLogo(provider)} alt={provider.displayName} />
|
||||
<img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Tooltip title={provider.type}>
|
||||
<img width={36} height={36} src={getProviderLogo(provider)} alt={provider.displayName} />
|
||||
<img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@ -305,5 +230,7 @@ export function getAuthUrl(application, provider, method) {
|
||||
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||
} else if (provider.type === "Steam") {
|
||||
return `${endpoint}?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=${window.location.origin}&openid.return_to=${redirectUri}?state=${state}`;
|
||||
} else if (provider.type === "Custom") {
|
||||
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.customScope}&response_type=code&state=${state}`;
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ class ResultPage extends React.Component {
|
||||
extra={[
|
||||
<Button type="primary" key="login" onClick={() => {
|
||||
let linkInStorage = sessionStorage.getItem("loginURL")
|
||||
if (linkInStorage != "") {
|
||||
if (linkInStorage !== null && linkInStorage !== "") {
|
||||
Setting.goToLink(linkInStorage)
|
||||
} else {
|
||||
Setting.goToLogin(this, application)
|
||||
|
@ -558,7 +558,7 @@ class SignupPage extends React.Component {
|
||||
{i18next.t("signup:Have account?")}
|
||||
<a onClick={() => {
|
||||
let linkInStorage = sessionStorage.getItem("loginURL")
|
||||
if(linkInStorage != ""){
|
||||
if(linkInStorage != null){
|
||||
Setting.goToLink(linkInStorage)
|
||||
}else{
|
||||
Setting.goToLogin(this, application)
|
||||
|
@ -98,11 +98,13 @@ export function getOAuthGetParameters(params) {
|
||||
const redirectUri = getRefinedValue(queries.get("redirect_uri"));
|
||||
const scope = getRefinedValue(queries.get("scope"));
|
||||
const state = getRefinedValue(queries.get("state"));
|
||||
const nonce = getRefinedValue(queries.get("nonce"))
|
||||
const challengeMethod = getRefinedValue(queries.get("code_challenge_method"))
|
||||
const codeChallenge = getRefinedValue(queries.get("code_challenge"))
|
||||
|
||||
if (clientId === undefined || clientId === null || clientId === "") {
|
||||
const nonce = getRefinedValue(queries.get("nonce"));
|
||||
const challengeMethod = getRefinedValue(queries.get("code_challenge_method"));
|
||||
const codeChallenge = getRefinedValue(queries.get("code_challenge"));
|
||||
const samlRequest = getRefinedValue(queries.get("SAMLRequest"));
|
||||
const relayState = getRefinedValue(queries.get("RelayState"));
|
||||
|
||||
if ((clientId === undefined || clientId === null || clientId === "") && (samlRequest === "" || samlRequest === undefined)) {
|
||||
// login
|
||||
return null;
|
||||
} else {
|
||||
@ -116,6 +118,8 @@ export function getOAuthGetParameters(params) {
|
||||
nonce: nonce,
|
||||
challengeMethod: challengeMethod,
|
||||
codeChallenge: codeChallenge,
|
||||
samlRequest: samlRequest,
|
||||
relayState: relayState,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -54,3 +54,10 @@ export function deleteSyncer(syncer) {
|
||||
body: JSON.stringify(newSyncer),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function runSyncer(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/run-syncer?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
@ -171,6 +171,7 @@
|
||||
"Signup application": "注册应用",
|
||||
"Signup application - Tooltip": "表示用户注册时通过哪个应用注册的",
|
||||
"Sorry, the page you visited does not exist.": "抱歉,您访问的页面不存在",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "抱歉,您访问的用户不存在或您无权访问该用户",
|
||||
"State": "状态",
|
||||
"State - Tooltip": "状态",
|
||||
"Swagger": "API文档",
|
||||
@ -252,7 +253,9 @@
|
||||
"Tags": "标签集合",
|
||||
"Tags - Tooltip": "可供用户选择的标签的集合",
|
||||
"Website URL": "网页地址",
|
||||
"Website URL - Tooltip": "网页地址"
|
||||
"Website URL - Tooltip": "网页地址",
|
||||
"Is profile public": "公开用户信息",
|
||||
"Is profile public - Tooltip": "关闭后,只有全局管理员或同组织才能访问用户信息"
|
||||
},
|
||||
"payment": {
|
||||
"Currency": "币种",
|
||||
@ -330,6 +333,8 @@
|
||||
"Agent ID - Tooltip": "Agent ID - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID - Tooltip",
|
||||
"Auth URL": "Auth URL",
|
||||
"Auth URL - Tooltip": "Auth URL - 工具提示",
|
||||
"Bucket": "存储桶",
|
||||
"Bucket - Tooltip": "Bucket名称",
|
||||
"Can not parse Metadata": "无法解析元数据",
|
||||
@ -377,6 +382,8 @@
|
||||
"Region endpoint for Internet": "地域节点 (外网)",
|
||||
"Region endpoint for Intranet": "地域节点 (内网)",
|
||||
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpoint (HTTP)",
|
||||
"Scope": "Scope",
|
||||
"Scope - Tooltip": "Scope - 工具提示",
|
||||
"SMS account": "SMS account",
|
||||
"SMS account - Tooltip": "SMS account - Tooltip",
|
||||
"SP ACS URL": "SP ACS URL",
|
||||
@ -400,8 +407,12 @@
|
||||
"Template Code - Tooltip": "模板CODE",
|
||||
"Terms of Use": "使用条款",
|
||||
"Terms of Use - Tooltip": "使用条款 - 工具提示",
|
||||
"Token URL": "Token URL",
|
||||
"Token URL - Tooltip": "Token URL - 工具提示",
|
||||
"Type": "类型",
|
||||
"Type - Tooltip": "类型",
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "UserInfo URL - 工具提示",
|
||||
"alertType": "警报类型",
|
||||
"canSignIn": "canSignIn",
|
||||
"canSignUp": "canSignUp",
|
||||
|
Reference in New Issue
Block a user