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]
|
||||
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
2
go.mod
|
@ -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
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/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=
|
||||
|
|
|
@ -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
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue