From 287f60353cd88309a9bd5e2290d199c412400e8a Mon Sep 17 00:00:00 2001 From: halozhy Date: Sat, 16 Apr 2022 17:17:45 +0800 Subject: [PATCH] feat: try to support custom OAuth provider (#667) * feat: try to support private provider * fix: modify code according to code review * feat: set example values for custom params --- controllers/auth.go | 2 +- idp/custom.go | 108 +++++++++++++++++++++++++++++++++++ idp/provider.go | 4 +- object/provider.go | 25 ++++---- object/user.go | 1 + web/src/ProviderEditPage.js | 79 +++++++++++++++++++++++++ web/src/Setting.js | 1 + web/src/auth/Provider.js | 8 +++ web/src/locales/zh/data.json | 8 +++ 9 files changed, 224 insertions(+), 12 deletions(-) create mode 100644 idp/custom.go diff --git a/controllers/auth.go b/controllers/auth.go index 6b70397a..f465c396 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -283,7 +283,7 @@ func (c *ApiController) Login() { clientSecret = provider.ClientSecret2 } - idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri, provider.Domain) + idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri, provider.Domain, provider.CustomAuthUrl, provider.CustomTokenUrl, provider.CustomUserInfoUrl) if idProvider == nil { c.ResponseError(fmt.Sprintf("The provider type: %s is not supported", provider.Type)) return diff --git a/idp/custom.go b/idp/custom.go new file mode 100644 index 00000000..7f40cda8 --- /dev/null +++ b/idp/custom.go @@ -0,0 +1,108 @@ +// Copyright 2022 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + _ "net/url" + _ "time" + + "golang.org/x/oauth2" +) + +type CustomIdProvider struct { + Client *http.Client + Config *oauth2.Config + UserInfoUrl string +} + +func NewCustomIdProvider(clientId string, clientSecret string, redirectUrl string, authUrl string, tokenUrl string, userInfoUrl string) *CustomIdProvider { + idp := &CustomIdProvider{} + idp.UserInfoUrl = userInfoUrl + + var config = &oauth2.Config{ + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: redirectUrl, + Endpoint: oauth2.Endpoint{ + AuthURL: authUrl, + TokenURL: tokenUrl, + }, + } + idp.Config = config + + return idp +} + +func (idp *CustomIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +func (idp *CustomIdProvider) GetToken(code string) (*oauth2.Token, error) { + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, idp.Client) + return idp.Config.Exchange(ctx, code) +} + +type CustomUserInfo struct { + Id string `json:"sub"` + Name string `json:"name"` + DisplayName string `json:"preferred_username"` + Email string `json:"email"` + AvatarUrl string `json:"picture"` + Status string `json:"status"` + Msg string `json:"msg"` +} + +func (idp *CustomIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + ctUserinfo := &CustomUserInfo{} + accessToken := token.AccessToken + request, err := http.NewRequest("GET", idp.UserInfoUrl, nil) + if err != nil { + return nil, err + } + //add accessToken to request header + request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + resp, err := idp.Client.Do(request) + if err != nil { + return nil, err + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(data, ctUserinfo) + if err != nil { + return nil, err + } + + if ctUserinfo.Status != "" { + return nil, fmt.Errorf("err: %s", ctUserinfo.Msg) + } + + userInfo := &UserInfo{ + Id: ctUserinfo.Id, + Username: ctUserinfo.Name, + DisplayName: ctUserinfo.DisplayName, + Email: ctUserinfo.Email, + AvatarUrl: ctUserinfo.AvatarUrl, + } + return userInfo, nil +} diff --git a/idp/provider.go b/idp/provider.go index 7f5689cb..360605a5 100644 --- a/idp/provider.go +++ b/idp/provider.go @@ -35,7 +35,7 @@ type IdProvider interface { GetUserInfo(token *oauth2.Token) (*UserInfo, error) } -func GetIdProvider(typ string, subType string, clientId string, clientSecret string, appId string, redirectUrl string, hostUrl string) IdProvider { +func GetIdProvider(typ string, subType string, clientId string, clientSecret string, appId string, redirectUrl string, hostUrl string, authUrl string, tokenUrl string, userInfoUrl string) IdProvider { if typ == "GitHub" { return NewGithubIdProvider(clientId, clientSecret, redirectUrl) } else if typ == "Google" { @@ -72,6 +72,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str return NewBaiduIdProvider(clientId, clientSecret, redirectUrl) } else if typ == "Alipay" { return NewAlipayIdProvider(clientId, clientSecret, redirectUrl) + } else if typ == "Custom" { + return NewCustomIdProvider(clientId, clientSecret, redirectUrl, authUrl, tokenUrl, userInfoUrl) } else if typ == "Infoflow" { if subType == "Internal" { return NewInfoflowInternalIdProvider(clientId, clientSecret, appId, redirectUrl) diff --git a/object/provider.go b/object/provider.go index 2a403a55..a41dc52c 100644 --- a/object/provider.go +++ b/object/provider.go @@ -27,16 +27,21 @@ type Provider struct { Name string `xorm:"varchar(100) notnull pk" json:"name"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"` - DisplayName string `xorm:"varchar(100)" json:"displayName"` - Category string `xorm:"varchar(100)" json:"category"` - Type string `xorm:"varchar(100)" json:"type"` - SubType string `xorm:"varchar(100)" json:"subType"` - Method string `xorm:"varchar(100)" json:"method"` - ClientId string `xorm:"varchar(100)" json:"clientId"` - ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"` - ClientId2 string `xorm:"varchar(100)" json:"clientId2"` - ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"` - Cert string `xorm:"varchar(100)" json:"cert"` + DisplayName string `xorm:"varchar(100)" json:"displayName"` + Category string `xorm:"varchar(100)" json:"category"` + Type string `xorm:"varchar(100)" json:"type"` + SubType string `xorm:"varchar(100)" json:"subType"` + Method string `xorm:"varchar(100)" json:"method"` + ClientId string `xorm:"varchar(100)" json:"clientId"` + ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"` + ClientId2 string `xorm:"varchar(100)" json:"clientId2"` + ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"` + Cert string `xorm:"varchar(100)" json:"cert"` + CustomAuthUrl string `xorm:"varchar(200)" json:"customAuthUrl"` + CustomScope string `xorm:"varchar(200)" json:"customScope"` + CustomTokenUrl string `xorm:"varchar(200)" json:"customTokenUrl"` + CustomUserInfoUrl string `xorm:"varchar(200)" json:"customUserInfoUrl"` + CustomLogo string `xorm:"varchar(200)" json:"customLogo"` Host string `xorm:"varchar(100)" json:"host"` Port int `json:"port"` diff --git a/object/user.go b/object/user.go index e75aa688..d874213f 100644 --- a/object/user.go +++ b/object/user.go @@ -94,6 +94,7 @@ type User struct { AzureAD string `xorm:"azuread varchar(100)" json:"azuread"` Slack string `xorm:"slack varchar(100)" json:"slack"` Steam string `xorm:"steam varchar(100)" json:"steam"` + Custom string `xorm:"custom varchar(100)" json:"custom"` Ldap string `xorm:"ldap varchar(100)" json:"ldap"` Properties map[string]string `json:"properties"` diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js index e85cd385..825fbd48 100644 --- a/web/src/ProviderEditPage.js +++ b/web/src/ProviderEditPage.js @@ -212,6 +212,12 @@ class ProviderEditPage extends React.Component { if (value === "Local File System") { this.updateProviderField('domain', Setting.getFullServerUrl()); } + if (value === "Custom") { + this.updateProviderField('customAuthUrl', 'https://door.casdoor.com/login/oauth/authorize'); + this.updateProviderField('customScope', 'openid profile email'); + this.updateProviderField('customTokenUrl', 'https://door.casdoor.com/api/login/oauth/access_token'); + this.updateProviderField('customUserInfoUrl', 'https://door.casdoor.com/api/userinfo'); + } })}> { Setting.getProviderTypeOptions(this.state.provider.category).map((providerType, index) => ) @@ -256,6 +262,79 @@ class ProviderEditPage extends React.Component { ) } + { + this.state.provider.type !== "Custom" ? null : ( + + + + {Setting.getLabel(i18next.t("provider:Auth URL"), i18next.t("provider:Auth URL - Tooltip"))} + + + { + this.updateProviderField('customAuthUrl', e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))} + + + { + this.updateProviderField('customScope', e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("provider:Token URL"), i18next.t("provider:Token URL - Tooltip"))} + + + { + this.updateProviderField('customTokenUrl', e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("provider:UserInfo URL"), i18next.t("provider:UserInfo URL - Tooltip"))} + + + { + this.updateProviderField('customUserInfoUrl', e.target.value); + }} /> + + + + + {Setting.getLabel( i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} : + + + + + {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : + + + } value={this.state.provider.customLogo} onChange={e => { + this.updateProviderField('customLogo', e.target.value); + }} /> + + + + + {i18next.t("general:Preview")}: + + + + {this.state.provider.customLogo} + + + + + + + ) + } {this.getClientIdLabel()} diff --git a/web/src/Setting.js b/web/src/Setting.js index 977105c2..3b40287a 100644 --- a/web/src/Setting.js +++ b/web/src/Setting.js @@ -414,6 +414,7 @@ export function getProviderTypeOptions(category) { {id: 'AzureAD', name: 'AzureAD'}, {id: 'Slack', name: 'Slack'}, {id: 'Steam', name: 'Steam'}, + {id: 'Custom', name: 'Custom'}, ] ); } else if (category === "Email") { diff --git a/web/src/auth/Provider.js b/web/src/auth/Provider.js index 401d600b..a34cbc6d 100644 --- a/web/src/auth/Provider.js +++ b/web/src/auth/Provider.js @@ -107,6 +107,9 @@ const authInfo = { Steam: { endpoint: "https://steamcommunity.com/openid/login", }, + Custom: { + endpoint: "https://example.com/", + }, }; const otherProviderInfo = { @@ -184,6 +187,9 @@ const otherProviderInfo = { export function getProviderLogo(provider) { if (provider.category === "OAuth") { + if (provider.type === "Custom") { + return provider.customLogo; + } return `${Setting.StaticBaseUrl}/img/social_${provider.type.toLowerCase()}.png`; } else { return otherProviderInfo[provider.category][provider.type].logo; @@ -308,5 +314,7 @@ export function getAuthUrl(application, provider, method) { return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`; } else if (provider.type === "Steam") { return `${endpoint}?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=${window.location.origin}&openid.return_to=${redirectUri}?state=${state}`; + } else if (provider.type === "Custom") { + return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.customScope}&response_type=code&state=${state}`; } } diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json index 1bdd7b74..8643c38b 100644 --- a/web/src/locales/zh/data.json +++ b/web/src/locales/zh/data.json @@ -333,6 +333,8 @@ "Agent ID - Tooltip": "Agent ID - Tooltip", "App ID": "App ID", "App ID - Tooltip": "App ID - Tooltip", + "Auth URL": "Auth URL", + "Auth URL - Tooltip": "Auth URL - 工具提示", "Bucket": "存储桶", "Bucket - Tooltip": "Bucket名称", "Can not parse Metadata": "无法解析元数据", @@ -380,6 +382,8 @@ "Region endpoint for Internet": "地域节点 (外网)", "Region endpoint for Intranet": "地域节点 (内网)", "SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpoint (HTTP)", + "Scope": "Scope", + "Scope - Tooltip": "Scope - 工具提示", "SMS account": "SMS account", "SMS account - Tooltip": "SMS account - Tooltip", "SP ACS URL": "SP ACS URL", @@ -403,8 +407,12 @@ "Template Code - Tooltip": "模板CODE", "Terms of Use": "使用条款", "Terms of Use - Tooltip": "使用条款 - 工具提示", + "Token URL": "Token URL", + "Token URL - Tooltip": "Token URL - 工具提示", "Type": "类型", "Type - Tooltip": "类型", + "UserInfo URL": "UserInfo URL", + "UserInfo URL - Tooltip": "UserInfo URL - 工具提示", "alertType": "警报类型", "canSignIn": "canSignIn", "canSignUp": "canSignUp",