Compare commits

..

8 Commits

Author SHA1 Message Date
3c2f7b7fc8 feat: add protection against attacks (#460)
Signed-off-by: 0x2a <stevesough@gmail.com>
2022-01-29 00:32:57 +08:00
fbc73de3bb Support WeCom Internal sub type. 2022-01-28 23:57:54 +08:00
479daf4fa4 Improve code format. 2022-01-28 17:45:41 +08:00
d129202b95 fix: no database check when using accessToken (#461)
Signed-off-by: 0x2a <stevesough@gmail.com>
2022-01-28 15:07:42 +08:00
c1f553440e feat: add wecom internal support (#452)
Signed-off-by: 0x2a <stevesough@gmail.com>
2022-01-28 12:44:45 +08:00
7dcae2d183 fix: add k8s deployments example (#446) 2022-01-28 09:25:25 +08:00
5ec0c7a890 fix: fix the SQL injection vulnerability in field filter (#442)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-01-26 19:36:36 +08:00
051752340d feat: add userinfo endpoint (#447)
* feat: add userinfo endpoint

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

* feat: add scope support

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

* fix: modify the endpoint of discovery

Signed-off-by: 0x2a <stevesough@gmail.com>
2022-01-26 11:56:01 +08:00
35 changed files with 554 additions and 115 deletions

View File

@ -80,6 +80,7 @@ p, *, *, POST, /api/login, *, *
p, *, *, GET, /api/get-app-login, *, *
p, *, *, POST, /api/logout, *, *
p, *, *, GET, /api/get-account, *, *
p, *, *, GET, /api/userinfo, *, *
p, *, *, POST, /api/login/oauth/access_token, *, *
p, *, *, POST, /api/login/oauth/refresh_token, *, *
p, *, *, GET, /api/get-application, *, *

View File

@ -18,7 +18,9 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@ -67,6 +69,18 @@ type Response struct {
Data2 interface{} `json:"data2"`
}
type Userinfo struct {
Sub string `json:"sub"`
Iss string `json:"iss"`
Aud string `json:"aud"`
Name string `json:"name,omitempty"`
DisplayName string `json:"preferred_username,omitempty"`
Email string `json:"email,omitempty"`
Avatar string `json:"picture,omitempty"`
Address string `json:"address,omitempty"`
Phone string `json:"phone,omitempty"`
}
type HumanCheck struct {
Type string `json:"type"`
AppKey string `json:"appKey"`
@ -231,6 +245,47 @@ func (c *ApiController) GetAccount() {
c.ServeJSON()
}
// UserInfo
// @Title UserInfo
// @Tag Account API
// @Description return user information according to OIDC standards
// @Success 200 {object} controllers.Userinfo The Response object
// @router /userinfo [get]
func (c *ApiController) GetUserinfo() {
userId, ok := c.RequireSignedIn()
if !ok {
return
}
user := object.GetUser(userId)
if user == nil {
c.ResponseError(fmt.Sprintf("The user: %s doesn't exist", userId))
return
}
scope, aud := c.GetSessionOidc()
iss := beego.AppConfig.String("origin")
resp := Userinfo{
Sub: user.Id,
Iss: iss,
Aud: aud,
}
if strings.Contains(scope, "profile") {
resp.Name = user.Name
resp.DisplayName = user.DisplayName
resp.Avatar = user.Avatar
}
if strings.Contains(scope, "email") {
resp.Email = user.Email
}
if strings.Contains(scope, "address") {
resp.Address = user.Location
}
if strings.Contains(scope, "phone") {
resp.Phone = user.Phone
}
c.Data["json"] = resp
c.ServeJSON()
}
// GetHumanCheck ...
// @Tag Login API
// @Title GetHumancheck

View File

@ -72,6 +72,28 @@ func (c *ApiController) GetSessionUsername() string {
return user.(string)
}
func (c *ApiController) GetSessionOidc() (string, string) {
sessionData := c.GetSessionData()
if sessionData != nil &&
sessionData.ExpireTime != 0 &&
sessionData.ExpireTime < time.Now().Unix() {
c.SetSessionUsername("")
c.SetSessionData(nil)
return "", ""
}
scopeValue := c.GetSession("scope")
audValue := c.GetSession("aud")
var scope, aud string
var ok bool
if scope, ok = scopeValue.(string); !ok {
scope = ""
}
if aud, ok = audValue.(string); !ok {
aud = ""
}
return scope, aud
}
// SetSessionUsername ...
func (c *ApiController) SetSessionUsername(user string) {
c.SetSession("username", user)

167
idp/wecom_inter.go Normal file
View File

@ -0,0 +1,167 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package idp
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"golang.org/x/oauth2"
)
//This idp is using wecom internal application api as idp
type WeComInternalIdProvider struct {
Client *http.Client
Config *oauth2.Config
}
func NewWeComInternalIdProvider(clientId string, clientSecret string, redirectUrl string) *WeComInternalIdProvider {
idp := &WeComInternalIdProvider{}
config := idp.getConfig(clientId, clientSecret, redirectUrl)
idp.Config = config
return idp
}
func (idp *WeComInternalIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
func (idp *WeComInternalIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
var config = &oauth2.Config{
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
}
return config
}
type WecomInterToken struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
// GetToken use code get access_token (*operation of getting code ought to be done in front)
// get more detail via: https://developer.work.weixin.qq.com/document/path/91039
func (idp *WeComInternalIdProvider) GetToken(code string) (*oauth2.Token, error) {
pTokenParams := &struct {
CorpId string `json:"corpid"`
Corpsecret string `json:"corpsecret"`
}{idp.Config.ClientID, idp.Config.ClientSecret}
resp, err := idp.Client.Get(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s", pTokenParams.CorpId, pTokenParams.Corpsecret))
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
pToken := &WecomInterToken{}
err = json.Unmarshal(data, pToken)
if err != nil {
return nil, err
}
if pToken.Errcode != 0 {
return nil, fmt.Errorf("pToken.Errcode = %d, pToken.Errmsg = %s", pToken.Errcode, pToken.Errmsg)
}
token := &oauth2.Token{
AccessToken: pToken.AccessToken,
Expiry: time.Unix(time.Now().Unix()+int64(pToken.ExpiresIn), 0),
}
raw := make(map[string]interface{})
raw["code"] = code
token = token.WithExtra(raw)
return token, nil
}
type WecomInternalUserResp struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
UserId string `json:"UserId"`
OpenId string `json:"OpenId"`
}
type WecomInternalUserInfo struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
Name string `json:"name"`
Email string `json:"email"`
Avatar string `json:"avatar"`
OpenId string `json:"open_userid"`
}
func (idp *WeComInternalIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
//Get userid first
accessToken := token.AccessToken
code := token.Extra("code").(string)
resp, err := idp.Client.Get(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s", accessToken, code))
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
userResp := &WecomInternalUserResp{}
err = json.Unmarshal(data, userResp)
if err != nil {
return nil, err
}
if userResp.Errcode != 0 {
return nil, fmt.Errorf("userIdResp.Errcode = %d, userIdResp.Errmsg = %s", userResp.Errcode, userResp.Errmsg)
}
if userResp.OpenId != "" {
return nil, fmt.Errorf("not an internal user")
}
//Use userid and accesstoken to get user information
resp, err = idp.Client.Get(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s", accessToken, userResp.UserId))
if err != nil {
return nil, err
}
data, err = io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
infoResp := &WecomInternalUserInfo{}
err = json.Unmarshal(data, infoResp)
if err != nil {
return nil, err
}
if infoResp.Errcode != 0 {
return nil, fmt.Errorf("userInfoResp.errcode = %d, userInfoResp.errmsg = %s", infoResp.Errcode, infoResp.Errmsg)
}
userInfo := UserInfo{
Id: infoResp.OpenId,
Username: infoResp.Name,
DisplayName: infoResp.Name,
Email: infoResp.Email,
AvatarUrl: infoResp.Avatar,
}
return &userInfo, nil
}

56
k8s.yaml Normal file
View File

@ -0,0 +1,56 @@
# this is only an EXAMPLE of deploying casddor in kubernetes
# please modify this file according to your requirements
apiVersion: v1
kind: Service
metadata:
#EDIT IT: if you don't want to run casdoor in default namespace, please modify this field
#namespace: casdoor
name: casdoor-svc
labels:
app: casdoor
spec:
#EDIT IT: if you don't want to run casdoor in default namespace, please modify this filed
type: NodePort
ports:
- port: 8000
selector:
app: casdoor
---
apiVersion: apps/v1
kind: Deployment
metadata:
#EDIT IT: if you don't want to run casdoor in default namespace, please modify this field
#namespace: casdoor
name: casdoor-deployment
labels:
app: casdoor
spec:
#EDIT IT: if you don't use redis, casdoor should not have multiple replicas
replicas: 1
selector:
matchLabels:
app: casdoor
template:
metadata:
labels:
app: casdoor
spec:
containers:
- name: casdoor-container
image: casbin/casdoor:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
volumeMounts:
# the mounted directory path in THE CONTAINER
- mountPath: /conf
name: conf
env:
- name: RUNNING_IN_DOCKER
value: "true"
#if you want to deploy this in real prod env, consider the config map
volumes:
- name: conf
hostPath:
#EDIT IT: the mounted directory path in THE HOST
path: /conf

View File

@ -190,12 +190,17 @@ func (a *Adapter) createTable() {
}
func GetSession(owner string, offset, limit int, field, value, sortField, sortOrder string) *xorm.Session {
session := adapter.Engine.Limit(limit, offset).Where("1=1")
session := adapter.Engine.Prepare()
if offset != -1 && limit != -1 {
session.Limit(limit, offset)
}
if owner != "" {
session = session.And("owner=?", owner)
}
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
if filterField(field) {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
}
if sortField == "" || sortOrder == "" {
sortField = "created_time"
@ -206,4 +211,4 @@ func GetSession(owner string, offset, limit int, field, value, sortField, sortOr
session = session.Desc(util.SnakeString(sortField))
}
return session
}
}

View File

@ -56,10 +56,7 @@ type Application struct {
}
func GetApplicationCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Application{})
if err != nil {
panic(err)

View File

@ -53,10 +53,7 @@ func GetMaskedCerts(certs []*Cert) []*Cert {
}
func GetCertCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Cert{})
if err != nil {
panic(err)

View File

@ -23,10 +23,14 @@ import (
goldap "github.com/go-ldap/ldap/v3"
)
var reWhiteSpace *regexp.Regexp
var (
reWhiteSpace *regexp.Regexp
reFieldWhiteList *regexp.Regexp
)
func init() {
reWhiteSpace, _ = regexp.Compile(`\s`)
reFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
}
func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, email string, phone string, affiliation string) string {
@ -179,3 +183,7 @@ func CheckUserPassword(organization string, username string, password string) (*
return user, ""
}
func filterField(field string) bool {
return reFieldWhiteList.MatchString(field)
}

View File

@ -54,7 +54,7 @@ func init() {
Issuer: origin,
AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", origin),
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", origin),
UserinfoEndpoint: fmt.Sprintf("%s/api/get-account", origin),
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", origin),
JwksUri: fmt.Sprintf("%s/api/certs", origin),
ResponseTypesSupported: []string{"id_token"},
ResponseModesSupported: []string{"login", "code", "link"},

View File

@ -15,8 +15,6 @@
package object
import (
"fmt"
"github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
@ -39,10 +37,7 @@ type Organization struct {
}
func GetOrganizationCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Organization{})
if err != nil {
panic(err)

View File

@ -39,10 +39,7 @@ type Permission struct {
}
func GetPermissionCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Permission{})
if err != nil {
panic(err)

View File

@ -29,6 +29,7 @@ type Provider struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Category string `xorm:"varchar(100)" json:"category"`
Type string `xorm:"varchar(100)" json:"type"`
SubType string `xorm:"varchar(100)" json:"subType"`
Method string `xorm:"varchar(100)" json:"method"`
ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
@ -81,10 +82,7 @@ func GetMaskedProviders(providers []*Provider) []*Provider {
}
func GetProviderCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Provider{})
if err != nil {
panic(err)

View File

@ -102,10 +102,7 @@ func AddRecord(record *Record) bool {
}
func GetRecordCount(field, value string) int {
session := adapter.Engine.Where("1=1")
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession("", -1, -1, field, value, "", "")
count, err := session.Count(&Record{})
if err != nil {
panic(err)

View File

@ -40,11 +40,8 @@ type Resource struct {
}
func GetResourceCount(owner, user, field, value string) int {
session := adapter.Engine.Where("owner=? and user=?", owner, user)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
count, err := session.Count(&Resource{})
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Resource{User: user})
if err != nil {
panic(err)
}

View File

@ -33,10 +33,7 @@ type Role struct {
}
func GetRoleCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Role{})
if err != nil {
panic(err)

View File

@ -56,10 +56,7 @@ type Syncer struct {
}
func GetSyncerCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Syncer{})
if err != nil {
panic(err)

View File

@ -19,6 +19,7 @@ import (
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
@ -45,6 +46,8 @@ type Token struct {
Scope string `xorm:"varchar(100)" json:"scope"`
TokenType string `xorm:"varchar(100)" json:"tokenType"`
CodeChallenge string `xorm:"varchar(100)" json:"codeChallenge"`
CodeIsUsed bool `json:"codeIsUsed"`
CodeExpireIn int64 `json:"codeExpireIn"`
}
type TokenWrapper struct {
@ -57,10 +60,7 @@ type TokenWrapper struct {
}
func GetTokenCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Token{})
if err != nil {
panic(err)
@ -109,8 +109,8 @@ func getToken(owner string, name string) *Token {
}
func getTokenByCode(code string) *Token {
token := Token{}
existed, err := adapter.Engine.Where("code=?", code).Get(&token)
token := Token{Code: code}
existed, err := adapter.Engine.Get(&token)
if err != nil {
panic(err)
}
@ -122,6 +122,15 @@ func getTokenByCode(code string) *Token {
return nil
}
func updateUsedByCode(token *Token) bool {
affected, err := adapter.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
if err != nil {
panic(err)
}
return affected != 0
}
func GetToken(id string) *Token {
owner, name := util.GetOwnerAndNameFromId(id)
return getToken(owner, name)
@ -159,6 +168,16 @@ func DeleteToken(token *Token) bool {
return affected != 0
}
func GetTokenByAccessToken(accessToken string) *Token {
//Check if the accessToken is in the database
token := Token{AccessToken: accessToken}
existed, err := adapter.Engine.Get(&token)
if err != nil || !existed {
return nil
}
return &token
}
func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string) (string, *Application) {
if responseType != "code" {
return "response_type should be \"code\"", nil
@ -208,12 +227,12 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
}
}
accessToken, refreshToken, err := generateJwtToken(application, user, nonce)
accessToken, refreshToken, err := generateJwtToken(application, user, nonce, scope)
if err != nil {
panic(err)
}
if challenge == "null"{
if challenge == "null" {
challenge = ""
}
@ -231,6 +250,8 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
Scope: scope,
TokenType: "Bearer",
CodeChallenge: challenge,
CodeIsUsed: false,
CodeExpireIn: time.Now().Add(time.Minute * 5).Unix(),
}
AddToken(token)
@ -304,7 +325,29 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
Scope: "",
}
}
if token.CodeIsUsed {
//Resist replay attacks, if the code is reused, the token generated with this code will be deleted
DeleteToken(token)
return &TokenWrapper{
AccessToken: "error: code has been used.",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if time.Now().Unix() > token.CodeExpireIn {
//can only use the code to generate a token within five minutes
DeleteToken(token)
return &TokenWrapper{
AccessToken: "error: code has expired",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
token.CodeIsUsed = true
updateUsedByCode(token)
tokenWrapper := &TokenWrapper{
AccessToken: token.AccessToken,
IdToken: token.AccessToken,
@ -376,7 +419,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
Scope: "",
}
}
newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "")
newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "", scope)
if err != nil {
panic(err)
}

View File

@ -27,6 +27,7 @@ type Claims struct {
*User
Nonce string `json:"nonce,omitempty"`
Tag string `json:"tag,omitempty"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}
@ -38,6 +39,7 @@ type UserShort struct {
type ClaimsShort struct {
*UserShort
Nonce string `json:"nonce,omitempty"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}
@ -53,12 +55,13 @@ func getShortClaims(claims Claims) ClaimsShort {
res := ClaimsShort{
UserShort: getShortUser(claims.User),
Nonce: claims.Nonce,
Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims,
}
return res
}
func generateJwtToken(application *Application, user *User, nonce string) (string, string, error) {
func generateJwtToken(application *Application, user *User, nonce string, scope string) (string, string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour)
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
@ -69,7 +72,8 @@ func generateJwtToken(application *Application, user *User, nonce string) (strin
User: user,
Nonce: nonce,
// FIXME: A workaround for custom claim by reusing `tag` in user info
Tag: user.Tag,
Tag: user.Tag,
Scope: scope,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: beego.AppConfig.String("origin"),
Subject: user.Id,

View File

@ -89,10 +89,7 @@ type User struct {
}
func GetGlobalUserCount(field, value string) int {
session := adapter.Engine.Where("1=1")
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession("", -1, -1, field, value, "", "")
count, err := session.Count(&User{})
if err != nil {
panic(err)
@ -123,10 +120,7 @@ func GetPaginationGlobalUsers(offset, limit int, field, value, sortField, sortOr
}
func GetUserCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&User{})
if err != nil {
panic(err)

View File

@ -43,10 +43,7 @@ type Webhook struct {
}
func GetWebhookCount(owner, field, value string) int {
session := adapter.Engine.Where("owner=?", owner)
if field != "" && value != "" {
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Webhook{})
if err != nil {
panic(err)

View File

@ -16,7 +16,6 @@ package routers
import (
"fmt"
"time"
"github.com/astaxie/beego/context"
"github.com/casdoor/casdoor/object"
@ -28,21 +27,28 @@ func AutoSigninFilter(ctx *context.Context) {
// return
//}
// "/page?access_token=123"
// GET parameter like "/page?access_token=123" or
// HTTP Bearer token like "Authorization: Bearer 123"
accessToken := ctx.Input.Query("accessToken")
if accessToken == "" {
accessToken = parseBearerToken(ctx)
}
if accessToken != "" {
cert := object.GetDefaultCert()
claims, err := object.ParseJwtToken(accessToken, cert)
if err != nil {
responseError(ctx, "invalid JWT token")
token := object.GetTokenByAccessToken(accessToken)
if token == nil {
responseError(ctx, "Access token doesn't exist")
return
}
if time.Now().Unix() > claims.ExpiresAt.Unix() {
responseError(ctx, "expired JWT token")
if !util.IsTokenExpired(token.CreatedTime, token.ExpiresIn) {
responseError(ctx, "Access token has expired")
return
}
userId := fmt.Sprintf("%s/%s", claims.User.Owner, claims.User.Name)
userId := fmt.Sprintf("%s/%s", token.Organization, token.User)
application, _ := object.GetApplicationByUserId(fmt.Sprintf("app/%s", token.Application))
setSessionUser(ctx, userId)
setSessionOidc(ctx, token.Scope, application.ClientId)
return
}
@ -68,18 +74,4 @@ func AutoSigninFilter(ctx *context.Context) {
return
}
// HTTP Bearer token
// Authorization: Bearer bearerToken
bearerToken := parseBearerToken(ctx)
if bearerToken != "" {
cert := object.GetDefaultCert()
claims, err := object.ParseJwtToken(bearerToken, cert)
if err != nil {
responseError(ctx, err.Error())
return
}
setSessionUser(ctx, fmt.Sprintf("%s/%s", claims.Owner, claims.Name))
setSessionExpire(ctx, claims.ExpiresAt.Unix())
}
}

View File

@ -97,6 +97,18 @@ func setSessionExpire(ctx *context.Context, ExpireTime int64) {
ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter)
}
func setSessionOidc(ctx *context.Context, scope string, aud string) {
err := ctx.Input.CruSession.Set("scope", scope)
if err != nil {
panic(err)
}
err = ctx.Input.CruSession.Set("aud", aud)
if err != nil {
panic(err)
}
ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter)
}
func parseBearerToken(ctx *context.Context) string {
header := ctx.Request.Header.Get("Authorization")
tokens := strings.Split(header, " ")

View File

@ -50,6 +50,7 @@ func initAPI() {
beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin")
beego.Router("/api/logout", &controllers.ApiController{}, "POST:Logout")
beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount")
beego.Router("/api/userinfo", &controllers.ApiController{}, "GET:GetUserinfo")
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")

View File

@ -28,3 +28,9 @@ func GetCurrentTime() string {
func GetCurrentUnixTime() string {
return strconv.FormatInt(time.Now().UnixNano(), 10)
}
func IsTokenExpired(createdTime string, expiresIn int) bool {
createdTimeObj, _ := time.Parse(time.RFC3339, createdTime)
expiresAtObj := createdTimeObj.Add(time.Duration(expiresIn) * time.Minute)
return time.Now().Before(expiresAtObj)
}

View File

@ -91,18 +91,21 @@ class ProviderEditPage extends React.Component {
getAppIdRow() {
let text, tooltip;
if (this.state.provider.category === "SMS" && this.state.provider.type === "Tencent Cloud SMS") {
text = "provider:App ID";
tooltip = "provider:App ID - Tooltip";
text = i18next.t("provider:App ID");
tooltip = i18next.t("provider:App ID - Tooltip");
} else if (this.state.provider.type === "WeCom" && this.state.provider.subType === "Internal") {
text = i18next.t("provider:Agent ID");
tooltip = i18next.t("provider:Agent ID - Tooltip");
} else if (this.state.provider.category === "SMS" && this.state.provider.type === "Volc Engine SMS") {
text = "provider:SMS account";
tooltip = "provider:SMS account - Tooltip";
text = i18next.t("provider:SMS account");
tooltip = i18next.t("provider:SMS account - Tooltip");
} else {
return null;
}
return <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t(text), i18next.t(tooltip))} :
{Setting.getLabel(text, tooltip)} :
</Col>
<Col span={22} >
<Input value={this.state.provider.appId} onChange={e => {
@ -205,20 +208,36 @@ class ProviderEditPage extends React.Component {
</Row>
{
this.state.provider.type !== "WeCom" ? null : (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Method"), i18next.t("provider:Method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.provider.method} onChange={value => {
this.updateProviderField('method', value);
}}>
{
[{name: "Normal"}, {name: "Silent"}].map((method, index) => <Option key={index} value={method.name}>{method.name}</Option>)
}
</Select>
</Col>
</Row>
<React.Fragment>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Sub type"), i18next.t("provider:Sub type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.provider.subType} onChange={value => {
this.updateProviderField('subType', value);
}}>
{
Setting.getProviderSubTypeOptions(this.state.provider.type).map((providerSubType, index) => <Option key={index} value={providerSubType.id}>{providerSubType.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Method"), i18next.t("provider:Method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.provider.method} onChange={value => {
this.updateProviderField('method', value);
}}>
{
[{name: "Normal"}, {name: "Silent"}].map((method, index) => <Option key={index} value={method.name}>{method.name}</Option>)
}
</Select>
</Col>
</Row>
</React.Fragment>
)
}
<Row style={{marginTop: '20px'}} >

View File

@ -437,6 +437,19 @@ export function getProviderTypeOptions(category) {
}
}
export function getProviderSubTypeOptions(type) {
if (type === "WeCom") {
return (
[
{id: 'Internal', name: 'Internal'},
{id: 'Third-party', name: 'Third-party'},
]
);
} else {
return [];
}
}
export function renderLogo(application) {
if (application === null) {
return null;

View File

@ -60,6 +60,7 @@ const authInfo = {
scope: "snsapi_userinfo",
endpoint: "https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect",
silentEndpoint: "https://open.weixin.qq.com/connect/oauth2/authorize",
internalEndpoint: "https://open.work.weixin.qq.com/wwopen/sso/qrConnect",
},
Lark: {
// scope: "email",
@ -192,7 +193,7 @@ export function getAuthUrl(application, provider, method) {
return "";
}
const endpoint = authInfo[provider.type].endpoint;
let endpoint = authInfo[provider.type].endpoint;
const redirectUri = `${window.location.origin}/callback`;
const scope = authInfo[provider.type].scope;
const state = Util.getQueryParamsToState(application.name, provider.name, method);
@ -220,12 +221,27 @@ export function getAuthUrl(application, provider, method) {
} else if (provider.type === "LinkedIn") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`;
} else if (provider.type === "WeCom") {
if (provider.method === "Silent") {
return `${authInfo[provider.type].silentEndpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&scope=${scope}&response_type=code#wechat_redirect`;
} else if (provider.method === "Normal") {
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&usertype=member`;
if (provider.subType === "Internal") {
if (provider.method === "Silent") {
endpoint = authInfo[provider.type].silentEndpoint;
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&scope=${scope}&response_type=code#wechat_redirect`;
} else if (provider.method === "Normal") {
endpoint = authInfo[provider.type].internalEndpoint;
return `${endpoint}?appid=${provider.clientId}&agentid=${provider.appId}&redirect_uri=${redirectUri}&state=${state}&usertype=member`;
} else {
return `https://error:not-supported-provider-method:${provider.method}`;
}
} else if (provider.subType === "Third-party") {
if (provider.method === "Silent") {
endpoint = authInfo[provider.type].silentEndpoint;
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&scope=${scope}&response_type=code#wechat_redirect`;
} else if (provider.method === "Normal") {
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&usertype=member`;
} else {
return `https://error:not-supported-provider-method:${provider.method}`;
}
} else {
return `https://error:not-supported-provider-method:${provider.method}`;
return `https://error:not-supported-provider-sub-type:${provider.subType}`;
}
} else if (provider.type === "Lark") {
return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}`;

View File

@ -247,6 +247,10 @@
"provider": {
"Access key": "Zugangsschlüssel",
"Access key - Tooltip": "Zugriffsschlüssel - Tooltip",
"Agent ID": "Agent ID",
"Agent ID - Tooltip": "Agent ID - Tooltip",
"App ID": "App ID",
"App ID - Tooltip": "App ID - Tooltip",
"Bucket": "Eimer",
"Bucket - Tooltip": "Storage bucket name",
"Can not parse Metadata": "Metadaten können nicht analysiert werden",
@ -293,6 +297,8 @@
"Region endpoint for Internet": "Region Endpunkt für Internet",
"Region endpoint for Intranet": "Region Endpunkt für Intranet",
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpoint (HTTP)",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip",
"SP ACS URL": "SP-ACS-URL",
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
"SP Entity ID": "SP Entity ID",
@ -308,6 +314,8 @@
"Signup HTML": "HTML registrieren",
"Signup HTML - Edit": "HTML registrieren - Bearbeiten",
"Signup HTML - Tooltip": "HTML registrieren - Tooltip",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Vorlagencode",
"Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "Nutzungsbedingungen",

View File

@ -247,6 +247,10 @@
"provider": {
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Agent ID": "Agent ID",
"Agent ID - Tooltip": "Agent ID - Tooltip",
"App ID": "App ID",
"App ID - Tooltip": "App ID - Tooltip",
"Bucket": "Bucket",
"Bucket - Tooltip": "Bucket - Tooltip",
"Can not parse Metadata": "Can not parse Metadata",
@ -293,6 +297,8 @@
"Region endpoint for Internet": "Region endpoint for Internet",
"Region endpoint for Intranet": "Region endpoint for Intranet",
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpoint (HTTP)",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
"SP Entity ID": "SP Entity ID",
@ -308,6 +314,8 @@
"Signup HTML": "Signup HTML",
"Signup HTML - Edit": "Signup HTML - Edit",
"Signup HTML - Tooltip": "Signup HTML - Tooltip",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Template Code",
"Template Code - Tooltip": "Template Code - Tooltip",
"Terms of Use": "Terms of Use",

View File

@ -247,6 +247,10 @@
"provider": {
"Access key": "Clé d'accès",
"Access key - Tooltip": "Touche d'accès - Infobulle",
"Agent ID": "Agent ID",
"Agent ID - Tooltip": "Agent ID - Tooltip",
"App ID": "App ID",
"App ID - Tooltip": "App ID - Tooltip",
"Bucket": "Seau",
"Bucket - Tooltip": "Storage bucket name",
"Can not parse Metadata": "Impossible d'analyser les métadonnées",
@ -293,6 +297,8 @@
"Region endpoint for Internet": "Point de terminaison de la région pour Internet",
"Region endpoint for Intranet": "Point de terminaison de la région pour Intranet",
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpoint (HTTP)",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip",
"SP ACS URL": "URL du SP ACS",
"SP ACS URL - Tooltip": "URL SP ACS - infobulle",
"SP Entity ID": "ID de l'entité SP",
@ -308,6 +314,8 @@
"Signup HTML": "Inscription HTML",
"Signup HTML - Edit": "Inscription HTML - Modifier",
"Signup HTML - Tooltip": "Inscription HTML - infobulle",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Code du modèle",
"Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "Conditions d'utilisation",

View File

@ -247,6 +247,10 @@
"provider": {
"Access key": "アクセスキー",
"Access key - Tooltip": "アクセスキー → ツールチップ",
"Agent ID": "Agent ID",
"Agent ID - Tooltip": "Agent ID - Tooltip",
"App ID": "App ID",
"App ID - Tooltip": "App ID - Tooltip",
"Bucket": "バケツ入りバケツ",
"Bucket - Tooltip": "Storage bucket name",
"Can not parse Metadata": "メタデータをパースできません",
@ -293,6 +297,8 @@
"Region endpoint for Internet": "インターネットのリージョンエンドポイント",
"Region endpoint for Intranet": "イントラネットのリージョンエンドポイント",
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpoint (HTTP)",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - ツールチップ",
"SP Entity ID": "SP ID",
@ -308,6 +314,8 @@
"Signup HTML": "HTMLの登録",
"Signup HTML - Edit": "HTMLの登録 - 編集",
"Signup HTML - Tooltip": "サインアップ HTML - ツールチップ",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "テンプレートコード",
"Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "利用規約",

View File

@ -247,6 +247,10 @@
"provider": {
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Agent ID": "Agent ID",
"Agent ID - Tooltip": "Agent ID - Tooltip",
"App ID": "App ID",
"App ID - Tooltip": "App ID - Tooltip",
"Bucket": "Bucket",
"Bucket - Tooltip": "Storage bucket name",
"Can not parse Metadata": "Can not parse Metadata",
@ -293,6 +297,8 @@
"Region endpoint for Internet": "Region endpoint for Internet",
"Region endpoint for Intranet": "Region endpoint for Intranet",
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpoint (HTTP)",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
"SP Entity ID": "SP Entity ID",
@ -308,6 +314,8 @@
"Signup HTML": "Signup HTML",
"Signup HTML - Edit": "Signup HTML - Edit",
"Signup HTML - Tooltip": "Signup HTML - Tooltip",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Template Code",
"Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "Terms of Use",

View File

@ -247,6 +247,10 @@
"provider": {
"Access key": "Ключ доступа",
"Access key - Tooltip": "Ключ доступа - Подсказка",
"Agent ID": "Agent ID",
"Agent ID - Tooltip": "Agent ID - Tooltip",
"App ID": "App ID",
"App ID - Tooltip": "App ID - Tooltip",
"Bucket": "Ведро",
"Bucket - Tooltip": "Storage bucket name",
"Can not parse Metadata": "Невозможно разобрать метаданные",
@ -293,6 +297,8 @@
"Region endpoint for Internet": "Конечная точка региона для Интернета",
"Region endpoint for Intranet": "Конечная точка региона Интранета",
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpoint (HTTP)",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - Подсказка",
"SP Entity ID": "Идентификатор сущности SP",
@ -308,6 +314,8 @@
"Signup HTML": "Регистрация HTML",
"Signup HTML - Edit": "Регистрация HTML - Редактировать",
"Signup HTML - Tooltip": "Регистрация HTML - Подсказка",
"Sub type": "Sub type",
"Sub type - Tooltip": "Sub type - Tooltip",
"Template Code": "Код шаблона",
"Template Code - Tooltip": "Unique string-style identifier",
"Terms of Use": "Условия использования",

View File

@ -247,6 +247,10 @@
"provider": {
"Access key": "访问密钥",
"Access key - Tooltip": "Access key",
"Agent ID": "Agent ID",
"Agent ID - Tooltip": "Agent ID - Tooltip",
"App ID": "App ID",
"App ID - Tooltip": "App ID - Tooltip",
"Bucket": "存储桶",
"Bucket - Tooltip": "Bucket名称",
"Can not parse Metadata": "无法解析元数据",
@ -293,6 +297,8 @@
"Region endpoint for Internet": "地域节点 (外网)",
"Region endpoint for Intranet": "地域节点 (内网)",
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpoint (HTTP)",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account - Tooltip",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - 工具提示",
"SP Entity ID": "SP 实体 ID",
@ -308,6 +314,8 @@
"Signup HTML": "注册 HTML",
"Signup HTML - Edit": "注册 HTML - 编辑",
"Signup HTML - Tooltip": "注册 HTML - 工具提示",
"Sub type": "子类型",
"Sub type - Tooltip": "子类型",
"Template Code": "模板CODE",
"Template Code - Tooltip": "模板CODE",
"Terms of Use": "使用条款",