mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 02:35:49 +08:00
feat: add Alipay support as idp (#638)
* feat: add alipay support as idp * fix: rename a static svg icon * fix: sort imports * fix: no longer use pkcs8 package
This commit is contained in:
parent
29aa379fb2
commit
8d13bf7e27
1
go.sum
1
go.sum
@ -381,6 +381,7 @@ golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220208233918-bba287dce954 h1:BkypuErRT9A9I/iljuaG3/zdMjd/J6m8tKKJQtGfSdA=
|
||||
|
292
idp/alipay.go
Normal file
292
idp/alipay.go
Normal file
@ -0,0 +1,292 @@
|
||||
// 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 (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type AlipayIdProvider struct {
|
||||
Client *http.Client
|
||||
Config *oauth2.Config
|
||||
}
|
||||
|
||||
// NewAlipayIdProvider ...
|
||||
func NewAlipayIdProvider(clientId string, clientSecret string, redirectUrl string) *AlipayIdProvider {
|
||||
idp := &AlipayIdProvider{}
|
||||
|
||||
config := idp.getConfig(clientId, clientSecret, redirectUrl)
|
||||
idp.Config = config
|
||||
|
||||
return idp
|
||||
}
|
||||
|
||||
// SetHttpClient ...
|
||||
func (idp *AlipayIdProvider) SetHttpClient(client *http.Client) {
|
||||
idp.Client = client
|
||||
}
|
||||
|
||||
// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow
|
||||
func (idp *AlipayIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
|
||||
var endpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://openauth.alipay.com/oauth2/publicAppAuthorize.htm",
|
||||
TokenURL: "https://openapi.alipay.com/gateway.do",
|
||||
}
|
||||
|
||||
var config = &oauth2.Config{
|
||||
Scopes: []string{"", ""},
|
||||
Endpoint: endpoint,
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: redirectUrl,
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
type AlipayAccessToken struct {
|
||||
Response AlipaySystemOauthTokenResponse `json:"alipay_system_oauth_token_response"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
type AlipaySystemOauthTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
AlipayUserId string `json:"alipay_user_id"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
ReExpiresIn int `json:"re_expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
UserId string `json:"user_id"`
|
||||
}
|
||||
|
||||
// GetToken use code to get access_token
|
||||
func (idp *AlipayIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||
pTokenParams := &struct {
|
||||
ClientId string `json:"app_id"`
|
||||
CharSet string `json:"charset"`
|
||||
Code string `json:"code"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Method string `json:"method"`
|
||||
SignType string `json:"sign_type"`
|
||||
TimeStamp string `json:"timestamp"`
|
||||
Version string `json:"version"`
|
||||
}{idp.Config.ClientID, "utf-8", code, "authorization_code", "alipay.system.oauth.token", "RSA2", time.Now().Format("2006-01-02 15:04:05"), "1.0"}
|
||||
|
||||
data, err := idp.postWithBody(pTokenParams, idp.Config.Endpoint.TokenURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pToken := &AlipayAccessToken{}
|
||||
err = json.Unmarshal(data, pToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: pToken.Response.AccessToken,
|
||||
Expiry: time.Unix(time.Now().Unix()+int64(pToken.Response.ExpiresIn), 0),
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"alipay_user_info_share_response":{
|
||||
"code":"10000",
|
||||
"msg":"Success",
|
||||
"avatar":"https:\/\/tfs.alipayobjects.com\/images\/partner\/T1.QxFXk4aXXXXXXXX",
|
||||
"nick_name":"zhangsan",
|
||||
"user_id":"2099222233334444"
|
||||
},
|
||||
"sign":"m8rWJeqfoa5tDQRRVnPhRHcpX7NZEgjIPTPF1QBxos6XXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
}
|
||||
*/
|
||||
|
||||
type AlipayUserResponse struct {
|
||||
AlipayUserInfoShareResponse AlipayUserInfoShareResponse `json:"alipay_user_info_share_response"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
type AlipayUserInfoShareResponse struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Avatar string `json:"avatar"`
|
||||
NickName string `json:"nick_name"`
|
||||
UserId string `json:"user_id"`
|
||||
}
|
||||
|
||||
// GetUserInfo Use access_token to get UserInfo
|
||||
func (idp *AlipayIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||
atUserInfo := &AlipayUserResponse{}
|
||||
accessToken := token.AccessToken
|
||||
|
||||
pTokenParams := &struct {
|
||||
ClientId string `json:"app_id"`
|
||||
CharSet string `json:"charset"`
|
||||
AuthToken string `json:"auth_token"`
|
||||
Method string `json:"method"`
|
||||
SignType string `json:"sign_type"`
|
||||
TimeStamp string `json:"timestamp"`
|
||||
Version string `json:"version"`
|
||||
}{idp.Config.ClientID, "utf-8", accessToken, "alipay.user.info.share", "RSA2", time.Now().Format("2006-01-02 15:04:05"), "1.0"}
|
||||
data, err := idp.postWithBody(pTokenParams, idp.Config.Endpoint.TokenURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, atUserInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userInfo := UserInfo{
|
||||
Id: atUserInfo.AlipayUserInfoShareResponse.UserId,
|
||||
Username: atUserInfo.AlipayUserInfoShareResponse.NickName,
|
||||
DisplayName: atUserInfo.AlipayUserInfoShareResponse.NickName,
|
||||
AvatarUrl: atUserInfo.AlipayUserInfoShareResponse.Avatar,
|
||||
}
|
||||
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
func (idp *AlipayIdProvider) postWithBody(body interface{}, targetUrl string) ([]byte, error) {
|
||||
bs, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bodyJson := make(map[string]interface{})
|
||||
err = json.Unmarshal(bs, &bodyJson)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
formData := url.Values{}
|
||||
for k := range bodyJson {
|
||||
formData.Set(k, bodyJson[k].(string))
|
||||
}
|
||||
|
||||
sign, err := rsaSignWithRSA256(getStringToSign(formData), idp.Config.ClientSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
formData.Set("sign", sign)
|
||||
|
||||
resp, err := idp.Client.PostForm(targetUrl, formData)
|
||||
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
|
||||
}
|
||||
|
||||
// get the string to sign, see https://opendocs.alipay.com/common/02kf5q
|
||||
func getStringToSign(formData url.Values) string {
|
||||
keys := make([]string, 0, len(formData))
|
||||
for k := range formData {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
str := ""
|
||||
for _, k := range keys {
|
||||
if k == "sign" || formData[k][0] == "" {
|
||||
continue
|
||||
} else {
|
||||
str += "&" + k + "=" + formData[k][0]
|
||||
}
|
||||
}
|
||||
str = strings.Trim(str, "&")
|
||||
return str
|
||||
}
|
||||
|
||||
// use privateKey to sign the content
|
||||
func rsaSignWithRSA256(signContent string, privateKey string) (string, error) {
|
||||
privateKey = formatPrivateKey(privateKey)
|
||||
block, _ := pem.Decode([]byte(privateKey))
|
||||
if block == nil {
|
||||
panic("fail to parse privateKey")
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(signContent))
|
||||
hashed := h.Sum(nil)
|
||||
|
||||
privateKeyRSA, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKeyRSA.(*rsa.PrivateKey), crypto.SHA256, hashed)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// privateKey in database is a string, format it to PEM style
|
||||
func formatPrivateKey(privateKey string) string {
|
||||
// each line length is 64
|
||||
preFmtPrivateKey := ""
|
||||
for i := 0; ; {
|
||||
if i+64 <= len(privateKey) {
|
||||
preFmtPrivateKey = preFmtPrivateKey + privateKey[i:i+64] + "\n"
|
||||
i += 64
|
||||
} else {
|
||||
preFmtPrivateKey = preFmtPrivateKey + privateKey[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
privateKey = strings.Trim(preFmtPrivateKey, "\n")
|
||||
|
||||
// add pkcs#8 BEGIN and END
|
||||
PemBegin := "-----BEGIN PRIVATE KEY-----\n"
|
||||
PemEnd := "\n-----END PRIVATE KEY-----"
|
||||
if !strings.HasPrefix(privateKey, PemBegin) {
|
||||
privateKey = PemBegin + privateKey
|
||||
}
|
||||
if !strings.HasSuffix(privateKey, PemEnd) {
|
||||
privateKey = privateKey + PemEnd
|
||||
}
|
||||
return privateKey
|
||||
}
|
@ -70,6 +70,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
|
||||
return NewAdfsIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
|
||||
} else if typ == "Baidu" {
|
||||
return NewBaiduIdProvider(clientId, clientSecret, redirectUrl)
|
||||
} else if typ == "Alipay" {
|
||||
return NewAlipayIdProvider(clientId, clientSecret, redirectUrl)
|
||||
} else if typ == "Infoflow" {
|
||||
if subType == "Internal" {
|
||||
return NewInfoflowInternalIdProvider(clientId, clientSecret, appId, redirectUrl)
|
||||
|
@ -33,7 +33,7 @@ type Provider struct {
|
||||
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(100)" json:"clientSecret"`
|
||||
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"`
|
||||
|
@ -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"`
|
||||
Alipay string `xorm:"alipay varchar(100)" json:"alipay"`
|
||||
Casdoor string `xorm:"casdoor varchar(100)" json:"casdoor"`
|
||||
Infoflow string `xorm:"infoflow varchar(100)" json:"infoflow"`
|
||||
Apple string `xorm:"apple varchar(100)" json:"apple"`
|
||||
|
@ -402,6 +402,7 @@ export function getProviderTypeOptions(category) {
|
||||
{id: 'GitLab', name: 'GitLab'},
|
||||
{id: 'Adfs', name: 'Adfs'},
|
||||
{id: 'Baidu', name: 'Baidu'},
|
||||
{id: 'Alipay', name: 'Alipay'},
|
||||
{id: 'Casdoor', name: 'Casdoor'},
|
||||
{id: 'Infoflow', name: 'Infoflow'},
|
||||
{id: 'Apple', name: 'Apple'},
|
||||
|
32
web/src/auth/AlipayLoginButton.js
Normal file
32
web/src/auth/AlipayLoginButton.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/alipay.svg`} alt="Sign in with Alipay" style={{width: 24, height: 24}} />;
|
||||
}
|
||||
|
||||
const config = {
|
||||
text: "Sign in with Alipay",
|
||||
icon: Icon,
|
||||
iconFormat: name => `fa fa-${name}`,
|
||||
style: {background: "#ffffff", color: "#000000"},
|
||||
activeStyle: {background: "#ededee"},
|
||||
};
|
||||
|
||||
const AlipayLoginButton = createButton(config);
|
||||
|
||||
export default AlipayLoginButton;
|
@ -36,6 +36,7 @@ import LarkLoginButton from "./LarkLoginButton";
|
||||
import GitLabLoginButton from "./GitLabLoginButton";
|
||||
import AdfsLoginButton from "./AdfsLoginButton";
|
||||
import BaiduLoginButton from "./BaiduLoginButton";
|
||||
import AlipayLoginButton from "./AlipayLoginButton";
|
||||
import CasdoorLoginButton from "./CasdoorLoginButton";
|
||||
import InfoflowLoginButton from "./InfoflowLoginButton";
|
||||
import AppleLoginButton from "./AppleLoginButton"
|
||||
@ -206,6 +207,8 @@ class LoginPage extends React.Component {
|
||||
return <CasdoorLoginButton text={text} align={"center"} />
|
||||
} else if (type === "Baidu") {
|
||||
return <BaiduLoginButton text={text} align={"center"} />
|
||||
} else if (type === "Alipay") {
|
||||
return <AlipayLoginButton text={text} align={"center"} />
|
||||
} else if (type === "Infoflow") {
|
||||
return <InfoflowLoginButton text={text} align={"center"} />
|
||||
} else if (type === "Apple") {
|
||||
|
@ -78,6 +78,10 @@ const authInfo = {
|
||||
scope: "basic",
|
||||
endpoint: "http://openapi.baidu.com/oauth/2.0/authorize",
|
||||
},
|
||||
Alipay: {
|
||||
scope: "basic",
|
||||
endpoint: "https://openauth.alipay.com/oauth2/publicAppAuthorize.htm",
|
||||
},
|
||||
Casdoor: {
|
||||
scope: "openid%20profile%20email",
|
||||
endpoint: "http://example.com",
|
||||
@ -287,6 +291,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 === "Alipay") {
|
||||
return `${endpoint}?app_id=${provider.clientId}&scope=auth_user&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"){
|
||||
|
Loading…
x
Reference in New Issue
Block a user