Compare commits

...

28 Commits

Author SHA1 Message Date
9877174780 fix: add independent error message in token endpoint (#662)
* fix: add independent error message in token endpoint

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: reduced use of variables

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: error messages use the same variable

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-04-14 10:22:56 +08:00
b178be9aef feat: implement proxy (#661) 2022-04-13 14:04:40 +08:00
7236cca8cf feat: implement CAS 3.0 (#659) 2022-04-11 21:11:31 +08:00
15daf5dbfe feat: add casdoor as saml idp support (#571)
* feat: add casdoor as saml idp support

Signed-off-by: 0x2a <stevesough@gmail.com>

* fix: merge code

Signed-off-by: 0x2a <stevesough@gmail.com>

* fix: modify response value

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: modify samlResponse generation method

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: generating a response using etree

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: change metadata url

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: modify front-end adaptation

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: recovering an incorrect override

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: change the samlResponse location

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: add relayState support

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-04-08 23:06:48 +08:00
0b546bba5e fix: grantTypes undefined err (#654)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-04-08 21:54:48 +08:00
938cdbccf4 fix: link type error (#653)
* fix: signin button error in signup page

* fix: type error
2022-04-08 20:01:30 +08:00
801302c6e7 feat: support user migration from Keycloak using syncer (#645)
* feat: support user migration from Keycloak using syncer

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* feat: add more Keycloak columns

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: requested changes

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-04-06 20:38:14 +08:00
91602d2b21 Enable extra pages. 2022-04-06 20:36:31 +08:00
86b3a078ef fix: sign In button in the result page has broken (#646)
* fix: sign In button in the result page has broken

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: code format

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-04-05 08:49:11 +08:00
abc15b88c8 fix: change goth version (#644)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-04-04 15:58:51 +08:00
3cf1b990be feat: support CAS with organizations and applications (#621) 2022-04-04 00:09:04 +08:00
2023795f3c fix: token endpoint supports json format (#641)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-04-03 21:32:00 +08:00
8d13bf7e27 feat: add Alipay support as idp (#638)
* feat: add alipay support as idp

* fix: rename a static svg icon

* fix: sort imports

* fix: no longer use pkcs8 package
2022-04-02 22:37:13 +08:00
29aa379fb2 fix: qq idp missing username (#636)
* fix: qq idp missing username

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: api uses the latest fields

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-04-01 11:46:33 +08:00
7a95b9c1d5 Init DB only when necessary. 2022-03-31 12:28:45 +08:00
0fc0ba0c76 feat: support global admin to modify the email and phone of other users (#633)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-03-30 20:27:23 +08:00
24459d852e fix: comparing hashed password with plain text password during password grant (#627)
* fix: use object.CheckPassword for password grant

* Apply suggestions from code review

fix: remove log per change request
2022-03-30 00:37:38 +08:00
e3f5bf93b2 fix: adjust the password check logic for ldap user (#597)
* fix: the password check logic for ldap user.
LDAP user should only use the ldap connection to check the password.

* fix: code format
2022-03-28 17:19:58 +08:00
879ca6a488 fix: refresh_token api return old token (#623)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-03-27 23:10:05 +08:00
544cd40a08 Disable the new syncer by default. 2022-03-27 23:06:52 +08:00
99f7883c7d Fix null bug in getCountryRegionData(). 2022-03-27 16:03:25 +08:00
88b0fb6e52 Add getPrice(). 2022-03-26 16:42:25 +08:00
fa9b49e25b fix: some idp error messages return unclear (#620)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-03-26 15:15:56 +08:00
cd76e9372e feat: delete the old token when refreshing token (#617)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-03-24 19:58:12 +08:00
04b9e05244 fix: WeComInternalIdProvider GetUserInfo method could not get the correct user id (#616) 2022-03-24 17:53:05 +08:00
a78b2de7b2 fix: panic when not select one provider (#614)
Signed-off-by: Sagilio <Sagilio@outlook.com>
2022-03-24 12:15:10 +08:00
d0952ae908 fix: docker-compose up can't work on linux (#606) 2022-03-22 18:43:02 +08:00
ade64693e4 fix: support lower go version(1.15) (#599)
* fix: support lower go version(1.15)

* fix: support lower go version(1.15)

* fix: support lower go version(1.15)
2022-03-21 21:55:16 +08:00
73 changed files with 2167 additions and 215 deletions

View File

@ -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

View File

@ -101,6 +101,8 @@ 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, *, *
`
sa := stringadapter.NewAdapter(ruleText)

11
build.sh Executable file
View 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

View File

@ -29,6 +29,8 @@ const (
ResponseTypeCode = "code"
ResponseTypeToken = "token"
ResponseTypeIdToken = "id_token"
ResponseTypeSaml = "saml"
ResponseTypeCas = "cas"
)
type RequestForm struct {
@ -60,6 +62,7 @@ type RequestForm struct {
AutoSignin bool `json:"autoSignin"`
RelayState string `json:"relayState"`
SamlRequest string `json:"samlRequest"`
SamlResponse string `json:"samlResponse"`
}

View File

@ -83,8 +83,32 @@ 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")
resp = wrapErrorResponse(nil)
if service != "" {
st, err := object.GenerateCasToken(userId, service)
if err != nil {
resp = wrapErrorResponse(err)
} else {
resp.Data = st
}
}
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
}
} else {
resp = &Response{Status: "error", Msg: fmt.Sprintf("Unknown response type: %s", form.Type)}
resp = wrapErrorResponse(fmt.Errorf("Unknown response type: %s", form.Type))
}
// if user did not check auto signin

271
controllers/cas.go Normal file
View File

@ -0,0 +1,271 @@
// 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 (
"encoding/xml"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/casdoor/casdoor/object"
)
const (
InvalidRequest string = "INVALID_REQUEST"
InvalidTicketSpec string = "INVALID_TICKET_SPEC"
UnauthorizedServiceProxy string = "UNAUTHORIZED_SERVICE_PROXY"
InvalidProxyCallback string = "INVALID_PROXY_CALLBACK"
InvalidTicket string = "INVALID_TICKET"
InvalidService string = "INVALID_SERVICE"
InteralError string = "INTERNAL_ERROR"
UnauthorizedService string = "UNAUTHORIZED_SERVICE"
)
func (c *RootController) CasValidate() {
ticket := c.Input().Get("ticket")
service := c.Input().Get("service")
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
if service == "" || ticket == "" {
c.Ctx.Output.Body([]byte("no\n"))
return
}
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)))
return
}
}
//token not found
c.Ctx.Output.Body([]byte("no\n"))
}
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")
pgtUrl := c.Input().Get("pgtUrl")
serviceResponse := object.CasServiceResponse{
Xmlns: "http://www.yale.edu/tp/cas",
}
//check whether all required parameters are met
if service == "" || ticket == "" {
c.sendCasAuthenticationResponseErr(InvalidRequest, "service and ticket must exist", format)
return
}
ok, response, issuedService, userId := object.GetCasTokenByTicket(ticket)
//find the token
if ok {
//check whether service is the one for which we previously issued token
if strings.HasPrefix(service, issuedService) {
serviceResponse.Success = response
} else {
//service not match
c.sendCasAuthenticationResponseErr(InvalidService, fmt.Sprintf("service %s and %s does not match", service, issuedService), format)
return
}
} else {
//token not found
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
return
}
if pgtUrl != "" && serviceResponse.Failure == nil {
//that means we are in proxy web flow
pgt := object.StoreCasTokenForPgt(serviceResponse.Success, service, userId)
pgtiou := serviceResponse.Success.ProxyGrantingTicket
//todo: check whether it is https
pgtUrlObj, err := url.Parse(pgtUrl)
if pgtUrlObj.Scheme != "https" {
c.sendCasAuthenticationResponseErr(InvalidProxyCallback, "callback is not https", format)
return
}
//make a request to pgturl passing pgt and pgtiou
if err != nil {
c.sendCasAuthenticationResponseErr(InteralError, err.Error(), format)
return
}
param := pgtUrlObj.Query()
param.Add("pgtId", pgt)
param.Add("pgtIou", pgtiou)
pgtUrlObj.RawQuery = param.Encode()
request, err := http.NewRequest("GET", pgtUrlObj.String(), nil)
if err != nil {
c.sendCasAuthenticationResponseErr(InteralError, err.Error(), format)
return
}
resp, err := http.DefaultClient.Do(request)
if err != nil || !(resp.StatusCode >= 200 && resp.StatusCode < 400) {
//failed to send request
c.sendCasAuthenticationResponseErr(InvalidProxyCallback, err.Error(), format)
return
}
}
// everything is ok, send the response
if format == "json" {
c.Data["json"] = serviceResponse
c.ServeJSON()
} else {
c.Data["xml"] = serviceResponse
c.ServeXML()
}
}
func (c *RootController) CasProxy() {
pgt := c.Input().Get("pgt")
targetService := c.Input().Get("targetService")
format := c.Input().Get("format")
if pgt == "" || targetService == "" {
c.sendCasProxyResponseErr(InvalidRequest, "pgt and targetService must exist", format)
return
}
ok, authenticationSuccess, issuedService, userId := object.GetCasTokenByPgt(pgt)
if !ok {
c.sendCasProxyResponseErr(UnauthorizedService, "service not authorized", format)
return
}
newAuthenticationSuccess := authenticationSuccess.DeepCopy()
if newAuthenticationSuccess.Proxies == nil {
newAuthenticationSuccess.Proxies = &object.CasProxies{}
}
newAuthenticationSuccess.Proxies.Proxies = append(newAuthenticationSuccess.Proxies.Proxies, issuedService)
proxyTicket := object.StoreCasTokenForProxyTicket(&newAuthenticationSuccess, targetService, userId)
serviceResponse := object.CasServiceResponse{
Xmlns: "http://www.yale.edu/tp/cas",
ProxySuccess: &object.CasProxySuccess{
ProxyTicket: proxyTicket,
},
}
if format == "json" {
c.Data["json"] = serviceResponse
c.ServeJSON()
} else {
c.Data["xml"] = serviceResponse
c.ServeXML()
}
}
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",
ProxyFailure: &object.CasProxyFailure{
Code: code,
Message: msg,
},
}
if format == "json" {
c.Data["json"] = serviceResponse
c.ServeJSON()
} else {
c.Data["xml"] = serviceResponse
c.ServeXML()
}
}
func (c *RootController) sendCasAuthenticationResponseErr(code, msg, format string) {
serviceResponse := object.CasServiceResponse{
Xmlns: "http://www.yale.edu/tp/cas",
Failure: &object.CasAuthenticationFailure{
Code: code,
Message: msg,
},
}
if format == "json" {
c.Data["json"] = serviceResponse
c.ServeJSON()
} else {
c.Data["xml"] = serviceResponse
c.ServeXML()
}
}

33
controllers/saml.go Normal file
View 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()
}

View File

@ -179,6 +179,20 @@ func (c *ApiController) GetOAuthToken() {
if clientId == "" && clientSecret == "" {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
}
if clientId == "" {
// If clientID is empty, try to read data from RequestBody
var tokenRequest TokenRequest
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest); err == nil {
clientId = tokenRequest.ClientId
clientSecret = tokenRequest.ClientSecret
grantType = tokenRequest.GrantType
code = tokenRequest.Code
verifier = tokenRequest.Verifier
scope = tokenRequest.Scope
username = tokenRequest.Username
password = tokenRequest.Password
}
}
host := c.Ctx.Request.Host
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host)
@ -204,6 +218,18 @@ func (c *ApiController) RefreshToken() {
clientSecret := c.Input().Get("client_secret")
host := c.Ctx.Request.Host
if clientId == "" {
// If clientID is empty, try to read data from RequestBody
var tokenRequest TokenRequest
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest); err == nil {
clientId = tokenRequest.ClientId
clientSecret = tokenRequest.ClientSecret
grantType = tokenRequest.GrantType
scope = tokenRequest.Scope
}
}
c.Data["json"] = object.RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
c.ServeJSON()
}

26
controllers/types.go Normal file
View File

@ -0,0 +1,26 @@
// 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
type TokenRequest struct {
GrantType string `json:"grant_type"`
Code string `json:"code"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Verifier string `json:"code_verifier"`
Scope string `json:"scope"`
Username string `json:"username"`
Password string `json:"password"`
}

View File

@ -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
}

39
cred/pbkdf2-salt.go Normal file
View 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)
}

View File

@ -11,6 +11,8 @@ services:
- db
environment:
RUNNING_IN_DOCKER: "true"
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./conf:/conf/
db:

10
go.mod
View File

@ -3,29 +3,33 @@ 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
github.com/casdoor/goth v1.69.0-FIX1
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
github.com/go-ldap/ldap/v3 v3.3.0
github.com/go-pay/gopay v1.5.72
github.com/go-sql-driver/mysql v1.5.0
github.com/golang-jwt/jwt/v4 v4.1.0
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/markbates/goth v1.68.1-0.20211006204042-9dc8905b41c8
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 // indirect
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

20
go.sum
View File

@ -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=
@ -79,10 +81,10 @@ github.com/casbin/casbin/v2 v2.30.1 h1:P5HWadDL7olwUXNdcuKUBk+x75Y2eitFxYTcLNKeK
github.com/casbin/casbin/v2 v2.30.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/xorm-adapter/v2 v2.5.1 h1:BkpIxRHKa0s3bSMx173PpuU7oTs+Zw7XmD0BIta0HGM=
github.com/casbin/xorm-adapter/v2 v2.5.1/go.mod h1:AeH4dBKHC9/zYxzdPVHhPDzF8LYLqjDdb767CWJoV54=
github.com/casdoor/go-sms-sender v0.0.5 h1:9qhlMM+UoSOvvY7puUULqSHBBA7fbe02Px/tzchQboo=
github.com/casdoor/go-sms-sender v0.0.5/go.mod h1:TMM/BsZQAa+7JVDXl2KqgxnzZgCjmHEX5MBN662mM5M=
github.com/casdoor/go-sms-sender v0.2.0 h1:52bin4EBOPzOee64s9UK7jxd22FODvT9/+Y/Z+PSHpg=
github.com/casdoor/go-sms-sender v0.2.0/go.mod h1:fsZsNnALvFIo+HFcE1U/oCQv4ZT42FdglXKMsEm3WSk=
github.com/casdoor/goth v1.69.0-FIX1 h1:24Y3tfaJxWGJbxickGe3F9y2c8X1PgsQynhxGXV1f9Q=
github.com/casdoor/goth v1.69.0-FIX1/go.mod h1:Om55nRo8CkeDkPSNBbzXW4G5uI28ZUkSk5S69dPek3s=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@ -135,10 +137,8 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -237,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=
@ -256,10 +258,10 @@ 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/markbates/goth v1.68.1-0.20211006204042-9dc8905b41c8 h1:JibQrkJapVsb0pweJ5T14jZuuYZZTjll0PZBw4XfSCI=
github.com/markbates/goth v1.68.1-0.20211006204042-9dc8905b41c8/go.mod h1:V2VcDMzDiMHW+YmqYl7i0cMiAUeCkAe4QE6jRKBhXZw=
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911 h1:erppMjjp69Rertg1zlgRbLJH1u+eCmRPxKjMZ5I8/Ro=
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -278,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=

View File

@ -19,7 +19,7 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"time"
@ -88,7 +88,7 @@ func (idp *AdfsIdProvider) GetToken(code string) (*oauth2.Token, error) {
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

292
idp/alipay.go Normal file
View File

@ -0,0 +1,292 @@
// 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 (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"io"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strings"
"time"
"golang.org/x/oauth2"
)
type AlipayIdProvider struct {
Client *http.Client
Config *oauth2.Config
}
// NewAlipayIdProvider ...
func NewAlipayIdProvider(clientId string, clientSecret string, redirectUrl string) *AlipayIdProvider {
idp := &AlipayIdProvider{}
config := idp.getConfig(clientId, clientSecret, redirectUrl)
idp.Config = config
return idp
}
// SetHttpClient ...
func (idp *AlipayIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow
func (idp *AlipayIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
var endpoint = oauth2.Endpoint{
AuthURL: "https://openauth.alipay.com/oauth2/publicAppAuthorize.htm",
TokenURL: "https://openapi.alipay.com/gateway.do",
}
var config = &oauth2.Config{
Scopes: []string{"", ""},
Endpoint: endpoint,
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
}
return config
}
type AlipayAccessToken struct {
Response AlipaySystemOauthTokenResponse `json:"alipay_system_oauth_token_response"`
Sign string `json:"sign"`
}
type AlipaySystemOauthTokenResponse struct {
AccessToken string `json:"access_token"`
AlipayUserId string `json:"alipay_user_id"`
ExpiresIn int `json:"expires_in"`
ReExpiresIn int `json:"re_expires_in"`
RefreshToken string `json:"refresh_token"`
UserId string `json:"user_id"`
}
// GetToken use code to get access_token
func (idp *AlipayIdProvider) GetToken(code string) (*oauth2.Token, error) {
pTokenParams := &struct {
ClientId string `json:"app_id"`
CharSet string `json:"charset"`
Code string `json:"code"`
GrantType string `json:"grant_type"`
Method string `json:"method"`
SignType string `json:"sign_type"`
TimeStamp string `json:"timestamp"`
Version string `json:"version"`
}{idp.Config.ClientID, "utf-8", code, "authorization_code", "alipay.system.oauth.token", "RSA2", time.Now().Format("2006-01-02 15:04:05"), "1.0"}
data, err := idp.postWithBody(pTokenParams, idp.Config.Endpoint.TokenURL)
if err != nil {
return nil, err
}
pToken := &AlipayAccessToken{}
err = json.Unmarshal(data, pToken)
if err != nil {
return nil, err
}
token := &oauth2.Token{
AccessToken: pToken.Response.AccessToken,
Expiry: time.Unix(time.Now().Unix()+int64(pToken.Response.ExpiresIn), 0),
}
return token, nil
}
/*
{
"alipay_user_info_share_response":{
"code":"10000",
"msg":"Success",
"avatar":"https:\/\/tfs.alipayobjects.com\/images\/partner\/T1.QxFXk4aXXXXXXXX",
"nick_name":"zhangsan",
"user_id":"2099222233334444"
},
"sign":"m8rWJeqfoa5tDQRRVnPhRHcpX7NZEgjIPTPF1QBxos6XXXXXXXXXXXXXXXXXXXXXXXXXX"
}
*/
type AlipayUserResponse struct {
AlipayUserInfoShareResponse AlipayUserInfoShareResponse `json:"alipay_user_info_share_response"`
Sign string `json:"sign"`
}
type AlipayUserInfoShareResponse struct {
Code string `json:"code"`
Msg string `json:"msg"`
Avatar string `json:"avatar"`
NickName string `json:"nick_name"`
UserId string `json:"user_id"`
}
// GetUserInfo Use access_token to get UserInfo
func (idp *AlipayIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
atUserInfo := &AlipayUserResponse{}
accessToken := token.AccessToken
pTokenParams := &struct {
ClientId string `json:"app_id"`
CharSet string `json:"charset"`
AuthToken string `json:"auth_token"`
Method string `json:"method"`
SignType string `json:"sign_type"`
TimeStamp string `json:"timestamp"`
Version string `json:"version"`
}{idp.Config.ClientID, "utf-8", accessToken, "alipay.user.info.share", "RSA2", time.Now().Format("2006-01-02 15:04:05"), "1.0"}
data, err := idp.postWithBody(pTokenParams, idp.Config.Endpoint.TokenURL)
if err != nil {
return nil, err
}
err = json.Unmarshal(data, atUserInfo)
if err != nil {
return nil, err
}
userInfo := UserInfo{
Id: atUserInfo.AlipayUserInfoShareResponse.UserId,
Username: atUserInfo.AlipayUserInfoShareResponse.NickName,
DisplayName: atUserInfo.AlipayUserInfoShareResponse.NickName,
AvatarUrl: atUserInfo.AlipayUserInfoShareResponse.Avatar,
}
return &userInfo, nil
}
func (idp *AlipayIdProvider) postWithBody(body interface{}, targetUrl string) ([]byte, error) {
bs, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyJson := make(map[string]interface{})
err = json.Unmarshal(bs, &bodyJson)
if err != nil {
return nil, err
}
formData := url.Values{}
for k := range bodyJson {
formData.Set(k, bodyJson[k].(string))
}
sign, err := rsaSignWithRSA256(getStringToSign(formData), idp.Config.ClientSecret)
if err != nil {
return nil, err
}
formData.Set("sign", sign)
resp, err := idp.Client.PostForm(targetUrl, formData)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(resp.Body)
return data, nil
}
// get the string to sign, see https://opendocs.alipay.com/common/02kf5q
func getStringToSign(formData url.Values) string {
keys := make([]string, 0, len(formData))
for k := range formData {
keys = append(keys, k)
}
sort.Strings(keys)
str := ""
for _, k := range keys {
if k == "sign" || formData[k][0] == "" {
continue
} else {
str += "&" + k + "=" + formData[k][0]
}
}
str = strings.Trim(str, "&")
return str
}
// use privateKey to sign the content
func rsaSignWithRSA256(signContent string, privateKey string) (string, error) {
privateKey = formatPrivateKey(privateKey)
block, _ := pem.Decode([]byte(privateKey))
if block == nil {
panic("fail to parse privateKey")
}
h := sha256.New()
h.Write([]byte(signContent))
hashed := h.Sum(nil)
privateKeyRSA, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return "", err
}
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKeyRSA.(*rsa.PrivateKey), crypto.SHA256, hashed)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}
// privateKey in database is a string, format it to PEM style
func formatPrivateKey(privateKey string) string {
// each line length is 64
preFmtPrivateKey := ""
for i := 0; ; {
if i+64 <= len(privateKey) {
preFmtPrivateKey = preFmtPrivateKey + privateKey[i:i+64] + "\n"
i += 64
} else {
preFmtPrivateKey = preFmtPrivateKey + privateKey[i:]
break
}
}
privateKey = strings.Trim(preFmtPrivateKey, "\n")
// add pkcs#8 BEGIN and END
PemBegin := "-----BEGIN PRIVATE KEY-----\n"
PemEnd := "\n-----END PRIVATE KEY-----"
if !strings.HasPrefix(privateKey, PemBegin) {
privateKey = PemBegin + privateKey
}
if !strings.HasSuffix(privateKey, PemEnd) {
privateKey = privateKey + PemEnd
}
return privateKey
}

View File

@ -18,7 +18,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"golang.org/x/oauth2"
@ -97,7 +97,7 @@ func (idp *BaiduIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -17,7 +17,6 @@ package idp
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
@ -133,7 +132,7 @@ func (idp *CasdoorIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -18,6 +18,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
@ -143,7 +144,7 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -178,7 +179,7 @@ func (idp *DingTalkIdProvider) postWithBody(body interface{}, url string) ([]byt
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -19,6 +19,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
@ -92,7 +93,7 @@ func (idp *GiteeIdProvider) GetToken(code string) (*oauth2.Token, error) {
if err != nil {
return nil, err
}
rbs, err := io.ReadAll(resp.Body)
rbs, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -15,11 +15,13 @@
package idp
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
"golang.org/x/oauth2"
@ -60,9 +62,38 @@ func (idp *GithubIdProvider) getConfig() *oauth2.Config {
return config
}
type GithubToken struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
Error string `json:"error"`
}
func (idp *GithubIdProvider) GetToken(code string) (*oauth2.Token, error) {
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, idp.Client)
return idp.Config.Exchange(ctx, code)
params := &struct {
Code string `json:"code"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
}{code, idp.Config.ClientID, idp.Config.ClientSecret}
data, err := idp.postWithBody(params, idp.Config.Endpoint.TokenURL)
if err != nil {
return nil, err
}
pToken := &GithubToken{}
if err = json.Unmarshal(data, pToken); err != nil {
return nil, err
}
if pToken.Error != "" {
return nil, fmt.Errorf("err: %s", pToken.Error)
}
token := &oauth2.Token{
AccessToken: pToken.AccessToken,
TokenType: "Bearer",
}
return token, nil
}
//{
@ -172,7 +203,7 @@ func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -192,3 +223,30 @@ func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
}
return &userInfo, nil
}
func (idp *GithubIdProvider) postWithBody(body interface{}, url string) ([]byte, error) {
bs, err := json.Marshal(body)
if err != nil {
return nil, err
}
r := strings.NewReader(string(bs))
req, _ := http.NewRequest("POST", url, r)
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
resp, err := idp.Client.Do(req)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(resp.Body)
return data, nil
}

View File

@ -17,7 +17,7 @@ package idp
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
@ -85,7 +85,7 @@ func (idp *GitlabIdProvider) GetToken(code string) (*oauth2.Token, error) {
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -209,7 +209,7 @@ func (idp *GitlabIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -19,7 +19,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"golang.org/x/oauth2"
@ -95,7 +95,7 @@ func (idp *GoogleIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -22,35 +22,35 @@ import (
"time"
"github.com/casdoor/casdoor/util"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/amazon"
"github.com/markbates/goth/providers/apple"
"github.com/markbates/goth/providers/azuread"
"github.com/markbates/goth/providers/bitbucket"
"github.com/markbates/goth/providers/digitalocean"
"github.com/markbates/goth/providers/discord"
"github.com/markbates/goth/providers/dropbox"
"github.com/markbates/goth/providers/facebook"
"github.com/markbates/goth/providers/gitea"
"github.com/markbates/goth/providers/github"
"github.com/markbates/goth/providers/gitlab"
"github.com/markbates/goth/providers/google"
"github.com/markbates/goth/providers/heroku"
"github.com/markbates/goth/providers/instagram"
"github.com/markbates/goth/providers/kakao"
"github.com/markbates/goth/providers/line"
"github.com/markbates/goth/providers/linkedin"
"github.com/markbates/goth/providers/microsoftonline"
"github.com/markbates/goth/providers/paypal"
"github.com/markbates/goth/providers/salesforce"
"github.com/markbates/goth/providers/shopify"
"github.com/markbates/goth/providers/slack"
"github.com/markbates/goth/providers/steam"
"github.com/markbates/goth/providers/tumblr"
"github.com/markbates/goth/providers/twitter"
"github.com/markbates/goth/providers/yahoo"
"github.com/markbates/goth/providers/yandex"
"github.com/markbates/goth/providers/zoom"
"github.com/casdoor/goth"
"github.com/casdoor/goth/providers/amazon"
"github.com/casdoor/goth/providers/apple"
"github.com/casdoor/goth/providers/azuread"
"github.com/casdoor/goth/providers/bitbucket"
"github.com/casdoor/goth/providers/digitalocean"
"github.com/casdoor/goth/providers/discord"
"github.com/casdoor/goth/providers/dropbox"
"github.com/casdoor/goth/providers/facebook"
"github.com/casdoor/goth/providers/gitea"
"github.com/casdoor/goth/providers/github"
"github.com/casdoor/goth/providers/gitlab"
"github.com/casdoor/goth/providers/google"
"github.com/casdoor/goth/providers/heroku"
"github.com/casdoor/goth/providers/instagram"
"github.com/casdoor/goth/providers/kakao"
"github.com/casdoor/goth/providers/line"
"github.com/casdoor/goth/providers/linkedin"
"github.com/casdoor/goth/providers/microsoftonline"
"github.com/casdoor/goth/providers/paypal"
"github.com/casdoor/goth/providers/salesforce"
"github.com/casdoor/goth/providers/shopify"
"github.com/casdoor/goth/providers/slack"
"github.com/casdoor/goth/providers/steam"
"github.com/casdoor/goth/providers/tumblr"
"github.com/casdoor/goth/providers/twitter"
"github.com/casdoor/goth/providers/yahoo"
"github.com/casdoor/goth/providers/yandex"
"github.com/casdoor/goth/providers/zoom"
"golang.org/x/oauth2"
)
@ -231,6 +231,10 @@ func (idp *GothIdProvider) GetToken(code string) (*oauth2.Token, error) {
value.Add("code", code)
}
accessToken, err := idp.Session.Authorize(idp.Provider, value)
if err != nil {
return nil, err
}
//Get ExpiresAt's value
valueOfExpire := reflect.ValueOf(idp.Session).Elem().FieldByName("ExpiresAt")
if valueOfExpire.IsValid() {
@ -240,7 +244,8 @@ func (idp *GothIdProvider) GetToken(code string) (*oauth2.Token, error) {
AccessToken: accessToken,
Expiry: expireAt,
}
return &token, err
return &token, nil
}
func (idp *GothIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {

View File

@ -17,7 +17,7 @@ package idp
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"golang.org/x/oauth2"
@ -69,7 +69,7 @@ func (idp *InfoflowInternalIdProvider) GetToken(code string) (*oauth2.Token, err
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -147,7 +147,7 @@ func (idp *InfoflowInternalIdProvider) GetUserInfo(token *oauth2.Token) (*UserIn
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -165,7 +165,7 @@ func (idp *InfoflowInternalIdProvider) GetUserInfo(token *oauth2.Token) (*UserIn
return nil, err
}
data, err = io.ReadAll(resp.Body)
data, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -18,6 +18,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
@ -143,7 +144,7 @@ func (idp *InfoflowIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -161,7 +162,7 @@ func (idp *InfoflowIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
return nil, err
}
data, err = io.ReadAll(resp.Body)
data, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -196,7 +197,7 @@ func (idp *InfoflowIdProvider) postWithBody(body interface{}, url string) ([]byt
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -17,6 +17,7 @@ package idp
import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
@ -168,7 +169,7 @@ func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
req.Header.Set("Authorization", "Bearer "+token.AccessToken)
resp, err := idp.Client.Do(req)
data, err = io.ReadAll(resp.Body)
data, err = ioutil.ReadAll(resp.Body)
err = resp.Body.Close()
if err != nil {
return nil, err
@ -200,7 +201,7 @@ func (idp *LarkIdProvider) postWithBody(body interface{}, url string) ([]byte, e
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -18,6 +18,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
@ -84,7 +85,7 @@ func (idp *LinkedInIdProvider) GetToken(code string) (*oauth2.Token, error) {
if err != nil {
return nil, err
}
rbs, err := io.ReadAll(resp.Body)
rbs, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -322,7 +323,7 @@ func (idp *LinkedInIdProvider) GetUrlRespWithAuthorization(url, token string) ([
}
}(resp.Body)
bs, err := io.ReadAll(resp.Body)
bs, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -70,6 +70,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
return NewAdfsIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
} else if typ == "Baidu" {
return NewBaiduIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "Alipay" {
return NewAlipayIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "Infoflow" {
if subType == "Internal" {
return NewInfoflowInternalIdProvider(clientId, clientSecret, appId, redirectUrl)

View File

@ -18,7 +18,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"regexp"
@ -75,7 +75,10 @@ func (idp *QqIdProvider) GetToken(code string) (*oauth2.Token, error) {
}
defer resp.Body.Close()
tokenContent, err := io.ReadAll(resp.Body)
tokenContent, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
re := regexp.MustCompile("token=(.*?)&")
matched := re.FindAllStringSubmatch(string(tokenContent), -1)
@ -145,7 +148,10 @@ func (idp *QqIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
}
defer resp.Body.Close()
openIdBody, err := io.ReadAll(resp.Body)
openIdBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
re := regexp.MustCompile("\"openid\":\"(.*?)\"}")
matched := re.FindAllStringSubmatch(string(openIdBody), -1)
@ -161,7 +167,7 @@ func (idp *QqIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
}
defer resp.Body.Close()
userInfoBody, err := io.ReadAll(resp.Body)
userInfoBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -178,6 +184,7 @@ func (idp *QqIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
userInfo := UserInfo{
Id: openId,
Username: qqUserInfo.Nickname,
DisplayName: qqUserInfo.Nickname,
AvatarUrl: qqUserInfo.FigureurlQq1,
}

View File

@ -17,7 +17,7 @@ package idp
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"time"
@ -72,7 +72,7 @@ func (idp *WeComInternalIdProvider) GetToken(code string) (*oauth2.Token, error)
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -111,6 +111,7 @@ type WecomInternalUserInfo struct {
Email string `json:"email"`
Avatar string `json:"avatar"`
OpenId string `json:"open_userid"`
UserId string `json:"userid"`
}
func (idp *WeComInternalIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
@ -122,7 +123,7 @@ func (idp *WeComInternalIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo,
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -143,7 +144,7 @@ func (idp *WeComInternalIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo,
return nil, err
}
data, err = io.ReadAll(resp.Body)
data, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@ -156,7 +157,7 @@ func (idp *WeComInternalIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo,
return nil, fmt.Errorf("userInfoResp.errcode = %d, userInfoResp.errmsg = %s", infoResp.Errcode, infoResp.Errmsg)
}
userInfo := UserInfo{
Id: infoResp.OpenId,
Id: infoResp.UserId,
Username: infoResp.Name,
DisplayName: infoResp.Name,
Email: infoResp.Email,

View File

@ -18,6 +18,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
@ -194,7 +195,7 @@ func (idp *WeComIdProvider) postWithBody(body interface{}, url string) ([]byte,
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -19,6 +19,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
@ -91,7 +92,7 @@ func (idp *WeiBoIdProvider) GetToken(code string) (*oauth2.Token, error) {
return
}
}(resp.Body)
bs, err := io.ReadAll(resp.Body)
bs, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -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

View File

@ -180,16 +180,15 @@ func CheckUserPassword(organization string, username string, password string) (*
return nil, "the user is forbidden to sign in, please contact the administrator"
}
msg := CheckPassword(user, password)
if msg != "" {
//for ldap users
if user.Ldap != "" {
return checkLdapUserPassword(user, password)
if user.Ldap != "" {
//ONLY for ldap users
return checkLdapUserPassword(user, password)
} else {
msg := CheckPassword(user, password)
if msg != "" {
return nil, msg
}
return nil, msg
}
return user, ""
}

View File

@ -15,29 +15,25 @@
package object
import (
_ "embed"
"io/ioutil"
"github.com/casdoor/casdoor/util"
)
//go:embed token_jwt_key.pem
var tokenJwtPublicKey string
//go:embed token_jwt_key.key
var tokenJwtPrivateKey string
func InitDb() {
initBuiltInOrganization()
initBuiltInUser()
initBuiltInApplication()
initBuiltInCert()
initBuiltInLdap()
existed := initBuiltInOrganization()
if !existed {
initBuiltInUser()
initBuiltInApplication()
initBuiltInCert()
initBuiltInLdap()
}
}
func initBuiltInOrganization() {
func initBuiltInOrganization() bool {
organization := getOrganization("admin", "built-in")
if organization != nil {
return
return true
}
organization = &Organization{
@ -53,6 +49,7 @@ func initBuiltInOrganization() {
Tags: []string{},
}
AddOrganization(organization)
return false
}
func initBuiltInUser() {
@ -122,7 +119,22 @@ func initBuiltInApplication() {
AddApplication(application)
}
func readTokenFromFile() (string, string) {
pemPath := "./object/token_jwt_key.pem"
keyPath := "./object/token_jwt_key.key"
pem, err := ioutil.ReadFile(pemPath)
if err != nil {
return "", ""
}
key, err := ioutil.ReadFile(keyPath)
if err != nil {
return "", ""
}
return string(pem), string(key)
}
func initBuiltInCert() {
tokenJwtPublicKey, tokenJwtPrivateKey := readTokenFromFile()
cert := getCert("admin", "cert-built-in")
if cert != nil {
return

View File

@ -33,7 +33,7 @@ type Provider struct {
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(100)" json:"clientSecret"`
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"`

View File

@ -34,6 +34,9 @@ func (application *Application) GetProviderItem(providerName string) *ProviderIt
}
func (pi *ProviderItem) IsProviderVisible() bool {
if pi.Provider == nil {
return false
}
return pi.Provider.Category == "OAuth" || pi.Provider.Category == "SAML"
}

352
object/saml_idp.go Normal file
View 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
}

View File

@ -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)

View File

@ -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

View File

@ -58,6 +58,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 {
@ -306,23 +307,28 @@ 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 {
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) {
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,
}
}
@ -338,11 +344,13 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
}
if err != nil {
errString = err.Error()
return &TokenWrapper{
AccessToken: err.Error(),
AccessToken: errString,
TokenType: "",
ExpiresIn: 0,
Scope: "",
Error: errString,
}
}
@ -361,62 +369,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)
@ -439,14 +460,15 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
TokenType: "Bearer",
}
AddToken(newToken)
DeleteToken(&token)
tokenWrapper := &TokenWrapper{
AccessToken: token.AccessToken,
IdToken: token.AccessToken,
RefreshToken: token.RefreshToken,
TokenType: token.TokenType,
ExpiresIn: token.ExpiresIn,
Scope: token.Scope,
AccessToken: newToken.AccessToken,
IdToken: newToken.AccessToken,
RefreshToken: newToken.RefreshToken,
TokenType: newToken.TokenType,
ExpiresIn: newToken.ExpiresIn,
Scope: newToken.Scope,
}
return tokenWrapper
@ -521,7 +543,8 @@ func GetPasswordToken(application *Application, username string, password string
if user == nil {
return nil, errors.New("error: the user does not exist")
}
if user.Password != password {
msg := CheckPassword(user, password)
if msg != "" {
return nil, errors.New("error: invalid username or password")
}
if user.IsForbidden {

327
object/token_cas.go Normal file
View File

@ -0,0 +1,327 @@
// 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 (
"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 {
XMLName xml.Name `xml:"cas:serviceResponse" json:"-"`
Xmlns string `xml:"xmlns:cas,attr"`
Failure *CasAuthenticationFailure
Success *CasAuthenticationSuccess
ProxySuccess *CasProxySuccess
ProxyFailure *CasProxyFailure
}
type CasAuthenticationFailure struct {
XMLName xml.Name `xml:"cas:authenticationFailure" json:"-"`
Code string `xml:"code,attr"`
Message string `xml:",innerxml"`
}
type CasAuthenticationSuccess struct {
XMLName xml.Name `xml:"cas:authenticationSuccess" json:"-"`
User string `xml:"cas:user"`
ProxyGrantingTicket string `xml:"cas:proxyGrantingTicket,omitempty"`
Proxies *CasProxies `xml:"cas:proxies"`
Attributes *CasAttributes `xml:"cas:attributes"`
ExtraAttributes []*CasAnyAttribute `xml:",any"`
}
type CasProxies struct {
XMLName xml.Name `xml:"cas:proxies" json:"-"`
Proxies []string `xml:"cas:proxy"`
}
type CasAttributes struct {
XMLName xml.Name `xml:"cas:attributes" json:"-"`
AuthenticationDate time.Time `xml:"cas:authenticationDate"`
LongTermAuthenticationRequestTokenUsed bool `xml:"cas:longTermAuthenticationRequestTokenUsed"`
IsFromNewLogin bool `xml:"cas:isFromNewLogin"`
MemberOf []string `xml:"cas:memberOf"`
UserAttributes *CasUserAttributes
ExtraAttributes []*CasAnyAttribute `xml:",any"`
}
type CasUserAttributes struct {
XMLName xml.Name `xml:"cas:userAttributes" json:"-"`
Attributes []*CasNamedAttribute `xml:"cas:attribute"`
AnyAttributes []*CasAnyAttribute `xml:",any"`
}
type CasNamedAttribute struct {
XMLName xml.Name `xml:"cas:attribute" json:"-"`
Name string `xml:"name,attr,omitempty"`
Value string `xml:",innerxml"`
}
type CasAnyAttribute struct {
XMLName xml.Name
Value string `xml:",chardata"`
}
type CasAuthenticationSuccessWrapper struct {
AuthenticationSuccess *CasAuthenticationSuccess // the token we issued
Service string //to which service this token is issued
UserId string
}
type CasProxySuccess struct {
XMLName xml.Name `xml:"cas:proxySuccess" json:"-"`
ProxyTicket string `xml:"cas:proxyTicket"`
}
type CasProxyFailure struct {
XMLName xml.Name `xml:"cas:proxyFailure" json:"-"`
Code string `xml:"code,attr"`
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, userId string) string {
pgt := fmt.Sprintf("PGT-%s", util.GenerateId())
pgtToServiceResponse.Store(pgt, &CasAuthenticationSuccessWrapper{
AuthenticationSuccess: token,
Service: service,
UserId: userId,
})
return pgt
}
func GenerateId() {
panic("unimplemented")
}
/**
@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, responseWrapperTypeCast.UserId
}
return false, nil, "", ""
}
/**
@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, responseWrapperTypeCast.UserId
}
return false, nil, "", ""
}
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,
Attributes: &CasAttributes{
AuthenticationDate: time.Now(),
UserAttributes: &CasUserAttributes{},
},
ProxyGrantingTicket: fmt.Sprintf("PGTIOU-%s", util.GenerateId()),
}
data, _ := json.Marshal(user)
tmp := map[string]string{}
json.Unmarshal(data, &tmp)
for k, v := range tmp {
if v != "" {
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
Name: k,
Value: v,
})
}
}
st := fmt.Sprintf("ST-%d", rand.Int())
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
}
func (c *CasAuthenticationSuccess) DeepCopy() CasAuthenticationSuccess {
res := *c
//copy proxy
if c.Proxies != nil {
tmp := c.Proxies.DeepCopy()
res.Proxies = &tmp
}
if c.Attributes != nil {
tmp := c.Attributes.DeepCopy()
res.Attributes = &tmp
}
res.ExtraAttributes = make([]*CasAnyAttribute, len(c.ExtraAttributes))
for i, e := range c.ExtraAttributes {
tmp := *e
res.ExtraAttributes[i] = &tmp
}
return res
}
func (c *CasProxies) DeepCopy() CasProxies {
res := CasProxies{
Proxies: make([]string, len(c.Proxies)),
}
copy(res.Proxies, c.Proxies)
return res
}
func (c *CasAttributes) DeepCopy() CasAttributes {
res := *c
if c.MemberOf != nil {
res.MemberOf = make([]string, len(c.MemberOf))
copy(res.MemberOf, c.MemberOf)
}
tmp := c.UserAttributes.DeepCopy()
res.UserAttributes = &tmp
res.ExtraAttributes = make([]*CasAnyAttribute, len(c.ExtraAttributes))
for i, e := range c.ExtraAttributes {
tmp := *e
res.ExtraAttributes[i] = &tmp
}
return res
}
func (c *CasUserAttributes) DeepCopy() CasUserAttributes {
res := CasUserAttributes{
AnyAttributes: make([]*CasAnyAttribute, len(c.AnyAttributes)),
Attributes: make([]*CasNamedAttribute, len(c.Attributes)),
}
for i, a := range c.AnyAttributes {
var tmp = *a
res.AnyAttributes[i] = &tmp
}
for i, a := range c.Attributes {
var tmp = *a
res.Attributes[i] = &tmp
}
return res
}

View File

@ -15,7 +15,6 @@
package object
import (
_ "embed"
"fmt"
"time"

View File

@ -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"`
@ -85,6 +86,7 @@ type User struct {
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"`
@ -304,7 +306,7 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties"}
}
if isGlobalAdmin {
columns = append(columns, "name")
columns = append(columns, "name", "email", "phone")
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols(columns...).Update(user)

View File

@ -100,10 +100,17 @@ func willLog(subOwner string, subName string, method string, urlPath string, obj
return true
}
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") || strings.HasSuffix(urlPath, "/p3/serviceValidate") || strings.HasSuffix(urlPath, "/p3/proxyValidate") || strings.HasSuffix(urlPath, "/samlValidate")) {
return "/cas"
}
return urlPath
}
func AuthzFilter(ctx *context.Context) {
subOwner, subName := getSubject(ctx)
method := ctx.Request.Method
urlPath := ctx.Request.URL.Path
urlPath := getUrlPath(ctx.Request.URL.Path)
objOwner, objName := getObject(ctx)
isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName)

View File

@ -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")
@ -171,4 +172,14 @@ 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: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")
}

View File

@ -27,6 +27,9 @@ 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") || strings.HasSuffix(urlPath, "/p3/serviceValidate") || strings.HasSuffix(urlPath, "/p3/proxyValidate") || strings.HasSuffix(urlPath, "/samlValidate")) {
return
}
path := "web/build"
if urlPath == "/" {

View File

@ -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"
},

View File

@ -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:

View File

@ -20,7 +20,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"os"
"io/ioutil"
"strconv"
"strings"
"time"
@ -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
}
@ -162,7 +166,7 @@ func GetMinLenStr(strs ...string) string {
}
func ReadStringFromPath(path string) string {
data, err := os.ReadFile(path)
data, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
@ -171,7 +175,7 @@ func ReadStringFromPath(path string) string {
}
func WriteStringToPath(s string, path string) {
err := os.WriteFile(path, []byte(s), 0644)
err := ioutil.WriteFile(path, []byte(s), 0644)
if err != nil {
panic(err)
}

View File

@ -18,6 +18,22 @@ module.exports = {
'/.well-known/openid-configuration': {
target: 'http://localhost:8000',
changeOrigin: true,
},
'/cas/serviceValidate': {
target: 'http://localhost:8000',
changeOrigin: true,
},
'/cas/proxyValidate': {
target: 'http://localhost:8000',
changeOrigin: true,
},
'/cas/proxy': {
target: 'http://localhost:8000',
changeOrigin: true,
},
'/cas/validate': {
target: 'http://localhost:8000',
changeOrigin: true,
}
},
},

View File

@ -68,6 +68,7 @@ import i18next from 'i18next';
import PromptPage from "./auth/PromptPage";
import OdicDiscoveryPage from "./auth/OidcDiscoveryPage";
import SamlCallback from './auth/SamlCallback';
import CasLogout from "./auth/CasLogout";
const { Header, Footer } = Layout;
@ -642,7 +643,8 @@ class App extends Component {
window.location.pathname.startsWith("/login") ||
window.location.pathname.startsWith("/callback") ||
window.location.pathname.startsWith("/prompt") ||
window.location.pathname.startsWith("/forget");
window.location.pathname.startsWith("/forget") ||
window.location.pathname.startsWith("/cas");
}
renderPage() {
@ -654,6 +656,9 @@ 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}/>
<Route exact path="/callback/saml" component={SamlCallback}/>
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...props} />)}/>

View File

@ -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({

View File

@ -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;

View File

@ -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>

View File

@ -83,6 +83,10 @@ class ProductBuyPage extends React.Component {
}
}
getPrice(product) {
return `${this.getCurrencySymbol(product)}${product?.price} (${this.getCurrencyText(product)})`;
}
getProviders(product) {
if (this.state.providers.length === 0 || product.providers.length === 0) {
return [];
@ -207,7 +211,9 @@ class ProductBuyPage extends React.Component {
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Price")}>
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
{`${this.getCurrencySymbol(product)}${product?.price} (${this.getCurrencyText(product)})`}
{
this.getPrice(product)
}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Quantity")}><span style={{fontSize: 16}}>{product?.quantity}</span></Descriptions.Item>

View File

@ -22,6 +22,7 @@ import copy from "copy-to-clipboard";
import {authConfig} from "./auth/Auth";
import {Helmet} from "react-helmet";
import moment from "moment";
import * as Conf from "./Conf";
export let ServerUrl = "";
@ -29,12 +30,17 @@ export let ServerUrl = "";
export const StaticBaseUrl = "https://cdn.casbin.org";
// https://catamphetamine.gitlab.io/country-flag-icons/3x2/index.html
export const CountryRegionData = getCountryRegionData()
export const CountryRegionData = getCountryRegionData();
export function getCountryRegionData() {
let language = i18next.language;
if (language === null || language === "null") {
language = Conf.DefaultLanguage;
}
var countries = require("i18n-iso-countries");
countries.registerLocale(require("i18n-iso-countries/langs/" + i18next.language + ".json"));
var data = countries.getNames(i18next.language, {select: "official"});
countries.registerLocale(require("i18n-iso-countries/langs/" + language + ".json"));
var data = countries.getNames(language, {select: "official"});
var result = []
for (var i in data)
result.push({code:i, name:data[i]})
@ -396,6 +402,7 @@ export function getProviderTypeOptions(category) {
{id: 'GitLab', name: 'GitLab'},
{id: 'Adfs', name: 'Adfs'},
{id: 'Baidu', name: 'Baidu'},
{id: 'Alipay', name: 'Alipay'},
{id: 'Casdoor', name: 'Casdoor'},
{id: 'Infoflow', name: 'Infoflow'},
{id: 'Apple', name: 'Apple'},
@ -647,3 +654,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 []
}
}

View File

@ -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>

View File

@ -42,7 +42,7 @@ class SyncerListPage extends BaseListPage {
affiliationTable: "",
avatarBaseUrl: "",
syncInterval: 10,
isEnabled: true,
isEnabled: false,
}
}

View File

@ -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>

View File

@ -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>

View File

@ -224,7 +224,11 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Email"), i18next.t("general:Email - Tooltip"))} :
</Col>
<Col style={{paddingRight: '20px'}} span={11} >
<Input value={this.state.user.email} disabled />
<Input value={this.state.user.email}
disabled={this.state.user.id === this.props.account?.id ? true : !Setting.isAdminUser(this.props.account)}
onChange={e => {
this.updateUserField('email', e.target.value);
}} />
</Col>
<Col span={11} >
{ this.state.user.id === this.props.account?.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Email...")} destType={"email"} />) : null}
@ -235,7 +239,11 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Phone"), i18next.t("general:Phone - Tooltip"))} :
</Col>
<Col style={{paddingRight: '20px'}} span={11} >
<Input value={this.state.user.phone} addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`} disabled />
<Input value={this.state.user.phone} addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`}
disabled={this.state.user.id === this.props.account?.id ? true : !Setting.isAdminUser(this.props.account)}
onChange={e => {
this.updateUserField('phone', e.target.value);
}}/>
</Col>
<Col span={11} >
{ this.state.user.id === this.props.account?.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null}

View File

@ -0,0 +1,32 @@
// 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.
import {createButton} from "react-social-login-buttons";
import {StaticBaseUrl} from "../Setting";
function Icon({ width = 24, height = 24, color }) {
return <img src={`${StaticBaseUrl}/buttons/alipay.svg`} alt="Sign in with Alipay" style={{width: 24, height: 24}} />;
}
const config = {
text: "Sign in with Alipay",
icon: Icon,
iconFormat: name => `fa fa-${name}`,
style: {background: "#ffffff", color: "#000000"},
activeStyle: {background: "#ededee"},
};
const AlipayLoginButton = createButton(config);
export default AlipayLoginButton;

View File

@ -62,6 +62,14 @@ export function login(values, oAuthParams) {
}).then(res => res.json());
}
export function loginCas(values, params) {
return fetch(`${authConfig.serverUrl}/api/login?service=${params.service}`, {
method: 'POST',
credentials: "include",
body: JSON.stringify(values),
}).then(res => res.json());
}
export function logout() {
return fetch(`${authConfig.serverUrl}/api/logout`, {
method: 'POST',

View File

@ -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({

68
web/src/auth/CasLogout.js Normal file
View File

@ -0,0 +1,68 @@
// 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.
import React from "react";
import {Spin} from "antd";
import {withRouter} from "react-router-dom";
import * as AuthBackend from "./AuthBackend";
import * as Setting from "../Setting";
import i18next from "i18next";
class CasLogout extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
msg: null,
};
if (props.match?.params.casApplicationName !== undefined) {
this.state.owner = props.match?.params.owner
this.state.applicationName = props.match?.params.casApplicationName
}
}
UNSAFE_componentWillMount() {
const params = new URLSearchParams(this.props.location.search);
AuthBackend.logout()
.then((res) => {
if (res.status === 'ok') {
Setting.showMessage("success", `Logged out successfully`);
this.props.clearAccount()
let redirectUri = res.data2;
if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") {
Setting.goToLink(redirectUri);
} else if (params.has("service")) {
Setting.goToLink(params.get("service"))
} else {
Setting.goToLinkSoft(this, `/cas/${this.state.owner}/${this.state.applicationName}/login`);
}
} else {
Setting.showMessage("error", `Failed to log out: ${res.msg}`);
}
});
}
render() {
return (
<div style={{textAlign: "center"}}>
{
<Spin size="large" tip={i18next.t("login:Logging out...")} style={{paddingTop: "10%"}} />
}
</div>
)
}
}
export default withRouter(CasLogout);

View File

@ -36,6 +36,7 @@ import LarkLoginButton from "./LarkLoginButton";
import GitLabLoginButton from "./GitLabLoginButton";
import AdfsLoginButton from "./AdfsLoginButton";
import BaiduLoginButton from "./BaiduLoginButton";
import AlipayLoginButton from "./AlipayLoginButton";
import CasdoorLoginButton from "./CasdoorLoginButton";
import InfoflowLoginButton from "./InfoflowLoginButton";
import AppleLoginButton from "./AppleLoginButton"
@ -52,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,13 +63,19 @@ class LoginPage extends React.Component {
validEmail: false,
validPhone: false,
};
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
this.state.owner = props.match?.params.owner
this.state.applicationName = props.match?.params.casApplicationName
}
}
UNSAFE_componentWillMount() {
if (this.state.type === "login") {
if (this.state.type === "login" || this.state.type === "cas") {
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}`);
}
@ -104,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;
@ -119,59 +140,98 @@ class LoginPage extends React.Component {
onFinish(values) {
const application = this.getApplicationObj();
const ths = this;
const oAuthParams = Util.getOAuthGetParameters();
if (oAuthParams !== null && oAuthParams.responseType!= null && oAuthParams.responseType !== "") {
values["type"] = oAuthParams.responseType
}else{
//here we are supposed to judge whether casdoor is working as a oauth server or CAS server
if (this.state.type === "cas") {
//cas
const casParams = Util.getCasParameters()
values["type"] = this.state.type;
}
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
AuthBackend.login(values, oAuthParams)
.then((res) => {
AuthBackend.loginCas(values, casParams).then((res) => {
if (res.status === 'ok') {
const responseType = values["type"];
if (responseType === "login") {
Util.showMessage("success", `Logged in successfully`);
const link = Setting.getFromLink();
Setting.goToLink(link);
} else if (responseType === "code") {
const code = res.data;
const concatChar = oAuthParams?.redirectUri?.includes('?') ? '&' : '?';
if (Setting.hasPromptPage(application)) {
AuthBackend.getAccount("")
.then((res) => {
let account = null;
if (res.status === "ok") {
account = res.data;
account.organization = res.data2;
this.onUpdateAccount(account);
if (Setting.isPromptAnswered(account, application)) {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
} else {
Setting.goToLinkSoft(ths, `/prompt/${application.name}?redirectUri=${oAuthParams.redirectUri}&code=${code}&state=${oAuthParams.state}`);
}
} else {
Setting.showMessage("error", `Failed to sign in: ${res.msg}`);
}
});
} else {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
}
// Util.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token") {
const accessToken = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}#${responseType}=${accessToken}?state=${oAuthParams.state}&token_type=bearer`);
let msg = "Logged in successfully. "
if (casParams.service == "") {
//If service was not specified, CAS MUST display a message notifying the client that it has successfully initiated a single sign-on session.
msg += "Now you can visit apps protected by casdoor."
}
Util.showMessage("success", msg);
if (casParams.service !== "") {
let st = res.data
window.location.href = casParams.service + "?ticket=" + st
}
} else {
Util.showMessage("error", `Failed to log in: ${res.msg}`);
}
});
})
} else {
//oauth
const oAuthParams = Util.getOAuthGetParameters();
if (oAuthParams !== null && oAuthParams.responseType != null && oAuthParams.responseType !== "") {
values["type"] = oAuthParams.responseType
}else{
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') {
const responseType = values["type"];
if (responseType === "login") {
Util.showMessage("success", `Logged in successfully`);
const link = Setting.getFromLink();
Setting.goToLink(link);
} else if (responseType === "code") {
const code = res.data;
const concatChar = oAuthParams?.redirectUri?.includes('?') ? '&' : '?';
if (Setting.hasPromptPage(application)) {
AuthBackend.getAccount("")
.then((res) => {
let account = null;
if (res.status === "ok") {
account = res.data;
account.organization = res.data2;
this.onUpdateAccount(account);
if (Setting.isPromptAnswered(account, application)) {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
} else {
Setting.goToLinkSoft(ths, `/prompt/${application.name}?redirectUri=${oAuthParams.redirectUri}&code=${code}&state=${oAuthParams.state}`);
}
} else {
Setting.showMessage("error", `Failed to sign in: ${res.msg}`);
}
});
} else {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
}
// Util.showMessage("success", `Authorization code: ${res.data}`);
} 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}`);
}
});
}
};
getSigninButton(type) {
@ -206,6 +266,8 @@ class LoginPage extends React.Component {
return <CasdoorLoginButton text={text} align={"center"} />
} else if (type === "Baidu") {
return <BaiduLoginButton text={text} align={"center"} />
} else if (type === "Alipay") {
return <AlipayLoginButton text={text} align={"center"} />
} else if (type === "Infoflow") {
return <InfoflowLoginButton text={text} align={"center"} />
} else if (type === "Apple") {

View File

@ -78,6 +78,10 @@ const authInfo = {
scope: "basic",
endpoint: "http://openapi.baidu.com/oauth/2.0/authorize",
},
Alipay: {
scope: "basic",
endpoint: "https://openauth.alipay.com/oauth2/publicAppAuthorize.htm",
},
Casdoor: {
scope: "openid%20profile%20email",
endpoint: "http://example.com",
@ -287,6 +291,8 @@ export function getAuthUrl(application, provider, method) {
return `${provider.domain}/adfs/oauth2/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&nonce=casdoor&scope=openid`;
} else if (provider.type === "Baidu") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}&display=popup`;
} else if (provider.type === "Alipay") {
return `${endpoint}?app_id=${provider.clientId}&scope=auth_user&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}&display=popup`;
} else if (provider.type === "Casdoor") {
return `${provider.domain}/login/oauth/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Infoflow"){

View File

@ -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)

View File

@ -558,7 +558,7 @@ class SignupPage extends React.Component {
&nbsp;&nbsp;{i18next.t("signup:Have account?")}&nbsp;
<a onClick={() => {
let linkInStorage = sessionStorage.getItem("loginURL")
if(linkInStorage != ""){
if(linkInStorage != null){
Setting.goToLink(linkInStorage)
}else{
Setting.goToLogin(this, application)

View File

@ -79,6 +79,18 @@ function getRefinedValue(value){
return (value === null)? "" : value
}
export function getCasParameters(params){
const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search);
const service = getRefinedValue(queries.get("service"))
const renew = getRefinedValue(queries.get("renew"))
const gateway = getRefinedValue(queries.get("gateway"))
return {
service: service,
renew: renew,
gateway: gateway,
}
}
export function getOAuthGetParameters(params) {
const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search);
const clientId = getRefinedValue(queries.get("client_id"));
@ -86,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 {
@ -104,6 +118,8 @@ export function getOAuthGetParameters(params) {
nonce: nonce,
challengeMethod: challengeMethod,
codeChallenge: codeChallenge,
samlRequest: samlRequest,
relayState: relayState,
};
}
}