Skip to content

Commit

Permalink
migrate DingTalk OAuth to new API and verify corpid
Browse files Browse the repository at this point in the history
  • Loading branch information
janusec2 committed Mar 23, 2024
1 parent 81e84eb commit a158962
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 17 deletions.
14 changes: 7 additions & 7 deletions data/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ func InitDefaultSettings() {
if !DAL.ExistsSetting("dingtalk_appsecret") {
DAL.SaveStringSetting("dingtalk_appsecret", "crrALdXUIj4T0zBekYh4u9sU_T1GZT")
}
if !DAL.ExistsSetting("dingtalk_corpid") {
// added on Mar 23, 2024
DAL.SaveStringSetting("dingtalk_corpid", "xxxx")
}
// AuthConfig feishu
if !DAL.ExistsSetting("feishu_display_name") {
DAL.SaveStringSetting("feishu_display_name", "Login with Feishu")
Expand Down Expand Up @@ -495,11 +499,13 @@ func GetDingtalkConfig() *models.DingtalkConfig {
callback := DAL.SelectStringSetting("dingtalk_callback")
appID := DAL.SelectStringSetting("dingtalk_appid")
appSecret := DAL.SelectStringSetting("dingtalk_appsecret")
corpID := DAL.SelectStringSetting("dingtalk_corpid")
dingtalkConfig := &models.DingtalkConfig{
DisplayName: displayName,
Callback: callback,
AppID: appID,
AppSecret: appSecret,
CorpID: corpID,
}
return dingtalkConfig
}
Expand All @@ -515,17 +521,11 @@ func UpdateDingtalkConfig(body []byte, clientIP string, authUser *models.AuthUse
return nil, err
}
dingtalkConfig := rpcDingtalkConfigRequest.Object
/*
dingtalkConfig := param["object"].(map[string]interface{})
displayName := dingtalkConfig["display_name"].(string)
callback := dingtalkConfig["callback"].(string)
appid := dingtalkConfig["appid"].(string)
appsecret := dingtalkConfig["appsecret"].(string)
*/
DAL.SaveStringSetting("dingtalk_display_name", dingtalkConfig.DisplayName)
DAL.SaveStringSetting("dingtalk_callback", dingtalkConfig.Callback)
DAL.SaveStringSetting("dingtalk_appid", dingtalkConfig.AppID)
DAL.SaveStringSetting("dingtalk_appsecret", dingtalkConfig.AppSecret)
DAL.SaveStringSetting("dingtalk_corpid", dingtalkConfig.CorpID)

NodeSetting.AuthConfig.Dingtalk = dingtalkConfig
go utils.OperationLog(clientIP, authUser.Username, "Update Dingtalk Config", dingtalkConfig.DisplayName)
Expand Down
7 changes: 7 additions & 0 deletions gateway/auth_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,16 @@ func GetOAuthInfo() (*OAuthInfo, error) {
oauthInfo.EntranceURL = entranceURL
return &oauthInfo, nil
case "dingtalk":
/* API v1
entranceURL := fmt.Sprintf("https://oapi.dingtalk.com/connect/qrconnect?appid=%s&response_type=code&scope=snsapi_login&state=admin&redirect_uri=%s",
data.NodeSetting.AuthConfig.Dingtalk.AppID,
data.NodeSetting.AuthConfig.Dingtalk.Callback)
*/
// API v2, added on Mar 23, 2024
entranceURL := fmt.Sprintf(`https://login.dingtalk.com/oauth2/auth?redirect_uri=%s&response_type=code&corpId=%s&client_id=%s&scope=openid%%20corpid&state=admin&prompt=consent`,
data.NodeSetting.AuthConfig.Dingtalk.Callback,
data.NodeSetting.AuthConfig.Dingtalk.CorpID,
data.NodeSetting.AuthConfig.Dingtalk.AppID)
oauthInfo.UseOAuth = true
oauthInfo.DisplayName = data.NodeSetting.AuthConfig.Dingtalk.DisplayName
oauthInfo.EntranceURL = entranceURL
Expand Down
20 changes: 19 additions & 1 deletion gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -577,10 +577,28 @@ func getOAuthEntrance(state string) (entranceURL string, err error) {
data.NodeSetting.AuthConfig.Wxwork.Callback,
state)
case "dingtalk":
entranceURL = fmt.Sprintf("https://oapi.dingtalk.com/connect/qrconnect?appid=%s&response_type=code&scope=snsapi_login&state=%s&redirect_uri=%s",
/* This is the api v1
entranceURL = fmt.Sprintf("https://oapi.dingtalk.com/connect/qrconnect?appid=%s&response_type=code&scope=snsapi_login&state=%s&redirect_uri=%s",
data.NodeSetting.AuthConfig.Dingtalk.AppID,
state,
data.NodeSetting.AuthConfig.Dingtalk.Callback)
*/
// API V2, added on Mar 23, 2024
// doc: https://open.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information
// Entrance URL Format: https://login.dingtalk.com/oauth2/auth?
// redirect_uri=https://test.janusec.com/oauth/dingtalk
// &response_type=code
// &client_id=...
// &scope=openid
// &state=...
// &prompt=consent
// &corpId=...
entranceURL = fmt.Sprintf(`https://login.dingtalk.com/oauth2/auth?redirect_uri=%s&response_type=code&corpId=%s&client_id=%s&scope=openid%%20corpid&state=%s&prompt=consent`,
data.NodeSetting.AuthConfig.Dingtalk.Callback,
data.NodeSetting.AuthConfig.Dingtalk.CorpID,
data.NodeSetting.AuthConfig.Dingtalk.AppID,
state)

case "feishu":
entranceURL = fmt.Sprintf("https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=%s&app_id=%s&state=%s",
data.NodeSetting.AuthConfig.Feishu.Callback,
Expand Down
3 changes: 3 additions & 0 deletions models/data_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ type DingtalkConfig struct {
Callback string `json:"callback"`
AppID string `json:"appid"`
AppSecret string `json:"appsecret"`

// CorpID for API v2, added on Mar 23, 2024
CorpID string `json:"corpid"`
}

type FeishuConfig struct {
Expand Down
133 changes: 124 additions & 9 deletions usermgmt/oauth_dingtalk.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,10 @@
package usermgmt

import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"strings"

"janusec/data"
"janusec/models"
Expand All @@ -27,18 +21,25 @@ import (
"github.com/patrickmn/go-cache"
)

type DingtalkResponse struct {
// DingtalkResponse V1
/*
type DingtalkResponseV1 struct {
ErrCode int64 `json:"errcode"`
ErrMsg string `json:"errmsg"`
UserInfo DingtalkUserInfo `json:"user_info"`
}
*/

// DingtalkUserInfo V1 & V2
// Doc: https://open.dingtalk.com/document/orgapp/dingtalk-retrieve-user-information
type DingtalkUserInfo struct {
Nick string `json:"nick"`
OpenID string `json:"openid"`
UnionID string `json:"unionid"`
}

/*
// GetSignature for API v1
func GetSignature(msg []byte, key []byte) string {
hmac := hmac.New(sha256.New, key)
_, err := hmac.Write(msg)
Expand All @@ -48,11 +49,23 @@ func GetSignature(msg []byte, key []byte) string {
digest := hmac.Sum(nil)
return url.QueryEscape(base64.StdEncoding.EncodeToString(digest))
}
*/

// AccessToken Response V2, added on Mar 23, 2024
// Doc: https://open.dingtalk.com/document/orgapp/obtain-user-token#h2-hxj-mpf-5bd
type AccessTokenResp struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpireIn int64 `json:"expireIn"`
CorpId string `json:"corpId"`
}

// This is the API v1, instead by v2
// https://ding-doc.dingtalk.com/doc#/serverapi3/mrugr3
// Step 1: To https://oapi.dingtalk.com/connect/qrconnect?appid=APPID&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI
// If state==admin, for janusec-admin; else for frontend applications
func DingtalkCallbackWithCode(w http.ResponseWriter, r *http.Request) {
/*
func DingtalkCallbackWithCodeV1(w http.ResponseWriter, r *http.Request) {
// Step 2.1: Callback with code, https://gate.janusec.com/janusec-admin/oauth/dingtalk?code=BM8k8U6RwtQtNY&state=admin
code := r.FormValue("code")
state := r.FormValue("state")
Expand Down Expand Up @@ -129,3 +142,105 @@ func DingtalkCallbackWithCode(w http.ResponseWriter, r *http.Request) {
//fmt.Println("Time expired")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
*/

// This is the API v2, added on Mar 23, 2024
// Doc: https://open.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information
// Step 1: https://login.dingtalk.com/oauth2/auth?redirect_uri=https://.../oauth/dingtalk&response_type=code&client_id=...&scope=openid&state=...&prompt=consent
// If state==admin, for janusec-admin; else for frontend applications
func DingtalkCallbackWithCode(w http.ResponseWriter, r *http.Request) {
// Step 2.1: Callback with code, GET
// https://test.janusec.com/oauth/dingtalk?authCode=18025489140734ecb0ccf637ee5439f9&state=...
authCode := r.FormValue("authCode")
state := r.FormValue("state")
// Step 2.2: Within Callback, acquire token
// Doc: https://open.dingtalk.com/document/orgapp/obtain-user-token
// POST https://api.dingtalk.com/v1.0/oauth2/userAccessToken
// body { "clientId": "...AppKey", "clientSecret": "...AppSecret", "code": "...authCode", "grantType": "authorization_code"}
accessTokenURL := `https://api.dingtalk.com/v1.0/oauth2/userAccessToken`
body := fmt.Sprintf(`{"clientId":"%s","clientSecret":"%s","code":"%s","grantType": "authorization_code"}`,
data.NodeSetting.AuthConfig.Dingtalk.AppID,
data.NodeSetting.AuthConfig.Dingtalk.AppSecret,
authCode)
request, _ := http.NewRequest("POST", accessTokenURL, strings.NewReader(body))
request.Header.Set("Content-Type", "application/json")
resp, err := utils.GetResponse(request)
if err != nil {
utils.DebugPrintln("DingtalkCallbackWithCode GetResponse accessToken", err)
}
accessTokenResp := AccessTokenResp{}
err = json.Unmarshal(resp, &accessTokenResp)
if err != nil {
utils.DebugPrintln("DingtalkCallbackWithCode json.Unmarshal AccessTokenResp error", err)
}
// check wether the corpid is valid
if accessTokenResp.CorpId != data.NodeSetting.AuthConfig.Dingtalk.CorpID {
w.WriteHeader(403)
w.Write([]byte("Error: You are not a member of the corporation now!"))
return
}

// Step 2.3: Get UserInfo
// Doc: https://open.dingtalk.com/document/orgapp/dingtalk-retrieve-user-information
// GET https://api.dingtalk.com/v1.0/contact/users/{unionId}
// Header x-acs-dingtalk-access-token:String
request, _ = http.NewRequest("GET", `https://api.dingtalk.com/v1.0/contact/users/me`, nil)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("x-acs-dingtalk-access-token", accessTokenResp.AccessToken)
resp, err = utils.GetResponse(request)
if err != nil {
utils.DebugPrintln("DingtalkCallbackWithCode GetResponse userinfo", err)
}
dingtalkUser := DingtalkUserInfo{}
err = json.Unmarshal(resp, &dingtalkUser)
if err != nil {
utils.DebugPrintln("DingtalkCallbackWithCode json.Unmarshal userinfo error", err)
}
if state == "admin" {
appUser := data.DAL.SelectAppUserByName(dingtalkUser.Nick)
var userID int64
if appUser == nil {
// Insert into db if not existed
userID, err = data.DAL.InsertIfNotExistsAppUser(dingtalkUser.Nick, "", "", "", false, false, false, false)
if err != nil {
w.WriteHeader(403)
w.Write([]byte("Error: " + err.Error()))
return
}
} else {
userID = appUser.ID
}
// create session
authUser := &models.AuthUser{
UserID: userID,
Username: dingtalkUser.Nick,
Logged: true,
IsSuperAdmin: appUser.IsSuperAdmin,
IsCertAdmin: appUser.IsCertAdmin,
IsAppAdmin: appUser.IsAppAdmin,
NeedModifyPWD: false}
session, _ := store.Get(r, "sessionid")
session.Values["authuser"] = authUser
session.Options = &sessions.Options{Path: "/janusec-admin/", MaxAge: 86400}
err = session.Save(r, w)
if err != nil {
utils.DebugPrintln("DingtalkCallbackWithCode session save error", err)
}
RecordAuthLog(r, authUser.Username, "DingTalk", data.CFG.PrimaryNode.Admin.Portal)
http.Redirect(w, r, data.CFG.PrimaryNode.Admin.Portal, http.StatusTemporaryRedirect)
return
}
// Gateway OAuth for employees and internal application
oauthStateI, found := OAuthCache.Get(state)
if found {
oauthState := oauthStateI.(models.OAuthState)
oauthState.UserID = dingtalkUser.Nick
oauthState.AccessToken = "N/A"
OAuthCache.Set(state, oauthState, cache.DefaultExpiration)
RecordAuthLog(r, oauthState.UserID, "DingTalk", oauthState.CallbackURL)
http.Redirect(w, r, oauthState.CallbackURL, http.StatusTemporaryRedirect)
return
}
//fmt.Println("Time expired")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}

0 comments on commit a158962

Please sign in to comment.