feat: add casdoor as itself idp support (#578)

Signed-off-by: Steve0x2a <stevesough@gmail.com>
This commit is contained in:
Yi Zhan 2022-03-18 18:28:46 +08:00 committed by GitHub
parent e5ff49f7a7
commit e8b9c67671
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 599 additions and 26 deletions

159
idp/casdoor.go Normal file
View File

@ -0,0 +1,159 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package idp
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"time"
"golang.org/x/oauth2"
)
type CasdoorIdProvider struct {
Client *http.Client
Config *oauth2.Config
Host string
}
func NewCasdoorIdProvider(clientId string, clientSecret string, redirectUrl string, hostUrl string) *CasdoorIdProvider {
idp := &CasdoorIdProvider{}
config := idp.getConfig(hostUrl)
config.ClientID = clientId
config.ClientSecret = clientSecret
config.RedirectURL = redirectUrl
idp.Config = config
idp.Host = hostUrl
return idp
}
func (idp *CasdoorIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
func (idp *CasdoorIdProvider) getConfig(hostUrl string) *oauth2.Config {
return &oauth2.Config{
Endpoint: oauth2.Endpoint{
TokenURL: hostUrl + "/api/login/oauth/access_token",
},
Scopes: []string{"openid email profile"},
}
}
type CasdoorToken struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
func (idp *CasdoorIdProvider) GetToken(code string) (*oauth2.Token, error) {
resp, err := http.PostForm(idp.Config.Endpoint.TokenURL, url.Values{
"client_id": {idp.Config.ClientID},
"client_secret": {idp.Config.ClientSecret},
"code": {code},
"grant_type": {"authorization_code"},
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
pToken := &CasdoorToken{}
err = json.Unmarshal(body, pToken)
if err != nil {
return nil, err
}
//check if token is expired
if pToken.ExpiresIn <= 0 {
return nil, fmt.Errorf("%s", pToken.AccessToken)
}
token := &oauth2.Token{
AccessToken: pToken.AccessToken,
Expiry: time.Unix(time.Now().Unix()+int64(pToken.ExpiresIn), 0),
}
return token, nil
}
/*
{
"sub": "2f80c349-4beb-407f-b1f0-528aac0f1acd",
"iss": "https://door.casbin.com",
"aud": "7a11****0fa2172",
"name": "admin",
"preferred_username": "Admin",
"email": "admin@example.com",
"picture": "https://casbin.org/img/casbin.svg",
"address": "Guangdong",
"phone": "12345678910"
}
*/
type CasdoorUserInfo struct {
Id string `json:"sub"`
Name string `json:"name"`
DisplayName string `json:"preferred_username"`
Email string `json:"email"`
AvatarUrl string `json:"picture"`
Status string `json:"status"`
Msg string `json:"msg"`
}
func (idp *CasdoorIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
cdUserinfo := &CasdoorUserInfo{}
accessToken := token.AccessToken
request, err := http.NewRequest("GET", fmt.Sprintf("%s/api/userinfo", idp.Host), nil)
if err != nil {
return nil, err
}
//add accesstoken to bearer token
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
resp, err := idp.Client.Do(request)
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(data, cdUserinfo)
if err != nil {
return nil, err
}
if cdUserinfo.Status != "" {
return nil, fmt.Errorf("err: %s", cdUserinfo.Msg)
}
userInfo := &UserInfo{
Id: cdUserinfo.Id,
Username: cdUserinfo.Name,
DisplayName: cdUserinfo.DisplayName,
Email: cdUserinfo.Email,
AvatarUrl: cdUserinfo.AvatarUrl,
}
return userInfo, nil
}

View File

@ -78,6 +78,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
} else {
return nil
}
} else if typ == "Casdoor" {
return NewCasdoorIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
} else if isGothSupport(typ) {
return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl)
}

View File

@ -85,6 +85,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"`
Casdoor string `xorm:"casdoor varchar(100)" json:"casdoor"`
Infoflow string `xorm:"infoflow varchar(100)" json:"infoflow"`
Apple string `xorm:"apple varchar(100)" json:"apple"`
AzureAD string `xorm:"azuread varchar(100)" json:"azuread"`

View File

@ -478,6 +478,39 @@
}
}
},
"/api/buy-product": {
"post": {
"tags": [
"Product API"
],
"description": "buy product",
"operationId": "ApiController.BuyProduct",
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id of the product",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "providerName",
"description": "The name of the provider",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/check-ldap-users-exist": {
"post": {
"tags": [
@ -1710,6 +1743,49 @@
}
}
},
"/api/get-user-payments": {
"get": {
"tags": [
"Payment API"
],
"description": "get payments for a user",
"operationId": "ApiController.GetUserPayments",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "The owner of payments",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "organization",
"description": "The organization of the user",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "user",
"description": "The username of the user",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/object.Payment"
}
}
}
}
}
},
"/api/get-users": {
"get": {
"tags": [
@ -1936,6 +2012,36 @@
}
}
},
"/api/login/oauth/introspect": {
"post": {
"description": "The introspection endpoint is an OAuth 2.0 endpoint that takes a",
"operationId": "ApiController.IntrospectToken",
"parameters": [
{
"in": "formData",
"name": "token",
"description": "access_token's value or refresh_token's value",
"required": true,
"type": "string"
},
{
"in": "formData",
"name": "token_type_hint",
"description": "the token type access_token or refresh_token",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.IntrospectionResponse"
}
}
}
}
},
"/api/login/oauth/logout": {
"get": {
"tags": [
@ -2015,7 +2121,6 @@
"in": "query",
"name": "client_secret",
"description": "OAuth client secret",
"required": true,
"type": "string"
}
],
@ -2046,6 +2151,34 @@
}
}
},
"/api/notify-payment": {
"post": {
"tags": [
"Payment API"
],
"description": "notify payment",
"operationId": "ApiController.NotifyPayment",
"parameters": [
{
"in": "body",
"name": "body",
"description": "The details of the payment",
"required": true,
"schema": {
"$ref": "#/definitions/object.Payment"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/send-verification-code": {
"post": {
"tags": [
@ -2664,11 +2797,11 @@
}
},
"definitions": {
"2015.0xc0000edb90.false": {
"2026.0xc000380de0.false": {
"title": "false",
"type": "object"
},
"2049.0xc0000edbc0.false": {
"2060.0xc000380e10.false": {
"title": "false",
"type": "object"
},
@ -2685,10 +2818,10 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/2015.0xc0000edb90.false"
"$ref": "#/definitions/2026.0xc000380de0.false"
},
"data2": {
"$ref": "#/definitions/2049.0xc0000edbc0.false"
"$ref": "#/definitions/2060.0xc000380e10.false"
},
"msg": {
"type": "string"
@ -2709,10 +2842,10 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/2015.0xc0000edb90.false"
"$ref": "#/definitions/2026.0xc000380de0.false"
},
"data2": {
"$ref": "#/definitions/2049.0xc0000edbc0.false"
"$ref": "#/definitions/2060.0xc000380e10.false"
},
"msg": {
"type": "string"
@ -2864,6 +2997,12 @@
"title": "Cert",
"type": "object",
"properties": {
"authorityPublicKey": {
"type": "string"
},
"authorityRootPublicKey": {
"type": "string"
},
"bitSize": {
"type": "integer",
"format": "int64"
@ -2913,6 +3052,54 @@
}
}
},
"object.IntrospectionResponse": {
"title": "IntrospectionResponse",
"type": "object",
"properties": {
"active": {
"type": "boolean"
},
"aud": {
"type": "array",
"items": {
"type": "string"
}
},
"client_id": {
"type": "string"
},
"exp": {
"type": "integer",
"format": "int64"
},
"iat": {
"type": "integer",
"format": "int64"
},
"iss": {
"type": "string"
},
"jti": {
"type": "string"
},
"nbf": {
"type": "integer",
"format": "int64"
},
"scope": {
"type": "string"
},
"sub": {
"type": "string"
},
"token_type": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"object.Organization": {
"title": "Organization",
"type": "object",
@ -2950,6 +3137,12 @@
"phonePrefix": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"websiteUrl": {
"type": "string"
}
@ -2959,19 +3152,19 @@
"title": "Payment",
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"createdTime": {
"type": "string"
},
"currency": {
"type": "string"
},
"detail": {
"type": "string"
},
"displayName": {
"type": "string"
},
"good": {
"message": {
"type": "string"
},
"name": {
@ -2983,12 +3176,31 @@
"owner": {
"type": "string"
},
"payUrl": {
"type": "string"
},
"price": {
"type": "number",
"format": "double"
},
"productDisplayName": {
"type": "string"
},
"productName": {
"type": "string"
},
"provider": {
"type": "string"
},
"returnUrl": {
"type": "string"
},
"state": {
"type": "string"
},
"tag": {
"type": "string"
},
"type": {
"type": "string"
},
@ -3074,8 +3286,8 @@
"type": "string"
},
"price": {
"type": "integer",
"format": "int64"
"type": "number",
"format": "double"
},
"providers": {
"type": "array",
@ -3087,6 +3299,9 @@
"type": "integer",
"format": "int64"
},
"returnUrl": {
"type": "string"
},
"sold": {
"type": "integer",
"format": "int64"
@ -3112,6 +3327,9 @@
"category": {
"type": "string"
},
"cert": {
"type": "string"
},
"clientId": {
"type": "string"
},
@ -3482,6 +3700,9 @@
"birthday": {
"type": "string"
},
"casdoor": {
"type": "string"
},
"createdIp": {
"type": "string"
},

View File

@ -309,6 +309,28 @@ paths:
description: object
schema:
$ref: '#/definitions/Response'
/api/buy-product:
post:
tags:
- Product API
description: buy product
operationId: ApiController.BuyProduct
parameters:
- in: query
name: id
description: The id of the product
required: true
type: string
- in: query
name: providerName
description: The name of the provider
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/check-ldap-users-exist:
post:
tags:
@ -1111,6 +1133,35 @@ paths:
responses:
"200":
description: '{int} int The count of filtered users for an organization'
/api/get-user-payments:
get:
tags:
- Payment API
description: get payments for a user
operationId: ApiController.GetUserPayments
parameters:
- in: query
name: owner
description: The owner of payments
required: true
type: string
- in: query
name: organization
description: The organization of the user
required: true
type: string
- in: query
name: user
description: The username of the user
required: true
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Payment'
/api/get-users:
get:
tags:
@ -1262,6 +1313,26 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/object.TokenWrapper'
/api/login/oauth/introspect:
post:
description: The introspection endpoint is an OAuth 2.0 endpoint that takes a
operationId: ApiController.IntrospectToken
parameters:
- in: formData
name: token
description: access_token's value or refresh_token's value
required: true
type: string
- in: formData
name: token_type_hint
description: the token type access_token or refresh_token
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.IntrospectionResponse'
/api/login/oauth/logout:
get:
tags:
@ -1318,7 +1389,6 @@ paths:
- in: query
name: client_secret
description: OAuth client secret
required: true
type: string
responses:
"200":
@ -1336,6 +1406,24 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/notify-payment:
post:
tags:
- Payment API
description: notify payment
operationId: ApiController.NotifyPayment
parameters:
- in: body
name: body
description: The details of the payment
required: true
schema:
$ref: '#/definitions/object.Payment'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/send-verification-code:
post:
tags:
@ -1743,10 +1831,10 @@ paths:
schema:
$ref: '#/definitions/object.Userinfo'
definitions:
2015.0xc0000edb90.false:
2026.0xc000380de0.false:
title: "false"
type: object
2049.0xc0000edbc0.false:
2060.0xc000380e10.false:
title: "false"
type: object
RequestForm:
@ -1760,9 +1848,9 @@ definitions:
type: object
properties:
data:
$ref: '#/definitions/2015.0xc0000edb90.false'
$ref: '#/definitions/2026.0xc000380de0.false'
data2:
$ref: '#/definitions/2049.0xc0000edbc0.false'
$ref: '#/definitions/2060.0xc000380e10.false'
msg:
type: string
name:
@ -1776,9 +1864,9 @@ definitions:
type: object
properties:
data:
$ref: '#/definitions/2015.0xc0000edb90.false'
$ref: '#/definitions/2026.0xc000380de0.false'
data2:
$ref: '#/definitions/2049.0xc0000edbc0.false'
$ref: '#/definitions/2060.0xc000380e10.false'
msg:
type: string
name:
@ -1880,6 +1968,10 @@ definitions:
title: Cert
type: object
properties:
authorityPublicKey:
type: string
authorityRootPublicKey:
type: string
bitSize:
type: integer
format: int64
@ -1912,6 +2004,39 @@ definitions:
type: string
value:
type: string
object.IntrospectionResponse:
title: IntrospectionResponse
type: object
properties:
active:
type: boolean
aud:
type: array
items:
type: string
client_id:
type: string
exp:
type: integer
format: int64
iat:
type: integer
format: int64
iss:
type: string
jti:
type: string
nbf:
type: integer
format: int64
scope:
type: string
sub:
type: string
token_type:
type: string
username:
type: string
object.Organization:
title: Organization
type: object
@ -1938,21 +2063,25 @@ definitions:
type: string
phonePrefix:
type: string
tags:
type: array
items:
type: string
websiteUrl:
type: string
object.Payment:
title: Payment
type: object
properties:
amount:
type: string
createdTime:
type: string
currency:
type: string
detail:
type: string
displayName:
type: string
good:
message:
type: string
name:
type: string
@ -1960,10 +2089,23 @@ definitions:
type: string
owner:
type: string
payUrl:
type: string
price:
type: number
format: double
productDisplayName:
type: string
productName:
type: string
provider:
type: string
returnUrl:
type: string
state:
type: string
tag:
type: string
type:
type: string
user:
@ -2021,8 +2163,8 @@ definitions:
owner:
type: string
price:
type: integer
format: int64
type: number
format: double
providers:
type: array
items:
@ -2030,6 +2172,8 @@ definitions:
quantity:
type: integer
format: int64
returnUrl:
type: string
sold:
type: integer
format: int64
@ -2047,6 +2191,8 @@ definitions:
type: string
category:
type: string
cert:
type: string
clientId:
type: string
clientId2:
@ -2296,6 +2442,8 @@ definitions:
type: string
birthday:
type: string
casdoor:
type: string
createdIp:
type: string
createdTime:

View File

@ -303,7 +303,7 @@ class ProviderEditPage extends React.Component {
)
}
{
this.state.provider.type !== "Adfs" ? null : (
this.state.provider.type !== "Adfs" && this.state.provider.type !== "Casdoor" ? null : (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :

View File

@ -404,6 +404,7 @@ export function getProviderTypeOptions(category) {
{id: 'GitLab', name: 'GitLab'},
{id: 'Adfs', name: 'Adfs'},
{id: 'Baidu', name: 'Baidu'},
{id: 'Casdoor', name: 'Casdoor'},
{id: 'Infoflow', name: 'Infoflow'},
{id: 'Apple', name: 'Apple'},
{id: 'AzureAD', name: 'AzureAD'},

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/casdoor.svg`} alt="Sign in with Casdoor" style={{width: 24, height: 24}} />;
}
const config = {
text: "Sign in with Casdoor",
icon: Icon,
iconFormat: name => `fa fa-${name}`,
style: {background: "#ffffff", color: "#000000"},
activeStyle: {background: "#ededee"},
};
const CasdoorLoginButton = createButton(config);
export default CasdoorLoginButton;

View File

@ -36,6 +36,7 @@ import LarkLoginButton from "./LarkLoginButton";
import GitLabLoginButton from "./GitLabLoginButton";
import AdfsLoginButton from "./AdfsLoginButton";
import BaiduLoginButton from "./BaiduLoginButton";
import CasdoorLoginButton from "./CasdoorLoginButton";
import InfoflowLoginButton from "./InfoflowLoginButton";
import AppleLoginButton from "./AppleLoginButton"
import AzureADLoginButton from "./AzureADLoginButton";
@ -198,6 +199,8 @@ class LoginPage extends React.Component {
return <GitLabLoginButton text={text} align={"center"} />
} else if (type === "Adfs") {
return <AdfsLoginButton text={text} align={"center"} />
} else if (type === "Casdoor") {
return <CasdoorLoginButton text={text} align={"center"} />
} else if (type === "Baidu") {
return <BaiduLoginButton text={text} align={"center"} />
} else if (type === "Infoflow") {

View File

@ -78,6 +78,10 @@ const authInfo = {
scope: "basic",
endpoint: "http://openapi.baidu.com/oauth/2.0/authorize",
},
Casdoor: {
scope: "openid%20profile%20email",
endpoint: "http://example.com",
},
Infoflow: {
endpoint: "https://xpc.im.baidu.com/oauth2/authorize",
},
@ -283,6 +287,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 === "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"){
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}?state=${state}`
} else if (provider.type === "Apple") {