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:
parent
65d8f80637
commit
352415662a
|
@ -149,6 +149,7 @@ Email = "mail"
|
||||||
|
|
||||||
[OIDC]
|
[OIDC]
|
||||||
Enable = false
|
Enable = false
|
||||||
|
DiaplayName = "OIDC登录"
|
||||||
RedirectURL = "http://n9e.com/callback"
|
RedirectURL = "http://n9e.com/callback"
|
||||||
SsoAddr = "http://sso.example.org"
|
SsoAddr = "http://sso.example.org"
|
||||||
ClientId = ""
|
ClientId = ""
|
||||||
|
@ -161,6 +162,52 @@ Nickname = "nickname"
|
||||||
Phone = "phone_number"
|
Phone = "phone_number"
|
||||||
Email = "email"
|
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]
|
[Redis]
|
||||||
# address, ip:port or ip1:port,ip2:port for cluster and sentinel(SentinelAddrs)
|
# address, ip:port or ip1:port,ip2:port for cluster and sentinel(SentinelAddrs)
|
||||||
Address = "127.0.0.1:6379"
|
Address = "127.0.0.1:6379"
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -23,7 +23,7 @@ require (
|
||||||
github.com/prometheus/common v0.32.1
|
github.com/prometheus/common v0.32.1
|
||||||
github.com/prometheus/prometheus v2.5.0+incompatible
|
github.com/prometheus/prometheus v2.5.0+incompatible
|
||||||
github.com/tidwall/gjson v1.14.0
|
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
|
github.com/urfave/cli/v2 v2.3.0
|
||||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c
|
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -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/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 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
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.3.1-0.20220824084030-9f9f830a05d5 h1:kMCwr2gNHjHEVgw+uNVdiPbGadj4TekbIfrTXElZeI0=
|
||||||
github.com/toolkits/pkg v1.2.9/go.mod h1:ZUsQAOoaR99PSbes+RXSirvwmtd6+XIUvizCmrjfUYc=
|
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 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 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ type ssoClient struct {
|
||||||
ssoAddr string
|
ssoAddr string
|
||||||
callbackAddr string
|
callbackAddr string
|
||||||
coverAttributes bool
|
coverAttributes bool
|
||||||
|
displayName string
|
||||||
attributes struct {
|
attributes struct {
|
||||||
username string
|
username string
|
||||||
nickname string
|
nickname string
|
||||||
|
@ -30,6 +31,7 @@ type ssoClient struct {
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Enable bool
|
Enable bool
|
||||||
|
DisplayName string
|
||||||
RedirectURL string
|
RedirectURL string
|
||||||
SsoAddr string
|
SsoAddr string
|
||||||
ClientId string
|
ClientId string
|
||||||
|
@ -59,6 +61,7 @@ func Init(cf Config) {
|
||||||
cli.attributes.nickname = cf.Attributes.Nickname
|
cli.attributes.nickname = cf.Attributes.Nickname
|
||||||
cli.attributes.phone = cf.Attributes.Phone
|
cli.attributes.phone = cf.Attributes.Phone
|
||||||
cli.attributes.email = cf.Attributes.Email
|
cli.attributes.email = cf.Attributes.Email
|
||||||
|
cli.displayName = cf.DisplayName
|
||||||
provider, err := oidc.NewProvider(context.Background(), cf.SsoAddr)
|
provider, err := oidc.NewProvider(context.Background(), cf.SsoAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -77,6 +80,10 @@ func Init(cf Config) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDisplayName() string {
|
||||||
|
return cli.displayName
|
||||||
|
}
|
||||||
|
|
||||||
func wrapStateKey(key string) string {
|
func wrapStateKey(key string) string {
|
||||||
return "n9e_oidc_" + key
|
return "n9e_oidc_" + key
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,11 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/koding/multiconfig"
|
"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/httpx"
|
||||||
"github.com/didi/nightingale/v5/src/pkg/ldapx"
|
"github.com/didi/nightingale/v5/src/pkg/ldapx"
|
||||||
"github.com/didi/nightingale/v5/src/pkg/logx"
|
"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/oidcc"
|
||||||
"github.com/didi/nightingale/v5/src/pkg/ormx"
|
"github.com/didi/nightingale/v5/src/pkg/ormx"
|
||||||
"github.com/didi/nightingale/v5/src/pkg/secu"
|
"github.com/didi/nightingale/v5/src/pkg/secu"
|
||||||
|
@ -135,6 +137,8 @@ type Config struct {
|
||||||
Clusters []ClusterOptions
|
Clusters []ClusterOptions
|
||||||
Ibex Ibex
|
Ibex Ibex
|
||||||
OIDC oidcc.Config
|
OIDC oidcc.Config
|
||||||
|
CAS cas.Config
|
||||||
|
OAuth oauth2x.Config
|
||||||
TargetMetrics map[string]string
|
TargetMetrics map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -134,8 +134,13 @@ func configRoute(r *gin.Engine, version string) {
|
||||||
pages.POST("/auth/logout", jwtMock(), logoutPost)
|
pages.POST("/auth/logout", jwtMock(), logoutPost)
|
||||||
pages.POST("/auth/refresh", jwtMock(), refreshPost)
|
pages.POST("/auth/refresh", jwtMock(), refreshPost)
|
||||||
|
|
||||||
|
pages.GET("/auth/sso-config", ssoConfigGet)
|
||||||
pages.GET("/auth/redirect", loginRedirect)
|
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", loginCallback)
|
||||||
|
pages.GET("/auth/callback/cas", loginCallbackCas)
|
||||||
|
pages.GET("/auth/callback/oauth", loginCallbackOAuth)
|
||||||
|
|
||||||
pages.GET("/metrics/desc", metricsDescGetFile)
|
pages.GET("/metrics/desc", metricsDescGetFile)
|
||||||
pages.POST("/metrics/desc", metricsDescGetMap)
|
pages.POST("/metrics/desc", metricsDescGetMap)
|
||||||
|
|
|
@ -13,6 +13,8 @@ import (
|
||||||
"github.com/toolkits/pkg/logger"
|
"github.com/toolkits/pkg/logger"
|
||||||
|
|
||||||
"github.com/didi/nightingale/v5/src/models"
|
"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/pkg/oidcc"
|
||||||
"github.com/didi/nightingale/v5/src/webapi/config"
|
"github.com/didi/nightingale/v5/src/webapi/config"
|
||||||
)
|
)
|
||||||
|
@ -259,3 +261,212 @@ func loginCallback(c *gin.Context) {
|
||||||
RefreshToken: ts.RefreshToken,
|
RefreshToken: ts.RefreshToken,
|
||||||
}, nil)
|
}, 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)
|
||||||
|
}
|
||||||
|
|
|
@ -10,9 +10,11 @@ import (
|
||||||
"github.com/toolkits/pkg/i18n"
|
"github.com/toolkits/pkg/i18n"
|
||||||
|
|
||||||
"github.com/didi/nightingale/v5/src/models"
|
"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/httpx"
|
||||||
"github.com/didi/nightingale/v5/src/pkg/ldapx"
|
"github.com/didi/nightingale/v5/src/pkg/ldapx"
|
||||||
"github.com/didi/nightingale/v5/src/pkg/logx"
|
"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/oidcc"
|
||||||
"github.com/didi/nightingale/v5/src/storage"
|
"github.com/didi/nightingale/v5/src/storage"
|
||||||
"github.com/didi/nightingale/v5/src/webapi/config"
|
"github.com/didi/nightingale/v5/src/webapi/config"
|
||||||
|
@ -101,6 +103,12 @@ func (a Webapi) initialize() (func(), error) {
|
||||||
// init oidc
|
// init oidc
|
||||||
oidcc.Init(config.C.OIDC)
|
oidcc.Init(config.C.OIDC)
|
||||||
|
|
||||||
|
// init cas
|
||||||
|
cas.Init(config.C.CAS)
|
||||||
|
|
||||||
|
// init oauth
|
||||||
|
oauth2x.Init(config.C.OAuth)
|
||||||
|
|
||||||
// init logger
|
// init logger
|
||||||
loggerClean, err := logx.Init(config.C.Log)
|
loggerClean, err := logx.Init(config.C.Log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in New Issue