mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-16 05:33:50 +08:00
feat: add Douyin OAuth provider (#753)
This commit is contained in:
198
idp/douyin.go
Normal file
198
idp/douyin.go
Normal 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
|
||||||
|
}
|
@ -86,6 +86,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
|
|||||||
return NewCasdoorIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
|
return NewCasdoorIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
|
||||||
} else if typ == "Okta" {
|
} else if typ == "Okta" {
|
||||||
return NewOktaIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
|
return NewOktaIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
|
||||||
|
} else if typ == "Douyin" {
|
||||||
|
return NewDouyinIdProvider(clientId, clientSecret, redirectUrl)
|
||||||
} else if isGothSupport(typ) {
|
} else if isGothSupport(typ) {
|
||||||
return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl)
|
return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl)
|
||||||
} else if typ == "Bilibili" {
|
} else if typ == "Bilibili" {
|
||||||
|
@ -96,6 +96,7 @@ type User struct {
|
|||||||
Steam string `xorm:"steam varchar(100)" json:"steam"`
|
Steam string `xorm:"steam varchar(100)" json:"steam"`
|
||||||
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
|
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
|
||||||
Okta string `xorm:"okta varchar(100)" json:"okta"`
|
Okta string `xorm:"okta varchar(100)" json:"okta"`
|
||||||
|
Douyin string `xorm:"douyin vachar(100)" json:"douyin"`
|
||||||
Custom string `xorm:"custom varchar(100)" json:"custom"`
|
Custom string `xorm:"custom varchar(100)" json:"custom"`
|
||||||
|
|
||||||
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
|
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
|
||||||
|
@ -556,6 +556,7 @@ export function getProviderTypeOptions(category) {
|
|||||||
{id: 'Steam', name: 'Steam'},
|
{id: 'Steam', name: 'Steam'},
|
||||||
{id: 'Bilibili', name: 'Bilibili'},
|
{id: 'Bilibili', name: 'Bilibili'},
|
||||||
{id: 'Okta', name: 'Okta'},
|
{id: 'Okta', name: 'Okta'},
|
||||||
|
{id: 'Douyin', name: 'Douyin'},
|
||||||
{id: 'Custom', name: 'Custom'},
|
{id: 'Custom', name: 'Custom'},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
32
web/src/auth/DouyinLoginButton.js
Normal file
32
web/src/auth/DouyinLoginButton.js
Normal 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;
|
@ -44,6 +44,7 @@ import AzureADLoginButton from "./AzureADLoginButton";
|
|||||||
import SlackLoginButton from "./SlackLoginButton";
|
import SlackLoginButton from "./SlackLoginButton";
|
||||||
import SteamLoginButton from "./SteamLoginButton";
|
import SteamLoginButton from "./SteamLoginButton";
|
||||||
import OktaLoginButton from "./OktaLoginButton";
|
import OktaLoginButton from "./OktaLoginButton";
|
||||||
|
import DouyinLoginButton from "./DouyinLoginButton";
|
||||||
import CustomGithubCorner from "../CustomGithubCorner";
|
import CustomGithubCorner from "../CustomGithubCorner";
|
||||||
import {CountDownInput} from "../common/CountDownInput";
|
import {CountDownInput} from "../common/CountDownInput";
|
||||||
import BilibiliLoginButton from "./BilibiliLoginButton";
|
import BilibiliLoginButton from "./BilibiliLoginButton";
|
||||||
@ -284,6 +285,8 @@ class LoginPage extends React.Component {
|
|||||||
return <BilibiliLoginButton text={text} align={"center"} />
|
return <BilibiliLoginButton text={text} align={"center"} />
|
||||||
} else if (type === "Okta") {
|
} else if (type === "Okta") {
|
||||||
return <OktaLoginButton text={text} align={"center"} />
|
return <OktaLoginButton text={text} align={"center"} />
|
||||||
|
} else if (type === "Douyin") {
|
||||||
|
return <DouyinLoginButton text={text} align={"center"} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
|
@ -111,6 +111,10 @@ const authInfo = {
|
|||||||
scope: "openid%20profile%20email",
|
scope: "openid%20profile%20email",
|
||||||
endpoint: "http://example.com",
|
endpoint: "http://example.com",
|
||||||
},
|
},
|
||||||
|
Douyin: {
|
||||||
|
scope: "user_info",
|
||||||
|
endpoint: "https://open.douyin.com/platform/oauth/connect",
|
||||||
|
},
|
||||||
Custom: {
|
Custom: {
|
||||||
endpoint: "https://example.com/",
|
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}`;
|
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") {
|
} else if (provider.type === "Okta") {
|
||||||
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
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") {
|
} else if (provider.type === "Custom") {
|
||||||
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.customScope}&response_type=code&state=${state}`;
|
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.customScope}&response_type=code&state=${state}`;
|
||||||
} else if (provider.type === "Bilibili") {
|
} else if (provider.type === "Bilibili") {
|
||||||
|
Reference in New Issue
Block a user