feat:CAS and OAuth2 login (#1236)

* Feat(cas login):Add CAS login

Signed-off-by: root <foursevenlove@gmail.com>

* Fix(CAS login):1.print logs of CAS Authentication Response's Attributes 2.modify fileds of ssoClient and CAS config.

Signed-off-by: root <foursevenlove@gmail.com>

* Fix(CAS login):Fields modifing

Signed-off-by: root <foursevenlove@gmail.com>

* Feat(OAuth Login):1.Add OAuth2 login 2.Add display name

Signed-off-by: root <foursevenlove@gmail.com>

* Fix(webapi.conf):Add example

Signed-off-by: root <foursevenlove@gmail.com>

* fix(webapi.conf):Modify default value of username in OAuth2

Signed-off-by: root <foursevenlove@gmail.com>

* Fix:Error handling

Signed-off-by: root <foursevenlove@gmail.com>

Signed-off-by: root <foursevenlove@gmail.com>
This commit is contained in:
47 2022-11-02 14:31:59 +08:00 committed by GitHub
parent 65d8f80637
commit 352415662a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 640 additions and 3 deletions

View File

@ -149,6 +149,7 @@ Email = "mail"
[OIDC]
Enable = false
DiaplayName = "OIDC登录"
RedirectURL = "http://n9e.com/callback"
SsoAddr = "http://sso.example.org"
ClientId = ""
@ -161,6 +162,52 @@ Nickname = "nickname"
Phone = "phone_number"
Email = "email"
[CAS]
Enable = false
DiaplayName = "CAS登录"
SsoAddr = "https://cas.example.com/cas/"
RedirectURL = "http://127.0.0.1:18000/callback/cas"
CoverAttributes = false
# cas user default roles
DefaultRoles = ["Standard"]
[CAS.Attributes]
Nickname = "nickname"
Phone = "phone_number"
Email = "email"
[OAuth]
Enable = false
DisplayName = "OAuth2登录"
RedirectURL = "http://127.0.0.1:18000/callback/oauth"
SsoAddr = "https://sso.example.com/oauth2/authorize"
TokenAddr = "https://sso.example.com/oauth2/token"
UserInfoAddr = "https://api.example.com/api/v1/user/info"
ClientId = ""
ClientSecret = ""
CoverAttributes = true
DefaultRoles = ["Standard"]
UserinfoIsArray = false
UserinfoPrefix = "data"
Scopes = ["profile", "email", "phone"]
[OAuth.Attributes]
# Username must be defined
Username = "username"
Nickname = "nickname"
Phone = "phone_number"
Email = "email"
# example
# # nested : UserinfoIsArray=false, UserinfoPrefix="data"
# # {"data":{"username":"123456","nickname":"姓名"},"code":0,"message":"ok"}
# # nested and array : UserinfoIsArray=true, UserinfoPrefix="data"
# # {"data":[{"username":"123456","nickname":"姓名"}],"code":0,"message":"ok"}
# # flat : UserinfoIsArray=false, UserinfoPrefix=""
# # {"username":"123456","nickname":"姓名"}
# # flat and array : UserinfoIsArray=true, UserinfoPrefix=""
# # [{"username":"123456","nickname":"姓名"}]
[Redis]
# address, ip:port or ip1:port,ip2:port for cluster and sentinel(SentinelAddrs)
Address = "127.0.0.1:6379"

2
go.mod
View File

@ -23,7 +23,7 @@ require (
github.com/prometheus/common v0.32.1
github.com/prometheus/prometheus v2.5.0+incompatible
github.com/tidwall/gjson v1.14.0
github.com/toolkits/pkg v1.2.9
github.com/toolkits/pkg v1.3.1-0.20220824084030-9f9f830a05d5
github.com/urfave/cli/v2 v2.3.0
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df

4
go.sum
View File

@ -368,8 +368,8 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/toolkits/pkg v1.2.9 h1:zGlrJDl+2sMBoxBRIoMtAwvKmW5wctuji2+qHCecMKk=
github.com/toolkits/pkg v1.2.9/go.mod h1:ZUsQAOoaR99PSbes+RXSirvwmtd6+XIUvizCmrjfUYc=
github.com/toolkits/pkg v1.3.1-0.20220824084030-9f9f830a05d5 h1:kMCwr2gNHjHEVgw+uNVdiPbGadj4TekbIfrTXElZeI0=
github.com/toolkits/pkg v1.3.1-0.20220824084030-9f9f830a05d5/go.mod h1:PvTBg/UxazPgBz6VaCM7FM7kJldjfVrsuN6k4HT/VuY=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=

150
src/pkg/cas/cas.go Normal file
View File

@ -0,0 +1,150 @@
package cas
import (
"bytes"
"context"
"net/url"
"strings"
"time"
"github.com/didi/nightingale/v5/src/storage"
"github.com/google/uuid"
"github.com/toolkits/pkg/cas"
"github.com/toolkits/pkg/logger"
)
type Config struct {
Enable bool
SsoAddr string
RedirectURL string
DisplayName string
CoverAttributes bool
Attributes struct {
Nickname string
Phone string
Email string
}
DefaultRoles []string
}
type ssoClient struct {
config Config
ssoAddr string
callbackAddr string
displayName string
attributes struct {
nickname string
phone string
email string
}
}
var (
cli ssoClient
)
func Init(cf Config) {
if !cf.Enable {
return
}
cli = ssoClient{}
cli.config = cf
cli.ssoAddr = cf.SsoAddr
cli.callbackAddr = cf.RedirectURL
cli.displayName = cf.DisplayName
cli.attributes.nickname = cf.Attributes.Nickname
cli.attributes.phone = cf.Attributes.Phone
cli.attributes.email = cf.Attributes.Email
}
func GetDisplayName() string {
return cli.displayName
}
// Authorize return the cas authorize location and state
func Authorize(redirect string) (string, string, error) {
state := uuid.New().String()
ctx := context.Background()
err := storage.Redis.Set(ctx, wrapStateKey(state), redirect, time.Duration(300*time.Second)).Err()
if err != nil {
return "", "", err
}
return cli.genRedirectURL(state), state, nil
}
func fetchRedirect(ctx context.Context, state string) (string, error) {
return storage.Redis.Get(ctx, wrapStateKey(state)).Result()
}
func deleteRedirect(ctx context.Context, state string) error {
return storage.Redis.Del(ctx, wrapStateKey(state)).Err()
}
func wrapStateKey(key string) string {
return "n9e_cas_" + key
}
func (cli *ssoClient) genRedirectURL(state string) string {
var buf bytes.Buffer
buf.WriteString(cli.ssoAddr + "login")
v := url.Values{
"service": {cli.callbackAddr},
}
if strings.Contains(cli.ssoAddr, "?") {
buf.WriteByte('&')
} else {
buf.WriteByte('?')
}
buf.WriteString(v.Encode())
return buf.String()
}
type CallbackOutput struct {
Redirect string `json:"redirect"`
Msg string `json:"msg"`
AccessToken string `json:"accessToken"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Phone string `yaml:"phone"`
Email string `yaml:"email"`
}
func ValidateServiceTicket(ctx context.Context, ticket, state string) (ret *CallbackOutput, err error) {
casUrl, err := url.Parse(cli.config.SsoAddr)
if err != nil {
logger.Error(err)
return
}
serviceUrl, err := url.Parse(cli.callbackAddr)
if err != nil {
logger.Error(err)
return
}
resOptions := &cas.RestOptions{
CasURL: casUrl,
ServiceURL: serviceUrl,
}
resCli := cas.NewRestClient(resOptions)
authRet, err := resCli.ValidateServiceTicket(cas.ServiceTicket(ticket))
if err != nil {
logger.Errorf("Ticket Validating Failed: %s", err)
return
}
ret = &CallbackOutput{}
ret.Username = authRet.User
ret.Nickname = authRet.Attributes.Get(cli.attributes.nickname)
logger.Debugf("CAS Authentication Response's Attributes--[Nickname]: %s", ret.Nickname)
ret.Email = authRet.Attributes.Get(cli.attributes.email)
logger.Debugf("CAS Authentication Response's Attributes--[Email]: %s", ret.Email)
ret.Phone = authRet.Attributes.Get(cli.attributes.phone)
logger.Debugf("CAS Authentication Response's Attributes--[Phone]: %s", ret.Phone)
ret.Redirect, err = fetchRedirect(ctx, state)
if err != nil {
logger.Debugf("get redirect err:%s state:%s", state, err)
}
err = deleteRedirect(ctx, state)
if err != nil {
logger.Debugf("delete redirect err:%s state:%s", state, err)
}
return
}

205
src/pkg/oauth2x/oauth2x.go Normal file
View File

@ -0,0 +1,205 @@
package oauth2x
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/didi/nightingale/v5/src/storage"
"github.com/toolkits/pkg/logger"
"github.com/google/uuid"
jsoniter "github.com/json-iterator/go"
"golang.org/x/oauth2"
)
type ssoClient struct {
config oauth2.Config
ssoAddr string
userInfoAddr string
callbackAddr string
displayName string
coverAttributes bool
attributes struct {
username string
nickname string
phone string
email string
}
userinfoIsArray bool
userinfoPrefix string
}
type Config struct {
Enable bool
DisplayName string
RedirectURL string
SsoAddr string
TokenAddr string
UserInfoAddr string
ClientId string
ClientSecret string
CoverAttributes bool
Attributes struct {
Username string
Nickname string
Phone string
Email string
}
DefaultRoles []string
UserinfoIsArray bool
UserinfoPrefix string
Scopes []string
}
var (
cli ssoClient
)
func Init(cf Config) {
if !cf.Enable {
return
}
cli.ssoAddr = cf.SsoAddr
cli.userInfoAddr = cf.UserInfoAddr
cli.callbackAddr = cf.RedirectURL
cli.displayName = cf.DisplayName
cli.coverAttributes = cf.CoverAttributes
cli.attributes.username = cf.Attributes.Username
cli.attributes.nickname = cf.Attributes.Nickname
cli.attributes.phone = cf.Attributes.Phone
cli.attributes.email = cf.Attributes.Email
cli.userinfoIsArray = cf.UserinfoIsArray
cli.userinfoPrefix = cf.UserinfoPrefix
cli.config = oauth2.Config{
ClientID: cf.ClientId,
ClientSecret: cf.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: cf.SsoAddr,
TokenURL: cf.TokenAddr,
},
RedirectURL: cf.RedirectURL,
Scopes: cf.Scopes,
}
}
func GetDisplayName() string {
return cli.displayName
}
func wrapStateKey(key string) string {
return "n9e_oauth_" + key
}
// Authorize return the sso authorize location with state
func Authorize(redirect string) (string, error) {
state := uuid.New().String()
ctx := context.Background()
err := storage.Redis.Set(ctx, wrapStateKey(state), redirect, time.Duration(300*time.Second)).Err()
if err != nil {
return "", err
}
return cli.config.AuthCodeURL(state), nil
}
func fetchRedirect(ctx context.Context, state string) (string, error) {
return storage.Redis.Get(ctx, wrapStateKey(state)).Result()
}
func deleteRedirect(ctx context.Context, state string) error {
return storage.Redis.Del(ctx, wrapStateKey(state)).Err()
}
// Callback 用 code 兑换 accessToken 以及 用户信息
func Callback(ctx context.Context, code, state string) (*CallbackOutput, error) {
ret, err := exchangeUser(code)
if err != nil {
return nil, fmt.Errorf("ilegal user:%v", err)
}
ret.Redirect, err = fetchRedirect(ctx, state)
if err != nil {
logger.Errorf("get redirect err:%v code:%s state:%s", code, state, err)
}
err = deleteRedirect(ctx, state)
if err != nil {
logger.Errorf("delete redirect err:%v code:%s state:%s", code, state, err)
}
return ret, nil
}
type CallbackOutput struct {
Redirect string `json:"redirect"`
Msg string `json:"msg"`
AccessToken string `json:"accessToken"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Phone string `yaml:"phone"`
Email string `yaml:"email"`
}
func exchangeUser(code string) (*CallbackOutput, error) {
ctx := context.Background()
oauth2Token, err := cli.config.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("failed to exchange token: %s", err)
}
userInfo, err := getUserInfo(cli.userInfoAddr, oauth2Token.AccessToken)
if err != nil {
logger.Errorf("failed to get user info: %s", err)
return nil, fmt.Errorf("failed to get user info: %s", err)
}
return &CallbackOutput{
AccessToken: oauth2Token.AccessToken,
Username: getUserinfoField(userInfo, cli.userinfoIsArray, cli.userinfoPrefix, cli.attributes.username),
Nickname: getUserinfoField(userInfo, cli.userinfoIsArray, cli.userinfoPrefix, cli.attributes.nickname),
Phone: getUserinfoField(userInfo, cli.userinfoIsArray, cli.userinfoPrefix, cli.attributes.phone),
Email: getUserinfoField(userInfo, cli.userinfoIsArray, cli.userinfoPrefix, cli.attributes.email),
}, nil
}
func getUserInfo(userInfoAddr, accessToken string) ([]byte, error) {
r, err := http.NewRequest("GET", userInfoAddr, nil)
if err != nil {
return nil, err
}
r.Header.Add("Authorization", "Bearer "+accessToken)
resp, err := http.DefaultClient.Do(r)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, nil
}
return body, err
}
func getUserinfoField(input []byte, isArray bool, prefix, field string) string {
if prefix == "" {
if isArray {
return jsoniter.Get(input, 0).Get(field).ToString()
} else {
return jsoniter.Get(input, field).ToString()
}
} else {
if isArray {
return jsoniter.Get(input, prefix, 0).Get(field).ToString()
} else {
return jsoniter.Get(input, prefix).Get(field).ToString()
}
}
}

View File

@ -20,6 +20,7 @@ type ssoClient struct {
ssoAddr string
callbackAddr string
coverAttributes bool
displayName string
attributes struct {
username string
nickname string
@ -30,6 +31,7 @@ type ssoClient struct {
type Config struct {
Enable bool
DisplayName string
RedirectURL string
SsoAddr string
ClientId string
@ -59,6 +61,7 @@ func Init(cf Config) {
cli.attributes.nickname = cf.Attributes.Nickname
cli.attributes.phone = cf.Attributes.Phone
cli.attributes.email = cf.Attributes.Email
cli.displayName = cf.DisplayName
provider, err := oidc.NewProvider(context.Background(), cf.SsoAddr)
if err != nil {
log.Fatal(err)
@ -77,6 +80,10 @@ func Init(cf Config) {
}
}
func GetDisplayName() string {
return cli.displayName
}
func wrapStateKey(key string) string {
return "n9e_oidc_" + key
}

View File

@ -9,9 +9,11 @@ import (
"github.com/gin-gonic/gin"
"github.com/koding/multiconfig"
"github.com/didi/nightingale/v5/src/pkg/cas"
"github.com/didi/nightingale/v5/src/pkg/httpx"
"github.com/didi/nightingale/v5/src/pkg/ldapx"
"github.com/didi/nightingale/v5/src/pkg/logx"
"github.com/didi/nightingale/v5/src/pkg/oauth2x"
"github.com/didi/nightingale/v5/src/pkg/oidcc"
"github.com/didi/nightingale/v5/src/pkg/ormx"
"github.com/didi/nightingale/v5/src/pkg/secu"
@ -135,6 +137,8 @@ type Config struct {
Clusters []ClusterOptions
Ibex Ibex
OIDC oidcc.Config
CAS cas.Config
OAuth oauth2x.Config
TargetMetrics map[string]string
}

View File

@ -134,8 +134,13 @@ func configRoute(r *gin.Engine, version string) {
pages.POST("/auth/logout", jwtMock(), logoutPost)
pages.POST("/auth/refresh", jwtMock(), refreshPost)
pages.GET("/auth/sso-config", ssoConfigGet)
pages.GET("/auth/redirect", loginRedirect)
pages.GET("/auth/redirect/cas", loginRedirectCas)
pages.GET("/auth/redirect/oauth", loginRedirectOAuth)
pages.GET("/auth/callback", loginCallback)
pages.GET("/auth/callback/cas", loginCallbackCas)
pages.GET("/auth/callback/oauth", loginCallbackOAuth)
pages.GET("/metrics/desc", metricsDescGetFile)
pages.POST("/metrics/desc", metricsDescGetMap)

View File

@ -13,6 +13,8 @@ import (
"github.com/toolkits/pkg/logger"
"github.com/didi/nightingale/v5/src/models"
"github.com/didi/nightingale/v5/src/pkg/cas"
"github.com/didi/nightingale/v5/src/pkg/oauth2x"
"github.com/didi/nightingale/v5/src/pkg/oidcc"
"github.com/didi/nightingale/v5/src/webapi/config"
)
@ -259,3 +261,212 @@ func loginCallback(c *gin.Context) {
RefreshToken: ts.RefreshToken,
}, nil)
}
type RedirectOutput struct {
Redirect string `json:"redirect"`
State string `json:"state"`
}
func loginRedirectCas(c *gin.Context) {
redirect := ginx.QueryStr(c, "redirect", "/")
v, exists := c.Get("userid")
if exists {
userid := v.(int64)
user, err := models.UserGetById(userid)
ginx.Dangerous(err)
if user == nil {
ginx.Bomb(200, "user not found")
}
if user.Username != "" { // already login
ginx.NewRender(c).Data(redirect, nil)
return
}
}
if !config.C.CAS.Enable {
logger.Error("cas is not enable")
ginx.NewRender(c).Data("", nil)
return
}
redirect, state, err := cas.Authorize(redirect)
ginx.Dangerous(err)
ginx.NewRender(c).Data(RedirectOutput{
Redirect: redirect,
State: state,
}, err)
}
func loginCallbackCas(c *gin.Context) {
ticket := ginx.QueryStr(c, "ticket", "")
state := ginx.QueryStr(c, "state", "")
ret, err := cas.ValidateServiceTicket(c.Request.Context(), ticket, state)
if err != nil {
logger.Errorf("ValidateServiceTicket: %s", err)
ginx.NewRender(c).Data("", err)
return
}
user, err := models.UserGet("username=?", ret.Username)
if err != nil {
logger.Errorf("UserGet: %s", err)
}
ginx.Dangerous(err)
if user != nil {
if config.C.CAS.CoverAttributes {
user.Nickname = ret.Nickname
user.Email = ret.Email
user.Phone = ret.Phone
user.UpdateAt = time.Now().Unix()
ginx.Dangerous(user.Update("email", "nickname", "phone", "update_at"))
}
} else {
now := time.Now().Unix()
user = &models.User{
Username: ret.Username,
Password: "******",
Nickname: ret.Nickname,
Portrait: "",
Roles: strings.Join(config.C.CAS.DefaultRoles, " "),
RolesLst: config.C.CAS.DefaultRoles,
Contacts: []byte("{}"),
Phone: ret.Phone,
Email: ret.Email,
CreateAt: now,
UpdateAt: now,
CreateBy: "CAS",
UpdateBy: "CAS",
}
// create user from cas
ginx.Dangerous(user.Add())
}
// set user login state
userIdentity := fmt.Sprintf("%d-%s", user.Id, user.Username)
ts, err := createTokens(config.C.JWTAuth.SigningKey, userIdentity)
if err != nil {
logger.Errorf("createTokens: %s", err)
}
ginx.Dangerous(err)
ginx.Dangerous(createAuth(c.Request.Context(), userIdentity, ts))
redirect := "/"
if ret.Redirect != "/login" {
redirect = ret.Redirect
}
ginx.NewRender(c).Data(CallbackOutput{
Redirect: redirect,
User: user,
AccessToken: ts.AccessToken,
RefreshToken: ts.RefreshToken,
}, nil)
}
func loginRedirectOAuth(c *gin.Context) {
redirect := ginx.QueryStr(c, "redirect", "/")
v, exists := c.Get("userid")
if exists {
userid := v.(int64)
user, err := models.UserGetById(userid)
ginx.Dangerous(err)
if user == nil {
ginx.Bomb(200, "user not found")
}
if user.Username != "" { // already login
ginx.NewRender(c).Data(redirect, nil)
return
}
}
if !config.C.OAuth.Enable {
ginx.NewRender(c).Data("", nil)
return
}
redirect, err := oauth2x.Authorize(redirect)
ginx.Dangerous(err)
ginx.NewRender(c).Data(redirect, err)
}
func loginCallbackOAuth(c *gin.Context) {
code := ginx.QueryStr(c, "code", "")
state := ginx.QueryStr(c, "state", "")
ret, err := oauth2x.Callback(c.Request.Context(), code, state)
if err != nil {
logger.Debugf("sso.callback() get ret %+v error %v", ret, err)
ginx.NewRender(c).Data(CallbackOutput{}, err)
return
}
user, err := models.UserGet("username=?", ret.Username)
ginx.Dangerous(err)
if user != nil {
if config.C.OAuth.CoverAttributes {
user.Nickname = ret.Nickname
user.Email = ret.Email
user.Phone = ret.Phone
user.UpdateAt = time.Now().Unix()
user.Update("email", "nickname", "phone", "update_at")
}
} else {
now := time.Now().Unix()
user = &models.User{
Username: ret.Username,
Password: "******",
Nickname: ret.Nickname,
Phone: ret.Phone,
Email: ret.Email,
Portrait: "",
Roles: strings.Join(config.C.OAuth.DefaultRoles, " "),
RolesLst: config.C.OAuth.DefaultRoles,
Contacts: []byte("{}"),
CreateAt: now,
UpdateAt: now,
CreateBy: "oauth2",
UpdateBy: "oauth2",
}
// create user from oidc
ginx.Dangerous(user.Add())
}
// set user login state
userIdentity := fmt.Sprintf("%d-%s", user.Id, user.Username)
ts, err := createTokens(config.C.JWTAuth.SigningKey, userIdentity)
ginx.Dangerous(err)
ginx.Dangerous(createAuth(c.Request.Context(), userIdentity, ts))
redirect := "/"
if ret.Redirect != "/login" {
redirect = ret.Redirect
}
ginx.NewRender(c).Data(CallbackOutput{
Redirect: redirect,
User: user,
AccessToken: ts.AccessToken,
RefreshToken: ts.RefreshToken,
}, nil)
}
type SsoConfigOutput struct {
OidcDisplayName string `json:"oidcDisplayName"`
CasDisplayName string `json:"casDisplayName"`
OauthDisplayName string `json:"oauthDisplayName"`
}
func ssoConfigGet(c *gin.Context) {
ginx.NewRender(c).Data(SsoConfigOutput{
OidcDisplayName: oidcc.GetDisplayName(),
CasDisplayName: cas.GetDisplayName(),
OauthDisplayName: oauth2x.GetDisplayName(),
}, nil)
}

View File

@ -10,9 +10,11 @@ import (
"github.com/toolkits/pkg/i18n"
"github.com/didi/nightingale/v5/src/models"
"github.com/didi/nightingale/v5/src/pkg/cas"
"github.com/didi/nightingale/v5/src/pkg/httpx"
"github.com/didi/nightingale/v5/src/pkg/ldapx"
"github.com/didi/nightingale/v5/src/pkg/logx"
"github.com/didi/nightingale/v5/src/pkg/oauth2x"
"github.com/didi/nightingale/v5/src/pkg/oidcc"
"github.com/didi/nightingale/v5/src/storage"
"github.com/didi/nightingale/v5/src/webapi/config"
@ -101,6 +103,12 @@ func (a Webapi) initialize() (func(), error) {
// init oidc
oidcc.Init(config.C.OIDC)
// init cas
cas.Init(config.C.CAS)
// init oauth
oauth2x.Init(config.C.OAuth)
// init logger
loggerClean, err := logx.Init(config.C.Log)
if err != nil {