diff --git a/idp/bilibili.go b/idp/bilibili.go new file mode 100644 index 000000000000..aaba436239aa --- /dev/null +++ b/idp/bilibili.go @@ -0,0 +1,220 @@ +// Copyright 2021 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" + "strings" + "time" + + "golang.org/x/oauth2" +) + +type BilibiliIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewBilibiliIdProvider(clientId string, clientSecret string, redirectUrl string) *BilibiliIdProvider { + idp := &BilibiliIdProvider{} + + config := idp.getConfig(clientId, clientSecret, redirectUrl) + idp.Config = config + + return idp +} + +func (idp *BilibiliIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow +func (idp *BilibiliIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config { + var endpoint = oauth2.Endpoint{ + TokenURL: "https://api.bilibili.com/x/account-oauth2/v1/token", + AuthURL: "http://member.bilibili.com/arcopen/fn/user/account/info", + } + + var config = &oauth2.Config{ + Scopes: []string{"", ""}, + Endpoint: endpoint, + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: redirectUrl, + } + + return config +} + +type BilibiliProviderToken struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` +} + +type BilibiliIdProviderTokenResponse struct { + Code int `json:"code"` + Message string `json:"message"` + TTL int `json:"ttl"` + Data BilibiliProviderToken `json:"data"` +} + +/* +{ + "code": 0, + "message": "0", + "ttl": 1, + "data": { + "access_token": "d30bedaa4d8eb3128cf35ddc1030e27d", + "expires_in": 1630220614, + "refresh_token": "WxFDKwqScZIQDm4iWmKDvetyFugM6HkX" + } +} +*/ +// GetToken use code get access_token (*operation of getting code ought to be done in front) +// get more detail via: https://openhome.bilibili.com/doc/4/eaf0e2b5-bde9-b9a0-9be1-019bb455701c +func (idp *BilibiliIdProvider) GetToken(code string) (*oauth2.Token, error) { + pTokenParams := &struct { + ClientId string `json:"client_id"` + ClientSecret string `json:"client_secret"` + GrantType string `json:"grant_type"` + Code string `json:"code"` + }{ + idp.Config.ClientID, + idp.Config.ClientSecret, + "authorization_code", + code, + } + + data, err := idp.postWithBody(pTokenParams, idp.Config.Endpoint.TokenURL) + + if err != nil { + return nil, err + } + + response := &BilibiliIdProviderTokenResponse{} + err = json.Unmarshal(data, response) + if err != nil { + return nil, err + } + + if response.Code != 0 { + return nil, fmt.Errorf("pToken.Errcode = %d, pToken.Errmsg = %s", response.Code, response.Message) + } + + token := &oauth2.Token{ + AccessToken: response.Data.AccessToken, + Expiry: time.Unix(time.Now().Unix()+int64(response.Data.ExpiresIn), 0), + RefreshToken: response.Data.RefreshToken, + } + + return token, nil +} + +/* +{ + "code": 0, + "message": "0", + "ttl": 1, + "data": { + "name":"bilibili", + "face":"http://i0.hdslb.com/bfs/face/e1c99895a9f9df4f260a70dc7e227bcb46cf319c.jpg", + "openid":"9205eeaa1879skxys969ed47874f225c3" + } +} +*/ + +type BilibiliUserInfo struct { + Name string `json:"name"` + Face string `json:"face"` + OpenId string `json:"openid` +} + +type BilibiliUserInfoResponse struct { + Code int `json:"code"` + Message string `json:"message"` + TTL int `json:"ttl"` + Data BilibiliUserInfo `json:"data"` +} + +// GetUserInfo Use access_token to get UserInfo +// get more detail via: https://openhome.bilibili.com/doc/4/feb66f99-7d87-c206-00e7-d84164cd701c +func (idp *BilibiliIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + accessToken := token.AccessToken + clientId := idp.Config.ClientID + + params := url.Values{} + params.Add("client_id", clientId) + params.Add("access_token", accessToken) + + userInfoUrl := fmt.Sprintf("%s?%s", idp.Config.Endpoint.AuthURL, params.Encode()) + + resp, err := idp.Client.Get(userInfoUrl) + + if err != nil { + return nil, err + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + bUserInfoResponse := &BilibiliUserInfoResponse{} + if err = json.Unmarshal(data, bUserInfoResponse); err != nil { + return nil, err + } + + if bUserInfoResponse.Code != 0 { + return nil, fmt.Errorf("userinfo.Errcode = %d, userinfo.Errmsg = %s", bUserInfoResponse.Code, bUserInfoResponse.Message) + } + + userInfo := &UserInfo{ + Id: bUserInfoResponse.Data.OpenId, + Username: bUserInfoResponse.Data.Name, + AvatarUrl: bUserInfoResponse.Data.Face, + } + + return userInfo, nil +} + +func (idp *BilibiliIdProvider) postWithBody(body interface{}, url string) ([]byte, error) { + bs, err := json.Marshal(body) + if err != nil { + return nil, err + } + r := strings.NewReader(string(bs)) + resp, err := idp.Client.Post(url, "application/json;charset=UTF-8", r) + if err != nil { + return nil, err + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + + return data, nil +} diff --git a/idp/provider.go b/idp/provider.go index cd5e76c1fb27..a29fc536eab1 100644 --- a/idp/provider.go +++ b/idp/provider.go @@ -88,6 +88,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str return NewOktaIdProvider(clientId, clientSecret, redirectUrl, hostUrl) } else if isGothSupport(typ) { return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl) + } else if typ == "Bilibili" { + return NewBilibiliIdProvider(clientId, clientSecret, redirectUrl) } return nil diff --git a/object/user.go b/object/user.go index ef3c0f6f48af..79a90969a559 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"` + Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"` Okta string `xorm:"okta varchar(100)" json:"okta"` Custom string `xorm:"custom varchar(100)" json:"custom"` diff --git a/web/src/Setting.js b/web/src/Setting.js index 17b155f5d931..1f108a4bed5f 100644 --- a/web/src/Setting.js +++ b/web/src/Setting.js @@ -554,6 +554,7 @@ export function getProviderTypeOptions(category) { {id: 'AzureAD', name: 'AzureAD'}, {id: 'Slack', name: 'Slack'}, {id: 'Steam', name: 'Steam'}, + {id: 'Bilibili', name: 'Bilibili'}, {id: 'Okta', name: 'Okta'}, {id: 'Custom', name: 'Custom'}, ] diff --git a/web/src/auth/BilibiliLoginButton.js b/web/src/auth/BilibiliLoginButton.js new file mode 100644 index 000000000000..3fa69141f5cb --- /dev/null +++ b/web/src/auth/BilibiliLoginButton.js @@ -0,0 +1,32 @@ +// Copyright 2021 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 Bilibili; +} + +const config = { + text: "Sign in with Bilibili", + icon: Icon, + iconFormat: name => `fa fa-${name}`, + style: {background: "#0191e0"}, + activeStyle: {background: "rgb(76,143,208)"}, +}; + +const BilibiliLoginButton = createButton(config); + +export default BilibiliLoginButton; diff --git a/web/src/auth/LoginPage.js b/web/src/auth/LoginPage.js index 0c2613803459..46f65e24a142 100644 --- a/web/src/auth/LoginPage.js +++ b/web/src/auth/LoginPage.js @@ -46,6 +46,7 @@ import SteamLoginButton from "./SteamLoginButton"; import OktaLoginButton from "./OktaLoginButton"; import CustomGithubCorner from "../CustomGithubCorner"; import {CountDownInput} from "../common/CountDownInput"; +import BilibiliLoginButton from "./BilibiliLoginButton"; class LoginPage extends React.Component { constructor(props) { @@ -279,6 +280,8 @@ class LoginPage extends React.Component { return } else if (type === "Steam") { return + } else if (type === "Bilibili") { + return } else if (type === "Okta") { return } diff --git a/web/src/auth/Provider.js b/web/src/auth/Provider.js index 428f1dc9daec..4170c7dbaa4c 100644 --- a/web/src/auth/Provider.js +++ b/web/src/auth/Provider.js @@ -114,6 +114,9 @@ const authInfo = { Custom: { endpoint: "https://example.com/", }, + Bilibili: { + endpoint: "https://passport.bilibili.com/register/pc_oauth2.html" + } }; export function getProviderUrl(provider) { @@ -238,5 +241,7 @@ export function getAuthUrl(application, provider, method) { return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`; } else if (provider.type === "Custom") { return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.customScope}&response_type=code&state=${state}`; - } + } else if (provider.type === "Bilibili") { + return `${endpoint}#/?client_id=${provider.clientId}&return_url=${redirectUri}&state=${state}&response_type=code` + } }