From e8b9c6767177ce3bfb5fbe9184052827515cf850 Mon Sep 17 00:00:00 2001 From: Yi Zhan Date: Fri, 18 Mar 2022 18:28:46 +0800 Subject: [PATCH] feat: add casdoor as itself idp support (#578) Signed-off-by: Steve0x2a --- idp/casdoor.go | 159 +++++++++++++++++++ idp/provider.go | 2 + object/user.go | 1 + swagger/swagger.json | 247 +++++++++++++++++++++++++++-- swagger/swagger.yml | 172 ++++++++++++++++++-- web/src/ProviderEditPage.js | 2 +- web/src/Setting.js | 1 + web/src/auth/CasdoorLoginButton.js | 32 ++++ web/src/auth/LoginPage.js | 3 + web/src/auth/Provider.js | 6 + 10 files changed, 599 insertions(+), 26 deletions(-) create mode 100644 idp/casdoor.go create mode 100644 web/src/auth/CasdoorLoginButton.js diff --git a/idp/casdoor.go b/idp/casdoor.go new file mode 100644 index 00000000..290459a9 --- /dev/null +++ b/idp/casdoor.go @@ -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 + +} diff --git a/idp/provider.go b/idp/provider.go index 8142c151..74261c6e 100644 --- a/idp/provider.go +++ b/idp/provider.go @@ -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) } diff --git a/object/user.go b/object/user.go index 350188c9..3a62717a 100644 --- a/object/user.go +++ b/object/user.go @@ -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"` diff --git a/swagger/swagger.json b/swagger/swagger.json index 469fd907..0af4890f 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -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" }, diff --git a/swagger/swagger.yml b/swagger/swagger.yml index 26e4da6b..ff55e2ce 100644 --- a/swagger/swagger.yml +++ b/swagger/swagger.yml @@ -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: diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js index 8993a0ef..e85cd385 100644 --- a/web/src/ProviderEditPage.js +++ b/web/src/ProviderEditPage.js @@ -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 : ( {Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} : diff --git a/web/src/Setting.js b/web/src/Setting.js index 7bdbd02d..75ef8a37 100644 --- a/web/src/Setting.js +++ b/web/src/Setting.js @@ -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'}, diff --git a/web/src/auth/CasdoorLoginButton.js b/web/src/auth/CasdoorLoginButton.js new file mode 100644 index 00000000..4e609c4f --- /dev/null +++ b/web/src/auth/CasdoorLoginButton.js @@ -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 Sign in with Casdoor; +} + +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; diff --git a/web/src/auth/LoginPage.js b/web/src/auth/LoginPage.js index b1ddf6ab..96d6bf30 100644 --- a/web/src/auth/LoginPage.js +++ b/web/src/auth/LoginPage.js @@ -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 } else if (type === "Adfs") { return + } else if (type === "Casdoor") { + return } else if (type === "Baidu") { return } else if (type === "Infoflow") { diff --git a/web/src/auth/Provider.js b/web/src/auth/Provider.js index 727d6f06..f75a71e7 100644 --- a/web/src/auth/Provider.js +++ b/web/src/auth/Provider.js @@ -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") {