feat: add Douyin OAuth provider (#753)

This commit is contained in:
greenhandatsjtu 2022-05-15 20:59:21 +08:00 committed by GitHub
parent e5b71a08ae
commit 98f77960de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 243 additions and 0 deletions

198
idp/douyin.go Normal file
View File

@ -0,0 +1,198 @@
// 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 (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"golang.org/x/oauth2"
)
type DouyinIdProvider struct {
Client *http.Client
Config *oauth2.Config
}
func NewDouyinIdProvider(clientId string, clientSecret string, redirectUrl string) *DouyinIdProvider {
idp := &DouyinIdProvider{}
idp.Config = idp.getConfig(clientId, clientSecret, redirectUrl)
return idp
}
func (idp *DouyinIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
func (idp *DouyinIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
var endpoint = oauth2.Endpoint{
TokenURL: "https://open.douyin.com/oauth/access_token",
AuthURL: "https://open.douyin.com/platform/oauth/connect",
}
var config = &oauth2.Config{
Scopes: []string{"user_info"},
Endpoint: endpoint,
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
}
return config
}
// get more details via: https://open.douyin.com/platform/doc?doc=docs/openapi/account-permission/get-access-token
/*
{
"data": {
"access_token": "access_token",
"description": "",
"error_code": "0",
"expires_in": "86400",
"open_id": "aaa-bbb-ccc",
"refresh_expires_in": "86400",
"refresh_token": "refresh_token",
"scope": "user_info"
},
"message": "<nil>"
}
*/
type DouyinTokenResp struct {
Data struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
OpenId string `json:"open_id"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
} `json:"data"`
Message string `json:"message"`
}
// GetToken use code to get access_token
// get more details via: https://open.douyin.com/platform/doc?doc=docs/openapi/account-permission/get-access-token
func (idp *DouyinIdProvider) GetToken(code string) (*oauth2.Token, error) {
payload := url.Values{}
payload.Set("code", code)
payload.Set("grant_type", "authorization_code")
payload.Set("client_key", idp.Config.ClientID)
payload.Set("client_secret", idp.Config.ClientSecret)
resp, err := idp.Client.PostForm(idp.Config.Endpoint.TokenURL, payload)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
tokenResp := &DouyinTokenResp{}
err = json.Unmarshal(data, tokenResp)
if err != nil {
return nil, fmt.Errorf("fail to unmarshal token response: %s", err.Error())
}
token := &oauth2.Token{
AccessToken: tokenResp.Data.AccessToken,
RefreshToken: tokenResp.Data.RefreshToken,
Expiry: time.Unix(time.Now().Unix()+tokenResp.Data.ExpiresIn, 0),
}
raw := make(map[string]interface{})
raw["open_id"] = tokenResp.Data.OpenId
token = token.WithExtra(raw)
return token, nil
}
// get more details via: https://open.douyin.com/platform/doc?doc=docs/openapi/account-management/get-account-open-info
/*
{
"data": {
"avatar": "https://example.com/x.jpeg",
"city": "上海",
"country": "中国",
"description": "",
"e_account_role": "<nil>",
"error_code": "0",
"gender": "<nil>",
"nickname": "张伟",
"open_id": "0da22181-d833-447f-995f-1beefea5bef3",
"province": "上海",
"union_id": "1ad4e099-4a0c-47d1-a410-bffb4f2f64a4"
}
}
*/
type DouyinUserInfo struct {
Data struct {
Avatar string `json:"avatar"`
City string `json:"city"`
Country string `json:"country"`
// 0->unknown, 1->male, 2->female
Gender int64 `json:"gender"`
Nickname string `json:"nickname"`
OpenId string `json:"open_id"`
Province string `json:"province"`
} `json:"data"`
}
// GetUserInfo use token to get user profile
func (idp *DouyinIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
body := &struct {
AccessToken string `json:"access_token"`
OpenId string `json:"open_id"`
}{token.AccessToken, token.Extra("open_id").(string)}
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", "https://open.douyin.com/oauth/userinfo/", bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Add("access-token", token.AccessToken)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
resp, err := idp.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var douyinUserInfo DouyinUserInfo
err = json.Unmarshal(respBody, &douyinUserInfo)
if err != nil {
return nil, err
}
userInfo := UserInfo{
Id: douyinUserInfo.Data.OpenId,
Username: douyinUserInfo.Data.Nickname,
DisplayName: douyinUserInfo.Data.Nickname,
AvatarUrl: douyinUserInfo.Data.Avatar,
}
return &userInfo, nil
}

View File

@ -86,6 +86,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
return NewCasdoorIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
} else if typ == "Okta" {
return NewOktaIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
} else if typ == "Douyin" {
return NewDouyinIdProvider(clientId, clientSecret, redirectUrl)
} else if isGothSupport(typ) {
return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl)
} else if typ == "Bilibili" {

View File

@ -96,6 +96,7 @@ type User struct {
Steam string `xorm:"steam varchar(100)" json:"steam"`
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
Okta string `xorm:"okta varchar(100)" json:"okta"`
Douyin string `xorm:"douyin vachar(100)" json:"douyin"`
Custom string `xorm:"custom varchar(100)" json:"custom"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`

View File

@ -556,6 +556,7 @@ export function getProviderTypeOptions(category) {
{id: 'Steam', name: 'Steam'},
{id: 'Bilibili', name: 'Bilibili'},
{id: 'Okta', name: 'Okta'},
{id: 'Douyin', name: 'Douyin'},
{id: 'Custom', name: 'Custom'},
]
);

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

View File

@ -44,6 +44,7 @@ import AzureADLoginButton from "./AzureADLoginButton";
import SlackLoginButton from "./SlackLoginButton";
import SteamLoginButton from "./SteamLoginButton";
import OktaLoginButton from "./OktaLoginButton";
import DouyinLoginButton from "./DouyinLoginButton";
import CustomGithubCorner from "../CustomGithubCorner";
import {CountDownInput} from "../common/CountDownInput";
import BilibiliLoginButton from "./BilibiliLoginButton";
@ -284,6 +285,8 @@ class LoginPage extends React.Component {
return <BilibiliLoginButton text={text} align={"center"} />
} else if (type === "Okta") {
return <OktaLoginButton text={text} align={"center"} />
} else if (type === "Douyin") {
return <DouyinLoginButton text={text} align={"center"} />
}
return text;

View File

@ -111,6 +111,10 @@ const authInfo = {
scope: "openid%20profile%20email",
endpoint: "http://example.com",
},
Douyin: {
scope: "user_info",
endpoint: "https://open.douyin.com/platform/oauth/connect",
},
Custom: {
endpoint: "https://example.com/",
},
@ -239,6 +243,8 @@ export function getAuthUrl(application, provider, method) {
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 === "Okta") {
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Douyin") {
return `${endpoint}?client_key=${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") {